AIX: Debugger: first version

This commit is contained in:
Enrico Ros
2025-02-27 20:21:31 -08:00
parent 801d34692b
commit 334df849b3
3 changed files with 272 additions and 0 deletions
@@ -0,0 +1,133 @@
import * as React from 'react';
import { useShallow } from 'zustand/react/shallow';
import { Box, Button, Divider, FormControl, FormLabel, Option, Select, Typography } from '@mui/joy';
import ClearAllIcon from '@mui/icons-material/ClearAll';
import { GoodModal } from '~/common/components/modals/GoodModal';
import { AixDebuggerFrame } from './AixDebuggerFrame';
import { aixClientDebuggerActions, useAixClientDebuggerStore } from './memstore-aix-client-debugger';
export function AixDebuggerDialog(props: {
onClose: () => void;
}) {
// external state - we subscribe to Any update - it's a temp debugger anyway
const { frames, activeFrameId, maxFrames } = useAixClientDebuggerStore(useShallow((state) => ({
frames: state.frames,
activeFrameId: state.activeFrameId,
maxFrames: state.maxFrames,
})));
// derived state
const activeFrame = frames.find(f => f.id === activeFrameId) ?? null;
// handlers
const handleSetMaxFrames = React.useCallback((value: number) => {
aixClientDebuggerActions().setMaxFrames(value);
}, []);
const handleSetActiveFrame = React.useCallback((value: number | null) => {
aixClientDebuggerActions().setActiveFrame(value);
}, []);
return (
<GoodModal
open
onClose={props.onClose}
title='AIX API Debugger'
sx={{ maxWidth: undefined }}
>
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', md: 'row' }, gap: 2 }}>
{/* Request Switcher */}
<FormControl sx={{ flex: 1, minWidth: 0 }}>
<FormLabel>Select Request</FormLabel>
<Select
size='sm'
variant='outlined'
value={activeFrameId}
onChange={(_, value) => handleSetActiveFrame(value)}
placeholder='No requests available'
disabled={!frames.length}
sx={{ backgroundColor: 'background.popup' }}
>
{frames.map((frame) => {
const label = `Request #${frame.id} - ${frame.context.contextName}`;
return (
<Option key={frame.id} value={frame.id} label={label + (frame.isComplete ? ` (${frame.particles.length})` : ' (Running)')}>
<div>{label} - {frame.particles.length} pts.</div>
<Box component='span' sx={{ marginLeft: 'auto', fontSize: 'xs' }}>{new Date(frame.timestamp).toLocaleTimeString()}</Box>
</Option>
);
})}
</Select>
</FormControl>
{/* History Size Preferenes */}
<Box sx={{ display: 'flex', alignItems: 'flex-end', gap: 2 }}>
<FormControl>
<FormLabel>History Size</FormLabel>
<Select
size='sm'
value={maxFrames}
onChange={(_, value) => value !== null && handleSetMaxFrames(value)}
sx={{ backgroundColor: 'background.popup' }}
>
<Option value={5}>Keep 5 requests</Option>
<Option value={10}>Keep 10 requests</Option>
<Option value={20}>Keep 20 requests</Option>
<Option value={50}>Keep 50 requests</Option>
</Select>
</FormControl>
{/* Clear History */}
<Button
size='sm'
color='danger'
onClick={aixClientDebuggerActions().clearHistory}
startDecorator={<ClearAllIcon />}
disabled={frames.length === 0}
>
Clear History
</Button>
</Box>
</Box>
<Divider />
{/* Zero State */}
{(!frames.length || !activeFrame) && (
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '200px' }}>
{!frames.length && <>
<Typography level='title-lg'>
No API requests recorded yet
</Typography>
<Typography sx={{ mt: 1 }}>
Make a request with the AI to see it here
</Typography>
</>}
{!activeFrame && (
<Typography>
Select a request to view details
</Typography>
)}
</Box>
)}
{/* Frame viewer */}
{!!activeFrame && (
<Box sx={{ overflow: 'hidden' }}>
<AixDebuggerFrame frame={activeFrame} />
</Box>
)}
</GoodModal>
);
}
@@ -0,0 +1,134 @@
import * as React from 'react';
import { Box, Card, Chip, Typography } from '@mui/joy';
import type { AixClientDebugger } from './memstore-aix-client-debugger';
const _styles = {
requestCard: {
overflow: 'auto',
boxShadow: 'inset 2px 0 4px -2px rgba(0, 0, 0, 0.2)',
fontFamily: 'code',
fontSize: 'xs',
gap: 1,
} as const,
requestCardText: {
whiteSpace: 'pre',
} as const,
particleNorminal: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
} as const,
particleAborted: {
// ..._styles.particleNorminal,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
// change look
backgroundColor: '#f9f9f9',
borderLeft: '3px solid orange',
} as const,
pTime: {
pl: 2,
fontSize: 'xs',
whiteSpace: 'nowrap',
} as const,
} as const;
export function AixDebuggerFrame(props: {
frame: AixClientDebugger.Frame;
}) {
const { frame } = props;
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{/* Frame Header */}
<Box sx={{ fontSize: 'sm', display: 'grid', gridTemplateColumns: { xs: 'auto 1fr', md: 'auto 1fr auto 1fr' }, gap: 1, alignItems: 'center' }}>
<Typography fontWeight='bold'>Request </Typography>
<Typography fontWeight='bold'>{frame.id}</Typography>
<div>status</div>
<Chip variant='soft' color={frame.isComplete ? 'success' : 'warning'}>{frame.isComplete ? 'Complete' : 'In Progress'}</Chip>
<div>Date</div>
<div>{new Date(frame.timestamp).toLocaleString()}</div>
<div>URL:</div>
<Chip>{frame.url || 'No URL data available'}</Chip>
<div>Context:</div>
<Chip>{frame.context.contextName}</Chip>
<div>Reference:</div>
<Chip>{frame.context.contextRef}</Chip>
</Box>
{/* Headers */}
<Typography color='warning' level='title-md' mb={-2}>
Headers:
</Typography>
<Card variant='soft' color='warning' sx={_styles.requestCard}>
<Box sx={_styles.requestCardText}>
{frame.headers || 'No headers data available'}
</Box>
</Card>
{/* Body */}
<Typography color='primary' level='title-md' mb={-2}>
Body:
</Typography>
<Card variant='soft' color='primary' sx={_styles.requestCard}>
<Box sx={_styles.requestCardText}>
{frame.body || 'No headers data available'}
</Box>
</Card>
{/* Particles List */}
<Typography level='title-md' mb={-2}>
Particles {frame.particles.length > 0 && `(${frame.particles.length})`}
{!frame.isComplete && ' • Streaming...'}
</Typography>
<Card variant='soft' sx={_styles.requestCard}>
{/* Zero state */}
{!frame.particles.length && (
<Typography>
No particles received yet
</Typography>
)}
{/* List of particles */}
{frame.particles.map((particle, idx) => {
// truncated preview of particle content
let jsonPreview = '';
try {
const content = particle.content;
jsonPreview = JSON.stringify(content).substring(0, 1024);
if (jsonPreview.length >= 1024) jsonPreview += '...';
} catch (e) {
jsonPreview = 'Error parsing content';
}
return (
<Box key={idx} sx={particle.isAborted ? _styles.particleAborted : _styles.particleNorminal}>
<Box className='agi-ellipsize'>
<span style={{ opacity: 0.5 }}>{idx + 1}:</span> {particle.isAborted ? ' (Aborted)' : ''} {jsonPreview}
</Box>
<Box sx={_styles.pTime}>
{new Date(particle.timestamp).toLocaleTimeString()}
</Box>
</Box>
);
})}
</Card>
</Box>
);
}
@@ -149,3 +149,8 @@ export const useAixClientDebuggerStore = create<AixClientDebuggerStore>((_set) =
}),
}));
export function aixClientDebuggerActions() {
return useAixClientDebuggerStore.getState() as AixClientDebuggerActions;
}