Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.1.2
1.2.0
336 changes: 189 additions & 147 deletions client/package-lock.json

Large diffs are not rendered by default.

105 changes: 105 additions & 0 deletions client/src/components/Performance/ModelUsageChart.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* @fileoverview Horizontal bar chart of assistant-turn count per model.
* Uses recharts with a responsive container so the chart resizes with
* its parent Card.
*/
import React from 'react';
import { Box, Typography, useTheme } from '@mui/material';
import {
ResponsiveContainer,
BarChart,
Bar,
Cell,
XAxis,
YAxis,
Tooltip,
CartesianGrid,
} from 'recharts';
import { RAINBOW_COLORS } from './colors';

/**
* barColor returns a distinct colour for each bar. The first entries use the
* shared rainbow palette; beyond that, evenly spaced hues are generated so no
* two models ever share a colour, however many models are present.
*
* @param {number} index - zero-based bar index
* @param {number} total - total number of bars
* @returns {string} CSS colour
*/
const barColor = (index, total) => {
if (total <= RAINBOW_COLORS.length) return RAINBOW_COLORS[index];
const hue = Math.round((index * 360) / total);
return `hsl(${hue}, 65%, 50%)`;
};

/**
* @param {Object} props
* @param {Array<{modelId: string, count: number}>} props.models
*/
const ModelUsageChart = ({ models }) => {
const theme = useTheme();

if (!models || models.length === 0) {
return (
<Box sx={{ py: 3, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
No model attribution data in this window.
</Typography>
</Box>
);
}

const data = models.map((m) => ({
modelId: m.modelId || 'unknown',
count: m.count,
}));
const height = Math.max(140, data.length * 40 + 56);

return (
<Box sx={{ width: '100%', height }}>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={data} layout="vertical" margin={{ top: 8, right: 24, bottom: 24, left: 8 }}>
<CartesianGrid strokeDasharray="3 3" horizontal={false} stroke={theme.palette.divider} />
<XAxis
type="number"
allowDecimals={false}
tick={{ fill: theme.palette.text.secondary, fontSize: 12 }}
stroke={theme.palette.divider}
label={{
value: 'Responses',
position: 'insideBottom',
offset: -8,
fill: theme.palette.text.secondary,
fontSize: 12,
}}
/>
<YAxis
type="category"
dataKey="modelId"
width={180}
tick={{ fill: theme.palette.text.secondary, fontSize: 12 }}
stroke={theme.palette.divider}
/>
<Tooltip
cursor={{ fill: theme.palette.action.hover }}
contentStyle={{
backgroundColor: theme.palette.background.paper,
border: `1px solid ${theme.palette.divider}`,
borderRadius: 8,
color: theme.palette.text.primary,
}}
formatter={(value) => [value.toLocaleString(), 'responses']}
labelFormatter={(label) => `Model: ${label}`}
/>
<Bar dataKey="count" radius={[0, 4, 4, 0]}>
{data.map((entry, index) => (
<Cell key={entry.modelId} fill={barColor(index, data.length)} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</Box>
);
};

export default ModelUsageChart;
158 changes: 158 additions & 0 deletions client/src/components/Performance/PerformanceSection.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/**
* @fileoverview Performance section embedded in the Dashboard: Kiro CLI
* usage summary and per-turn model routing share, sourced from local
* session logs plus the /usage command.
*/
import React from 'react';
import {
Box,
Typography,
Stack,
ToggleButton,
ToggleButtonGroup,
Button,
Alert,
CircularProgress,
Card,
CardContent,
} from '@mui/material';
import { Refresh as RefreshIcon } from '@mui/icons-material';
import { useQuery, useQueryClient } from 'react-query';
import { getPerformanceSummary, getKiroUsage } from '../../services/api';
import usePersistedState from '../../hooks/usePersistedState';
import SummaryCards from './SummaryCards';
import PlanUsageCard from './PlanUsageCard';
import ModelUsageChart from './ModelUsageChart';
import { RAINBOW_COLORS, RAINBOW_GRADIENT_ID } from './colors';

const WINDOWS = [
{ value: '24h', label: 'Last 24h' },
{ value: '7d', label: 'Last 7 days' },
{ value: '30d', label: 'Last 30 days' },
{ value: 'all', label: 'All time' },
];

const PerformanceSection = () => {
const [range, setRange] = usePersistedState('performance.timeRange', '24h');
const queryClient = useQueryClient();

// Local session-log summary. No polling — it loads on mount, on range
// change, and when the user clicks Refresh.
const summaryQuery = useQuery(
['performance-summary', range],
() => getPerformanceSummary(range),
);

// Plan usage fetches live from kiro-cli (slow, no caching). Fetch once on
// load; no polling. On failure the card shows guidance and the user clicks
// Refresh to try again.
const usageQuery = useQuery(
['kiro-usage'],
() => getKiroUsage(),
{ retry: false, refetchOnWindowFocus: false, staleTime: Infinity },
);

const handleRangeChange = (_e, next) => {
if (next) setRange(next);
};

const handleRefresh = () => {
// Refresh the whole section on demand: local summary + live plan usage.
// Errors stay isolated per query (a usage failure only affects its card).
queryClient.invalidateQueries(['performance-summary', range]);
queryClient.invalidateQueries(['kiro-usage']);
};

const refreshing = summaryQuery.isFetching || usageQuery.isFetching;

const summary = summaryQuery.data;
const error = summaryQuery.error;
const isLoading = summaryQuery.isLoading;

return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Stack direction="row" alignItems="center" justifyContent="space-between" flexWrap="wrap" spacing={2}>
<Typography variant="h6">
Performance
</Typography>
<Stack direction="row" spacing={1} alignItems="center">
<ToggleButtonGroup
value={range}
exclusive
size="small"
onChange={handleRangeChange}
aria-label="time range"
>
{WINDOWS.map((w) => (
<ToggleButton key={w.value} value={w.value} sx={{ textTransform: 'none' }}>
{w.label}
</ToggleButton>
))}
</ToggleButtonGroup>
{summaryQuery.dataUpdatedAt > 0 && (
<Typography variant="caption" color="text.secondary">
Updated {new Date(summaryQuery.dataUpdatedAt).toLocaleString()}
</Typography>
)}
{/* Gradient definition used to paint the refresh spinner. */}
<Box component="svg" aria-hidden width={0} height={0} sx={{ position: 'absolute' }}>
<defs>
<linearGradient id={RAINBOW_GRADIENT_ID} x1="0%" y1="0%" x2="100%" y2="100%">
{RAINBOW_COLORS.map((c, i) => (
<stop key={c} offset={`${(i * 100) / (RAINBOW_COLORS.length - 1)}%`} stopColor={c} />
))}
</linearGradient>
</defs>
</Box>
<Button
size="small"
variant="contained"
startIcon={
refreshing ? (
<CircularProgress
size={14}
sx={{ '& .MuiCircularProgress-circle': { stroke: `url(#${RAINBOW_GRADIENT_ID})` } }}
/>
) : (
<RefreshIcon />
)
}
onClick={handleRefresh}
disabled={refreshing}
>
{refreshing ? 'Refreshing…' : 'Refresh'}
</Button>
</Stack>
</Stack>

{error && (
<Alert severity="error">
Failed to load performance data: {error.message || 'unknown error'}
</Alert>
)}

<PlanUsageCard
usage={usageQuery.data}
loading={usageQuery.isLoading}
error={usageQuery.error}
/>

<SummaryCards summary={summary} loading={isLoading} />

<Card>
<CardContent>
<Typography variant="h6" sx={{ mb: 2 }}>Model usage</Typography>
{isLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
<CircularProgress size={24} />
</Box>
) : (
<ModelUsageChart models={summary?.models || []} />
)}
</CardContent>
</Card>
</Box>
);
};

export default PerformanceSection;
124 changes: 124 additions & 0 deletions client/src/components/Performance/PlanUsageCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* @fileoverview Plan and credits snapshot sourced from
* `kiro-cli chat --no-interactive "/usage"` via the backend, with
* graceful loading / error states and a linear progress bar.
*/
import React from 'react';
import {
Card,
CardContent,
Typography,
Stack,
LinearProgress,
Chip,
Alert,
Skeleton,
useTheme,
} from '@mui/material';
import { RAINBOW_GRADIENT } from './colors';

/**
* @param {Object} props
* @param {?Object} props.usage
* @param {boolean} props.loading
* @param {?Object} props.error
*/
const PlanUsageCard = ({ usage, loading, error }) => {
const theme = useTheme();

if (error) {
return (
<Alert severity="warning" variant="outlined">
Plan usage unavailable: {error.message || 'unknown error'}.
Make sure you are logged in — run <code>kiro-cli login</code> (or verify
with <code>kiro-cli chat "/usage"</code>), then click <strong>Refresh</strong> above.
</Alert>
);
}

if (loading || !usage) {
return (
<Card>
<CardContent>
<Skeleton variant="text" width="30%" height={28} />
<Skeleton variant="text" width="70%" />
<Skeleton variant="rectangular" height={8} sx={{ mt: 1, borderRadius: 1 }} />
</CardContent>
</Card>
);
}

const percent = Math.max(0, usage.percent || 0);
const barValue = Math.min(percent, 100);
const overLimit = percent > 100;

const usedLabel = usage.hasLimit
? `${usage.used.toLocaleString(undefined, { maximumFractionDigits: 2 })} of ${usage.limit.toLocaleString()} ${usage.metric.toLowerCase()}`
: `${usage.used.toLocaleString(undefined, { maximumFractionDigits: 2 })} ${usage.metric.toLowerCase()} used`;

return (
<Card>
<CardContent>
<Stack direction="row" alignItems="center" justifyContent="space-between" flexWrap="wrap" spacing={1} sx={{ mb: 1 }}>
<Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap">
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Plan usage
</Typography>
{usage.planName && (
<Chip label={usage.planName} size="small" color="primary" variant="outlined" />
)}
{usage.isEnterprise && (
<Chip label="Managed by org" size="small" variant="outlined" />
)}
</Stack>
{usage.resetsOn && (
<Typography variant="caption" color="text.secondary">
Resets on {usage.resetsOn}
</Typography>
)}
</Stack>

<Stack direction="row" alignItems="baseline" spacing={1} sx={{ mb: 0.5 }}>
<Typography variant="body2" color="text.secondary">
{usedLabel}
</Typography>
<Typography
variant="body2"
sx={{ fontWeight: 600, color: overLimit ? 'error.main' : percent >= 90 ? 'warning.main' : 'text.primary' }}
>
{percent.toFixed(1)}%
</Typography>
</Stack>

{usage.hasLimit && (
<LinearProgress
variant="determinate"
value={barValue}
sx={{
height: 8,
borderRadius: 1,
backgroundColor: 'action.hover',
'& .MuiLinearProgress-bar': {
borderRadius: 1,
// Rainbow fill, matching the "Features Utilized" card. Over the
// allowance the bar turns solid red so the breach still reads
// as a warning rather than decoration.
background: overLimit
? theme.palette.error.main
: RAINBOW_GRADIENT,
},
}}
/>
)}

{overLimit && (
<Typography variant="caption" color="error.main" sx={{ display: 'block', mt: 1 }}>
Over the in-plan allowance. Overages (if configured) are billed separately.
</Typography>
)}
</CardContent>
</Card>
);
};

export default PlanUsageCard;
Loading