feat(chat): optimize ui for execution graph card (#885)
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
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 { useTranslation } from 'react-i18next';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { TaskStep } from './task-visualization';
|
import type { TaskStep } from './task-visualization';
|
||||||
@@ -47,18 +47,17 @@ function StepDetailCard({ step }: { step: TaskStep }) {
|
|||||||
const isNarration = step.kind === 'message';
|
const isNarration = step.kind === 'message';
|
||||||
const isTool = step.kind === 'tool';
|
const isTool = step.kind === 'tool';
|
||||||
const isThinking = step.kind === 'thinking';
|
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 hideStatusText = isTool && step.status === 'completed';
|
||||||
const detailPreview = step.detail?.replace(/\s+/g, ' ').trim();
|
const detailPreview = step.detail?.replace(/\s+/g, ' ').trim();
|
||||||
const canExpand = hasDetail;
|
const canExpand = hasDetail;
|
||||||
const usePlainExpandedDetail = isTool || isThinking;
|
const displayLabel = isThinking ? t('executionGraph.thinkingLabel') : step.label;
|
||||||
const displayLabel = isThinking ? t('executionGraph.thinkingLabel') : step.label;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'min-w-0 flex-1 text-muted-foreground',
|
'min-w-0 flex-1 text-muted-foreground',
|
||||||
isTool || isNarration
|
isTool || isNarration || isThinking
|
||||||
? 'px-0 py-0'
|
? '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]',
|
: '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">
|
<div className="min-w-0 flex-1">
|
||||||
{!isNarration && (
|
{(!isNarration && !isThinking || expanded) && (
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
<p className="shrink-0 text-sm font-medium text-muted-foreground">{displayLabel}</p>
|
<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 && (
|
{isTool && detailPreview && !expanded && (
|
||||||
<p className="min-w-0 truncate text-[12px] leading-4 text-muted-foreground/80">
|
<p className="min-w-0 truncate text-[12px] leading-4 text-muted-foreground/80">
|
||||||
{detailPreview}
|
{detailPreview}
|
||||||
@@ -104,10 +115,8 @@ function StepDetailCard({ step }: { step: TaskStep }) {
|
|||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground',
|
'text-muted-foreground',
|
||||||
isThinking
|
isThinking
|
||||||
? 'mt-0.5 text-[12px] leading-5 line-clamp-1'
|
? 'mt-0.5 text-[13px] leading-5 line-clamp-2'
|
||||||
: isNarration
|
: 'text-[13px] leading-6 text-muted-foreground line-clamp-2',
|
||||||
? 'text-[13px] leading-6 text-muted-foreground line-clamp-2'
|
|
||||||
: 'mt-0.5 text-[12px] leading-5 line-clamp-2',
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{step.detail}
|
{step.detail}
|
||||||
@@ -120,29 +129,30 @@ function StepDetailCard({ step }: { step: TaskStep }) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
{step.detail && expanded && canExpand && (
|
{step.detail && expanded && canExpand && isTool && (() => {
|
||||||
usePlainExpandedDetail ? (
|
let formatted = step.detail;
|
||||||
<pre
|
try {
|
||||||
className={cn(
|
formatted = JSON.stringify(JSON.parse(step.detail), null, 2);
|
||||||
'mt-0.5 whitespace-pre-wrap text-[12px] leading-5 text-muted-foreground',
|
} catch { /* not valid JSON */ }
|
||||||
isTool ? 'break-all' : 'break-words',
|
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
|
||||||
{step.detail}
|
className="whitespace-pre-wrap text-[12px] leading-5 text-muted-foreground"
|
||||||
</pre>
|
>
|
||||||
) : (
|
{formatted}
|
||||||
<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>
|
||||||
<pre
|
</div>
|
||||||
className={cn(
|
);
|
||||||
'whitespace-pre-wrap text-[12px] leading-5',
|
})()}
|
||||||
isNarration ? 'text-muted-foreground' : 'break-all text-muted-foreground',
|
{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
|
||||||
{step.detail}
|
className="whitespace-pre-wrap break-words text-[12px] leading-5 text-muted-foreground"
|
||||||
</pre>
|
>
|
||||||
</div>
|
{step.detail}
|
||||||
)
|
</pre>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -260,7 +270,7 @@ export function ExecutionGraphCard({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{step.kind === 'thinking'
|
{step.kind === 'thinking'
|
||||||
? <AnimatedDots className="text-[14px]" />
|
? <MessageSquare className="h-3.5 w-3.5" />
|
||||||
: step.kind === 'tool'
|
: step.kind === 'tool'
|
||||||
? <Wrench className="h-3.5 w-3.5" />
|
? <Wrench className="h-3.5 w-3.5" />
|
||||||
: step.kind === 'message'
|
: step.kind === 'message'
|
||||||
|
|||||||
@@ -443,7 +443,7 @@ export function Chat() {
|
|||||||
keys.add(runKey);
|
keys.add(runKey);
|
||||||
}
|
}
|
||||||
return keys;
|
return keys;
|
||||||
}, [currentSessionKey, messages, replyTextOverrides, userRunCards]);
|
}, [currentSessionKey, messages, userRunCards]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userRunCards.length === 0) return;
|
if (userRunCards.length === 0) return;
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export interface TaskStep {
|
|||||||
detail?: string;
|
detail?: string;
|
||||||
depth: number;
|
depth: number;
|
||||||
parentId?: string;
|
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 {
|
function normalizeText(text: string | null | undefined): string | undefined {
|
||||||
if (!text) return undefined;
|
if (!text) return undefined;
|
||||||
const normalized = text.replace(/\s+/g, ' ').trim();
|
const normalized = text.replace(/[ \t]+/g, ' ').trim();
|
||||||
if (!normalized) return undefined;
|
if (!normalized) return undefined;
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
@@ -271,6 +273,8 @@ export function deriveTaskSteps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
toolUses.forEach((tool, index) => {
|
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({
|
upsertStep({
|
||||||
id: tool.id || makeToolId(`history-tool-${message.id || messageIndex}`, tool.name, index),
|
id: tool.id || makeToolId(`history-tool-${message.id || messageIndex}`, tool.name, index),
|
||||||
label: tool.name,
|
label: tool.name,
|
||||||
@@ -278,6 +282,7 @@ export function deriveTaskSteps({
|
|||||||
kind: 'tool',
|
kind: 'tool',
|
||||||
detail: normalizeText(JSON.stringify(tool.input, null, 2)),
|
detail: normalizeText(JSON.stringify(tool.input, null, 2)),
|
||||||
depth: 1,
|
depth: 1,
|
||||||
|
url,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -334,6 +339,8 @@ export function deriveTaskSteps({
|
|||||||
extractToolUse(streamMessage).forEach((tool, index) => {
|
extractToolUse(streamMessage).forEach((tool, index) => {
|
||||||
const id = tool.id || makeToolId('stream-tool', tool.name, index);
|
const id = tool.id || makeToolId('stream-tool', tool.name, index);
|
||||||
if (activeToolIds.has(id) || activeToolNamesWithoutIds.has(tool.name)) return;
|
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({
|
upsertStep({
|
||||||
id,
|
id,
|
||||||
label: tool.name,
|
label: tool.name,
|
||||||
@@ -341,6 +348,7 @@ export function deriveTaskSteps({
|
|||||||
kind: 'tool',
|
kind: 'tool',
|
||||||
detail: normalizeText(JSON.stringify(tool.input, null, 2)),
|
detail: normalizeText(JSON.stringify(tool.input, null, 2)),
|
||||||
depth: 1,
|
depth: 1,
|
||||||
|
url,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user