diff --git a/packages/client/src/components/hermes/usage/DailyTrend.vue b/packages/client/src/components/hermes/usage/DailyTrend.vue index 44fdf19..c446faf 100644 --- a/packages/client/src/components/hermes/usage/DailyTrend.vue +++ b/packages/client/src/components/hermes/usage/DailyTrend.vue @@ -25,7 +25,7 @@ function cacheHitRate(d: { input_tokens: number; cache_read_tokens: number }): s } const maxTokens = computed(() => - Math.max(...usageStore.dailyUsage.map(d => d.input_tokens + d.output_tokens), 1), + Math.max(...usageStore.dailyUsage.map(d => d.visualTokens), 1), ) @@ -41,12 +41,25 @@ const maxTokens = computed(() => >
+ class="bar-stack" + :style="{ height: (d.visualTokens / maxTokens * 100) + '%' }" + > +
+
+
+
{{ d.date }}
@@ -65,6 +78,12 @@ const maxTokens = computed(() => {{ usageStore.dailyUsage[usageStore.dailyUsage.length - 1]?.date.slice(5) }}
+
+
{{ t('usage.inputTokens') }}
+
{{ t('usage.outputTokens') }}
+
{{ t('usage.cacheRead') }}
+
+
@@ -80,7 +99,7 @@ const maxTokens = computed(() => - + @@ -135,21 +154,38 @@ const maxTokens = computed(() => border-radius: 2px 2px 0 0; display: flex; align-items: flex-end; + overflow: hidden; + position: relative; } -.bar-fill { +.bar-stack { + width: 100%; + display: flex; + flex-direction: column-reverse; + justify-content: flex-start; + overflow: hidden; + transition: height 0.3s ease; +} + +.bar-segment { width: 100%; - border-radius: 2px 2px 0 0; min-height: 0; transition: height 0.3s ease; - /* Bottom = output (teal), top = input (blue) */ - background: linear-gradient( - to top, - #26a69a 0%, - #26a69a var(--output-pct, 50%), - #5c6bc0 var(--output-pct, 50%), - #5c6bc0 100% - ); +} + +.bar-segment.output, +.legend-swatch.output { + background: #26a69a; +} + +.bar-segment.input, +.legend-swatch.input { + background: #5c6bc0; +} + +.bar-segment.cache, +.legend-swatch.cache { + background: #f6ad55; } .bar-tooltip { @@ -184,12 +220,10 @@ const maxTokens = computed(() => .tooltip-date { font-weight: 600; - margin-bottom: 2px; + margin-bottom: 4px; } .tooltip-row { - font-size: 10px; - opacity: 0.85; line-height: 1.5; } @@ -198,46 +232,59 @@ const maxTokens = computed(() => justify-content: space-between; font-size: 10px; color: $text-muted; - margin-top: 4px; - margin-bottom: 16px; + margin-bottom: 12px; +} + +.chart-legend { + display: flex; + flex-wrap: wrap; + gap: 8px 14px; + margin: 0 0 16px; + color: $text-muted; + font-size: 11px; +} + +.legend-item { + display: inline-flex; + align-items: center; + gap: 5px; +} + +.legend-swatch { + width: 8px; + height: 8px; + border-radius: 2px; + flex-shrink: 0; } .trend-table { overflow-x: auto; -} -table { - width: 100%; - border-collapse: collapse; - font-size: 12px; -} + table { + width: 100%; + border-collapse: collapse; + font-size: 11px; + } -thead { - position: sticky; - top: 0; -} + th, + td { + text-align: right; + padding: 6px 8px; + border-bottom: 1px solid $border-color; + } -th { - text-align: left; - padding: 8px 10px; - font-weight: 600; - color: $text-muted; - border-bottom: 1px solid $border-color; - background: $bg-card; - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.3px; -} + th:first-child, + td:first-child { + text-align: left; + } -td { - padding: 6px 10px; - color: $text-secondary; - border-bottom: 1px solid $border-light; - font-family: $font-code; - font-size: 11px; -} + th { + color: $text-muted; + font-weight: 500; + } -tr:last-child td { - border-bottom: none; + td { + color: $text-secondary; + } } diff --git a/packages/client/src/components/hermes/usage/ModelBreakdown.vue b/packages/client/src/components/hermes/usage/ModelBreakdown.vue index 01984b9..5cc57ef 100644 --- a/packages/client/src/components/hermes/usage/ModelBreakdown.vue +++ b/packages/client/src/components/hermes/usage/ModelBreakdown.vue @@ -5,28 +5,61 @@ import { useUsageStore } from '@/stores/hermes/usage' const { t } = useI18n() const usageStore = useUsageStore() -const maxModelTokens = computed(() => Math.max(usageStore.modelUsage[0]?.totalTokens || 0, 1)) +const maxModelTokens = computed(() => Math.max(...usageStore.modelUsage.map(m => m.visualTokens), 1)) function formatTokens(n: number): string { if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M' if (n >= 1000) return (n / 1000).toFixed(1) + 'K' return String(n) } + +function cacheHitRate(m: { inputTokens: number; cacheTokens: number }): string { + const total = m.inputTokens + m.cacheTokens + if (total === 0) return '--' + return ((m.cacheTokens / total) * 100).toFixed(1) + '%' +}
{{ d.date }} {{ formatTokens(d.input_tokens) }} {{ formatTokens(d.output_tokens) }}