188 lines
4.3 KiB
Vue
188 lines
4.3 KiB
Vue
<template>
|
|
<div class="card p-4">
|
|
<h3 class="mb-4 text-sm font-semibold text-gray-900 dark:text-white">
|
|
{{ t('admin.dashboard.tokenUsageTrend') }}
|
|
</h3>
|
|
<div v-if="loading" class="flex h-48 items-center justify-center">
|
|
<LoadingSpinner />
|
|
</div>
|
|
<div v-else-if="trendData.length > 0 && chartData" class="h-48">
|
|
<Line :data="chartData" :options="lineOptions" />
|
|
</div>
|
|
<div
|
|
v-else
|
|
class="flex h-48 items-center justify-center text-sm text-gray-500 dark:text-gray-400"
|
|
>
|
|
{{ t('admin.dashboard.noDataAvailable') }}
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import {
|
|
Chart as ChartJS,
|
|
CategoryScale,
|
|
LinearScale,
|
|
PointElement,
|
|
LineElement,
|
|
Title,
|
|
Tooltip,
|
|
Legend,
|
|
Filler
|
|
} from 'chart.js'
|
|
import { Line } from 'vue-chartjs'
|
|
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
|
|
import type { TrendDataPoint } from '@/types'
|
|
|
|
ChartJS.register(
|
|
CategoryScale,
|
|
LinearScale,
|
|
PointElement,
|
|
LineElement,
|
|
Title,
|
|
Tooltip,
|
|
Legend,
|
|
Filler
|
|
)
|
|
|
|
const { t } = useI18n()
|
|
|
|
const props = defineProps<{
|
|
trendData: TrendDataPoint[]
|
|
loading?: boolean
|
|
}>()
|
|
|
|
const isDarkMode = computed(() => {
|
|
return document.documentElement.classList.contains('dark')
|
|
})
|
|
|
|
const chartColors = computed(() => ({
|
|
text: isDarkMode.value ? '#e5e7eb' : '#374151',
|
|
grid: isDarkMode.value ? '#374151' : '#e5e7eb',
|
|
input: '#3b82f6',
|
|
output: '#10b981',
|
|
cache: '#f59e0b'
|
|
}))
|
|
|
|
const chartData = computed(() => {
|
|
if (!props.trendData?.length) return null
|
|
|
|
return {
|
|
labels: props.trendData.map((d) => d.date),
|
|
datasets: [
|
|
{
|
|
label: 'Input',
|
|
data: props.trendData.map((d) => d.input_tokens),
|
|
borderColor: chartColors.value.input,
|
|
backgroundColor: `${chartColors.value.input}20`,
|
|
fill: true,
|
|
tension: 0.3
|
|
},
|
|
{
|
|
label: 'Output',
|
|
data: props.trendData.map((d) => d.output_tokens),
|
|
borderColor: chartColors.value.output,
|
|
backgroundColor: `${chartColors.value.output}20`,
|
|
fill: true,
|
|
tension: 0.3
|
|
},
|
|
{
|
|
label: 'Cache',
|
|
data: props.trendData.map((d) => d.cache_tokens),
|
|
borderColor: chartColors.value.cache,
|
|
backgroundColor: `${chartColors.value.cache}20`,
|
|
fill: true,
|
|
tension: 0.3
|
|
}
|
|
]
|
|
}
|
|
})
|
|
|
|
const lineOptions = computed(() => ({
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
interaction: {
|
|
intersect: false,
|
|
mode: 'index' as const
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
position: 'top' as const,
|
|
labels: {
|
|
color: chartColors.value.text,
|
|
usePointStyle: true,
|
|
pointStyle: 'circle',
|
|
padding: 15,
|
|
font: {
|
|
size: 11
|
|
}
|
|
}
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: (context: any) => {
|
|
return `${context.dataset.label}: ${formatTokens(context.raw)}`
|
|
},
|
|
footer: (tooltipItems: any) => {
|
|
const dataIndex = tooltipItems[0]?.dataIndex
|
|
if (dataIndex !== undefined && props.trendData[dataIndex]) {
|
|
const data = props.trendData[dataIndex]
|
|
return `Actual: $${formatCost(data.actual_cost)} | Standard: $${formatCost(data.cost)}`
|
|
}
|
|
return ''
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
grid: {
|
|
color: chartColors.value.grid
|
|
},
|
|
ticks: {
|
|
color: chartColors.value.text,
|
|
font: {
|
|
size: 10
|
|
}
|
|
}
|
|
},
|
|
y: {
|
|
grid: {
|
|
color: chartColors.value.grid
|
|
},
|
|
ticks: {
|
|
color: chartColors.value.text,
|
|
font: {
|
|
size: 10
|
|
},
|
|
callback: (value: string | number) => formatTokens(Number(value))
|
|
}
|
|
}
|
|
}
|
|
}))
|
|
|
|
const formatTokens = (value: number): string => {
|
|
if (value >= 1_000_000_000) {
|
|
return `${(value / 1_000_000_000).toFixed(2)}B`
|
|
} else if (value >= 1_000_000) {
|
|
return `${(value / 1_000_000).toFixed(2)}M`
|
|
} else if (value >= 1_000) {
|
|
return `${(value / 1_000).toFixed(2)}K`
|
|
}
|
|
return value.toLocaleString()
|
|
}
|
|
|
|
const formatCost = (value: number): string => {
|
|
if (value >= 1000) {
|
|
return (value / 1000).toFixed(2) + 'K'
|
|
} else if (value >= 1) {
|
|
return value.toFixed(2)
|
|
} else if (value >= 0.01) {
|
|
return value.toFixed(3)
|
|
}
|
|
return value.toFixed(4)
|
|
}
|
|
</script>
|