Copy button — on all messages, Copied! feedback, checkmark icon
This commit is contained in:
+50
-34
@@ -1,9 +1,57 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useStore } from '@/lib/store';
|
import { useStore } from '@/lib/store';
|
||||||
import { renderMarkdown } from '@/lib/markdown';
|
import { renderMarkdown } from '@/lib/markdown';
|
||||||
|
|
||||||
|
function CopyRow({ content, sources }: { content: string; sources: string[] }) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
function copy() {
|
||||||
|
navigator.clipboard.writeText(content).then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3 mt-1.5">
|
||||||
|
<button
|
||||||
|
onPointerUp={copy}
|
||||||
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '2px 6px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
color: copied ? 'var(--accent)' : 'var(--text3)',
|
||||||
|
fontSize: '11px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '4px',
|
||||||
|
transition: 'color 0.15s',
|
||||||
|
}}
|
||||||
|
title="Copy"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||||
|
<polyline points="20 6 9 17 4 12"/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
|
||||||
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{copied ? 'Copied!' : 'Copy'}
|
||||||
|
</button>
|
||||||
|
{sources && sources.length > 0 && (
|
||||||
|
<div className="text-xs italic" style={{ color: 'var(--text3)' }}>
|
||||||
|
Sources: {[...new Set(sources)].join(', ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function MessageList() {
|
export default function MessageList() {
|
||||||
const { messages, isLoading } = useStore();
|
const { messages, isLoading } = useStore();
|
||||||
const bottomRef = useRef<HTMLDivElement>(null);
|
const bottomRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -60,39 +108,7 @@ export default function MessageList() {
|
|||||||
<span style={{ whiteSpace: 'pre-wrap' }}>{m.content}</span>
|
<span style={{ whiteSpace: 'pre-wrap' }}>{m.content}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 mt-1.5">
|
<CopyRow content={m.content} sources={m.sources} />
|
||||||
{m.role === 'assistant' && (
|
|
||||||
<button
|
|
||||||
onPointerUp={() => {
|
|
||||||
navigator.clipboard.writeText(m.content);
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
background: 'none',
|
|
||||||
border: 'none',
|
|
||||||
cursor: 'pointer',
|
|
||||||
padding: '2px 6px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
color: 'var(--text3)',
|
|
||||||
fontSize: '11px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '4px',
|
|
||||||
}}
|
|
||||||
title="Copy response"
|
|
||||||
>
|
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
|
|
||||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
|
||||||
</svg>
|
|
||||||
Copy
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{m.sources && m.sources.length > 0 && (
|
|
||||||
<div className="text-xs italic" style={{ color: 'var(--text3)' }}>
|
|
||||||
Sources: {[...new Set(m.sources)].join(', ')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user