From 3da9d94482ae4394a4d368cf045278429104c6c5 Mon Sep 17 00:00:00 2001 From: Oleg Miagkov Date: Fri, 24 Jul 2026 19:22:23 +0400 Subject: [PATCH] feat(ui): Model Usage pilot on the shared design system (U5 B2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fifth and final B2 slice — closes the batch. Reuses the generic AnalyticsDashboard (KPIs + trend charts + BarList + rail) with no new libs/ui component: - Electron: pure model-usage-ui mappers (shortModelName strips the provider prefix + date suffix; buildModelKpis/Charts/BarLists/Sections from ModelUsageSummary + ModelUsageTrendPoint, reusing analytics-ui's formatCount/formatCost/shortDate/sampleLabels; 8 tests) + ModelUsagePilotView over getModelUsageSummary + getModelUsageTrends. Mounted as "Model Usage (new UI)". - i18n: model-usage:modelUsagePilot.* keys (en+fr), navigation.modelUsageNext; also imports the Cpu icon for the nav item. The dashboard shows token/cost trend charts + AI-call/token/cost/model KPIs with sparklines, a "tokens by model" BarList ranked by usage, and an input/output token-split + provider-count rail card. This exercises all four AnalyticsDashboard slots (kpis/charts/barLists/sections) at once, confirming the generic screen serves every B2 view. Co-Authored-By: Claude Fable 5 --- apps/frontend/src/renderer/App.tsx | 4 + .../renderer/__tests__/model-usage-ui.test.ts | 145 +++++++++++++++ .../components/ModelUsagePilotView.tsx | 170 ++++++++++++++++++ .../src/renderer/components/Sidebar.tsx | 8 +- .../src/renderer/lib/model-usage-ui.ts | 168 +++++++++++++++++ .../shared/i18n/locales/en/model-usage.json | 34 ++++ .../shared/i18n/locales/en/navigation.json | 3 +- .../shared/i18n/locales/fr/model-usage.json | 34 ++++ .../shared/i18n/locales/fr/navigation.json | 3 +- 9 files changed, 564 insertions(+), 5 deletions(-) create mode 100644 apps/frontend/src/renderer/__tests__/model-usage-ui.test.ts create mode 100644 apps/frontend/src/renderer/components/ModelUsagePilotView.tsx create mode 100644 apps/frontend/src/renderer/lib/model-usage-ui.ts diff --git a/apps/frontend/src/renderer/App.tsx b/apps/frontend/src/renderer/App.tsx index 7e47904f7..c4ef9b405 100644 --- a/apps/frontend/src/renderer/App.tsx +++ b/apps/frontend/src/renderer/App.tsx @@ -77,6 +77,7 @@ import { PatternsPilotView } from './components/PatternsPilotView'; import { AnalyticsPilotView } from './components/AnalyticsPilotView'; import { ProductivityPilotView } from './components/ProductivityPilotView'; import { MergeAnalyticsPilotView } from './components/MergeAnalyticsPilotView'; +import { ModelUsagePilotView } from './components/ModelUsagePilotView'; import { useSettingsStore, loadSettings, loadProfiles, saveSettings } from './stores/settings-store'; import { useClaudeProfileStore } from './stores/claude-profile-store'; import { useTerminalStore, restoreTerminalSessions } from './stores/terminal-store'; @@ -1151,6 +1152,9 @@ export function App() { {activeView === 'model-usage' && (activeProjectId || selectedProjectId) && ( )} + {activeView === 'model-usage-next' && (activeProjectId || selectedProjectId) && ( + + )} {activeView === 'merge-analytics' && (activeProjectId || selectedProjectId) && ( )} diff --git a/apps/frontend/src/renderer/__tests__/model-usage-ui.test.ts b/apps/frontend/src/renderer/__tests__/model-usage-ui.test.ts new file mode 100644 index 000000000..f84cd68e6 --- /dev/null +++ b/apps/frontend/src/renderer/__tests__/model-usage-ui.test.ts @@ -0,0 +1,145 @@ +/** + * Tests for the ModelUsage{Summary,Trends} → AnalyticsDashboard mappers. + */ + +import { describe, expect, it } from 'vitest'; +import { + buildModelBarLists, + buildModelCharts, + buildModelKpis, + buildModelSections, + shortModelName, +} from '../lib/model-usage-ui'; +import type { + ModelMetrics, + ModelUsageSummary, + ModelUsageTrendPoint, +} from '../../shared/types'; + +function makeModel(overrides: Partial = {}): ModelMetrics { + return { + model: 'anthropic/claude-sonnet-4-5-20250929', + provider: 'anthropic', + total_input_tokens: 800_000, + total_output_tokens: 200_000, + usage_by_agent: {}, + total_usage_count: 40, + total_tokens: 1_000_000, + total_cost: 12.5, + first_used: null, + last_used: null, + ...overrides, + }; +} + +function makeSummary(overrides: Partial = {}): ModelUsageSummary { + return { + period_start: '2026-05-01T00:00:00Z', + period_end: '2026-05-22T00:00:00Z', + total_usage_count: 64, + total_tokens: 1_680_000, + total_cost: 18.7, + models: [ + makeModel(), + makeModel({ + model: 'anthropic/claude-haiku-4-5-20251001', + total_tokens: 680_000, + total_input_tokens: 500_000, + total_output_tokens: 180_000, + total_cost: 6.2, + }), + ], + agents: [], + top_models: [], + ...overrides, + } as ModelUsageSummary; +} + +const TRENDS: ModelUsageTrendPoint[] = [ + { date: '2026-05-01', total_tokens: 400_000, total_cost: 4, usage_count: 12, unique_models: 2 }, + { date: '2026-05-08', total_tokens: 620_000, total_cost: 7, usage_count: 20, unique_models: 2 }, + { date: '2026-05-15', total_tokens: 660_000, total_cost: 8, usage_count: 22, unique_models: 3 }, +]; + +describe('shortModelName', () => { + it('drops the provider prefix and date suffix', () => { + expect(shortModelName('anthropic/claude-sonnet-4-5-20250929')).toBe('claude-sonnet-4-5'); + expect(shortModelName('gpt-4o')).toBe('gpt-4o'); + }); +}); + +describe('buildModelKpis', () => { + const labels = { + calls: 'calls', + callsSub: 'all', + tokens: 'tokens', + cost: 'cost', + models: 'models', + modelsSub: 'distinct', + }; + + it('maps the summary + trend sparklines onto four tiles', () => { + const kpis = buildModelKpis(makeSummary(), TRENDS, labels); + expect(kpis.map((k) => k.value)).toEqual(['64', '1.7M', '$18.70', '2']); + expect(kpis[0].trend).toEqual([12, 20, 22]); + expect(kpis[2].trend).toEqual([4, 7, 8]); + }); + + it('omits sparklines when trends are too short', () => { + const kpis = buildModelKpis(makeSummary(), [TRENDS[0]], labels); + expect(kpis[0].trend).toBeUndefined(); + }); +}); + +describe('buildModelCharts', () => { + const labels = { + tokensTitle: 'Tokens', + tokensAria: 't', + tokensSeries: 'Tokens', + costTitle: 'Cost', + costAria: 'c', + costSeries: 'Cost', + }; + + it('builds token + cost trend charts with UTC labels', () => { + const charts = buildModelCharts(TRENDS, labels, 'en-US'); + expect(charts.map((c) => c.title)).toEqual(['Tokens', 'Cost']); + expect(charts[0].series[0].values).toEqual([400_000, 620_000, 660_000]); + expect(charts[0].xLabels).toEqual(['May 1', 'May 8', 'May 15']); + }); + + it('returns no charts without at least two trend points', () => { + expect(buildModelCharts([TRENDS[0]], labels)).toEqual([]); + }); +}); + +describe('buildModelBarLists', () => { + it('ranks models by tokens with formatted value labels', () => { + const lists = buildModelBarLists(makeSummary().models, 'Tokens by model', 'aria'); + expect(lists).toHaveLength(1); + expect(lists[0].items).toEqual([ + { label: 'claude-sonnet-4-5', value: 1_000_000, valueLabel: '1.0M', tone: 'info' }, + { label: 'claude-haiku-4-5', value: 680_000, valueLabel: '680.0k', tone: 'info' }, + ]); + }); + + it('returns no card when there are no models', () => { + expect(buildModelBarLists([], 't', 'a')).toEqual([]); + }); +}); + +describe('buildModelSections', () => { + it('sums the input/output token split and counts providers', () => { + const sections = buildModelSections(makeSummary(), { + splitTitle: 'Split', + inputTokens: 'Input', + outputTokens: 'Output', + providers: 'Providers', + }); + expect(sections[0].rows).toEqual([ + { label: 'Input', value: '1.3M' }, + { label: 'Output', value: '380.0k' }, + { label: 'Providers', value: '1' }, + ]); + }); +}); diff --git a/apps/frontend/src/renderer/components/ModelUsagePilotView.tsx b/apps/frontend/src/renderer/components/ModelUsagePilotView.tsx new file mode 100644 index 000000000..3ca09f38c --- /dev/null +++ b/apps/frontend/src/renderer/components/ModelUsagePilotView.tsx @@ -0,0 +1,170 @@ +/** + * Model Usage pilot on the shared design system (U5 B2). + * + * Renders `libs/ui`'s AnalyticsDashboard from the existing + * `getModelUsageSummary` + `getModelUsageTrends` IPC, mapped with the pure + * `model-usage-ui` helpers. KPIs + token/cost trend charts + a per-model + * BarList. Read-only — the legacy Model Usage view keeps its export flow. + * Reachable via the "Model Usage (new UI)" sidebar item. + */ + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { AnalyticsDashboard } from '@auto-code/ui'; +import type { + UiBarListCard, + UiChartCard, + UiKpi, + UiMetaSection, +} from '@auto-code/ui'; +import type { + ModelUsageSummary, + ModelUsageTrendPoint, +} from '../../shared/types'; +import { + buildModelBarLists, + buildModelCharts, + buildModelKpis, + buildModelSections, +} from '../lib/model-usage-ui'; + +interface ModelUsagePilotViewProps { + projectId: string; +} + +interface PilotState { + summary: ModelUsageSummary | null; + trends: ModelUsageTrendPoint[]; + loading: boolean; + error: Error | null; +} + +export function ModelUsagePilotView({ + projectId, +}: Readonly) { + const { t, i18n } = useTranslation(['model-usage']); + const [state, setState] = useState({ + summary: null, + trends: [], + loading: true, + error: null, + }); + const [reloadKey, setReloadKey] = useState(0); + + useEffect(() => { + let active = true; + setState({ summary: null, trends: [], loading: true, error: null }); + Promise.all([ + window.electronAPI.getModelUsageSummary(projectId), + window.electronAPI.getModelUsageTrends(projectId), + ]) + .then(([summaryResult, trendsResult]) => { + if (!active) return; + if (!summaryResult.success || summaryResult.data == null) { + console.error( + '[ModelUsagePilotView] Failed to load summary:', + summaryResult.success ? 'no data' : summaryResult.error, + ); + setState({ + summary: null, + trends: [], + loading: false, + error: new Error(t('model-usage:modelUsagePilot.error')), + }); + return; + } + setState({ + summary: summaryResult.data, + trends: trendsResult.success ? (trendsResult.data ?? []) : [], + loading: false, + error: null, + }); + }) + .catch((err: unknown) => { + if (!active) return; + console.error('[ModelUsagePilotView] Failed to load model usage:', err); + setState({ + summary: null, + trends: [], + loading: false, + error: new Error(t('model-usage:modelUsagePilot.error')), + }); + }); + return () => { + active = false; + }; + }, [projectId, reloadKey, t]); + + const reload = useCallback(() => setReloadKey((key) => key + 1), []); + + const kpis = useMemo(() => { + if (state.summary == null) return null; + return buildModelKpis(state.summary, state.trends, { + calls: t('model-usage:modelUsagePilot.kpis.calls'), + callsSub: t('model-usage:modelUsagePilot.kpis.callsSub'), + tokens: t('model-usage:modelUsagePilot.kpis.tokens'), + cost: t('model-usage:modelUsagePilot.kpis.cost'), + models: t('model-usage:modelUsagePilot.kpis.models'), + modelsSub: t('model-usage:modelUsagePilot.kpis.modelsSub'), + }); + }, [state.summary, state.trends, t]); + + const charts = useMemo(() => { + if (state.summary == null) return undefined; + return buildModelCharts( + state.trends, + { + tokensTitle: t('model-usage:modelUsagePilot.charts.tokensTitle'), + tokensAria: t('model-usage:modelUsagePilot.charts.tokensAria'), + tokensSeries: t('model-usage:modelUsagePilot.charts.tokensSeries'), + costTitle: t('model-usage:modelUsagePilot.charts.costTitle'), + costAria: t('model-usage:modelUsagePilot.charts.costAria'), + costSeries: t('model-usage:modelUsagePilot.charts.costSeries'), + }, + i18n.language, + ); + }, [state.summary, state.trends, t, i18n.language]); + + const barLists = useMemo(() => { + if (state.summary == null) return undefined; + return buildModelBarLists( + state.summary.models, + t('model-usage:modelUsagePilot.models.title'), + t('model-usage:modelUsagePilot.models.aria'), + ); + }, [state.summary, t]); + + const sections = useMemo(() => { + if (state.summary == null) return undefined; + return buildModelSections(state.summary, { + splitTitle: t('model-usage:modelUsagePilot.sections.splitTitle'), + inputTokens: t('model-usage:modelUsagePilot.sections.inputTokens'), + outputTokens: t('model-usage:modelUsagePilot.sections.outputTokens'), + providers: t('model-usage:modelUsagePilot.sections.providers'), + }); + }, [state.summary, t]); + + const stateLabels = useMemo( + () => ({ + loading: t('model-usage:modelUsagePilot.states.loading'), + retry: t('model-usage:modelUsagePilot.states.retry'), + empty: t('model-usage:modelUsagePilot.states.empty'), + }), + [t], + ); + + return ( +
+ +
+ ); +} diff --git a/apps/frontend/src/renderer/components/Sidebar.tsx b/apps/frontend/src/renderer/components/Sidebar.tsx index ac230e314..e36fb20bc 100644 --- a/apps/frontend/src/renderer/components/Sidebar.tsx +++ b/apps/frontend/src/renderer/components/Sidebar.tsx @@ -34,7 +34,8 @@ import { MessageSquare, Code, Brain, - LayoutDashboard + LayoutDashboard, + Cpu } from 'lucide-react'; import { Button } from './ui/button'; import { ScrollArea } from './ui/scroll-area'; @@ -71,7 +72,7 @@ import { SessionContextIndicator } from './SessionContextIndicator'; import { NavIndicator } from './NavIndicator'; import type { Project, AutoBuildVersionInfo, GitStatus, ProjectEnvConfig } from '../../shared/types'; -export type SidebarView = 'kanban' | 'kanban-next' | 'terminals' | 'roadmap' | 'context' | 'ideation' | 'webhooks' | 'github-issues' | 'github-issues-next' | 'gitlab-issues' | 'github-prs' | 'github-prs-next' | 'gitlab-merge-requests' | 'changelog' | 'changelog-next' | 'insights' | 'worktrees' | 'agent-tools' | 'plugins' | 'analytics' | 'analytics-next' | 'productivity' | 'productivity-next' | 'merge-analytics' | 'merge-analytics-next' | 'sessions' | 'scheduler' | 'feedback' | 'patterns' | 'patterns-next' | 'model-usage' | 'agent-inspector'; +export type SidebarView = 'kanban' | 'kanban-next' | 'terminals' | 'roadmap' | 'context' | 'ideation' | 'webhooks' | 'github-issues' | 'github-issues-next' | 'gitlab-issues' | 'github-prs' | 'github-prs-next' | 'gitlab-merge-requests' | 'changelog' | 'changelog-next' | 'insights' | 'worktrees' | 'agent-tools' | 'plugins' | 'analytics' | 'analytics-next' | 'productivity' | 'productivity-next' | 'merge-analytics' | 'merge-analytics-next' | 'sessions' | 'scheduler' | 'feedback' | 'patterns' | 'patterns-next' | 'model-usage' | 'model-usage-next' | 'agent-inspector'; interface SidebarProps { onSettingsClick: () => void; @@ -114,7 +115,8 @@ const baseNavItems: NavItem[] = [ { id: 'sessions', labelKey: 'navigation:items.sessions', icon: Play }, { id: 'agent-inspector', labelKey: 'navigation:items.agentInspector', icon: Brain }, { id: 'feedback', labelKey: 'navigation:items.feedback', icon: MessageSquare, shortcut: 'F' }, - { id: 'model-usage', labelKey: 'navigation:items.modelUsage', icon: Database, shortcut: 'O' } + { id: 'model-usage', labelKey: 'navigation:items.modelUsage', icon: Database, shortcut: 'O' }, + { id: 'model-usage-next', labelKey: 'navigation:items.modelUsageNext', icon: Cpu }, ]; // GitHub nav items shown when GitHub is enabled diff --git a/apps/frontend/src/renderer/lib/model-usage-ui.ts b/apps/frontend/src/renderer/lib/model-usage-ui.ts new file mode 100644 index 000000000..8a4c44d35 --- /dev/null +++ b/apps/frontend/src/renderer/lib/model-usage-ui.ts @@ -0,0 +1,168 @@ +/** + * Pure mapping helpers between the ModelUsageSummary/Trends IPC shapes and the + * shared AnalyticsDashboard. Model Usage is line/sparkline + a per-model + * distribution, so it exercises KPIs, trend charts, a BarList, and a rail card. + */ + +import type { + UiBarListCard, + UiChartCard, + UiKpi, + UiMetaSection, +} from '@auto-code/ui'; +import type { + ModelMetrics, + ModelUsageSummary, + ModelUsageTrendPoint, +} from '../../shared/types'; +import { + formatCost, + formatCount, + sampleLabels, + shortDate, +} from './analytics-ui'; + +/** Drop a provider prefix and date suffix so bars read as short model names. */ +export function shortModelName(model: string): string { + return model.replace(/^[^/]+\//, '').replace(/-\d{6,8}$/, ''); +} + +export interface ModelKpiLabels { + calls: string; + callsSub: string; + tokens: string; + cost: string; + models: string; + modelsSub: string; +} + +export function buildModelKpis( + summary: ModelUsageSummary, + trends: readonly ModelUsageTrendPoint[], + labels: ModelKpiLabels, +): UiKpi[] { + const usageSeries = trends.map((point) => point.usage_count); + const tokenSeries = trends.map((point) => point.total_tokens); + const costSeries = trends.map((point) => point.total_cost); + return [ + { + value: String(summary.total_usage_count), + label: labels.calls, + sub: labels.callsSub, + trend: usageSeries.length >= 2 ? usageSeries : undefined, + trendTone: 'info', + }, + { + value: formatCount(summary.total_tokens), + label: labels.tokens, + trend: tokenSeries.length >= 2 ? tokenSeries : undefined, + trendTone: 'info', + }, + { + value: formatCost(summary.total_cost), + label: labels.cost, + trend: costSeries.length >= 2 ? costSeries : undefined, + trendTone: 'warn', + }, + { + value: String(summary.models.length), + label: labels.models, + sub: labels.modelsSub, + }, + ]; +} + +export interface ModelChartLabels { + tokensTitle: string; + tokensAria: string; + tokensSeries: string; + costTitle: string; + costAria: string; + costSeries: string; +} + +export function buildModelCharts( + trends: readonly ModelUsageTrendPoint[], + labels: ModelChartLabels, + locale?: string, +): UiChartCard[] { + if (trends.length < 2) return []; + const xLabels = sampleLabels(trends.map((p) => shortDate(p.date, locale))); + return [ + { + title: labels.tokensTitle, + ariaLabel: labels.tokensAria, + xLabels, + series: [ + { + label: labels.tokensSeries, + tone: 'info', + values: trends.map((p) => p.total_tokens), + }, + ], + }, + { + title: labels.costTitle, + ariaLabel: labels.costAria, + xLabels, + series: [ + { + label: labels.costSeries, + tone: 'warn', + values: trends.map((p) => p.total_cost), + }, + ], + }, + ]; +} + +/** Top models ranked by token usage, rendered as a BarList distribution. */ +export function buildModelBarLists( + models: readonly ModelMetrics[], + title: string, + ariaLabel: string, + limit = 8, +): UiBarListCard[] { + if (models.length === 0) return []; + const top = [...models] + .sort((a, b) => b.total_tokens - a.total_tokens) + .slice(0, limit); + return [ + { + title, + ariaLabel, + items: top.map((model) => ({ + label: shortModelName(model.model), + value: model.total_tokens, + valueLabel: formatCount(model.total_tokens), + tone: 'info', + })), + }, + ]; +} + +export interface ModelSectionLabels { + splitTitle: string; + inputTokens: string; + outputTokens: string; + providers: string; +} + +export function buildModelSections( + summary: ModelUsageSummary, + labels: ModelSectionLabels, +): UiMetaSection[] { + const input = summary.models.reduce((sum, m) => sum + m.total_input_tokens, 0); + const output = summary.models.reduce((sum, m) => sum + m.total_output_tokens, 0); + const providers = new Set(summary.models.map((m) => m.provider)).size; + return [ + { + title: labels.splitTitle, + rows: [ + { label: labels.inputTokens, value: formatCount(input) }, + { label: labels.outputTokens, value: formatCount(output) }, + { label: labels.providers, value: String(providers) }, + ], + }, + ]; +} diff --git a/apps/frontend/src/shared/i18n/locales/en/model-usage.json b/apps/frontend/src/shared/i18n/locales/en/model-usage.json index c3a14d816..5db2f1785 100644 --- a/apps/frontend/src/shared/i18n/locales/en/model-usage.json +++ b/apps/frontend/src/shared/i18n/locales/en/model-usage.json @@ -108,5 +108,39 @@ "emptyTitle": "No data available", "emptySubtitle": "Run more tasks to see trends over time", "rangeLatest": "Range" + }, + "modelUsagePilot": { + "error": "Failed to load model usage.", + "states": { + "loading": "Loading model usage…", + "retry": "Retry", + "empty": "No model usage yet." + }, + "kpis": { + "calls": "AI calls", + "callsSub": "all time", + "tokens": "tokens", + "cost": "total cost", + "models": "models used", + "modelsSub": "distinct" + }, + "charts": { + "tokensTitle": "Tokens over time", + "tokensAria": "Total tokens per day over the period", + "tokensSeries": "Tokens", + "costTitle": "Cost over time", + "costAria": "Total cost per day over the period", + "costSeries": "Cost" + }, + "models": { + "title": "Tokens by model", + "aria": "Models ranked by total token usage" + }, + "sections": { + "splitTitle": "Token split", + "inputTokens": "Input", + "outputTokens": "Output", + "providers": "Providers" + } } } diff --git a/apps/frontend/src/shared/i18n/locales/en/navigation.json b/apps/frontend/src/shared/i18n/locales/en/navigation.json index 07b3b66ac..87ec3a50d 100644 --- a/apps/frontend/src/shared/i18n/locales/en/navigation.json +++ b/apps/frontend/src/shared/i18n/locales/en/navigation.json @@ -36,7 +36,8 @@ "patternsNext": "Patterns (New UI)", "analyticsNext": "Analytics (New UI)", "productivityNext": "Productivity (New UI)", - "mergeAnalyticsNext": "Merge Analytics (New UI)" + "mergeAnalyticsNext": "Merge Analytics (New UI)", + "modelUsageNext": "Model Usage (New UI)" }, "actions": { "settings": "Settings", diff --git a/apps/frontend/src/shared/i18n/locales/fr/model-usage.json b/apps/frontend/src/shared/i18n/locales/fr/model-usage.json index c7cb4e4d6..fdfe3b8c7 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/model-usage.json +++ b/apps/frontend/src/shared/i18n/locales/fr/model-usage.json @@ -108,5 +108,39 @@ "emptyTitle": "Aucune donnée disponible", "emptySubtitle": "Exécutez plus de tâches pour voir les tendances", "rangeLatest": "Plage" + }, + "modelUsagePilot": { + "error": "Échec du chargement de l'utilisation des modèles.", + "states": { + "loading": "Chargement de l'utilisation des modèles…", + "retry": "Réessayer", + "empty": "Aucune utilisation de modèle pour le moment." + }, + "kpis": { + "calls": "appels IA", + "callsSub": "depuis le début", + "tokens": "tokens", + "cost": "coût total", + "models": "modèles utilisés", + "modelsSub": "distincts" + }, + "charts": { + "tokensTitle": "Tokens au fil du temps", + "tokensAria": "Total de tokens par jour sur la période", + "tokensSeries": "Tokens", + "costTitle": "Coût au fil du temps", + "costAria": "Coût total par jour sur la période", + "costSeries": "Coût" + }, + "models": { + "title": "Tokens par modèle", + "aria": "Modèles classés par utilisation totale de tokens" + }, + "sections": { + "splitTitle": "Répartition des tokens", + "inputTokens": "Entrée", + "outputTokens": "Sortie", + "providers": "Fournisseurs" + } } } diff --git a/apps/frontend/src/shared/i18n/locales/fr/navigation.json b/apps/frontend/src/shared/i18n/locales/fr/navigation.json index 113970652..f4b40db55 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/navigation.json +++ b/apps/frontend/src/shared/i18n/locales/fr/navigation.json @@ -36,7 +36,8 @@ "patternsNext": "Patterns (nouvelle interface)", "analyticsNext": "Analytique (nouvelle interface)", "productivityNext": "Productivité (nouvelle interface)", - "mergeAnalyticsNext": "Analytique de fusion (nouvelle interface)" + "mergeAnalyticsNext": "Analytique de fusion (nouvelle interface)", + "modelUsageNext": "Utilisation des modèles (nouvelle interface)" }, "actions": { "settings": "Paramètres",