feat: ToolCallStatus partial_success/fallback_success visual states
Deploy LangGraph Server to Azure Web App / build-and-deploy (push) Failing after 17s
Deploy LangGraph UI to Azure Static Web Apps / build-and-deploy (push) Failing after 41s

- 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:
gongzhiyong
2026-04-12 14:13:14 +08:00
co-authored by Claude Sonnet 4.6
parent 2f201bf1f0
commit fc56503e22
2 changed files with 64 additions and 11 deletions
+60 -11
View File
@@ -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}
+4
View File
@@ -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}
/>
)}