feat: ToolCallStatus partial_success/fallback_success visual states
- Add CornerDownRight, AlertCircle icons for fallback/partial states - New isPartial/isFallback props on ToolCallRow with hint banners - Read execution_log from thread.values in main.tsx, pass to ToolCallStatus - Match by tc.name === entry.tool to determine partial/fallback per row Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
2f201bf1f0
commit
fc56503e22
@@ -1,4 +1,4 @@
|
||||
import { Loader2, CheckCircle2, ChevronRight, ChevronDown, XCircle, ChevronsUpDown } from "lucide-react";
|
||||
import { Loader2, CheckCircle2, ChevronRight, ChevronDown, XCircle, ChevronsUpDown, AlertCircle, CornerDownRight } from "lucide-react";
|
||||
import { useState, useCallback } from "react";
|
||||
import { LoadExternalComponent } from "@langchain/langgraph-sdk/react-ui";
|
||||
|
||||
@@ -57,12 +57,15 @@ interface ToolCallStatusProps {
|
||||
uiItems?: UIMsgLocal[];
|
||||
stream?: Parameters<typeof LoadExternalComponent>[0]["stream"];
|
||||
components?: Parameters<typeof LoadExternalComponent>[0]["components"];
|
||||
executionLog?: Array<{ tool: string; status: string; inputSummary?: string }>;
|
||||
}
|
||||
|
||||
interface ToolCallRowProps {
|
||||
tc: ToolCall;
|
||||
isDone: boolean;
|
||||
isFailed: boolean;
|
||||
isPartial?: boolean;
|
||||
isFallback?: boolean;
|
||||
uiItem?: UIMsgLocal;
|
||||
stream?: ToolCallStatusProps["stream"];
|
||||
components?: ToolCallStatusProps["components"];
|
||||
@@ -74,6 +77,8 @@ function ToolCallRow({
|
||||
tc,
|
||||
isDone,
|
||||
isFailed,
|
||||
isPartial,
|
||||
isFallback,
|
||||
uiItem,
|
||||
stream,
|
||||
components,
|
||||
@@ -88,7 +93,33 @@ function ToolCallRow({
|
||||
}
|
||||
}, [globalExpanded]);
|
||||
const label = tc.name ? (TOOL_NAME_MAP[tc.name] ?? tc.name) : "工具调用";
|
||||
const canExpand = isDone && (!!uiItem || isFailed);
|
||||
const canExpand = isDone && (!!uiItem || isFailed || isPartial || isFallback);
|
||||
|
||||
// Determine icon and color by priority: isFailed > isFallback > isPartial > isDone > loading
|
||||
function renderIcon() {
|
||||
if (isFailed) return <XCircle className="size-3.5 text-red-500 shrink-0" />;
|
||||
if (isFallback) return <CornerDownRight className="size-3.5 text-orange-500 shrink-0" />;
|
||||
if (isPartial) return <AlertCircle className="size-3.5 text-yellow-500 shrink-0" />;
|
||||
if (isDone) return <CheckCircle2 className="size-3.5 text-green-500 shrink-0" />;
|
||||
return <Loader2 className="size-3.5 text-muted-foreground animate-spin shrink-0" />;
|
||||
}
|
||||
|
||||
function renderLabel() {
|
||||
if (isFailed) return `${label} · 失败`;
|
||||
if (isFallback) {
|
||||
if (canExpand) return `${label} · 已切换到备用 · ${expanded ? "收起" : "查看结果"}`;
|
||||
return `${label} · 已切换到备用`;
|
||||
}
|
||||
if (isPartial) {
|
||||
if (canExpand) return `${label} · 结果不完整 · ${expanded ? "收起" : "查看结果"}`;
|
||||
return `${label} · 结果不完整`;
|
||||
}
|
||||
if (isDone) {
|
||||
if (canExpand) return `${label} · ${expanded ? "收起" : "查看结果"}`;
|
||||
return `${label} · 已完成`;
|
||||
}
|
||||
return tc.name ? (TOOL_LOADING_MAP[tc.name] ?? `正在${label}...`) : "思考中...";
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -107,14 +138,8 @@ function ToolCallRow({
|
||||
) : (
|
||||
<span className="size-3.5 shrink-0" />
|
||||
)}
|
||||
{isFailed ? (
|
||||
<XCircle className="size-3.5 text-red-500 shrink-0" />
|
||||
) : isDone ? (
|
||||
<CheckCircle2 className="size-3.5 text-green-500 shrink-0" />
|
||||
) : (
|
||||
<Loader2 className="size-3.5 animate-spin shrink-0" />
|
||||
)}
|
||||
<span>{isFailed ? `${label} · 失败` : isDone ? (canExpand ? `${label} · ${expanded ? "收起" : "查看结果"}` : `${label} · 已完成`) : (tc.name ? (TOOL_LOADING_MAP[tc.name] ?? `正在${label}...`) : "思考中...")}</span>
|
||||
{renderIcon()}
|
||||
<span>{renderLabel()}</span>
|
||||
</button>
|
||||
{expanded && isFailed && (
|
||||
<div className="mt-2 ml-7 rounded-md border border-red-200 bg-red-50 dark:bg-red-950/20 dark:border-red-900 px-3 py-2 animate-in fade-in duration-300">
|
||||
@@ -123,6 +148,16 @@ function ToolCallRow({
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{expanded && isPartial && !isFailed && (
|
||||
<div className="mt-2 ml-7 bg-yellow-50 border border-yellow-200 rounded p-2 text-xs text-yellow-700 animate-in fade-in duration-300">
|
||||
查询完成,但未找到完整匹配结果
|
||||
</div>
|
||||
)}
|
||||
{expanded && isFallback && !isFailed && (
|
||||
<div className="mt-2 ml-7 bg-orange-50 border border-orange-200 rounded p-2 text-xs text-orange-700 animate-in fade-in duration-300">
|
||||
主工具执行失败,已自动切换至备选方案
|
||||
</div>
|
||||
)}
|
||||
{expanded && !isFailed && uiItem && stream && components && (
|
||||
<div className="mt-2 ml-7">
|
||||
<div className="animate-in fade-in duration-300">
|
||||
@@ -134,7 +169,13 @@ function ToolCallRow({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{expanded && !isFailed && !uiItem && (
|
||||
{expanded && !isFailed && !uiItem && (isPartial || isFallback) && (
|
||||
<div className="mt-2 ml-7 space-y-2 animate-pulse">
|
||||
<div className="h-3 bg-muted rounded w-3/4" />
|
||||
<div className="h-3 bg-muted rounded w-1/2" />
|
||||
</div>
|
||||
)}
|
||||
{expanded && !isFailed && !uiItem && !isPartial && !isFallback && (
|
||||
<div className="mt-2 ml-7 space-y-2 animate-pulse">
|
||||
<div className="h-3 bg-muted rounded w-3/4" />
|
||||
<div className="h-3 bg-muted rounded w-1/2" />
|
||||
@@ -154,6 +195,7 @@ export default function ToolCallStatus({
|
||||
uiItems = [],
|
||||
stream,
|
||||
components,
|
||||
executionLog = [],
|
||||
}: ToolCallStatusProps) {
|
||||
if (!toolCalls.length) return null;
|
||||
|
||||
@@ -210,12 +252,19 @@ export default function ToolCallStatus({
|
||||
uiItem = candidates[matchIdx];
|
||||
}
|
||||
|
||||
// Look up execution_log status by tool name
|
||||
const logEntry = tc.name ? executionLog.find((e) => e.tool === tc.name) : undefined;
|
||||
const isPartial = logEntry?.status === "partial_success";
|
||||
const isFallbackStatus = logEntry?.status === "fallback_success";
|
||||
|
||||
return (
|
||||
<ToolCallRow
|
||||
key={tc.id ?? i}
|
||||
tc={tc}
|
||||
isDone={!!isDone}
|
||||
isFailed={isFailed}
|
||||
isPartial={isPartial}
|
||||
isFallback={isFallbackStatus}
|
||||
uiItem={uiItem}
|
||||
stream={stream}
|
||||
components={components}
|
||||
|
||||
@@ -426,6 +426,9 @@ function App() {
|
||||
.filter(Boolean),
|
||||
);
|
||||
|
||||
// ── Read execution_log for partial/fallback status ────────────────────────
|
||||
const executionLog = ((thread.values as any)?.execution_log ?? []) as Array<{ tool: string; status: string; inputSummary?: string }>;
|
||||
|
||||
// ── Find last AI message index ──────────────────────────────────────────
|
||||
const lastAiIdx = thread.messages.reduce((last, m, i) => (m.type === "ai" ? i : last), -1);
|
||||
|
||||
@@ -624,6 +627,7 @@ function App() {
|
||||
uiItems={uiItems}
|
||||
stream={thread}
|
||||
components={ComponentMap as any}
|
||||
executionLog={executionLog}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user