feat(chat): optimize ui for execution graph card (#885)
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user