Skip to content

Commit bdf2362

Browse files
Michael Borckclaude
andcommitted
feat: copy button on tutor messages
Copies the message beside the read-aloud control, with brief copied feedback; both buttons reveal on hover or keyboard focus. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent a4bc4bb commit bdf2362

1 file changed

Lines changed: 45 additions & 13 deletions

File tree

components/Chat.tsx

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export default function Chat({
9696
const [showSources, setShowSources] = useState(false);
9797
const [isRecording, setIsRecording] = useState(false);
9898
const [speakingIndex, setSpeakingIndex] = useState<number | null>(null);
99+
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
99100
const recorderRef = useRef<{ stop: () => Promise<Blob> } | null>(null);
100101
const lastReadRef = useRef<number>(-1);
101102

@@ -146,6 +147,19 @@ export default function Chat({
146147
[audioSettings.voiceGender, speakingIndex],
147148
);
148149

150+
const copyMessage = useCallback(async (text: string, index: number) => {
151+
try {
152+
await navigator.clipboard.writeText(text);
153+
setCopiedIndex(index);
154+
setTimeout(
155+
() => setCopiedIndex((prev) => (prev === index ? null : prev)),
156+
1500,
157+
);
158+
} catch (e) {
159+
console.error("Copy failed:", e);
160+
}
161+
}, []);
162+
149163
// Auto-read new assistant messages
150164
useEffect(() => {
151165
if (!audioSettings.autoRead || disabled) return;
@@ -304,19 +318,37 @@ export default function Chat({
304318
<ReactMarkdown className="w-full pl-10">
305319
{message.content}
306320
</ReactMarkdown>
307-
{/* Speaker button */}
308-
<button
309-
onClick={() => readAloud(message.content, index)}
310-
className="absolute right-0 top-0 rounded-soft p-1 text-ink-quiet opacity-0 transition-all duration-normal hover:text-accent focus:opacity-100 group-focus-within:opacity-100 group-hover:opacity-100"
311-
title={speakingIndex === index ? "Stop reading" : "Read aloud"}
312-
aria-label={speakingIndex === index ? "Stop reading" : "Read aloud"}
313-
>
314-
{speakingIndex === index ? (
315-
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
316-
) : (
317-
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0014 8.5v7a4.49 4.49 0 002.5-3.5zM14 3.23v2.06a6.51 6.51 0 010 13.42v2.06A8.51 8.51 0 0014 3.23z"/></svg>
318-
)}
319-
</button>
321+
{/* Copy + speaker buttons */}
322+
<div className="absolute right-0 top-0 flex items-center gap-1 opacity-0 transition-all duration-normal focus-within:opacity-100 group-hover:opacity-100">
323+
<button
324+
onClick={() => copyMessage(message.content, index)}
325+
className={`rounded-soft p-1 transition-colors duration-normal ${
326+
copiedIndex === index
327+
? "text-accent"
328+
: "text-ink-quiet hover:text-accent"
329+
}`}
330+
title={copiedIndex === index ? "Copied" : "Copy"}
331+
aria-label={copiedIndex === index ? "Copied" : "Copy message"}
332+
>
333+
{copiedIndex === index ? (
334+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
335+
) : (
336+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
337+
)}
338+
</button>
339+
<button
340+
onClick={() => readAloud(message.content, index)}
341+
className="rounded-soft p-1 text-ink-quiet transition-colors duration-normal hover:text-accent"
342+
title={speakingIndex === index ? "Stop reading" : "Read aloud"}
343+
aria-label={speakingIndex === index ? "Stop reading" : "Read aloud"}
344+
>
345+
{speakingIndex === index ? (
346+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
347+
) : (
348+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0014 8.5v7a4.49 4.49 0 002.5-3.5zM14 3.23v2.06a6.51 6.51 0 010 13.42v2.06A8.51 8.51 0 0014 3.23z"/></svg>
349+
)}
350+
</button>
351+
</div>
320352
</div>
321353
) : (
322354
<p

0 commit comments

Comments
 (0)