Spaces:
Sleeping
Sleeping
wuyiqunLu
commited on
fix: parse stream log and add result display (#69)
Browse files<img width="896" alt="image"
src="https://github.com/landing-ai/vision-agent-ui/assets/132986242/4db20e09-adca-4c48-9452-35f6e1b8deca">
- components/chat/ChatMessage.tsx +77 -18
- components/ui/CodeBlock.tsx +1 -0
- components/ui/Icons.tsx +43 -0
- lib/messageUtils.ts +14 -2
components/chat/ChatMessage.tsx
CHANGED
@@ -18,6 +18,8 @@ import {
|
|
18 |
IconListUnordered,
|
19 |
IconTerminalWindow,
|
20 |
IconUser,
|
|
|
|
|
21 |
} from '@/components/ui/Icons';
|
22 |
import { MessageBase } from '../../lib/types';
|
23 |
import Img from '../ui/Img';
|
@@ -203,9 +205,28 @@ const ChunkPayloadAction: React.FC<{
|
|
203 |
<TableBody>
|
204 |
{payload.map((line, index) => (
|
205 |
<TableRow className="border-primary/50" key={index}>
|
206 |
-
{keyArray.map(header =>
|
207 |
-
|
208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
209 |
</TableRow>
|
210 |
))}
|
211 |
</TableBody>
|
@@ -233,27 +254,65 @@ const CodeResultDisplay: React.FC<{
|
|
233 |
codeResult: CodeResult;
|
234 |
}> = ({ codeResult }) => {
|
235 |
const { code, test, result } = codeResult;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
236 |
return (
|
237 |
<div className="rounded-lg overflow-hidden relative max-w-5xl">
|
238 |
<CodeBlock language="python" value={code} />
|
239 |
<div className="rounded-lg relative">
|
240 |
<Separator />
|
241 |
-
<
|
242 |
-
<
|
243 |
-
<
|
244 |
-
variant="ghost"
|
245 |
-
|
246 |
-
|
247 |
-
>
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
255 |
</div>
|
256 |
-
<CodeBlock language="output" value={
|
257 |
</div>
|
258 |
);
|
259 |
};
|
|
|
18 |
IconListUnordered,
|
19 |
IconTerminalWindow,
|
20 |
IconUser,
|
21 |
+
IconOutput,
|
22 |
+
IconLog,
|
23 |
} from '@/components/ui/Icons';
|
24 |
import { MessageBase } from '../../lib/types';
|
25 |
import Img from '../ui/Img';
|
|
|
205 |
<TableBody>
|
206 |
{payload.map((line, index) => (
|
207 |
<TableRow className="border-primary/50" key={index}>
|
208 |
+
{keyArray.map(header =>
|
209 |
+
header === 'documentation' ? (
|
210 |
+
<TableCell key={header}>
|
211 |
+
<Tooltip>
|
212 |
+
<TooltipTrigger asChild>
|
213 |
+
<Button
|
214 |
+
variant="ghost"
|
215 |
+
size="icon"
|
216 |
+
className="size-8 ml-[40%]"
|
217 |
+
>
|
218 |
+
<IconTerminalWindow className="text-teal-500 size-4" />
|
219 |
+
</Button>
|
220 |
+
</TooltipTrigger>
|
221 |
+
<TooltipContent>
|
222 |
+
<CodeBlock language="md" value={line[header]} />
|
223 |
+
</TooltipContent>
|
224 |
+
</Tooltip>
|
225 |
+
</TableCell>
|
226 |
+
) : (
|
227 |
+
<TableCell key={header}>{line[header]}</TableCell>
|
228 |
+
),
|
229 |
+
)}
|
230 |
</TableRow>
|
231 |
))}
|
232 |
</TableBody>
|
|
|
254 |
codeResult: CodeResult;
|
255 |
}> = ({ codeResult }) => {
|
256 |
const { code, test, result } = codeResult;
|
257 |
+
const getDetail = () => {
|
258 |
+
if (!result) return {};
|
259 |
+
try {
|
260 |
+
const detail = JSON.parse(result);
|
261 |
+
return {
|
262 |
+
results: detail.results,
|
263 |
+
stderr: detail.logs.stderr,
|
264 |
+
stdout: detail.logs.stdout,
|
265 |
+
};
|
266 |
+
} catch {
|
267 |
+
return {};
|
268 |
+
}
|
269 |
+
};
|
270 |
+
|
271 |
+
const { results, stderr, stdout } = getDetail();
|
272 |
+
|
273 |
return (
|
274 |
<div className="rounded-lg overflow-hidden relative max-w-5xl">
|
275 |
<CodeBlock language="python" value={code} />
|
276 |
<div className="rounded-lg relative">
|
277 |
<Separator />
|
278 |
+
<div className="absolute left-1/2 -translate-x-1/2 -top-4 z-10">
|
279 |
+
<Tooltip>
|
280 |
+
<TooltipTrigger asChild>
|
281 |
+
<Button variant="ghost" size="icon" className="size-8">
|
282 |
+
<IconTerminalWindow className="text-teal-500 size-4" />
|
283 |
+
</Button>
|
284 |
+
</TooltipTrigger>
|
285 |
+
<TooltipContent>
|
286 |
+
<CodeBlock language="python" value={test} />
|
287 |
+
</TooltipContent>
|
288 |
+
</Tooltip>
|
289 |
+
{Array.isArray(stdout) && (
|
290 |
+
<Tooltip>
|
291 |
+
<TooltipTrigger asChild>
|
292 |
+
<Button variant="ghost" size="icon" className="size-8">
|
293 |
+
<IconOutput className="text-blue-500 size-4" />
|
294 |
+
</Button>
|
295 |
+
</TooltipTrigger>
|
296 |
+
<TooltipContent>
|
297 |
+
<CodeBlock language="vim" value={stdout.join('').trim()} />
|
298 |
+
</TooltipContent>
|
299 |
+
</Tooltip>
|
300 |
+
)}
|
301 |
+
{Array.isArray(stderr) && (
|
302 |
+
<Tooltip>
|
303 |
+
<TooltipTrigger asChild>
|
304 |
+
<Button variant="ghost" size="icon" className="size-8">
|
305 |
+
<IconLog className="text-gray-500 size-4" />
|
306 |
+
</Button>
|
307 |
+
</TooltipTrigger>
|
308 |
+
<TooltipContent>
|
309 |
+
<CodeBlock language="vim" value={stderr.join('').trim()} />
|
310 |
+
</TooltipContent>
|
311 |
+
</Tooltip>
|
312 |
+
)}
|
313 |
+
</div>
|
314 |
</div>
|
315 |
+
<CodeBlock language="output" value={results} />
|
316 |
</div>
|
317 |
);
|
318 |
};
|
components/ui/CodeBlock.tsx
CHANGED
@@ -24,6 +24,7 @@ export const programmingLanguages: languageMap = {
|
|
24 |
javascript: '.js',
|
25 |
python: '.py',
|
26 |
java: '.java',
|
|
|
27 |
c: '.c',
|
28 |
cpp: '.cpp',
|
29 |
'c++': '.cpp',
|
|
|
24 |
javascript: '.js',
|
25 |
python: '.py',
|
26 |
java: '.java',
|
27 |
+
vim: '.txt',
|
28 |
c: '.c',
|
29 |
cpp: '.cpp',
|
30 |
'c++': '.cpp',
|
components/ui/Icons.tsx
CHANGED
@@ -728,6 +728,47 @@ function IconListUnordered({
|
|
728 |
</svg>
|
729 |
);
|
730 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
731 |
export {
|
732 |
IconEdit,
|
733 |
IconLandingAI,
|
@@ -768,4 +809,6 @@ export {
|
|
768 |
IconTerminalWindow,
|
769 |
IconCodeWrap,
|
770 |
IconListUnordered,
|
|
|
|
|
771 |
};
|
|
|
728 |
</svg>
|
729 |
);
|
730 |
}
|
731 |
+
|
732 |
+
function IconLog({ className, ...props }: React.ComponentProps<'svg'>) {
|
733 |
+
return (
|
734 |
+
<svg
|
735 |
+
height="16"
|
736 |
+
strokeLinejoin="round"
|
737 |
+
viewBox="0 0 16 16"
|
738 |
+
width="16"
|
739 |
+
className={cn('size-4', className)}
|
740 |
+
{...props}
|
741 |
+
>
|
742 |
+
<path
|
743 |
+
fill-rule="evenodd"
|
744 |
+
clip-rule="evenodd"
|
745 |
+
d="M3 2.5C3 2.22386 3.22386 2 3.5 2H9.08579C9.21839 2 9.34557 2.05268 9.43934 2.14645L11.8536 4.56066C11.9473 4.65443 12 4.78161 12 4.91421V12.5C12 12.7761 11.7761 13 11.5 13H3.5C3.22386 13 3 12.7761 3 12.5V2.5ZM3.5 1C2.67157 1 2 1.67157 2 2.5V12.5C2 13.3284 2.67157 14 3.5 14H11.5C12.3284 14 13 13.3284 13 12.5V4.91421C13 4.51639 12.842 4.13486 12.5607 3.85355L10.1464 1.43934C9.86514 1.15804 9.48361 1 9.08579 1H3.5ZM4.5 4C4.22386 4 4 4.22386 4 4.5C4 4.77614 4.22386 5 4.5 5H7.5C7.77614 5 8 4.77614 8 4.5C8 4.22386 7.77614 4 7.5 4H4.5ZM4.5 7C4.22386 7 4 7.22386 4 7.5C4 7.77614 4.22386 8 4.5 8H10.5C10.7761 8 11 7.77614 11 7.5C11 7.22386 10.7761 7 10.5 7H4.5ZM4.5 10C4.22386 10 4 10.2239 4 10.5C4 10.7761 4.22386 11 4.5 11H10.5C10.7761 11 11 10.7761 11 10.5C11 10.2239 10.7761 10 10.5 10H4.5Z"
|
746 |
+
fill="currentColor"
|
747 |
+
/>
|
748 |
+
</svg>
|
749 |
+
);
|
750 |
+
}
|
751 |
+
|
752 |
+
function IconOutput({ className, ...props }: React.ComponentProps<'svg'>) {
|
753 |
+
return (
|
754 |
+
<svg
|
755 |
+
height="16"
|
756 |
+
strokeLinejoin="round"
|
757 |
+
viewBox="0 0 16 16"
|
758 |
+
width="16"
|
759 |
+
className={cn('size-4', className)}
|
760 |
+
{...props}
|
761 |
+
>
|
762 |
+
<path
|
763 |
+
fill-rule="evenodd"
|
764 |
+
clip-rule="evenodd"
|
765 |
+
d="M5 2V1H10V2H5ZM4.75 0C4.33579 0 4 0.335786 4 0.75V1H3.5C2.67157 1 2 1.67157 2 2.5V12.5C2 13.3284 2.67157 14 3.5 14H11.5C12.3284 14 13 13.3284 13 12.5V2.5C13 1.67157 12.3284 1 11.5 1H11V0.75C11 0.335786 10.6642 0 10.25 0H4.75ZM11 2V2.25C11 2.66421 10.6642 3 10.25 3H4.75C4.33579 3 4 2.66421 4 2.25V2H3.5C3.22386 2 3 2.22386 3 2.5V12.5C3 12.7761 3.22386 13 3.5 13H11.5C11.7761 13 12 12.7761 12 12.5V2.5C12 2.22386 11.7761 2 11.5 2H11Z"
|
766 |
+
fill="currentColor"
|
767 |
+
/>
|
768 |
+
</svg>
|
769 |
+
);
|
770 |
+
}
|
771 |
+
|
772 |
export {
|
773 |
IconEdit,
|
774 |
IconLandingAI,
|
|
|
809 |
IconTerminalWindow,
|
810 |
IconCodeWrap,
|
811 |
IconListUnordered,
|
812 |
+
IconLog,
|
813 |
+
IconOutput,
|
814 |
};
|
lib/messageUtils.ts
CHANGED
@@ -350,14 +350,26 @@ export const formatStreamLogs = (
|
|
350 |
): [ChunkBody[], CodeResult?] => {
|
351 |
const streamLogs = content.split('\n').filter(log => !!log);
|
352 |
|
353 |
-
|
|
|
354 |
try {
|
355 |
-
|
|
|
|
|
356 |
} catch {
|
357 |
toast.error('Error parsing stream logs');
|
358 |
return [[], undefined];
|
359 |
}
|
360 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
361 |
// Merge consecutive logs of the same type to the latest status
|
362 |
const groupedSections = parsedStreamLogs.reduce((acc, curr) => {
|
363 |
if (acc.length > 0 && acc[acc.length - 1].type === curr.type) {
|
|
|
350 |
): [ChunkBody[], CodeResult?] => {
|
351 |
const streamLogs = content.split('\n').filter(log => !!log);
|
352 |
|
353 |
+
const buffer = streamLogs.pop();
|
354 |
+
const parsedStreamLogs: ChunkBody[] = [];
|
355 |
try {
|
356 |
+
streamLogs.forEach(streamLog =>
|
357 |
+
parsedStreamLogs.push(JSON.parse(streamLog)),
|
358 |
+
);
|
359 |
} catch {
|
360 |
toast.error('Error parsing stream logs');
|
361 |
return [[], undefined];
|
362 |
}
|
363 |
|
364 |
+
if (buffer) {
|
365 |
+
try {
|
366 |
+
const lastLog = JSON.parse(buffer);
|
367 |
+
parsedStreamLogs.push(lastLog);
|
368 |
+
} catch {
|
369 |
+
console.log(buffer);
|
370 |
+
}
|
371 |
+
}
|
372 |
+
|
373 |
// Merge consecutive logs of the same type to the latest status
|
374 |
const groupedSections = parsedStreamLogs.reduce((acc, curr) => {
|
375 |
if (acc.length > 0 && acc[acc.length - 1].type === curr.type) {
|