fix: simplify tool call badges

This commit is contained in:
john
2026-07-02 18:48:16 +08:00
parent 90c776065c
commit b5781680a8
5 changed files with 56 additions and 57 deletions
+47 -46
View File
@@ -38,60 +38,47 @@ function CheckIcon() {
); );
} }
function ToolDoneIcon() { function ToolActivityIcon({ spinning }: { spinning: boolean }) {
return ( return (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"> <span className={`tool-badge-spinner${spinning ? ' is-spinning' : ''}`} aria-hidden="true">
<path <span />
d="M5 12.5l4.5 4.5L19 7.5" </span>
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
); );
} }
function resolveToolCallName( function ToolBadgeItem({ spinning }: { spinning: boolean }) {
tool: Extract<Message['content'][number], { type: 'toolRequest' }>, return (
): string { <span className="tool-badge">
const toolCall = tool.toolCall as { <ToolActivityIcon spinning={spinning} />
functionName?: string; <span></span>
value?: { name?: string }; </span>
}; );
return String(toolCall.functionName ?? toolCall.value?.name ?? '').trim();
} }
function ToolBadge({ message }: { message: Message }) { 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 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'); const tools = message.content.filter((c) => c.type === 'toolRequest' || c.type === 'toolResponse');
if (tools.length === 0) return null; if (tools.length === 0) return null;
const completed = tools.filter((t) => t.type === 'toolResponse').length;
const total = tools.length;
return ( return (
<div className="tool-badges"> <div className="tool-badges">
{tools.map((tool, i) => { {tools.map((tool, i) => (
const isRequest = tool.type === 'toolRequest'; <ToolBadgeItem key={`${message.id}-${tool.id}-${i}`} spinning={active} />
const toolName = isRequest ? resolveToolCallName(tool) : ''; ))}
let label: string;
if (isRequest && !toolName) {
label = `正在加载 tools (${completed}/${total})`;
} else {
label = isRequest ? toolName : '工具完成';
}
return (
<span
key={`${message.id}-${i}`}
className={`tool-badge${isRequest ? ' is-pending' : ' is-complete'}`}
>
{!isRequest ? <ToolDoneIcon /> : null}
<span>{label}</span>
</span>
);
})}
</div> </div>
); );
} }
@@ -198,6 +185,7 @@ function MessageRow({
publishUsername, publishUsername,
sessionId, sessionId,
compact = false, compact = false,
activeToolMessage = false,
}: { }: {
message: Message; message: Message;
avatarUrl: string | null; avatarUrl: string | null;
@@ -208,6 +196,7 @@ function MessageRow({
publishUsername?: string; publishUsername?: string;
sessionId?: string; sessionId?: string;
compact?: boolean; compact?: boolean;
activeToolMessage?: boolean;
}) { }) {
const [actionsOpen, setActionsOpen] = useState(false); const [actionsOpen, setActionsOpen] = useState(false);
const rawText = getDisplayText(message); const rawText = getDisplayText(message);
@@ -224,7 +213,7 @@ function MessageRow({
<div className={`msg-row msg-row-assistant`}> <div className={`msg-row msg-row-assistant`}>
<TKMindAvatar /> <TKMindAvatar />
<div className="msg-content"> <div className="msg-content">
<ToolBadge message={message} /> <ToolBadge message={message} active={activeToolMessage} />
</div> </div>
</div> </div>
); );
@@ -273,7 +262,7 @@ function MessageRow({
dangerouslySetInnerHTML={{ __html: renderMarkdown(text) }} dangerouslySetInnerHTML={{ __html: renderMarkdown(text) }}
/> />
)} )}
<ToolBadge message={message} /> <ToolBadge message={message} active={activeToolMessage} />
</div> </div>
{showActionsToggle && ( {showActionsToggle && (
<button <button
@@ -361,11 +350,22 @@ export function MessageList({
compact?: boolean; compact?: boolean;
}) { }) {
const { avatarUrl } = useUserAvatar(); 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,
);
return ( return (
<div className="message-list"> <div className="message-list">
{messages.length === 0 && <ChatWelcomePanel compact={compact} />} {messages.length === 0 && <ChatWelcomePanel compact={compact} />}
{messages.filter(shouldShowChatMessage).map((message, index, visibleMessages) => { {visibleMessages.map((message, index) => {
const prev = index > 0 ? visibleMessages[index - 1] : undefined; const prev = index > 0 ? visibleMessages[index - 1] : undefined;
const showTime = shouldShowTimestamp(message.created, prev?.created); const showTime = shouldShowTimestamp(message.created, prev?.created);
const anchorId = message.id ?? `msg-${index}-${message.role}-${message.created}`; const anchorId = message.id ?? `msg-${index}-${message.role}-${message.created}`;
@@ -386,6 +386,7 @@ export function MessageList({
publishUsername={publishUsername} publishUsername={publishUsername}
sessionId={sessionId} sessionId={sessionId}
compact={compact} compact={compact}
activeToolMessage={message === activeToolMessage}
/> />
</div> </div>
); );
+2 -1
View File
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { useChat } from '../context/ChatProvider'; import { useChat } from '../context/ChatProvider';
import { buildSelectedAssetGreeting } from '../utils/mindspaceChatContext'; import { buildSelectedAssetGreeting } from '../utils/mindspaceChatContext';
import { shouldShowChatMessage } from '../utils/message';
import type { Message, MindSpaceChatContext, MindSpaceSaveCategory, PortalUser } from '../types'; import type { Message, MindSpaceChatContext, MindSpaceSaveCategory, PortalUser } from '../types';
import { SpaceChatFab } from './SpaceChatFab'; import { SpaceChatFab } from './SpaceChatFab';
import { SpaceChatPanel } from './SpaceChatPanel'; import { SpaceChatPanel } from './SpaceChatPanel';
@@ -90,7 +91,7 @@ export function MindSpaceSpaceChat({
const prefillMessages = useMemo(() => { const prefillMessages = useMemo(() => {
if (chatBridge || !open) return []; if (chatBridge || !open) return [];
if (messages.some((message) => message.role === 'user')) return []; if (messages.some((message) => message.role === 'user' && shouldShowChatMessage(message))) return [];
const selected = context.selectedAssets ?? []; const selected = context.selectedAssets ?? [];
if (selected.length !== 1) return []; if (selected.length !== 1) return [];
return [buildSelectedAssetGreetingMessage(selected[0])]; return [buildSelectedAssetGreetingMessage(selected[0])];
+2 -1
View File
@@ -4,6 +4,7 @@ import { INSUFFICIENT_BALANCE_NOTICE } from '../hooks/useTKMindChat';
import type { PageEditSubChatBridge } from '../hooks/usePageEditSubChat'; import type { PageEditSubChatBridge } from '../hooks/usePageEditSubChat';
import type { MindSpaceChatContext, MindSpaceSaveCategory, PortalUser, Message } from '../types'; import type { MindSpaceChatContext, MindSpaceSaveCategory, PortalUser, Message } from '../types';
import { formatContextChip } from '../utils/mindspaceChatContext'; import { formatContextChip } from '../utils/mindspaceChatContext';
import { shouldShowChatMessage } from '../utils/message';
import { ChatPanel } from './ChatPanel'; import { ChatPanel } from './ChatPanel';
import { TKMindAvatar } from './TKMindAvatar'; import { TKMindAvatar } from './TKMindAvatar';
@@ -64,7 +65,7 @@ export function SpaceChatPanel({
const displayMessages = useMemo(() => { const displayMessages = useMemo(() => {
if (!prefillMessages.length) return messages; if (!prefillMessages.length) return messages;
if (messages.some((message) => message.role === 'user')) return messages; if (messages.some((message) => message.role === 'user' && shouldShowChatMessage(message))) return messages;
const extras = prefillMessages.filter( const extras = prefillMessages.filter(
(item) => !messages.some((message) => message.id && message.id === item.id), (item) => !messages.some((message) => message.id && message.id === item.id),
); );
+3 -8
View File
@@ -1948,14 +1948,6 @@ body,
color: var(--color-text-muted); color: var(--color-text-muted);
} }
.tool-badge.is-pending {
color: var(--color-text-secondary);
}
.tool-badge.is-complete {
color: var(--color-success, #7dd3a7);
}
.tool-badge-spinner { .tool-badge-spinner {
width: 14px; width: 14px;
height: 14px; height: 14px;
@@ -1971,6 +1963,9 @@ body,
border-radius: 999px; border-radius: 999px;
border: 1.5px solid currentColor; border: 1.5px solid currentColor;
border-right-color: transparent; border-right-color: transparent;
}
.tool-badge-spinner.is-spinning span {
animation: tool-badge-spin 0.75s linear infinite; animation: tool-badge-spin 0.75s linear infinite;
} }
+2 -1
View File
@@ -191,10 +191,11 @@ export function getDisplayText(message: Message): string {
} }
export function shouldShowChatMessage(message: Message): boolean { export function shouldShowChatMessage(message: Message): boolean {
if (message.role === 'user') return true;
if (getDisplayText(message).trim()) return true; if (getDisplayText(message).trim()) return true;
if (message.role === 'user' && getImageUrls(message).length > 0) return true;
if (getThinking(message)) return true; if (getThinking(message)) return true;
if (getSystemNotificationText(message)) return true; if (getSystemNotificationText(message)) return true;
if (message.role !== 'assistant') return false;
return message.content.some( return message.content.some(
(item) => item.type === 'toolRequest' || item.type === 'toolResponse' || item.type === 'actionRequired', (item) => item.type === 'toolRequest' || item.type === 'toolResponse' || item.type === 'actionRequired',
); );