Files
Aether/frontend/src/features/overview/dashboard/demo.ts
T

164 lines
8.7 KiB
TypeScript
Raw Normal View History

import type { IntervalTimelineResponse } from '@/api/cache'
import type { DailyStat, DailyStatsResponse, ModelBreakdown } from '@/api/dashboard'
import type { OverviewDashboardSummary } from '@/api/overview'
import type { DateRangeParams } from '@/features/usage/types'
const DAY_MS = 86_400_000
const HOUR_MS = 3_600_000
const modelNames = ['gpt-5.4', 'claude-sonnet-4', 'gemini-2.5-pro']
const providerNames = ['OpenAI', 'Anthropic', 'Google']
function dateKey(instant: number, timezone: string): string {
const parts = new Intl.DateTimeFormat('en-CA', {
timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit',
}).formatToParts(new Date(instant))
const part = (type: string) => parts.find(item => item.type === type)?.value ?? ''
return `${part('year')}-${part('month')}-${part('day')}`
}
function shiftDate(date: string, days: number): string {
return new Date(Date.parse(`${date}T00:00:00Z`) + days * DAY_MS).toISOString().slice(0, 10)
}
function dayStart(date: string, timezone: string): number {
// Locate the actual local date boundary, including midnight DST transitions.
const nominal = Date.parse(`${date}T00:00:00Z`)
let left = nominal - 36 * HOUR_MS
let right = nominal + 36 * HOUR_MS
while (left < right) {
const middle = Math.floor((left + right) / 2)
if (dateKey(middle, timezone) < date) left = middle + 1
else right = middle
}
return left
}
function dailyModels(date: string, today: string): ModelBreakdown[] {
const age = Math.round((Date.parse(`${today}T00:00:00Z`) - Date.parse(`${date}T00:00:00Z`)) / DAY_MS)
const serial = Math.floor(Date.parse(`${date}T00:00:00Z`) / DAY_MS)
const total = age < 0 || age >= 90 ? 0 : 1800 + ((serial % 13 + 13) % 13) * 120
const requests = [Math.floor(total * 0.5), Math.floor(total * 0.3)]
requests.push(total - requests[0] - requests[1])
return modelNames.map((model, index) => ({
model, requests: requests[index], tokens: requests[index] * [1100, 1600, 850][index],
cost: requests[index] * [3, 5, 2][index] / 100,
}))
}
function dailyRow(date: string, models: ModelBreakdown[]): DailyStat {
const requests = models.reduce((sum, model) => sum + model.requests, 0)
return {
date, requests, tokens: models.reduce((sum, model) => sum + model.tokens, 0),
cost: Number(models.reduce((sum, model) => sum + model.cost, 0).toFixed(2)),
avg_response_time: requests ? 1.52 : 0, unique_models: requests ? models.length : 0,
unique_providers: requests ? providerNames.length : 0, model_breakdown: models,
}
}
/** Synthetic display data only; no account, usage, or billing writes. */
export function createDashboardDemo(timezone: string): OverviewDashboardSummary {
const now = Date.now()
const today = dateKey(now, timezone)
const todayFrom = dayStart(today, timezone)
const activityDays = Array.from({ length: 365 }, (_, index) => {
const date = shiftDate(today, index - 364)
return { date, requests: dailyModels(date, today).reduce((sum, model) => sum + model.requests, 0) }
})
const days = activityDays.slice(-90).map(day => dailyRow(day.date, dailyModels(day.date, today)))
const current = days[days.length - 1]
const input = Math.round(current.tokens * 0.75)
const stream = Math.round(current.requests * 0.91)
const amount = (value: number) => ({ value: value.toFixed(8), currency: 'USD', basis: 'billable', status: 'known' })
const observedFrom = Math.max(todayFrom, now - 2 * HOUR_MS)
return {
stats_since: new Date(dayStart(shiftDate(today, -89), timezone)).toISOString(),
generated_at: new Date(now).toISOString(), timezone,
today_from: new Date(todayFrom).toISOString(), window_seconds: Math.floor((now - todayFrom) / 1000),
today: {
request_count: current.requests, input_tokens: input, output_tokens: current.tokens - input,
total_tokens: current.tokens, billable_amount: amount(current.cost), active_users: 24,
cache_read_tokens: Math.round(input * 0.63), cache_creation_tokens: Math.round(input * 0.12), cache_input_tokens: input,
avg_first_byte_ms: 247, avg_response_ms: 1520, stream_requests: stream, standard_requests: current.requests - stream,
},
total: {
request_count: days.reduce((sum, day) => sum + day.requests, 0),
total_tokens: days.reduce((sum, day) => sum + day.tokens, 0),
billable_amount: amount(days.reduce((sum, day) => sum + day.cost, 0)),
cache_read_tokens: Math.round(days.reduce((sum, day) => sum + Math.round(day.tokens * 0.75), 0) * 0.58),
cache_input_tokens: days.reduce((sum, day) => sum + Math.round(day.tokens * 0.75), 0),
},
users: { total: 63, created_today: 4, deleted_today: 1 }, consecutive_active_days: 90, active_days: 90, activity_days: activityDays,
concurrency: {
avg: 6.84, peak: 23, observed_from: new Date(observedFrom).toISOString(), observed_through: new Date(now).toISOString(),
scope: 'node', coverage: observedFrom > todayFrom ? 'partial' : 'complete',
},
}
}
export function createDashboardTimelineDemo(): IntervalTimelineResponse {
const now = Date.now()
const userIds = ['demo-dev', 'demo-production', 'demo-sandbox']
const points = Array.from({ length: 108 }, (_, index) => ({
x: new Date(now - DAY_MS + (index + 1) * DAY_MS / 109).toISOString(),
y: Number((1.5 + (index * 17 % 240) + (index % 3) * 0.4).toFixed(1)),
user_id: userIds[index % userIds.length], model: modelNames[index % modelNames.length],
}))
return {
analysis_period_hours: 24, total_points: points.length, points,
users: { 'demo-dev': '开发团队', 'demo-production': '生产应用', 'demo-sandbox': '测试环境' }, models: [...modelNames],
}
}
export function createDashboardDailyDemo(params: DateRangeParams): DailyStatsResponse {
const timezone = params.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
const now = Date.now()
const today = dateKey(now, timezone)
const periodDays = params.preset === 'last90days' ? 90 : params.preset === 'last30days' ? 30
: params.preset === 'today' || params.preset === 'yesterday' ? 1 : 7
const preciseFrom = params.from ? Date.parse(params.from) : NaN
const preciseTo = params.to ? Date.parse(params.to) : NaN
const precise = Number.isFinite(preciseFrom) && Number.isFinite(preciseTo) && preciseFrom < preciseTo
const validDate = (value: string | undefined) => value && /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value)) ? value : undefined
let endDate = precise ? dateKey(preciseTo - 1, timezone)
: validDate(params.end_date) || shiftDate(today, params.preset === 'yesterday' ? -1 : 0)
let startDate = precise ? dateKey(preciseFrom, timezone)
: validDate(params.start_date) || shiftDate(endDate, 1 - periodDays)
if (startDate > endDate) [startDate, endDate] = [endDate, startDate]
startDate = startDate < shiftDate(endDate, -365) ? shiftDate(endDate, -365) : startDate
const days = Math.round((Date.parse(endDate) - Date.parse(startDate)) / DAY_MS) + 1
const rows: DailyStat[] = []
for (let index = 0; index < days; index += 1) {
const date = shiftDate(startDate, index)
const models = dailyModels(date, today)
if (params.granularity !== 'hour') {
rows.push(dailyRow(date, models))
continue
}
const from = dayStart(date, timezone)
const to = Math.min(dayStart(shiftDate(date, 1), timezone), now + 1)
const hours = Math.max(0, Math.ceil((to - from) / HOUR_MS))
for (let hour = 0; hour < hours; hour += 1) {
const bucket = from + hour * HOUR_MS
if (precise && (bucket >= preciseTo || bucket + HOUR_MS <= preciseFrom)) continue
const buckets = models.map(model => {
const requests = Math.floor(model.requests / hours) + (hour < model.requests % hours ? 1 : 0)
const share = model.requests ? requests / model.requests : 0
return { ...model, requests, tokens: Math.round(model.tokens * share), cost: Number((model.cost * share).toFixed(2)) }
})
rows.push(dailyRow(new Date(bucket).toISOString(), buckets))
}
}
const modelSummary = modelNames.map((model, index) => {
const entries = rows.map(row => row.model_breakdown[index])
const requests = entries.reduce((sum, entry) => sum + entry.requests, 0)
const tokens = entries.reduce((sum, entry) => sum + entry.tokens, 0)
const cost = Number(entries.reduce((sum, entry) => sum + entry.cost, 0).toFixed(2))
return { model, requests, tokens, cost, avg_response_time: requests ? 1.52 : 0, cost_per_request: requests ? cost / requests : 0, tokens_per_request: requests ? tokens / requests : 0 }
})
return {
daily_stats: rows, model_summary: modelSummary,
provider_summary: modelSummary.map((model, index) => ({ provider: providerNames[index], requests: model.requests, tokens: model.tokens, cost: model.cost })),
period: { start_date: startDate, end_date: endDate, days },
}
}