chore: bootstrap hermes-ui skeleton from 0.6.29 snapshot
Build / build (push) Canceled after 0s
NPM Lockfile Check / npm ci --ignore-scripts (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s

Import the client, server, skills, tests, and build scripts as the starting
point for the rebuild, then trim the surfaces this project does not ship:
Electron desktop packaging, the marketing website, and the ESP32 firmware
tree. CI keeps the web build, e2e, lockfile, release, and Docker workflows.

Rename the package and CLI entry points to hermes-ui, drop the prepare hook so
npm ci no longer triggers a build, and drop the removed tsconfig and nodemon
references. Rewrite AGENTS.md, ARCHITECTURE.md, DEVELOPMENT.md, and the READMEs
so the documented package map and hard rules match this repository.

reference_code/ is ignored: it is a read-only snapshot, not an input to the build.
This commit is contained in:
2026-10-16 10:34:35 +08:00
commit 1e9ea89d86
912 changed files with 287328 additions and 0 deletions
+216
View File
@@ -0,0 +1,216 @@
import { fetchUsageStats, type UsageStatsResponse } from '@/api/hermes/sessions'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
interface DailyUsage {
date: string
input_tokens: number
output_tokens: number
cache_read_tokens: number
cache_write_tokens: number
sessions: number
errors: number
cost: number
visualTokens: number
inputPercent: number
outputPercent: number
cachePercent: number
}
interface ModelUsage {
model: string
inputTokens: number
outputTokens: number
cacheTokens: number
cacheWriteTokens: number
totalTokens: number
visualTokens: number
sessions: number
color: string
inputPercent: number
outputPercent: number
cachePercent: number
}
interface AgentUsage {
agent: string
inputTokens: number
outputTokens: number
cacheTokens: number
cacheWriteTokens: number
totalTokens: number
visualTokens: number
sessions: number
color: string
inputPercent: number
outputPercent: number
cachePercent: number
}
const MODEL_COLORS = [
'#4fd1c5',
'#63b3ed',
'#f6ad55',
'#b794f4',
'#68d391',
'#fc8181',
'#f687b3',
'#90cdf4',
'#fbd38d',
'#9ae6b4',
]
function normalizeModel(model: string | null | undefined): string {
const trimmed = (model || '').trim()
return trimmed || 'unknown'
}
function percent(part: number, total: number): number {
if (total <= 0) return 0
return part / total * 100
}
function getModelColor(model: string): string {
const normalized = normalizeModel(model)
let hash = 0
for (let i = 0; i < normalized.length; i += 1) {
hash = ((hash << 5) - hash) + normalized.charCodeAt(i)
hash |= 0
}
return MODEL_COLORS[Math.abs(hash) % MODEL_COLORS.length]
}
export const useUsageStore = defineStore('usage', () => {
const stats = ref<UsageStatsResponse | null>(null)
const isLoading = ref(false)
let latestRequestId = 0
async function loadSessions(days = 30) {
const requestId = ++latestRequestId
isLoading.value = true
try {
const response = await fetchUsageStats(days)
if (requestId === latestRequestId) {
stats.value = response
}
} catch (err) {
if (requestId === latestRequestId) {
console.error('Failed to load usage stats:', err)
}
} finally {
if (requestId === latestRequestId) {
isLoading.value = false
}
}
}
const hasData = computed(() => !!stats.value && stats.value.total_sessions > 0)
const totalInputTokens = computed(() => stats.value?.total_input_tokens ?? 0)
const totalOutputTokens = computed(() => stats.value?.total_output_tokens ?? 0)
const totalTokens = computed(() => totalInputTokens.value + totalOutputTokens.value)
const totalSessions = computed(() => stats.value?.total_sessions ?? 0)
const totalCacheTokens = computed(() => stats.value?.total_cache_read_tokens ?? 0)
const cacheHitRate = computed(() => {
const total = totalInputTokens.value + totalCacheTokens.value
if (total === 0) return null
return ((totalCacheTokens.value / total) * 100)
})
const estimatedCost = computed(() => stats.value?.total_cost ?? 0)
const modelUsage = computed<ModelUsage[]>(() => {
if (!stats.value) return []
return stats.value.model_usage.map(m => {
const model = normalizeModel(m.model)
const totalTokens = m.input_tokens + m.output_tokens
const visualTokens = totalTokens + m.cache_read_tokens
return {
model,
inputTokens: m.input_tokens,
outputTokens: m.output_tokens,
cacheTokens: m.cache_read_tokens,
cacheWriteTokens: m.cache_write_tokens,
totalTokens,
visualTokens,
sessions: m.sessions,
color: getModelColor(model),
inputPercent: percent(m.input_tokens, visualTokens),
outputPercent: percent(m.output_tokens, visualTokens),
cachePercent: percent(m.cache_read_tokens, visualTokens),
}
}).sort((a, b) => b.visualTokens - a.visualTokens)
})
const modelLegend = computed(() => {
const seen = new Set<string>()
return modelUsage.value.filter(m => {
if (seen.has(m.model)) return false
seen.add(m.model)
return true
}).map(m => ({ model: m.model, color: m.color }))
})
const agentUsage = computed<AgentUsage[]>(() => {
if (!stats.value) return []
return (stats.value.agent_usage ?? []).map(a => {
const agent = (a.agent || '').trim() || 'unknown'
const totalTokens = a.input_tokens + a.output_tokens
const visualTokens = totalTokens + a.cache_read_tokens
return {
agent,
inputTokens: a.input_tokens,
outputTokens: a.output_tokens,
cacheTokens: a.cache_read_tokens,
cacheWriteTokens: a.cache_write_tokens,
totalTokens,
visualTokens,
sessions: a.sessions,
color: getModelColor(`agent:${agent}`),
inputPercent: percent(a.input_tokens, visualTokens),
outputPercent: percent(a.output_tokens, visualTokens),
cachePercent: percent(a.cache_read_tokens, visualTokens),
}
}).sort((a, b) => b.visualTokens - a.visualTokens)
})
const dailyUsage = computed<DailyUsage[]>(() => (stats.value?.daily_usage ?? []).map(d => {
const visualTokens = d.input_tokens + d.output_tokens + d.cache_read_tokens
return {
...d,
visualTokens,
inputPercent: percent(d.input_tokens, visualTokens),
outputPercent: percent(d.output_tokens, visualTokens),
cachePercent: percent(d.cache_read_tokens, visualTokens),
}
}))
const avgSessionsPerDay = computed(() => {
if (!stats.value || stats.value.daily_usage.length === 0) return 0
const daysWithActivity = stats.value.daily_usage.filter(d => d.sessions > 0).length
const days = Math.max(1, daysWithActivity)
return totalSessions.value / days
})
return {
stats,
isLoading,
hasData,
loadSessions,
totalInputTokens,
totalOutputTokens,
totalTokens,
totalSessions,
totalCacheTokens,
cacheHitRate,
estimatedCost,
modelUsage,
modelLegend,
agentUsage,
dailyUsage,
avgSessionsPerDay,
getModelColor,
}
})