Skip to content

Commit eef9384

Browse files
Orcish-Ygemini-code-assist[bot]ninelcc336lkn
authored
AVA.analysis() 方法加入分步进度报告能力,并在 site 侧新增 AnalysisSteps UI 组件实现可视化展示。 (#958)
* feat: add step-level progress reporting to analysis with UI progress component * feat: 修复类型错误 * Update site/components/GPTVisRenderer.tsx Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * feat: 翻译成英语 * feat: translate the analysis step component into English * feat: optimize the truncation position of the detailed analysis steps --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: lknHHH <113652919+ninelcc336@users.noreply.github.com> Co-authored-by: lkn <lkn02425726@antgroup.com>
1 parent b7f7ec0 commit eef9384

7 files changed

Lines changed: 470 additions & 18 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,6 @@ temp.diff
8989
**/*/src/.umi-production
9090
**/*/src/.umi-test
9191
**/*/.env.local
92+
93+
#pnpm-lock
94+
pnpm-lock.yaml

site/components/AnalysisSteps.tsx

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
'use client';
2+
import React, { useState, useMemo } from 'react';
3+
import type { AnalysisStep, StepStatus } from '@antv/ava';
4+
5+
interface AnalysisStepsProps {
6+
steps: AnalysisStep[];
7+
collapsed: boolean;
8+
}
9+
10+
function getStepTime(step: AnalysisStep, index: number, sorted: AnalysisStep[]): string | null {
11+
if (step.status !== 'done') return null;
12+
if (index === 0) return null;
13+
const prevTimestamp = sorted[index - 1].timestamp;
14+
const duration = (step.timestamp - prevTimestamp) / 1000;
15+
if (duration <= 0) return null;
16+
return `${duration.toFixed(1)}s`;
17+
}
18+
19+
function DoneIcon() {
20+
return (
21+
<svg
22+
className="w-4 h-4 text-green-500 flex-shrink-0"
23+
viewBox="0 0 24 24"
24+
fill="none"
25+
stroke="currentColor"
26+
strokeWidth="3"
27+
strokeLinecap="round"
28+
strokeLinejoin="round"
29+
>
30+
<polyline points="20 6 9 17 4 12" />
31+
</svg>
32+
);
33+
}
34+
35+
function RunningIcon() {
36+
return (
37+
<svg className="w-4 h-4 text-[#78d3f8] flex-shrink-0 animate-spin" fill="none" viewBox="0 0 24 24">
38+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
39+
<path
40+
className="opacity-75"
41+
fill="currentColor"
42+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
43+
/>
44+
</svg>
45+
);
46+
}
47+
48+
function PendingIcon() {
49+
return (
50+
<svg
51+
className="w-4 h-4 text-gray-300 flex-shrink-0"
52+
viewBox="0 0 24 24"
53+
fill="none"
54+
stroke="currentColor"
55+
strokeWidth="2"
56+
strokeLinecap="round"
57+
strokeLinejoin="round"
58+
>
59+
<circle cx="12" cy="12" r="10" />
60+
</svg>
61+
);
62+
}
63+
64+
function ErrorIcon() {
65+
return (
66+
<svg
67+
className="w-4 h-4 text-red-500 flex-shrink-0"
68+
viewBox="0 0 24 24"
69+
fill="none"
70+
stroke="currentColor"
71+
strokeWidth="3"
72+
strokeLinecap="round"
73+
strokeLinejoin="round"
74+
>
75+
<line x1="18" y1="6" x2="6" y2="18" />
76+
<line x1="6" y1="6" x2="18" y2="18" />
77+
</svg>
78+
);
79+
}
80+
81+
function StepIcon({ status }: { status: StepStatus }) {
82+
switch (status) {
83+
case 'done':
84+
return <DoneIcon />;
85+
case 'running':
86+
return <RunningIcon />;
87+
case 'error':
88+
return <ErrorIcon />;
89+
case 'pending':
90+
return <PendingIcon />;
91+
}
92+
}
93+
94+
export default function AnalysisSteps({ steps, collapsed }: AnalysisStepsProps) {
95+
const [expandedDetail, setExpandedDetail] = useState<string | null>(null);
96+
const [forceExpanded, setForceExpanded] = useState(false);
97+
98+
if (!steps || steps.length === 0) return null;
99+
100+
const sorted = useMemo(() => {
101+
return [...steps].sort((a, b) => a.timestamp - b.timestamp);
102+
}, [steps]);
103+
104+
const totalDuration = useMemo(() => {
105+
if (sorted.length <= 1) return null;
106+
const duration = (sorted[sorted.length - 1].timestamp - sorted[0].timestamp) / 1000;
107+
if (duration <= 0) return null;
108+
return duration;
109+
}, [sorted]);
110+
111+
const hasError = sorted.some((s) => s.status === 'error');
112+
const allDone = sorted.every((s) => s.status === 'done');
113+
114+
// Always show expanded view when there are errors, regardless of collapsed prop
115+
const isCollapsed = collapsed && !hasError && !forceExpanded && allDone;
116+
117+
const title = (
118+
<span className="flex items-center gap-2 text-sm">
119+
<DoneIcon />
120+
<span>
121+
Analysis Complete · {sorted.length} Steps{totalDuration !== null && ` · ${totalDuration.toFixed(1)}s`}
122+
</span>
123+
</span>
124+
);
125+
126+
if (isCollapsed) {
127+
return (
128+
<div className="mb-4 p-3 bg-gray-50 rounded-xl border border-gray-100">
129+
<button
130+
onClick={() => setForceExpanded(true)}
131+
className="w-full flex items-center justify-between text-sm text-gray-600 hover:text-gray-800 transition-colors"
132+
>
133+
{title}
134+
<span className="text-[#78d3f8] font-medium flex items-center gap-1">
135+
Expand
136+
<svg
137+
className="w-3 h-3"
138+
viewBox="0 0 24 24"
139+
fill="none"
140+
stroke="currentColor"
141+
strokeWidth="3"
142+
strokeLinecap="round"
143+
strokeLinejoin="round"
144+
>
145+
<polyline points="6 9 12 15 18 9" />
146+
</svg>
147+
</span>
148+
</button>
149+
</div>
150+
);
151+
}
152+
153+
return (
154+
<div className="mb-4 p-3 bg-gray-50 rounded-xl border border-gray-100">
155+
{/* Header with summary and collapse button when expanded */}
156+
{allDone && (
157+
<div className="flex items-center justify-between mb-2">
158+
{title}
159+
<button
160+
onClick={() => setForceExpanded(false)}
161+
className="text-xs text-gray-400 hover:text-gray-600 transition-colors flex items-center gap-1 flex-shrink-0"
162+
>
163+
Collapse
164+
<svg
165+
className="w-3 h-3"
166+
viewBox="0 0 24 24"
167+
fill="none"
168+
stroke="currentColor"
169+
strokeWidth="3"
170+
strokeLinecap="round"
171+
strokeLinejoin="round"
172+
>
173+
<polyline points="18 15 12 9 6 15" />
174+
</svg>
175+
</button>
176+
</div>
177+
)}
178+
179+
<div className="space-y-2">
180+
{sorted.map((step, index) => {
181+
const stepTime = getStepTime(step, index, sorted);
182+
const isExpanded = expandedDetail === step.id;
183+
184+
return (
185+
<div key={step.id}>
186+
<div className={`flex items-center gap-2.5 text-sm ${step.status === 'pending' ? 'opacity-50' : ''}`}>
187+
<StepIcon status={step.status} />
188+
<span
189+
className={`flex-1 ${
190+
step.status === 'done'
191+
? 'text-gray-700'
192+
: step.status === 'running'
193+
? 'text-gray-800 font-medium'
194+
: step.status === 'error'
195+
? 'text-red-600'
196+
: 'text-gray-400'
197+
}`}
198+
>
199+
{step.label}
200+
</span>
201+
202+
{stepTime && <span className="text-xs text-gray-400">{stepTime}</span>}
203+
204+
{/* View/Hide button for done steps with detail */}
205+
{step.status === 'done' && step.detail && (
206+
<button
207+
onClick={() => setExpandedDetail(isExpanded ? null : step.id)}
208+
className={`text-xs font-medium transition-colors flex items-center gap-1 flex-shrink-0 ${
209+
isExpanded ? 'text-[#78d3f8]' : 'text-gray-400 hover:text-[#78d3f8]'
210+
}`}
211+
>
212+
{isExpanded ? 'Collapse' : 'View'}
213+
<svg
214+
className={`w-3 h-3 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
215+
viewBox="0 0 24 24"
216+
fill="none"
217+
stroke="currentColor"
218+
strokeWidth="3"
219+
strokeLinecap="round"
220+
strokeLinejoin="round"
221+
>
222+
<polyline points="6 9 12 15 18 9" />
223+
</svg>
224+
</button>
225+
)}
226+
</div>
227+
228+
{/* Error message */}
229+
{step.status === 'error' && step.error && (
230+
<div className="mt-1 ml-6.5 text-xs text-red-500">{step.error}</div>
231+
)}
232+
233+
{/* Expanded detail code block */}
234+
{isExpanded && step.detail && (
235+
<div className="mt-1.5 ml-6.5">
236+
<pre className="p-3 bg-gray-900 text-gray-100 rounded-lg text-xs overflow-x-auto font-mono leading-relaxed">
237+
{step.detail.slice(0, 200)}
238+
</pre>
239+
</div>
240+
)}
241+
</div>
242+
);
243+
})}
244+
</div>
245+
</div>
246+
);
247+
}

site/components/GPTVisRenderer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const GPTVisRenderer: React.FC<GPTVisRendererProps> = ({ syntax, width, height,
3131
instance.destroy();
3232
instanceRef.current = null;
3333
};
34-
}, []);
34+
}, [width, height]);
3535

3636
// Update: re-render when syntax or dimensions change.
3737
useEffect(() => {

site/components/Visualization.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import React, { useState, useCallback, useEffect } from 'react';
33
import ReactMarkdown from 'react-markdown';
44
import remarkGfm from 'remark-gfm';
55
import { AVA } from '@antv/ava';
6-
import type { AnalysisResponse, SuggestResult } from '@antv/ava';
6+
import type { AnalysisResponse, AnalysisStep, SuggestResult } from '@antv/ava';
77
import type { DataRow } from './types';
88
import SuggestionCards from './SuggestionCards';
99
import { loadAppState, saveAppState } from './utils';
1010
import GPTVisRenderer from './GPTVisRenderer';
11+
import AnalysisSteps from './AnalysisSteps';
1112

1213
interface VisualizationProps {
1314
avaInstance: AVA | null;
@@ -23,6 +24,7 @@ const Visualization: React.FC<VisualizationProps> = ({ avaInstance, data, isInit
2324
const [result, setResult] = useState<AnalysisResponse | null>(null);
2425
const [error, setError] = useState<string | null>(null);
2526
const [showCode, setShowCode] = useState(false);
27+
const [steps, setSteps] = useState<AnalysisStep[]>([]);
2628

2729
// Restore query and result from localStorage on mount
2830
useEffect(() => {
@@ -47,10 +49,14 @@ const Visualization: React.FC<VisualizationProps> = ({ avaInstance, data, isInit
4749

4850
setIsLoading(true);
4951
setError(null);
52+
setSteps([]); // Clear old steps on new analysis
5053

5154
try {
52-
// Use the global AVA instance's analysis method to process the query
53-
const analysisResult = await avaInstance.analysis(query);
55+
const analysisResult = await avaInstance.analysis(query, {
56+
onProgress: (steps) => {
57+
setSteps(steps);
58+
},
59+
});
5460
setResult(analysisResult);
5561

5662
// Save to localStorage
@@ -176,6 +182,11 @@ const Visualization: React.FC<VisualizationProps> = ({ avaInstance, data, isInit
176182
</div>
177183
)}
178184

185+
{/* Analysis Progress Steps */}
186+
{steps.length > 0 && (
187+
<AnalysisSteps steps={steps} collapsed={!isLoading} />
188+
)}
189+
179190
{/* Analysis Summary - Always show when result.text exists, rendered with react-markdown */}
180191
{result && result.text && (
181192
<div className="mb-4 p-4 bg-gray-50 rounded-xl relative">

0 commit comments

Comments
 (0)