@@ -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