|
1 | 1 | import { useEffect, useMemo, useState } from 'react' |
2 | | -import { AreaChart, Area, CartesianGrid, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts' |
| 2 | +import { AreaChart, Area, CartesianGrid, XAxis, YAxis, Tooltip, ResponsiveContainer, ComposedChart, Bar, Line } from 'recharts' |
3 | 3 | import { supabase } from '../lib/supabase' |
4 | 4 | import { CHART_COLORS, CHART_TYPOGRAPHY } from '../lib/brandColors' |
5 | 5 | import ChartDialog from './ChartDialog' |
@@ -28,6 +28,13 @@ const formatTprAxis = (value) => { |
28 | 28 | return Math.round(value).toLocaleString('en-US') |
29 | 29 | } |
30 | 30 |
|
| 31 | +const SERIES_COLORS = { |
| 32 | + requests: '#8b5cf6', |
| 33 | + input: 'var(--color-info)', |
| 34 | + output: 'var(--color-cyan)', |
| 35 | + cost: 'var(--color-success)', |
| 36 | +} |
| 37 | + |
31 | 38 | const getStatus = (status) => status === 'failure' ? 'failure' : 'success' |
32 | 39 | const getSkillLabel = (value) => value || 'Unknown' |
33 | 40 | const getProjectLabel = (value) => value || 'Unknown Project' |
@@ -363,8 +370,6 @@ function SkillsPanel({ skillRuns = [], skillDailyStats = [], dateRange, customRa |
363 | 370 | }, [activeSkillName, baseRuns]) |
364 | 371 |
|
365 | 372 | const trendSeries = trendTime === 'hour' ? overviewHourlySeries : overviewDailySeries |
366 | | - const hasTokenSignal = trendSeries.some(p => (p.input_tokens || 0) > 0 || (p.output_tokens || 0) > 0) |
367 | | - const useRunFallbackSeries = trendSeries.length > 0 && !hasTokenSignal |
368 | 373 |
|
369 | 374 | const detailRuns = useMemo(() => { |
370 | 375 | if (!activeSkillName) return [] |
@@ -638,6 +643,13 @@ function SkillsPanel({ skillRuns = [], skillDailyStats = [], dateRange, customRa |
638 | 643 | { key: 'top_skill', label: 'Top Skill' }, |
639 | 644 | ] |
640 | 645 |
|
| 646 | + const runAxisTickFormatter = (v) => formatNumber(v) |
| 647 | + const tokenAxisTickFormatter = (v) => { |
| 648 | + if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M` |
| 649 | + if (v >= 1_000) return `${(v / 1_000).toFixed(1)}K` |
| 650 | + return formatNumber(v) |
| 651 | + } |
| 652 | + |
641 | 653 | return ( |
642 | 654 | <div className="skills-panel"> |
643 | 655 | <div className="stats-grid"> |
@@ -719,47 +731,105 @@ function SkillsPanel({ skillRuns = [], skillDailyStats = [], dateRange, customRa |
719 | 731 | <div className="charts-row"> |
720 | 732 | <div className="chart-card chart-full"> |
721 | 733 | <div className="chart-header"> |
722 | | - <h3>Skill Funnel & Token Usage Over Time</h3> |
| 734 | + <h3>Skill Requests + Token Trend</h3> |
723 | 735 | </div> |
724 | 736 | <div className="chart-body chart-body-dark"> |
725 | 737 | {trendSeries.length > 0 ? ( |
726 | | - <ResponsiveContainer width="100%" height={280}> |
727 | | - <AreaChart data={trendSeries} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}> |
728 | | - <defs> |
729 | | - <linearGradient id="gradSkillTokens" x1="0" y1="0" x2="0" y2="1"> |
730 | | - <stop offset="0%" stopColor="#3b82f6" stopOpacity={0.4} /> |
731 | | - <stop offset="100%" stopColor="#3b82f6" stopOpacity={0} /> |
732 | | - </linearGradient> |
733 | | - </defs> |
734 | | - <CartesianGrid strokeDasharray="4 4" stroke={isDarkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)'} /> |
735 | | - <XAxis dataKey="label" stroke={isDarkMode ? '#6e7681' : '#57606a'} tick={CHART_TYPOGRAPHY.axisTick} axisLine={false} tickLine={false} /> |
736 | | - <YAxis stroke={isDarkMode ? '#6e7681' : '#57606a'} tick={CHART_TYPOGRAPHY.axisTick} axisLine={false} tickLine={false} tickFormatter={(v) => v >= 1_000_000 ? `${(v / 1_000_000).toFixed(1)}M` : v.toLocaleString()} /> |
737 | | - <Tooltip content={({ active, payload, label }) => { |
738 | | - if (!active || !payload?.length) return null |
739 | | - const item = payload[0].payload |
740 | | - return ( |
741 | | - <div style={{ padding: '8px 10px', background: isDarkMode ? 'rgba(15,23,42,0.95)' : 'white', border: `1px solid ${isDarkMode ? 'rgba(255,255,255,0.08)' : '#e2e8f0'}`, borderRadius: 8 }}> |
742 | | - <div style={{ ...CHART_TYPOGRAPHY.tooltipLabel, marginBottom: 4 }}>{label}</div> |
743 | | - <div style={CHART_TYPOGRAPHY.tooltipItem}>Attempts: {(item.run_count || 0).toLocaleString()}</div> |
744 | | - <div style={CHART_TYPOGRAPHY.tooltipItem}>Success: {(item.success_count || 0).toLocaleString()}</div> |
745 | | - <div style={CHART_TYPOGRAPHY.tooltipItem}>Failure: {(item.failure_count || 0).toLocaleString()}</div> |
746 | | - <div style={CHART_TYPOGRAPHY.tooltipItem}>Input: {formatNumber(item.input_tokens)}</div> |
747 | | - <div style={CHART_TYPOGRAPHY.tooltipItem}>Output: {formatNumber(item.output_tokens)}</div> |
748 | | - {useRunFallbackSeries && <div style={CHART_TYPOGRAPHY.tooltipItem}>Runs: {formatNumber(item.run_count)}</div>} |
749 | | - <div style={{ ...CHART_TYPOGRAPHY.tooltipItem, color: '#10b981' }}>Cost: {formatCost(item.estimated_cost)}</div> |
750 | | - </div> |
751 | | - ) |
752 | | - }} /> |
753 | | - {useRunFallbackSeries ? ( |
754 | | - <Area type="monotone" dataKey="run_count" name="Runs" stroke="#f59e0b" fillOpacity={0.25} fill="#f59e0b" strokeWidth={2} /> |
755 | | - ) : ( |
756 | | - <> |
757 | | - <Area type="monotone" dataKey="input_tokens" name="Input" stroke="#3b82f6" fill="url(#gradSkillTokens)" strokeWidth={2} /> |
758 | | - <Area type="monotone" dataKey="output_tokens" name="Output" stroke="#8b5cf6" fillOpacity={0.2} fill="#8b5cf6" strokeWidth={2} /> |
759 | | - </> |
760 | | - )} |
761 | | - </AreaChart> |
762 | | - </ResponsiveContainer> |
| 738 | + <> |
| 739 | + <div className="skills-mixed-chart-legend"> |
| 740 | + <span className="skills-legend-chip requests"> |
| 741 | + <span className="skills-legend-dot requests" aria-hidden="true" /> |
| 742 | + Requests (column) |
| 743 | + </span> |
| 744 | + <span className="skills-legend-chip input"> |
| 745 | + <span className="skills-legend-dot input" aria-hidden="true" /> |
| 746 | + Input Tokens (line) |
| 747 | + </span> |
| 748 | + <span className="skills-legend-chip output"> |
| 749 | + <span className="skills-legend-dot output" aria-hidden="true" /> |
| 750 | + Output Tokens (line) |
| 751 | + </span> |
| 752 | + </div> |
| 753 | + <ResponsiveContainer width="100%" height={280}> |
| 754 | + <ComposedChart data={trendSeries} margin={{ top: 10, right: 18, left: 4, bottom: 0 }}> |
| 755 | + <defs> |
| 756 | + <linearGradient id="gradSkillRequests" x1="0" y1="0" x2="1" y2="0"> |
| 757 | + <stop offset="0%" stopColor="#8b5cf6" stopOpacity={0.3} /> |
| 758 | + <stop offset="100%" stopColor="#8b5cf6" stopOpacity={0.9} /> |
| 759 | + </linearGradient> |
| 760 | + </defs> |
| 761 | + <CartesianGrid strokeDasharray="3 3" stroke={isDarkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)'} vertical={false} /> |
| 762 | + <XAxis dataKey="label" stroke={isDarkMode ? '#6e7681' : '#57606a'} tick={CHART_TYPOGRAPHY.axisTick} axisLine={false} tickLine={false} /> |
| 763 | + <YAxis |
| 764 | + yAxisId="left" |
| 765 | + stroke={isDarkMode ? '#6e7681' : '#57606a'} |
| 766 | + tick={CHART_TYPOGRAPHY.axisTick} |
| 767 | + axisLine={false} |
| 768 | + tickLine={false} |
| 769 | + tickFormatter={runAxisTickFormatter} |
| 770 | + width={52} |
| 771 | + /> |
| 772 | + <YAxis |
| 773 | + yAxisId="right" |
| 774 | + orientation="right" |
| 775 | + stroke={isDarkMode ? '#6e7681' : '#57606a'} |
| 776 | + tick={CHART_TYPOGRAPHY.axisTick} |
| 777 | + axisLine={false} |
| 778 | + tickLine={false} |
| 779 | + tickFormatter={tokenAxisTickFormatter} |
| 780 | + width={56} |
| 781 | + /> |
| 782 | + <Tooltip |
| 783 | + cursor={{ fill: isDarkMode ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.03)' }} |
| 784 | + content={({ active, payload, label }) => { |
| 785 | + if (!active || !payload?.length) return null |
| 786 | + const item = payload[0].payload |
| 787 | + return ( |
| 788 | + <div className="skills-chart-tooltip"> |
| 789 | + <div className="skills-chart-tooltip-label">{label}</div> |
| 790 | + <div className="skills-chart-tooltip-item requests">Requests: {formatNumber(item.run_count || 0)}</div> |
| 791 | + <div className="skills-chart-tooltip-item input">Input Tokens: {formatNumber(item.input_tokens || 0)}</div> |
| 792 | + <div className="skills-chart-tooltip-item output">Output Tokens: {formatNumber(item.output_tokens || 0)}</div> |
| 793 | + <div className="skills-chart-tooltip-item">Success: {(item.success_count || 0).toLocaleString()}</div> |
| 794 | + <div className="skills-chart-tooltip-item">Failure: {(item.failure_count || 0).toLocaleString()}</div> |
| 795 | + <div className="skills-chart-tooltip-item cost">Cost: {formatCost(item.estimated_cost)}</div> |
| 796 | + </div> |
| 797 | + ) |
| 798 | + }} |
| 799 | + /> |
| 800 | + <Bar |
| 801 | + yAxisId="left" |
| 802 | + dataKey="run_count" |
| 803 | + name="Requests" |
| 804 | + fill="url(#gradSkillRequests)" |
| 805 | + stroke={SERIES_COLORS.requests} |
| 806 | + strokeWidth={1} |
| 807 | + radius={[0, 4, 4, 0]} |
| 808 | + maxBarSize={30} |
| 809 | + /> |
| 810 | + <Line |
| 811 | + yAxisId="right" |
| 812 | + type="monotone" |
| 813 | + dataKey="input_tokens" |
| 814 | + name="Input" |
| 815 | + stroke={SERIES_COLORS.input} |
| 816 | + strokeWidth={2.4} |
| 817 | + dot={false} |
| 818 | + activeDot={{ r: 4, strokeWidth: 2, fill: isDarkMode ? '#0b1220' : '#ffffff' }} |
| 819 | + /> |
| 820 | + <Line |
| 821 | + yAxisId="right" |
| 822 | + type="monotone" |
| 823 | + dataKey="output_tokens" |
| 824 | + name="Output" |
| 825 | + stroke={SERIES_COLORS.output} |
| 826 | + strokeWidth={2.4} |
| 827 | + dot={false} |
| 828 | + activeDot={{ r: 4, strokeWidth: 2, fill: isDarkMode ? '#0b1220' : '#ffffff' }} |
| 829 | + /> |
| 830 | + </ComposedChart> |
| 831 | + </ResponsiveContainer> |
| 832 | + </> |
763 | 833 | ) : ( |
764 | 834 | <div className="empty-state">No {trendTime}ly stats yet</div> |
765 | 835 | )} |
|
0 commit comments