Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions src/components/ChatBox/MessageItem/AgentMessageCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -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
Expand Down
44 changes: 38 additions & 6 deletions src/components/ChatBox/MessageItem/FeedbackCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = 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]);

const handleCopy = () => {
navigator.clipboard.writeText(content);
};
useEffect(() => {
return () => {
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
}
};
}, []);

return (
<div
Expand All @@ -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>

Expand Down
29 changes: 23 additions & 6 deletions src/components/ChatBox/MessageItem/UserMessageCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = () => {
Expand Down Expand Up @@ -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">
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/ar/setting.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "إصدار أيجنت السحابي",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/de/setting.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/locales/en-us/setting.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/es/setting.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/fr/setting.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/it/setting.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/locales/ja/setting.json
Original file line number Diff line number Diff line change
Expand Up @@ -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クラウドバージョン",
Expand Down Expand Up @@ -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クラウドバージョン",
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/locales/ko/setting.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 클라우드 버전",
Expand Down Expand Up @@ -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 클라우드 버전",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/ru/setting.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 Версия",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/zh-Hans/setting.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 云端版本",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/zh-Hant/setting.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 雲端版本",
Expand Down