// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import { useHost } from '@/host'; import { Check, Copy, FileText, ThumbsDown, ThumbsUp } from 'lucide-react'; import { useCallback, useEffect, useState, type ReactNode } 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; className?: string; typewriter?: boolean; attaches?: File[]; /** Shown only after markdown (and typewriter, if enabled) has finished rendering — e.g. generated file chips. */ deferredFooter?: ReactNode; onTyping?: () => void; onMarkdownRenderComplete?: () => void; } // Tracks agent messages that have already played the typewriter (by stable message id). const completedTypewriterByMessageId = new Map(); export function AgentMessageCard({ id, content, typewriter = true, onTyping, onMarkdownRenderComplete, className, attaches, deferredFooter, }: AgentMessageCardProps) { const host = useHost(); const ipcRenderer = host?.ipcRenderer; const [markdownAndTypingComplete, setMarkdownAndTypingComplete] = useState( () => completedTypewriterByMessageId.has(id) ); useEffect(() => { setMarkdownAndTypingComplete(completedTypewriterByMessageId.has(id)); }, [id]); const isCompleted = completedTypewriterByMessageId.has(id); const enableTypewriter = !isCompleted; const [copied, setCopied] = useState(false); const { t } = useTranslation(); const handleTypingComplete = () => { if (!completedTypewriterByMessageId.has(id)) { completedTypewriterByMessageId.set(id, true); } if (onTyping) { onTyping(); } }; 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]); const handleMarkdownRenderComplete = useCallback(() => { setMarkdownAndTypingComplete(true); onMarkdownRenderComplete?.(); }, [onMarkdownRenderComplete]); const showDeferredFileUi = markdownAndTypingComplete && ((attaches && attaches.length > 0) || deferredFooter != null); return (
{showDeferredFileUi && attaches && attaches.length > 0 && (
{attaches?.map((file) => { return (
{ e.stopPropagation(); ipcRenderer?.invoke('reveal-in-folder', file.filePath); }} key={'attache-' + file.fileName} className="gap-2 rounded-2xl py-1 pl-2 flex w-full cursor-pointer items-center border border-solid border-ds-border-neutral-subtle-default bg-ds-bg-neutral-default-default" >
{file?.fileName?.split('.')[0]}
{file?.fileName?.split('.')[1]}
); })}
)} {showDeferredFileUi && deferredFooter != null && (
{deferredFooter}
)} {markdownAndTypingComplete && (
)}
); }