Feat #1255: update copy button status by displaying popup and setting… (#1259)

Co-authored-by: bytecii <994513625@qq.com>
This commit is contained in:
dataCenter430 2026-02-17 16:53:12 -07:00 committed by GitHub
parent f93643e93f
commit 167d112e04
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 99 additions and 18 deletions

View file

@ -12,11 +12,15 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { Copy, FileText } from 'lucide-react';
import { useMemo } from 'react';
import { Check, Copy, FileText } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { Button } from '../../ui/button';
import { MarkDown } from './MarkDown';
const COPIED_RESET_MS = 2000;
interface AgentMessageCardProps {
id: string;
content: string;
@ -48,6 +52,9 @@ export function AgentMessageCard({
// if completed, disable typewriter effect
const enableTypewriter = !isCompleted;
const [copied, setCopied] = useState(false);
const { t } = useTranslation();
// when typewriter effect is completed, record to global Map
const handleTypingComplete = () => {
if (!isCompleted) {
@ -58,9 +65,16 @@ export function AgentMessageCard({
}
};
const handleCopy = () => {
navigator.clipboard.writeText(content);
};
const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(content);
toast.success(t('setting.copied-to-clipboard'));
setCopied(true);
setTimeout(() => setCopied(false), COPIED_RESET_MS);
} catch {
toast.error('Failed to copy to clipboard');
}
}, [content, t]);
return (
<div
@ -69,7 +83,11 @@ export function AgentMessageCard({
>
<div className="absolute bottom-[0px] right-1 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Button onClick={handleCopy} variant="ghost" size="icon">
<Copy />
{copied ? (
<Check className="h-4 w-4 text-text-success" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
<MarkDown

View file

@ -13,8 +13,12 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { Button } from '@/components/ui/button';
import { Copy } from 'lucide-react';
import { useState } from 'react';
import { Check, Copy } from 'lucide-react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
const COPIED_RESET_MS = 2000;
interface FeedbackCardProps {
id: string;
@ -34,10 +38,34 @@ export function FeedbackCard({
className,
}: FeedbackCardProps) {
const [_isHovered, setIsHovered] = useState(false);
const [copied, setCopied] = useState(false);
const timeoutRef = useRef<number | null>(null);
const { t } = useTranslation();
const handleCopy = () => {
navigator.clipboard.writeText(content);
};
const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(content);
toast.success(t('setting.copied-to-clipboard'));
setCopied(true);
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = window.setTimeout(() => {
setCopied(false);
timeoutRef.current = null;
}, COPIED_RESET_MS);
} catch {
toast.error(t('setting.failed-to-copy-to-clipboard'));
}
}, [content, t]);
useEffect(() => {
return () => {
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return (
<div
@ -49,7 +77,11 @@ export function FeedbackCard({
{/* Copy button - appears on hover */}
<div className="absolute bottom-1 right-1 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Button onClick={handleCopy} variant="ghost" size="icon">
<Copy className="h-4 w-4" />
{copied ? (
<Check className="h-4 w-4 text-text-success" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>

View file

@ -13,11 +13,15 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { cn } from '@/lib/utils';
import { Copy, FileText, Image } from 'lucide-react';
import { useRef, useState } from 'react';
import { Check, Copy, FileText, Image } from 'lucide-react';
import { useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { Button } from '../../ui/button';
import { Popover, PopoverContent, PopoverTrigger } from '../../ui/popover';
const COPIED_RESET_MS = 2000;
interface UserMessageCardProps {
id: string;
content: string;
@ -33,11 +37,20 @@ export function UserMessageCard({
}: UserMessageCardProps) {
const [_hoveredFilePath, setHoveredFilePath] = useState<string | null>(null);
const [isRemainingOpen, setIsRemainingOpen] = useState(false);
const [copied, setCopied] = useState(false);
const hoverCloseTimerRef = useRef<number | null>(null);
const { t } = useTranslation();
const handleCopy = () => {
navigator.clipboard.writeText(content);
};
const handleCopy = useCallback(async () => {
try {
await navigator.clipboard.writeText(content);
toast.success(t('setting.copied-to-clipboard'));
setCopied(true);
setTimeout(() => setCopied(false), COPIED_RESET_MS);
} catch {
toast.error('Failed to copy to clipboard');
}
}, [content, t]);
// Popover handles outside clicks; no manual listener needed
const openRemainingPopover = () => {
@ -73,7 +86,11 @@ export function UserMessageCard({
>
<div className="absolute bottom-[0px] right-1 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Button onClick={handleCopy} variant="ghost" size="icon">
<Copy />
{copied ? (
<Check className="h-4 w-4 text-text-success" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
<div className="whitespace-pre-wrap break-words text-body-sm text-text-body">

View file

@ -42,6 +42,7 @@
"validate-failed": "فشل التحقق",
"copy": "نسخ",
"copied-to-clipboard": "تم النسخ إلى الحافظة",
"failed-to-copy-to-clipboard": "فشل النسخ إلى الحافظة",
"endpoint-url-can-not-be-empty": "!لا يمكن أن يكون عنوان يورل لنقطة النهاية فارغًا",
"verification-failed-please-check-endpoint-url": "فشل التحقق، يرجى المراجعة على النقطة النهائية يورل",
"eigent-cloud-version": "إصدار أيجنت السحابي",

View file

@ -42,6 +42,7 @@
"validate-failed": "Validierung fehlgeschlagen",
"copy": "Kopieren",
"copied-to-clipboard": "In die Zwischenablage kopiert",
"failed-to-copy-to-clipboard": "Kopieren in die Zwischenablage fehlgeschlagen",
"endpoint-url-can-not-be-empty": "Endpunkt-URL darf nicht leer sein!",
"verification-failed-please-check-endpoint-url": "Verifizierung fehlgeschlagen, bitte überprüfen Sie die Endpunkt-URL",
"eigent-cloud-version": "Eigent Cloud-Version",

View file

@ -42,6 +42,7 @@
"validate-failed": "Validate failed",
"copy": "Copy",
"copied-to-clipboard": "Copied to clipboard",
"failed-to-copy-to-clipboard": "Failed to copy to clipboard",
"endpoint-url-can-not-be-empty": "Endpoint URL can not be empty!",
"verification-failed-please-check-endpoint-url": "Verification failed, please check Endpoint URL",
"eigent-cloud-version": "Eigent Cloud Version",
@ -258,6 +259,7 @@
"validate-failed": "Validate failed",
"copy": "Copy",
"copied-to-clipboard": "Copied to clipboard",
"failed-to-copy-to-clipboard": "Failed to copy to clipboard",
"endpoint-url-can-not-be-empty": "Endpoint URL can not be empty!",
"verification-failed-please-check-endpoint-url": "Verification failed, please check Endpoint URL",
"eigent-cloud-version": "Eigent Cloud Version",

View file

@ -42,6 +42,7 @@
"validate-failed": "Validación fallida",
"copy": "Copiar",
"copied-to-clipboard": "Copiado al portapapeles",
"failed-to-copy-to-clipboard": "Error al copiar al portapapeles",
"endpoint-url-can-not-be-empty": "Endpoint URL no puede estar vacío!",
"verification-failed-please-check-endpoint-url": "Verificación fallida, por favor verifique Endpoint URL",
"eigent-cloud-version": "Eigent Cloud Version",

View file

@ -42,6 +42,7 @@
"validate-failed": "Validate failed",
"copy": "Copy",
"copied-to-clipboard": "Copied to clipboard",
"failed-to-copy-to-clipboard": "Échec de la copie dans le presse-papiers",
"endpoint-url-can-not-be-empty": "Endpoint URL can not be empty!",
"verification-failed-please-check-endpoint-url": "Verification failed, please check Endpoint URL",
"eigent-cloud-version": "Eigent Cloud Version",

View file

@ -42,6 +42,7 @@
"validate-failed": "Validazione fallita",
"copy": "Copia",
"copied-to-clipboard": "Copiato negli appunti",
"failed-to-copy-to-clipboard": "Impossibile copiare negli appunti",
"endpoint-url-can-not-be-empty": "L'URL dell'endpoint non può essere vuoto!",
"verification-failed-please-check-endpoint-url": "Verifica fallita, controlla l'URL dell'endpoint",
"eigent-cloud-version": "Versione cloud di Eigent",

View file

@ -42,6 +42,7 @@
"validate-failed": "検証失敗",
"copy": "コピー",
"copied-to-clipboard": "クリップボードにコピーしました",
"failed-to-copy-to-clipboard": "クリップボードへのコピーに失敗しました",
"endpoint-url-can-not-be-empty": "エンドポイントURLは空にできません",
"verification-failed-please-check-endpoint-url": "検証に失敗しました。エンドポイントURLを確認してください",
"eigent-cloud-version": "Eigentクラウドバージョン",
@ -110,6 +111,7 @@
"validate-failed": "検証失敗",
"copy": "コピー",
"copied-to-clipboard": "クリップボードにコピーしました",
"failed-to-copy-to-clipboard": "クリップボードにコピーに失敗しました",
"endpoint-url-can-not-be-empty": "エンドポイントURLは空にできません",
"verification-failed-please-check-endpoint-url": "検証に失敗しました。エンドポイントURLを確認してください",
"eigent-cloud-version": "Eigentクラウドバージョン",

View file

@ -42,6 +42,7 @@
"validate-failed": "유효성 검사 실패",
"copy": "복사",
"copied-to-clipboard": "클립보드에 복사됨",
"failed-to-copy-to-clipboard": "클립보드로 복사하지 못했습니다",
"endpoint-url-can-not-be-empty": "엔드포인트 URL은 비워둘 수 없습니다!",
"verification-failed-please-check-endpoint-url": "확인 실패, 엔드포인트 URL을 확인하세요",
"eigent-cloud-version": "Eigent 클라우드 버전",
@ -110,6 +111,7 @@
"validate-failed": "유효성 검사 실패",
"copy": "복사",
"copied-to-clipboard": "클립보드에 복사됨",
"failed-to-copy-to-clipboard": "클립보드에 복사에 실패했습니다",
"endpoint-url-can-not-be-empty": "엔드포인트 URL은 비워둘 수 없습니다!",
"verification-failed-please-check-endpoint-url": "확인 실패, 엔드포인트 URL을 확인하세요",
"eigent-cloud-version": "Eigent 클라우드 버전",

View file

@ -42,6 +42,7 @@
"validate-failed": "Проверка не удалась",
"copy": "Копировать",
"copied-to-clipboard": "Скопировано в буфер обмена",
"failed-to-copy-to-clipboard": "Не удалось скопировать в буфер обмена",
"endpoint-url-can-not-be-empty": "URL конечной точки не может быть пустым!",
"verification-failed-please-check-endpoint-url": "Проверка не удалась, проверьте URL конечной точки",
"eigent-cloud-version": "Eigent Cloud Версия",

View file

@ -44,6 +44,7 @@
"validate-failed": "验证失败",
"copy": "复制",
"copied-to-clipboard": "复制到剪贴板",
"failed-to-copy-to-clipboard": "复制到剪贴板失败",
"endpoint-url-can-not-be-empty": "Endpoint URL 不能为空!",
"verification-failed-please-check-endpoint-url": "验证失败,请检查 Endpoint URL",
"eigent-cloud-version": "Eigent 云端版本",

View file

@ -41,6 +41,7 @@
"validate-failed": "驗證失敗",
"copy": "複製",
"copied-to-clipboard": "已複製到剪貼板",
"failed-to-copy-to-clipboard": "複製到剪貼板失敗",
"endpoint-url-can-not-be-empty": "端點 URL 不可為空!",
"verification-failed-please-check-endpoint-url": "驗證失敗,請檢查端點 URL",
"eigent-cloud-version": "Eigent 雲端版本",