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 (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M5 12.5l4.5 4.5L19 7.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<span className={`tool-badge-spinner${spinning ? ' is-spinning' : ''}`} aria-hidden="true">
<span />
</span>
);
}
function resolveToolCallName(
tool: Extract<Message['content'][number], { type: 'toolRequest' }>,
): string {
const toolCall = tool.toolCall as {
functionName?: string;
value?: { name?: string };
};
return String(toolCall.functionName ?? toolCall.value?.name ?? '').trim();
function ToolBadgeItem({ spinning }: { spinning: boolean }) {
return (
<span className="tool-badge">
<ToolActivityIcon spinning={spinning} />
<span></span>
</span>
);
}
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');
if (tools.length === 0) return null;
const completed = tools.filter((t) => t.type === 'toolResponse').length;
const total = tools.length;
return (
<div className="tool-badges">
{tools.map((tool, i) => {
const isRequest = tool.type === 'toolRequest';
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>
);
})}
{tools.map((tool, i) => (
<ToolBadgeItem key={`${message.id}-${tool.id}-${i}`} spinning={active} />
))}
</div>
);
}
@@ -198,6 +185,7 @@ function MessageRow({
publishUsername,
sessionId,
compact = false,
activeToolMessage = false,
}: {
message: Message;
avatarUrl: string | null;
@@ -208,6 +196,7 @@ function MessageRow({
publishUsername?: string;
sessionId?: string;
compact?: boolean;
activeToolMessage?: boolean;
}) {
const [actionsOpen, setActionsOpen] = useState(false);
const rawText = getDisplayText(message);
@@ -224,7 +213,7 @@ function MessageRow({
<div className={`msg-row msg-row-assistant`}>
<TKMindAvatar />
<div className="msg-content">
<ToolBadge message={message} />
<ToolBadge message={message} active={activeToolMessage} />
</div>
</div>
);
@@ -273,7 +262,7 @@ function MessageRow({
dangerouslySetInnerHTML={{ __html: renderMarkdown(text) }}
/>
)}
<ToolBadge message={message} />
<ToolBadge message={message} active={activeToolMessage} />
</div>
{showActionsToggle && (
<button
@@ -361,11 +350,22 @@ export function MessageList({
compact?: boolean;
}) {
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 (
<div className="message-list">
{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 showTime = shouldShowTimestamp(message.created, prev?.created);
const anchorId = message.id ?? `msg-${index}-${message.role}-${message.created}`;
@@ -386,6 +386,7 @@ export function MessageList({
publishUsername={publishUsername}
sessionId={sessionId}
compact={compact}
activeToolMessage={message === activeToolMessage}
/>
</div>
);
+2 -1
View File
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useChat } from '../context/ChatProvider';
import { buildSelectedAssetGreeting } from '../utils/mindspaceChatContext';
import { shouldShowChatMessage } from '../utils/message';
import type { Message, MindSpaceChatContext, MindSpaceSaveCategory, PortalUser } from '../types';
import { SpaceChatFab } from './SpaceChatFab';
import { SpaceChatPanel } from './SpaceChatPanel';
@@ -90,7 +91,7 @@ export function MindSpaceSpaceChat({
const prefillMessages = useMemo(() => {
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 ?? [];
if (selected.length !== 1) return [];
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 { MindSpaceChatContext, MindSpaceSaveCategory, PortalUser, Message } from '../types';
import { formatContextChip } from '../utils/mindspaceChatContext';
import { shouldShowChatMessage } from '../utils/message';
import { ChatPanel } from './ChatPanel';
import { TKMindAvatar } from './TKMindAvatar';
@@ -64,7 +65,7 @@ export function SpaceChatPanel({
const displayMessages = useMemo(() => {
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(
(item) => !messages.some((message) => message.id && message.id === item.id),
);
+3 -8
View File
@@ -1948,14 +1948,6 @@ body,
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 {
width: 14px;
height: 14px;
@@ -1971,6 +1963,9 @@ body,
border-radius: 999px;
border: 1.5px solid currentColor;
border-right-color: transparent;
}
.tool-badge-spinner.is-spinning span {
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 {
if (message.role === 'user') return true;
if (getDisplayText(message).trim()) return true;
if (message.role === 'user' && getImageUrls(message).length > 0) return true;
if (getThinking(message)) return true;
if (getSystemNotificationText(message)) return true;
if (message.role !== 'assistant') return false;
return message.content.some(
(item) => item.type === 'toolRequest' || item.type === 'toolResponse' || item.type === 'actionRequired',
);