feat(chat): optimize ui for execution graph card (#885)

This commit is contained in:
Tao Yiping
2026-04-23 11:49:54 +08:00
committed by GitHub
parent eda34ad9ce
commit d0c79f6a0a
3 changed files with 54 additions and 36 deletions

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { CheckCircle2, ChevronDown, ChevronRight, CircleDashed, GitBranch, MessageSquare, Wrench, XCircle } from 'lucide-react';
import { CheckCircle2, ChevronDown, ChevronRight, CircleDashed, GitBranch, Link, MessageSquare, Wrench, XCircle } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
import type { TaskStep } from './task-visualization';
@@ -47,18 +47,17 @@ function StepDetailCard({ step }: { step: TaskStep }) {
const isNarration = step.kind === 'message';
const isTool = step.kind === 'tool';
const isThinking = step.kind === 'thinking';
const showRunningDots = isTool && step.status === 'running';
const showRunningDots = (isTool || isThinking) && step.status === 'running';
const hideStatusText = isTool && step.status === 'completed';
const detailPreview = step.detail?.replace(/\s+/g, ' ').trim();
const canExpand = hasDetail;
const usePlainExpandedDetail = isTool || isThinking;
const displayLabel = isThinking ? t('executionGraph.thinkingLabel') : step.label;
const displayLabel = isThinking ? t('executionGraph.thinkingLabel') : step.label;
return (
<div
className={cn(
'min-w-0 flex-1 text-muted-foreground',
isTool || isNarration
isTool || isNarration || isThinking
? 'px-0 py-0'
: 'rounded-xl border border-black/10 bg-white/40 px-3 py-2 dark:border-white/10 dark:bg-white/[0.03]',
)}
@@ -76,9 +75,21 @@ function StepDetailCard({ step }: { step: TaskStep }) {
}}
>
<div className="min-w-0 flex-1">
{!isNarration && (
{(!isNarration && !isThinking || expanded) && (
<div className="flex min-w-0 items-center gap-2">
<p className="shrink-0 text-sm font-medium text-muted-foreground">{displayLabel}</p>
{isTool && step.label === 'web_fetch' && step.url && (
<a
href={step.url}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="shrink-0 text-muted-foreground hover:text-foreground"
title={step.url}
>
<Link className="h-3.5 w-3.5" />
</a>
)}
{isTool && detailPreview && !expanded && (
<p className="min-w-0 truncate text-[12px] leading-4 text-muted-foreground/80">
{detailPreview}
@@ -104,10 +115,8 @@ function StepDetailCard({ step }: { step: TaskStep }) {
className={cn(
'text-muted-foreground',
isThinking
? 'mt-0.5 text-[12px] leading-5 line-clamp-1'
: isNarration
? 'text-[13px] leading-6 text-muted-foreground line-clamp-2'
: 'mt-0.5 text-[12px] leading-5 line-clamp-2',
? 'mt-0.5 text-[13px] leading-5 line-clamp-2'
: 'text-[13px] leading-6 text-muted-foreground line-clamp-2',
)}
>
{step.detail}
@@ -120,29 +129,30 @@ function StepDetailCard({ step }: { step: TaskStep }) {
</span>
)}
</button>
{step.detail && expanded && canExpand && (
usePlainExpandedDetail ? (
<pre
className={cn(
'mt-0.5 whitespace-pre-wrap text-[12px] leading-5 text-muted-foreground',
isTool ? 'break-all' : 'break-words',
)}
>
{step.detail}
</pre>
) : (
<div className="mt-3 rounded-lg border border-black/10 bg-black/[0.03] px-3 py-2 dark:border-white/10 dark:bg-white/[0.03]">
<pre
className={cn(
'whitespace-pre-wrap text-[12px] leading-5',
isNarration ? 'text-muted-foreground' : 'break-all text-muted-foreground',
)}
>
{step.detail}
</pre>
</div>
)
)}
{step.detail && expanded && canExpand && isTool && (() => {
let formatted = step.detail;
try {
formatted = JSON.stringify(JSON.parse(step.detail), null, 2);
} catch { /* not valid JSON */ }
return (
<div className="mt-3 rounded-lg border border-black/10 bg-black/[0.03] px-3 py-2 dark:border-white/10 dark:bg-white/[0.03]">
<pre
className="whitespace-pre-wrap text-[12px] leading-5 text-muted-foreground"
>
{formatted}
</pre>
</div>
);
})()}
{step.detail && expanded && canExpand && (isNarration || isThinking) && (
<div className="mt-3 rounded-lg border border-black/10 bg-black/[0.03] px-3 py-2 dark:border-white/10 dark:bg-white/[0.03]">
<pre
className="whitespace-pre-wrap break-words text-[12px] leading-5 text-muted-foreground"
>
{step.detail}
</pre>
</div>
)}
</div>
);
}
@@ -260,7 +270,7 @@ export function ExecutionGraphCard({
)}
>
{step.kind === 'thinking'
? <AnimatedDots className="text-[14px]" />
? <MessageSquare className="h-3.5 w-3.5" />
: step.kind === 'tool'
? <Wrench className="h-3.5 w-3.5" />
: step.kind === 'message'

View File

@@ -443,7 +443,7 @@ export function Chat() {
keys.add(runKey);
}
return keys;
}, [currentSessionKey, messages, replyTextOverrides, userRunCards]);
}, [currentSessionKey, messages, userRunCards]);
useEffect(() => {
if (userRunCards.length === 0) return;

View File

@@ -11,6 +11,8 @@ export interface TaskStep {
detail?: string;
depth: number;
parentId?: string;
/** Extracted URL for web_fetch tool, used to render a clickable link icon. */
url?: string;
}
/**
@@ -56,7 +58,7 @@ export interface SubagentCompletionInfo {
function normalizeText(text: string | null | undefined): string | undefined {
if (!text) return undefined;
const normalized = text.replace(/\s+/g, ' ').trim();
const normalized = text.replace(/[ \t]+/g, ' ').trim();
if (!normalized) return undefined;
return normalized;
}
@@ -271,6 +273,8 @@ export function deriveTaskSteps({
});
toolUses.forEach((tool, index) => {
const input = tool.input as Record<string, unknown>;
const url = tool.name === 'web_fetch' && typeof input?.url === 'string' ? input.url : undefined;
upsertStep({
id: tool.id || makeToolId(`history-tool-${message.id || messageIndex}`, tool.name, index),
label: tool.name,
@@ -278,6 +282,7 @@ export function deriveTaskSteps({
kind: 'tool',
detail: normalizeText(JSON.stringify(tool.input, null, 2)),
depth: 1,
url,
});
});
}
@@ -334,6 +339,8 @@ export function deriveTaskSteps({
extractToolUse(streamMessage).forEach((tool, index) => {
const id = tool.id || makeToolId('stream-tool', tool.name, index);
if (activeToolIds.has(id) || activeToolNamesWithoutIds.has(tool.name)) return;
const input = tool.input as Record<string, unknown>;
const url = tool.name === 'web_fetch' && typeof input?.url === 'string' ? input.url : undefined;
upsertStep({
id,
label: tool.name,
@@ -341,6 +348,7 @@ export function deriveTaskSteps({
kind: 'tool',
detail: normalizeText(JSON.stringify(tool.input, null, 2)),
depth: 1,
url,
});
});
}