feat: add usage statistics page, CLI improvements, and UI enhancements
- Add Usage Stats page with token breakdown, model distribution, and 30-day trend - Pass through cache/cost token fields in BFF (cache_read/write_tokens, reasoning_tokens, actual_cost_usd) - Add CLI commands: -v/--version, -h/--help, update/upgrade with auto-restart - Auto-open browser on startup, auto-kill port conflicts (cross-platform) - Validate all api_server config fields on startup (enabled, host, port, key, cors_origins) - Add streaming thinking video animation with tool calls panel - Add context token usage display (used / total) in chat header - Sidebar: white logo area with shadow, dance video beside logo (canvas seamless loop) - Fix sidebar nav scroll (app-main overflow-y: auto) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import { fetchSessions, type SessionSummary } from '@/api/sessions'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
interface DailyUsage {
|
||||
date: string
|
||||
tokens: number
|
||||
cache: number
|
||||
sessions: number
|
||||
cost: number
|
||||
}
|
||||
|
||||
interface ModelUsage {
|
||||
model: string
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheTokens: number
|
||||
totalTokens: number
|
||||
sessions: number
|
||||
}
|
||||
|
||||
export const useUsageStore = defineStore('usage', () => {
|
||||
const sessions = ref<SessionSummary[]>([])
|
||||
const isLoading = ref(false)
|
||||
|
||||
async function loadSessions() {
|
||||
isLoading.value = true
|
||||
try {
|
||||
sessions.value = await fetchSessions()
|
||||
} catch (err) {
|
||||
console.error('Failed to load sessions for usage:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const totalInputTokens = computed(() =>
|
||||
sessions.value.reduce((sum, s) => sum + (s.input_tokens || 0), 0),
|
||||
)
|
||||
|
||||
const totalOutputTokens = computed(() =>
|
||||
sessions.value.reduce((sum, s) => sum + (s.output_tokens || 0), 0),
|
||||
)
|
||||
|
||||
const totalTokens = computed(() => totalInputTokens.value + totalOutputTokens.value)
|
||||
|
||||
const totalSessions = computed(() => sessions.value.length)
|
||||
|
||||
const totalCacheTokens = computed(() =>
|
||||
sessions.value.reduce((sum, s) => sum + (s.cache_read_tokens || 0), 0),
|
||||
)
|
||||
|
||||
const cacheHitRate = computed(() => {
|
||||
const total = totalInputTokens.value
|
||||
if (total === 0) return null
|
||||
return ((totalCacheTokens.value / total) * 100)
|
||||
})
|
||||
|
||||
const estimatedCost = computed(() =>
|
||||
sessions.value.reduce((sum, s) => {
|
||||
const cost = s.actual_cost_usd ?? s.estimated_cost_usd ?? 0
|
||||
return sum + cost
|
||||
}, 0),
|
||||
)
|
||||
|
||||
const modelUsage = computed<ModelUsage[]>(() => {
|
||||
const map = new Map<string, ModelUsage>()
|
||||
for (const s of sessions.value) {
|
||||
const key = s.model || 'unknown'
|
||||
if (!map.has(key)) {
|
||||
map.set(key, {
|
||||
model: key,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheTokens: 0,
|
||||
totalTokens: 0,
|
||||
sessions: 0,
|
||||
})
|
||||
}
|
||||
const entry = map.get(key)!
|
||||
entry.inputTokens += s.input_tokens || 0
|
||||
entry.outputTokens += s.output_tokens || 0
|
||||
entry.cacheTokens += s.cache_read_tokens || 0
|
||||
entry.totalTokens += (s.input_tokens || 0) + (s.output_tokens || 0)
|
||||
entry.sessions += 1
|
||||
}
|
||||
return [...map.values()].sort((a, b) => b.totalTokens - a.totalTokens)
|
||||
})
|
||||
|
||||
const dailyUsage = computed<DailyUsage[]>(() => {
|
||||
const map = new Map<string, DailyUsage>()
|
||||
const now = new Date()
|
||||
|
||||
// Initialize last 30 days
|
||||
for (let i = 29; i >= 0; i--) {
|
||||
const d = new Date(now)
|
||||
d.setDate(d.getDate() - i)
|
||||
const key = d.toISOString().slice(0, 10)
|
||||
map.set(key, { date: key, tokens: 0, cache: 0, sessions: 0, cost: 0 })
|
||||
}
|
||||
|
||||
for (const s of sessions.value) {
|
||||
const d = new Date(s.started_at * 1000)
|
||||
const key = d.toISOString().slice(0, 10)
|
||||
const entry = map.get(key)
|
||||
if (entry) {
|
||||
entry.tokens += (s.input_tokens || 0) + (s.output_tokens || 0)
|
||||
entry.cache += s.cache_read_tokens || 0
|
||||
entry.sessions += 1
|
||||
const cost = s.actual_cost_usd ?? s.estimated_cost_usd ?? 0
|
||||
entry.cost += cost
|
||||
}
|
||||
}
|
||||
|
||||
return [...map.values()]
|
||||
})
|
||||
|
||||
const avgSessionsPerDay = computed(() => {
|
||||
const firstDate = sessions.value.length > 0
|
||||
? new Date(sessions.value[sessions.value.length - 1].started_at * 1000)
|
||||
: new Date()
|
||||
const days = Math.max(1, Math.ceil((Date.now() - firstDate.getTime()) / (1000 * 60 * 60 * 24)))
|
||||
return totalSessions.value / days
|
||||
})
|
||||
|
||||
return {
|
||||
sessions,
|
||||
isLoading,
|
||||
loadSessions,
|
||||
totalInputTokens,
|
||||
totalOutputTokens,
|
||||
totalTokens,
|
||||
totalSessions,
|
||||
totalCacheTokens,
|
||||
cacheHitRate,
|
||||
estimatedCost,
|
||||
modelUsage,
|
||||
dailyUsage,
|
||||
avgSessionsPerDay,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user