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) => (
))}
);
}