import { useEffect, useRef, useState } from 'react'; import { Check, Copy, Loader2, ShieldCheck, Trash2, Volume2, X } from 'lucide-react'; import { copyTextToClipboard, formatTime } from '../app-core-utils.js'; import { isVisibleActivityStep } from '../app-message-state.js'; import { GeneratedImage } from './MessageImages.jsx'; export { ImagePreviewModal } from './MessageImages.jsx'; const COPY_FEEDBACK_RESET_DELAY_MS = 1500; export function ActivityMessage({ message }) { const running = message.status === 'running' || message.status === 'queued'; const failed = message.status === 'failed'; const activities = message.activities || []; const visibleSteps = activities.filter((activity) => isVisibleActivityStep(activity, message.status)).slice(-4); const headline = running ? '正在思考中' : message.label || message.content || '正在处理'; const failedDetail = failed ? String(message.detail || '').trim() : ''; const showFailedDetail = failedDetail && failedDetail !== headline && failedDetail !== '任务失败'; return (
{running ? : failed ? : } {headline}
{showFailedDetail ?
{failedDetail}
: null} {visibleSteps.length ? (
{visibleSteps.map((activity) => (
{activity.label}
))}
) : null} {message.timestamp ? : null}
); } export function MessageContent({ content, onPreviewImage }) { const text = String(content || ''); const parts = []; const pattern = /!\[([^\]]*)\]\((\/generated\/[^)\s]+)\)/g; let lastIndex = 0; let match; while ((match = pattern.exec(text))) { if (match.index > lastIndex) { parts.push({ type: 'text', value: text.slice(lastIndex, match.index) }); } parts.push({ type: 'image', alt: match[1] || '生成图片', url: match[2] }); lastIndex = match.index + match[0].length; } if (lastIndex < text.length) { parts.push({ type: 'text', value: text.slice(lastIndex) }); } if (!parts.length) { return
{renderInlineText(text, 'message-root')}
; } const rendered = []; parts.forEach((part, index) => { if (part.type === 'image') { rendered.push(); return; } rendered.push(...renderInlineText(part.value, `message-${index}`)); }); return (
{rendered}
); } export function normalizeInlineHref(value) { const raw = String(value || '').trim(); if (!raw) { return ''; } if (/^https?:\/\//i.test(raw) || /^mailto:/i.test(raw)) { return raw; } return `https://${raw}`; } export function renderInlineText(text, keyPrefix) { const value = String(text || ''); const pattern = /\[([^\]]+)\]\(((?:https?:\/\/|www\.)[^\s)]+)\)|((?:https?:\/\/|www\.)[^\s<>()]+)/gi; const nodes = []; let lastIndex = 0; let match; let partIndex = 0; while ((match = pattern.exec(value))) { if (match.index > lastIndex) { nodes.push({value.slice(lastIndex, match.index)}); } if (match[1] && match[2]) { const href = normalizeInlineHref(match[2]); nodes.push( {match[1]} ); } else if (match[3]) { const href = normalizeInlineHref(match[3]); nodes.push( {match[3]} ); } lastIndex = pattern.lastIndex; } if (lastIndex < value.length) { nodes.push({value.slice(lastIndex)}); } return nodes.length ? nodes : [{value}]; } function MessageActions({ message, onDeleteMessage, onSpeakMessage, speakingMessageId, speechLoadingMessageId }) { const isAssistant = message.role === 'assistant'; const canAct = isAssistant || message.role === 'user'; const messageId = String(message.id || ''); const speechActive = isAssistant && speakingMessageId === messageId; const speechLoading = isAssistant && speechLoadingMessageId === messageId; if (!canAct) { return null; } return (
{isAssistant ? ( onSpeakMessage?.(message)} /> ) : null}
); } function SpeechActionButton({ active, loading, onClick }) { return ( ); } function MessageCopyButton({ content }) { const [copied, setCopied] = useState(false); const copiedTimerRef = useRef(null); useEffect(() => () => { if (copiedTimerRef.current) { window.clearTimeout(copiedTimerRef.current); } }, []); const handleCopy = async () => { const copiedText = await copyTextToClipboard(content); if (!copiedText) { window.alert('复制失败'); return; } setCopied(true); if (copiedTimerRef.current) { window.clearTimeout(copiedTimerRef.current); } copiedTimerRef.current = window.setTimeout(() => setCopied(false), COPY_FEEDBACK_RESET_DELAY_MS); }; return ( ); } export function ChatMessage({ message, onPreviewImage, onDeleteMessage, onSpeakMessage, speakingMessageId, speechLoadingMessageId }) { if (message.role === 'activity') { return ; } const isUser = message.role === 'user'; return (
{message.timestamp ? : null}
); } export function ChatPane({ messages, selectedSession, running, onPreviewImage, onDeleteMessage, onSpeakMessage, speakingMessageId, speechLoadingMessageId, backgroundInert = false }) { const bottomRef = useRef(null); const inertProps = backgroundInert ? { inert: '' } : {}; useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }); }, [messages, running]); if (!messages.length) { return (

{selectedSession ? selectedSession.title : '新对话'}

问 Codex 任何事。

); } return (
{messages.map((message) => ( ))}
); }