fix: 状态指示器重叠与 Trace Viewer 状态丢失
1. StreamingIndicator: 最后一条是 assistant 消息时不显示'正在连接', 避免与 AssistantMessage 的'正在思考'重叠 2. useAgentStream: 跳过 INIT(iteration=0) 的 trace step, 引擎初始化无实际内容不应展示 3. useAgentStream: 状态转换追加到 states 数组而非覆盖 state 字段, 保留完整 THINKING->EXECUTING->OBSERVING 进度 4. TraceStep: header 显示状态进度链(如'思考 -> 执行 -> 观察')而非仅最终状态 5. agent-store: 旧数据兼容, 加载时为缺少 states 的 trace step 补全
This commit is contained in:
@@ -17,12 +17,9 @@ export function StreamingIndicator(): React.JSX.Element | null {
|
||||
if (!isStreaming) return null;
|
||||
|
||||
const lastMsg = messages[messages.length - 1];
|
||||
// 仅当最后一条是 assistant 消息且有内容时才隐藏
|
||||
// 如果最后一条是 user 消息(刚发送,等待 AI 响应),应显示加载指示器
|
||||
const isLastAssistant = lastMsg?.role === 'assistant';
|
||||
const hasVisibleContent = isLastAssistant &&
|
||||
(!!lastMsg?.content || !!lastMsg?.reasoningContent || !!lastMsg?.toolCalls?.length);
|
||||
if (hasVisibleContent) return null;
|
||||
// 最后一条是 assistant 消息时隐藏(即使内容为空,AssistantMessage 会显示"正在思考...")
|
||||
// 仅当最后一条是 user 消息(刚发送,等待 AI 响应)时显示加载指示器
|
||||
if (lastMsg?.role === 'assistant') return null;
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, py: 2, pl: 5, animation: 'fadeIn 200ms ease-out' }}>
|
||||
|
||||
@@ -64,13 +64,18 @@ export function TraceStep({ step, isCurrent }: TraceStepProps): React.JSX.Elemen
|
||||
const label = TRACE_STATE_LABELS[step.state] ?? step.state;
|
||||
const duration = step.completedAt ? step.completedAt - step.startedAt : null;
|
||||
|
||||
// 构建状态进度链(如 "思考 → 执行 → 观察")
|
||||
const stateChain = (step.states ?? [step.state])
|
||||
.map((s) => TRACE_STATE_LABELS[s] ?? s)
|
||||
.join(' → ');
|
||||
|
||||
return (
|
||||
<Box sx={{ borderRadius: 1, border: '1px solid', borderColor: 'divider', bgcolor: 'secondary.main', transition: 'all 150ms', ...(isCurrent ? { boxShadow: `0 0 0 1px ${color}` } : {}) }}>
|
||||
<IconButton size="small" onClick={handleToggle} sx={{ width: '100%', justifyContent: 'flex-start', gap: 1, px: 1.5, py: 1, borderRadius: '4px 4px 0 0', color: 'text.primary', fontSize: 12 }}>
|
||||
{expanded ? <ChevronDown size={10} /> : <ChevronRight size={10} />}
|
||||
{isCurrent ? <Loader2 size={12} style={{ color, animation: 'spin 1s linear infinite' }} /> : step.completedAt ? <CheckCircle size={12} style={{ color }} /> : <CircleDot size={12} style={{ color }} />}
|
||||
<Typography component="span" sx={{ fontWeight: 600, color, fontSize: 12 }}>#{step.iteration}</Typography>
|
||||
<Typography component="span" sx={{ textTransform: 'uppercase', color: 'text.secondary', fontSize: 12 }}>{label}</Typography>
|
||||
<Typography component="span" sx={{ color: 'text.secondary', fontSize: 12 }}>{stateChain}</Typography>
|
||||
{duration != null && <Typography variant="caption" sx={{ ml: 'auto', color: 'text.secondary' }}>{formatDuration(duration)}</Typography>}
|
||||
{step.tokenUsage && <Typography variant="caption" sx={{ color: 'text.secondary' }}>{formatTokens(step.tokenUsage.totalTokens)} tok</Typography>}
|
||||
</IconButton>
|
||||
|
||||
+32
-20
@@ -265,29 +265,41 @@ export function useAgentStream(): void {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Trace 步骤管理(每轮迭代一个步骤,状态转换时更新而非新建)---
|
||||
// --- Trace 步骤管理(每轮迭代一个步骤,状态转换时追加到 states 数组)---
|
||||
if (data.state && data.iteration != null) {
|
||||
const traceSteps = store.traceSteps;
|
||||
const lastStep = traceSteps[traceSteps.length - 1];
|
||||
|
||||
// 判断是否需要创建新步骤:同一迭代的首次状态(INIT/THINKING)创建新步骤
|
||||
// 后续状态转换(EXECUTING/OBSERVING等)只更新当前步骤的 state 字段
|
||||
const isSameIteration = lastStep && lastStep.iteration === data.iteration;
|
||||
|
||||
if (isSameIteration) {
|
||||
// 同一迭代内的状态转换 → 更新当前步骤状态
|
||||
store.updateLastTraceStep({ state: data.state });
|
||||
// 跳过 INIT (iteration=0):引擎初始化阶段无实际内容,不创建 trace step
|
||||
if (data.state === 'INIT' && data.iteration === 0) {
|
||||
// 仍需更新 Agent 状态映射
|
||||
} else {
|
||||
// 新迭代 → 标记上一步完成,创建新步骤
|
||||
if (lastStep && !lastStep.completedAt) {
|
||||
store.updateLastTraceStep({ completedAt: Date.now() });
|
||||
const traceSteps = store.traceSteps;
|
||||
const lastStep = traceSteps[traceSteps.length - 1];
|
||||
|
||||
// 判断是否需要创建新步骤:同一迭代的首次状态创建新步骤
|
||||
// 后续状态转换(EXECUTING/OBSERVING等)追加到 states 数组
|
||||
const isSameIteration = lastStep && lastStep.iteration === data.iteration;
|
||||
|
||||
if (isSameIteration) {
|
||||
// 同一迭代内的状态转换 → 追加状态到 states 数组,更新当前 state
|
||||
const currentStates = lastStep.states ?? [lastStep.state];
|
||||
if (currentStates[currentStates.length - 1] !== data.state) {
|
||||
store.updateLastTraceStep({
|
||||
state: data.state,
|
||||
states: [...currentStates, data.state],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 新迭代 → 标记上一步完成,创建新步骤
|
||||
if (lastStep && !lastStep.completedAt) {
|
||||
store.updateLastTraceStep({ completedAt: Date.now() });
|
||||
}
|
||||
store.addTraceStep({
|
||||
id: `trace_${data.iteration}_${data.state}_${Date.now()}`,
|
||||
iteration: data.iteration,
|
||||
state: data.state,
|
||||
states: [data.state],
|
||||
startedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
store.addTraceStep({
|
||||
id: `trace_${data.iteration}_${data.state}_${Date.now()}`,
|
||||
iteration: data.iteration,
|
||||
state: data.state,
|
||||
startedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
// --- Agent 状态映射 ---
|
||||
|
||||
@@ -59,6 +59,7 @@ export interface TraceStep {
|
||||
id: string;
|
||||
iteration: number;
|
||||
state: string;
|
||||
states: string[];
|
||||
startedAt: number;
|
||||
completedAt?: number;
|
||||
thought?: string;
|
||||
@@ -163,10 +164,11 @@ export const useAgentStore = create<AgentState>((set, get) => ({
|
||||
window.metona.sessions.getTrace(id).then((data) => {
|
||||
if (data) {
|
||||
if (data.traceSteps) {
|
||||
// 兼容旧数据:为缺少 id 的 trace 步骤生成 id
|
||||
// 兼容旧数据:为缺少 id/states 的 trace 步骤补全
|
||||
const steps = (data.traceSteps as TraceStep[]).map((t, i) => ({
|
||||
...t,
|
||||
id: t.id ?? `trace_legacy_${t.iteration}_${t.state}_${i}`,
|
||||
states: t.states ?? [t.state],
|
||||
}));
|
||||
set({ traceSteps: steps });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user