import { useRef, useState } from 'react';
import { useUserAvatar } from '../hooks/useUserAvatar';
import type { Message } from '../types';
import { getDisplayText, getFileAttachments, getImageUrls, getRenderableImageUrls, getThinking, shouldShowChatMessage } from '../utils/message';
import { resolveMessageRecallKey } from '../utils/memoryFeedback';
import { getMessageSaveActions } from '../utils/messageSave';
import { renderMarkdown } from '../utils/markdown';
import { filterText } from '../utils/wordFilter';
import { formatChatTime, shouldShowTimestamp } from '../utils/time';
import { TKMindAvatar } from './TKMindAvatar';
import { UserAvatar } from './UserAvatar';
import { ChatWelcomePanel } from './ChatWelcomePanel';
function CopyIcon() {
return (
);
}
function CheckIcon() {
return (
);
}
function PageIcon() {
return (
);
}
function ImageIcon() {
return (
);
}
function DocumentIcon() {
return (
);
}
function PlazaIcon() {
return (
);
}
function LinkIcon() {
return (
);
}
function ToolActivityIcon({ spinning }: { spinning: boolean }) {
return (
);
}
function ToolBadgeItem({ spinning }: { spinning: boolean }) {
return (
工具调用
);
}
function hasToolActivity(message: Message) {
return message.content.some((c) => c.type === 'toolRequest' || c.type === 'toolResponse');
}
function isToolOnlyAssistantMessage(message: Message) {
return (
message.role === 'assistant' &&
hasToolActivity(message) &&
!getDisplayText(message).trim() &&
!getThinking(message) &&
getImageUrls(message).length === 0
);
}
function TypingBubble() {
return (
);
}
function shouldShowEndTypingIndicator(messages: Message[], streaming: boolean) {
if (!streaming) return false;
const lastUserIndex = messages.findLastIndex((message) => message.role === 'user');
if (lastUserIndex < 0) return true;
const assistantReply = messages
.slice(lastUserIndex + 1)
.find((message) => message.role === 'assistant');
if (!assistantReply) return true;
if (getDisplayText(assistantReply).trim() || getThinking(assistantReply)) return false;
if (hasToolActivity(assistantReply)) return false;
return true;
}
function MemoryRecallBadgeItem({
recall,
}: {
recall: { count: number; previews: string[] };
}) {
const [open, setOpen] = useState(false);
const hasPreviews = recall.previews.length > 0;
return (
{open && hasPreviews && (
{recall.previews.map((preview, index) => (
- {preview}
))}
)}
);
}
function ToolBadge({ message, active }: { message: Message; active: boolean }) {
if (message.role !== 'assistant') return null;
const tools = message.content.filter((c) => c.type === 'toolRequest' || c.type === 'toolResponse');
if (tools.length === 0) return null;
return (
{tools.map((tool, i) => (
))}
);
}
function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
setCopied(true);
window.setTimeout(() => setCopied(false), 1500);
} catch {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
setCopied(true);
window.setTimeout(() => setCopied(false), 1500);
}
};
return (
);
}
function PublicLinkCopyButton({ url }: { url: string }) {
const [copied, setCopied] = useState(false);
const copyTimer = useRef | null>(null);
const copy = async () => {
try {
await navigator.clipboard.writeText(url);
setCopied(true);
if (copyTimer.current) window.clearTimeout(copyTimer.current);
copyTimer.current = window.setTimeout(() => setCopied(false), 1800);
} catch {
setCopied(false);
}
};
return (
);
}
function TimeDivider({ timestamp }: { timestamp: number }) {
return (
{formatChatTime(timestamp)}
);
}
function ChevronIcon({ open }: { open: boolean }) {
return (
);
}
function MessageRow({
message,
avatarUrl,
onAvatarClick,
onSaveAsPage,
onShareToPlaza,
onDownloadLongImage,
onDownloadDocx,
saveDisabled,
publishUserId,
publishUsername,
sessionId,
compact = false,
activeToolMessage = false,
showInlineTyping = false,
memoryRecallByMessageId = {},
messageIndex = 0,
}: {
message: Message;
avatarUrl: string | null;
onAvatarClick?: () => void;
onSaveAsPage?: (message: Message) => void;
onShareToPlaza?: (message: Message) => void;
onDownloadLongImage?: (message: Message, publicUrl: string) => void;
onDownloadDocx?: (message: Message) => void;
saveDisabled?: boolean;
publishUserId?: string;
publishUsername?: string;
sessionId?: string;
compact?: boolean;
activeToolMessage?: boolean;
showInlineTyping?: boolean;
memoryRecallByMessageId?: Record;
messageIndex?: number;
}) {
const [actionsOpen, setActionsOpen] = useState(false);
const rawText = getDisplayText(message);
const text = filterText(rawText);
const memoryRecall = memoryRecallByMessageId[resolveMessageRecallKey(message, messageIndex)];
const saveActions = getMessageSaveActions(text, { userId: publishUserId, username: publishUsername });
const thinking = getThinking(message);
const isUser = message.role === 'user';
const imageUrls = getImageUrls(message);
const fileAttachments = getFileAttachments(message);
const renderableImageUrls = getRenderableImageUrls(message);
const copyText = [thinking ? `【思考】\n${thinking}` : '', text].filter(Boolean).join('\n\n');
if (!text && !thinking && message.role === 'assistant') {
return (
{showInlineTyping ? (
) : (
<>
{memoryRecall && }
>
)}
);
}
const hasAssistantActions =
!isUser && Boolean(message.id && onSaveAsPage && copyText);
const hasPageDownloadActions = hasAssistantActions && saveActions.kind === 'page' && Boolean(saveActions.previewUrl);
const hasPlazaAction = hasPageDownloadActions && Boolean(onShareToPlaza);
const showActionsToggle = compact && Boolean(copyText);
const renderPageActions = () => (
{saveActions.previewUrl &&
}
{hasPageDownloadActions && (
<>
{hasPlazaAction && (
)}
>
)}
);
return (
{!isUser &&
}
{thinking && (
思考中…
{thinking}
)}
{imageUrls.length > 0 && (
{renderableImageUrls.map((image, index) => (
))}
)}
{fileAttachments.length > 0 && (
)}
{text && (
)}
{memoryRecall &&
}
{showActionsToggle && (
)}
{copyText && !compact && (
{hasAssistantActions && renderPageActions()}
)}
{copyText && compact && actionsOpen && (
{hasAssistantActions && renderPageActions()}
)}
{isUser && (
)}
);
}
export function MessageList({
messages,
streaming,
onAvatarClick,
onSaveAsPage,
onShareToPlaza,
onDownloadLongImage,
onDownloadDocx,
publishUserId,
publishUsername,
compact = false,
memoryRecallByMessageId = {},
}: {
messages: Message[];
streaming: boolean;
onAvatarClick?: () => void;
onSaveAsPage?: (message: Message) => void;
onShareToPlaza?: (message: Message) => void;
onDownloadLongImage?: (message: Message, publicUrl: string) => void;
onDownloadDocx?: (message: Message) => void;
publishUserId?: string;
publishUsername?: string;
compact?: boolean;
memoryRecallByMessageId?: Record;
}) {
const { avatarUrl } = useUserAvatar();
const renderableMessages = messages.filter(shouldShowChatMessage);
const lastRenderableIndex = renderableMessages.length - 1;
const activeToolMessage =
streaming &&
lastRenderableIndex >= 0 &&
hasToolActivity(renderableMessages[lastRenderableIndex])
? renderableMessages[lastRenderableIndex]
: null;
const visibleMessages = renderableMessages.filter(
(message) => !isToolOnlyAssistantMessage(message) || message === activeToolMessage,
);
const showEndTyping = shouldShowEndTypingIndicator(renderableMessages, streaming);
const lastUserIndex = renderableMessages.findLastIndex((message) => message.role === 'user');
const inlineTypingMessage =
lastUserIndex >= 0
? renderableMessages
.slice(lastUserIndex + 1)
.find(
(message) =>
message.role === 'assistant' &&
!getDisplayText(message).trim() &&
!getThinking(message) &&
!hasToolActivity(message),
) ?? null
: null;
return (
{messages.length === 0 &&
}
{visibleMessages.map((message, index) => {
const prev = index > 0 ? visibleMessages[index - 1] : undefined;
const showTime = shouldShowTimestamp(message.created, prev?.created);
const anchorId = message.id ?? `msg-${index}-${message.role}-${message.created}`;
return (
{showTime && }
);
})}
{showEndTyping && !inlineTypingMessage && (
)}
);
}