程序员阿江(Relakkes) 3c5290eec1 fix: reduce copy ambiguity in chat sessions
The session view exposed copy affordances in a way that implied the
whole conversation would be copied, while user prompts had no copy
path at all. This changes copy to a per-message footer action so the
scope is explicit for both prompts and assistant replies, and reuses
the shared clipboard helper instead of maintaining a second copy path.

Constraint: The worktree contains unrelated in-progress changes that must stay uncommitted
Rejected: Keep a floating action above each message | it interrupted reading order and still looked session-scoped
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep copy affordances attached to individual message blocks so the copied scope stays obvious
Tested: bun run test src/components/chat/MessageList.test.tsx; bun run lint
Not-tested: Manual desktop visual QA across dense multi-message sessions
2026-04-08 23:19:31 +08:00

59 lines
1.2 KiB
TypeScript

import { useEffect, useState } from 'react'
import { copyTextToClipboard } from '../chat/clipboard'
type Props = {
text: string
label?: string
copiedLabel?: string
displayLabel?: string
displayCopiedLabel?: string
className?: string
}
export function CopyButton({
text,
label = 'Copy',
copiedLabel = 'Copied',
displayLabel,
displayCopiedLabel,
className = '',
}: Props) {
const [copied, setCopied] = useState(false)
useEffect(() => {
if (!copied) return
const timer = window.setTimeout(() => setCopied(false), 1500)
return () => window.clearTimeout(timer)
}, [copied])
const handleCopy = async () => {
try {
const ok = await copyTextToClipboard(text)
if (!ok) {
setCopied(false)
return
}
setCopied(true)
} catch {
setCopied(false)
}
}
const currentLabel = copied ? copiedLabel : label
const buttonText = copied
? (displayCopiedLabel ?? copiedLabel)
: (displayLabel ?? label)
return (
<button
type="button"
onClick={handleCopy}
className={className}
aria-label={currentLabel}
title={currentLabel}
>
{buttonText}
</button>
)
}