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) {
|