@open-mercato/ai-assistant 0.6.1-develop.3291.1.6fad645fd0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +30 -4
- package/dist/frontend/components/AiChatButton.js +3 -2
- package/dist/frontend/components/AiChatButton.js.map +2 -2
- package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.js +364 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js +7 -7
- package/dist/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.js.map +2 -2
- package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js +182 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js +316 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js +8 -7
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/chat/route.js +43 -20
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/settings/route.js +4 -3
- package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/usage/daily/route.js +111 -0
- package/dist/modules/ai_assistant/api/usage/daily/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js +108 -0
- package/dist/modules/ai_assistant/api/usage/sessions/[sessionId]/route.js.map +7 -0
- package/dist/modules/ai_assistant/api/usage/sessions/route.js +153 -0
- package/dist/modules/ai_assistant/api/usage/sessions/route.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +335 -38
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js +2 -7
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +44 -35
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.js +282 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.js +25 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/cli.js +12 -0
- package/dist/modules/ai_assistant/cli.js.map +2 -2
- package/dist/modules/ai_assistant/components/AiAssistantSettingsPageClient.js.map +1 -1
- package/dist/modules/ai_assistant/data/entities.js +177 -1
- package/dist/modules/ai_assistant/data/entities.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +104 -2
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiTokenUsageRepository.js +168 -0
- package/dist/modules/ai_assistant/data/repositories/AiTokenUsageRepository.js.map +7 -0
- package/dist/modules/ai_assistant/events.js +8 -0
- package/dist/modules/ai_assistant/events.js.map +2 -2
- package/dist/modules/ai_assistant/i18n/de.json +74 -1
- package/dist/modules/ai_assistant/i18n/en.json +74 -1
- package/dist/modules/ai_assistant/i18n/es.json +75 -2
- package/dist/modules/ai_assistant/i18n/pl.json +74 -1
- package/dist/modules/ai_assistant/lib/agent-policy.js.map +2 -2
- package/dist/modules/ai_assistant/lib/agent-runtime.js +588 -23
- package/dist/modules/ai_assistant/lib/agent-runtime.js.map +3 -3
- package/dist/modules/ai_assistant/lib/agent-tools.js +6 -1
- package/dist/modules/ai_assistant/lib/agent-tools.js.map +2 -2
- package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
- package/dist/modules/ai_assistant/lib/model-factory.js +63 -22
- package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
- package/dist/modules/ai_assistant/lib/token-usage-recorder.js +78 -0
- package/dist/modules/ai_assistant/lib/token-usage-recorder.js.map +7 -0
- package/dist/modules/ai_assistant/lib/usage-serialization.js +33 -0
- package/dist/modules/ai_assistant/lib/usage-serialization.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.js +25 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.js.map +7 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.js +88 -0
- package/dist/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.js.map +7 -0
- package/dist/modules/ai_assistant/setup.js +34 -0
- package/dist/modules/ai_assistant/setup.js.map +2 -2
- package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js +114 -0
- package/dist/modules/ai_assistant/workers/ai-token-usage-prune.js.map +7 -0
- package/generated/entities/ai_agent_runtime_override/index.ts +7 -0
- package/generated/entities/ai_token_usage_daily/index.ts +16 -0
- package/generated/entities/ai_token_usage_event/index.ts +19 -0
- package/generated/entities.ids.generated.ts +2 -0
- package/generated/entity-fields-registry.ts +47 -1
- package/package.json +15 -7
- package/src/frontend/components/AiChatButton.tsx +3 -2
- package/src/modules/ai_assistant/__integration__/TC-AI-AGENT-LOOP-001-006.spec.ts +521 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-RUNTIME-OVERRIDES-006-model-picker.spec.ts +8 -8
- package/src/modules/ai_assistant/__integration__/TC-AI-TOKEN-USAGE-001-005.spec.ts +231 -0
- package/src/modules/ai_assistant/__tests__/events.test.ts +4 -3
- package/src/modules/ai_assistant/__tests__/settings-page-logic.test.ts +5 -5
- package/src/modules/ai_assistant/__tests__/token-usage-recorder.test.ts +109 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/loop-override/route.ts +388 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/__tests__/route.test.ts +5 -0
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/route.ts +8 -7
- package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +102 -5
- package/src/modules/ai_assistant/api/ai/chat/route.ts +55 -18
- package/src/modules/ai_assistant/api/settings/route.ts +5 -3
- package/src/modules/ai_assistant/api/usage/daily/__tests__/route.test.ts +159 -0
- package/src/modules/ai_assistant/api/usage/daily/route.ts +126 -0
- package/src/modules/ai_assistant/api/usage/sessions/[sessionId]/__tests__/route.test.ts +143 -0
- package/src/modules/ai_assistant/api/usage/sessions/[sessionId]/route.ts +130 -0
- package/src/modules/ai_assistant/api/usage/sessions/__tests__/route.test.ts +123 -0
- package/src/modules/ai_assistant/api/usage/sessions/route.ts +184 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +372 -16
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.tsx +1 -4
- package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +26 -9
- package/src/modules/ai_assistant/backend/config/ai-assistant/usage/AiUsageStatsPageClient.tsx +469 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/usage/page.meta.ts +23 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/usage/page.tsx +12 -0
- package/src/modules/ai_assistant/cli.ts +18 -0
- package/src/modules/ai_assistant/components/AiAssistantSettingsPageClient.tsx +1 -1
- package/src/modules/ai_assistant/data/entities.ts +237 -0
- package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +135 -3
- package/src/modules/ai_assistant/data/repositories/AiTokenUsageRepository.ts +213 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiAgentRuntimeOverrideRepository.test.ts +223 -0
- package/src/modules/ai_assistant/data/repositories/__tests__/AiTokenUsageRepository.test.ts +58 -0
- package/src/modules/ai_assistant/events.ts +8 -0
- package/src/modules/ai_assistant/i18n/de.json +74 -1
- package/src/modules/ai_assistant/i18n/en.json +74 -1
- package/src/modules/ai_assistant/i18n/es.json +75 -2
- package/src/modules/ai_assistant/i18n/pl.json +74 -1
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase0.test.ts +439 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase1.test.ts +243 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase2.test.ts +388 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-loop-phase3.test.ts +359 -0
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-phase4a.test.ts +2 -2
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime.test.ts +2 -1
- package/src/modules/ai_assistant/lib/__tests__/max-steps-budget.integration.test.ts +12 -13
- package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +77 -14
- package/src/modules/ai_assistant/lib/agent-policy.ts +9 -0
- package/src/modules/ai_assistant/lib/agent-runtime.ts +1148 -43
- package/src/modules/ai_assistant/lib/agent-tools.ts +5 -1
- package/src/modules/ai_assistant/lib/ai-agent-definition.ts +289 -2
- package/src/modules/ai_assistant/lib/model-factory.ts +128 -43
- package/src/modules/ai_assistant/lib/token-usage-recorder.ts +122 -0
- package/src/modules/ai_assistant/lib/usage-serialization.ts +29 -0
- package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +791 -0
- package/src/modules/ai_assistant/migrations/Migration20260508160000_ai_agent_loop_overrides.ts +25 -0
- package/src/modules/ai_assistant/migrations/Migration20260508170000_ai_token_usage.ts +89 -0
- package/src/modules/ai_assistant/setup.ts +49 -0
- package/src/modules/ai_assistant/workers/__tests__/ai-token-usage-prune.test.ts +144 -0
- package/src/modules/ai_assistant/workers/ai-token-usage-prune.ts +188 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useQuery } from '@tanstack/react-query'
|
|
5
|
+
import { BarChart2, ChevronRight, Loader2 } from 'lucide-react'
|
|
6
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
8
|
+
import { Input } from '@open-mercato/ui/primitives/input'
|
|
9
|
+
import { Label } from '@open-mercato/ui/primitives/label'
|
|
10
|
+
import {
|
|
11
|
+
Dialog,
|
|
12
|
+
DialogContent,
|
|
13
|
+
DialogHeader,
|
|
14
|
+
DialogTitle,
|
|
15
|
+
} from '@open-mercato/ui/primitives/dialog'
|
|
16
|
+
import { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
17
|
+
|
|
18
|
+
type DailyRow = {
|
|
19
|
+
id: string
|
|
20
|
+
tenantId: string
|
|
21
|
+
organizationId: string | null
|
|
22
|
+
day: string
|
|
23
|
+
agentId: string
|
|
24
|
+
modelId: string
|
|
25
|
+
providerId: string
|
|
26
|
+
inputTokens: string
|
|
27
|
+
outputTokens: string
|
|
28
|
+
cachedInputTokens: string
|
|
29
|
+
reasoningTokens: string
|
|
30
|
+
stepCount: string
|
|
31
|
+
turnCount: string
|
|
32
|
+
sessionCount: string
|
|
33
|
+
createdAt: string
|
|
34
|
+
updatedAt: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type DailyResponse = {
|
|
38
|
+
rows: DailyRow[]
|
|
39
|
+
total: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type SessionSummary = {
|
|
43
|
+
sessionId: string
|
|
44
|
+
agentId: string
|
|
45
|
+
moduleId: string
|
|
46
|
+
userId: string
|
|
47
|
+
startedAt: string
|
|
48
|
+
lastEventAt: string
|
|
49
|
+
stepCount: number
|
|
50
|
+
turnCount: number
|
|
51
|
+
inputTokens: number
|
|
52
|
+
outputTokens: number
|
|
53
|
+
cachedInputTokens: number
|
|
54
|
+
reasoningTokens: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type SessionsResponse = {
|
|
58
|
+
sessions: SessionSummary[]
|
|
59
|
+
total: number
|
|
60
|
+
limit: number
|
|
61
|
+
offset: number
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type StepEvent = {
|
|
65
|
+
id: string
|
|
66
|
+
tenantId: string
|
|
67
|
+
organizationId: string | null
|
|
68
|
+
userId: string
|
|
69
|
+
agentId: string
|
|
70
|
+
moduleId: string
|
|
71
|
+
sessionId: string
|
|
72
|
+
turnId: string
|
|
73
|
+
stepIndex: number
|
|
74
|
+
providerId: string
|
|
75
|
+
modelId: string
|
|
76
|
+
inputTokens: number
|
|
77
|
+
outputTokens: number
|
|
78
|
+
cachedInputTokens: number | null
|
|
79
|
+
reasoningTokens: number | null
|
|
80
|
+
finishReason: string | null
|
|
81
|
+
loopAbortReason: string | null
|
|
82
|
+
createdAt: string
|
|
83
|
+
updatedAt: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type SessionDetailResponse = {
|
|
87
|
+
events: StepEvent[]
|
|
88
|
+
total: number
|
|
89
|
+
sessionId: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function todayIso(): string {
|
|
93
|
+
return new Date().toISOString().slice(0, 10)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function daysAgoIso(days: number): string {
|
|
97
|
+
const date = new Date()
|
|
98
|
+
date.setDate(date.getDate() - days)
|
|
99
|
+
return date.toISOString().slice(0, 10)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function fetchDailyRollup(from: string, to: string): Promise<DailyResponse> {
|
|
103
|
+
const params = new URLSearchParams({ from, to })
|
|
104
|
+
const { result, status } = await apiCallOrThrow<DailyResponse>(
|
|
105
|
+
`/api/ai_assistant/usage/daily?${params}`,
|
|
106
|
+
{ method: 'GET', credentials: 'include' },
|
|
107
|
+
{ errorMessage: 'Failed to load daily token usage' },
|
|
108
|
+
)
|
|
109
|
+
if (!result) throw new Error(`Failed to load daily usage (${status})`)
|
|
110
|
+
return result
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function fetchSessions(from: string, to: string, offset: number): Promise<SessionsResponse> {
|
|
114
|
+
const params = new URLSearchParams({ from, to, limit: '50', offset: String(offset) })
|
|
115
|
+
const { result, status } = await apiCallOrThrow<SessionsResponse>(
|
|
116
|
+
`/api/ai_assistant/usage/sessions?${params}`,
|
|
117
|
+
{ method: 'GET', credentials: 'include' },
|
|
118
|
+
{ errorMessage: 'Failed to load session list' },
|
|
119
|
+
)
|
|
120
|
+
if (!result) throw new Error(`Failed to load sessions (${status})`)
|
|
121
|
+
return result
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function fetchSessionDetail(sessionId: string): Promise<SessionDetailResponse> {
|
|
125
|
+
const { result, status } = await apiCallOrThrow<SessionDetailResponse>(
|
|
126
|
+
`/api/ai_assistant/usage/sessions/${encodeURIComponent(sessionId)}`,
|
|
127
|
+
{ method: 'GET', credentials: 'include' },
|
|
128
|
+
{ errorMessage: 'Failed to load session detail' },
|
|
129
|
+
)
|
|
130
|
+
if (!result) throw new Error(`Failed to load session detail (${status})`)
|
|
131
|
+
return result
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function sumBigintRows(rows: DailyRow[], field: keyof DailyRow): number {
|
|
135
|
+
return rows.reduce((acc, row) => acc + parseInt(String(row[field] ?? '0'), 10), 0)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function formatNumber(value: number): string {
|
|
139
|
+
return value.toLocaleString()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function formatDate(iso: string): string {
|
|
143
|
+
return new Date(iso).toLocaleString()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function shortId(id: string): string {
|
|
147
|
+
return id.slice(0, 8)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function AiUsageStatsPageClient() {
|
|
151
|
+
const t = useT()
|
|
152
|
+
|
|
153
|
+
const defaultFrom = daysAgoIso(30)
|
|
154
|
+
const defaultTo = todayIso()
|
|
155
|
+
|
|
156
|
+
const [from, setFrom] = React.useState(defaultFrom)
|
|
157
|
+
const [to, setTo] = React.useState(defaultTo)
|
|
158
|
+
const [appliedFrom, setAppliedFrom] = React.useState(defaultFrom)
|
|
159
|
+
const [appliedTo, setAppliedTo] = React.useState(defaultTo)
|
|
160
|
+
const [sessionsOffset, setSessionsOffset] = React.useState(0)
|
|
161
|
+
const [selectedSessionId, setSelectedSessionId] = React.useState<string | null>(null)
|
|
162
|
+
|
|
163
|
+
const dailyQuery = useQuery({
|
|
164
|
+
queryKey: ['ai-usage-daily', appliedFrom, appliedTo],
|
|
165
|
+
queryFn: () => fetchDailyRollup(appliedFrom, appliedTo),
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const sessionsQuery = useQuery({
|
|
169
|
+
queryKey: ['ai-usage-sessions', appliedFrom, appliedTo, sessionsOffset],
|
|
170
|
+
queryFn: () => fetchSessions(appliedFrom, appliedTo, sessionsOffset),
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const sessionDetailQuery = useQuery({
|
|
174
|
+
queryKey: ['ai-usage-session-detail', selectedSessionId],
|
|
175
|
+
queryFn: () => fetchSessionDetail(selectedSessionId!),
|
|
176
|
+
enabled: selectedSessionId !== null,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
function applyFilter() {
|
|
180
|
+
setSessionsOffset(0)
|
|
181
|
+
setAppliedFrom(from)
|
|
182
|
+
setAppliedTo(to)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const dailyRows = dailyQuery.data?.rows ?? []
|
|
186
|
+
const totalInputTokens = sumBigintRows(dailyRows, 'inputTokens')
|
|
187
|
+
const totalOutputTokens = sumBigintRows(dailyRows, 'outputTokens')
|
|
188
|
+
const totalSteps = sumBigintRows(dailyRows, 'stepCount')
|
|
189
|
+
const totalSessions = sumBigintRows(dailyRows, 'sessionCount')
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<div className="space-y-6 max-w-5xl">
|
|
193
|
+
<div className="flex items-center gap-2">
|
|
194
|
+
<BarChart2 className="text-muted-foreground" size={20} />
|
|
195
|
+
<h2 className="text-lg font-semibold">
|
|
196
|
+
{t('ai_assistant.usage.title', 'Token Usage Statistics')}
|
|
197
|
+
</h2>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{/* Date range filter */}
|
|
201
|
+
<div className="flex items-end gap-4 flex-wrap">
|
|
202
|
+
<div className="flex flex-col gap-1.5">
|
|
203
|
+
<Label htmlFor="usage-from">
|
|
204
|
+
{t('ai_assistant.usage.from', 'From')}
|
|
205
|
+
</Label>
|
|
206
|
+
<Input
|
|
207
|
+
id="usage-from"
|
|
208
|
+
type="date"
|
|
209
|
+
value={from}
|
|
210
|
+
onChange={(e) => setFrom(e.target.value)}
|
|
211
|
+
className="w-40"
|
|
212
|
+
/>
|
|
213
|
+
</div>
|
|
214
|
+
<div className="flex flex-col gap-1.5">
|
|
215
|
+
<Label htmlFor="usage-to">
|
|
216
|
+
{t('ai_assistant.usage.to', 'To')}
|
|
217
|
+
</Label>
|
|
218
|
+
<Input
|
|
219
|
+
id="usage-to"
|
|
220
|
+
type="date"
|
|
221
|
+
value={to}
|
|
222
|
+
onChange={(e) => setTo(e.target.value)}
|
|
223
|
+
className="w-40"
|
|
224
|
+
/>
|
|
225
|
+
</div>
|
|
226
|
+
<Button variant="secondary" onClick={applyFilter}>
|
|
227
|
+
{t('ai_assistant.usage.apply', 'Apply')}
|
|
228
|
+
</Button>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
{/* Summary tiles */}
|
|
232
|
+
{dailyQuery.isLoading && (
|
|
233
|
+
<div className="flex items-center gap-2 text-muted-foreground text-sm">
|
|
234
|
+
<Loader2 size={14} className="animate-spin" />
|
|
235
|
+
{t('ai_assistant.usage.loading', 'Loading usage data...')}
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
{dailyQuery.isError && (
|
|
239
|
+
<p className="text-status-error-text text-sm">
|
|
240
|
+
{t('ai_assistant.usage.error', 'Failed to load usage data.')}
|
|
241
|
+
</p>
|
|
242
|
+
)}
|
|
243
|
+
{dailyQuery.isSuccess && (
|
|
244
|
+
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
|
245
|
+
{[
|
|
246
|
+
{ label: t('ai_assistant.usage.inputTokens', 'Input tokens'), value: formatNumber(totalInputTokens) },
|
|
247
|
+
{ label: t('ai_assistant.usage.outputTokens', 'Output tokens'), value: formatNumber(totalOutputTokens) },
|
|
248
|
+
{ label: t('ai_assistant.usage.steps', 'Steps'), value: formatNumber(totalSteps) },
|
|
249
|
+
{ label: t('ai_assistant.usage.sessions', 'Sessions'), value: formatNumber(totalSessions) },
|
|
250
|
+
].map((tile) => (
|
|
251
|
+
<div key={tile.label} className="rounded-lg border border-border p-4 space-y-1">
|
|
252
|
+
<p className="text-muted-foreground text-xs">{tile.label}</p>
|
|
253
|
+
<p className="font-semibold text-xl">{tile.value}</p>
|
|
254
|
+
</div>
|
|
255
|
+
))}
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
|
|
259
|
+
{/* Daily breakdown table */}
|
|
260
|
+
{dailyQuery.isSuccess && dailyRows.length > 0 && (
|
|
261
|
+
<div className="space-y-2">
|
|
262
|
+
<h3 className="text-sm font-medium text-muted-foreground">
|
|
263
|
+
{t('ai_assistant.usage.dailyBreakdown', 'Daily breakdown')}
|
|
264
|
+
</h3>
|
|
265
|
+
<div className="overflow-x-auto rounded-lg border border-border">
|
|
266
|
+
<table className="min-w-full text-sm">
|
|
267
|
+
<thead>
|
|
268
|
+
<tr className="border-b border-border bg-muted/40">
|
|
269
|
+
<th className="px-3 py-2 text-left font-medium text-muted-foreground">
|
|
270
|
+
{t('ai_assistant.usage.col.day', 'Day')}
|
|
271
|
+
</th>
|
|
272
|
+
<th className="px-3 py-2 text-left font-medium text-muted-foreground">
|
|
273
|
+
{t('ai_assistant.usage.col.agent', 'Agent')}
|
|
274
|
+
</th>
|
|
275
|
+
<th className="px-3 py-2 text-right font-medium text-muted-foreground">
|
|
276
|
+
{t('ai_assistant.usage.col.inputTokens', 'Input')}
|
|
277
|
+
</th>
|
|
278
|
+
<th className="px-3 py-2 text-right font-medium text-muted-foreground">
|
|
279
|
+
{t('ai_assistant.usage.col.outputTokens', 'Output')}
|
|
280
|
+
</th>
|
|
281
|
+
<th className="px-3 py-2 text-right font-medium text-muted-foreground">
|
|
282
|
+
{t('ai_assistant.usage.col.sessions', 'Sessions')}
|
|
283
|
+
</th>
|
|
284
|
+
</tr>
|
|
285
|
+
</thead>
|
|
286
|
+
<tbody>
|
|
287
|
+
{dailyRows.map((row) => (
|
|
288
|
+
<tr key={row.id} className="border-b border-border last:border-b-0">
|
|
289
|
+
<td className="px-3 py-2 tabular-nums">{row.day}</td>
|
|
290
|
+
<td className="px-3 py-2 font-mono text-xs">{row.agentId}</td>
|
|
291
|
+
<td className="px-3 py-2 text-right tabular-nums">{formatNumber(parseInt(row.inputTokens, 10))}</td>
|
|
292
|
+
<td className="px-3 py-2 text-right tabular-nums">{formatNumber(parseInt(row.outputTokens, 10))}</td>
|
|
293
|
+
<td className="px-3 py-2 text-right tabular-nums">{row.sessionCount}</td>
|
|
294
|
+
</tr>
|
|
295
|
+
))}
|
|
296
|
+
</tbody>
|
|
297
|
+
</table>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
{/* Sessions list */}
|
|
303
|
+
<div className="space-y-2">
|
|
304
|
+
<h3 className="text-sm font-medium text-muted-foreground">
|
|
305
|
+
{t('ai_assistant.usage.sessionsList', 'Sessions')}
|
|
306
|
+
</h3>
|
|
307
|
+
{sessionsQuery.isLoading && (
|
|
308
|
+
<div className="flex items-center gap-2 text-muted-foreground text-sm">
|
|
309
|
+
<Loader2 size={14} className="animate-spin" />
|
|
310
|
+
{t('ai_assistant.usage.loadingSessions', 'Loading sessions...')}
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
{sessionsQuery.isError && (
|
|
314
|
+
<p className="text-status-error-text text-sm">
|
|
315
|
+
{t('ai_assistant.usage.errorSessions', 'Failed to load sessions.')}
|
|
316
|
+
</p>
|
|
317
|
+
)}
|
|
318
|
+
{sessionsQuery.isSuccess && (sessionsQuery.data?.sessions ?? []).length === 0 && (
|
|
319
|
+
<p className="text-muted-foreground text-sm">
|
|
320
|
+
{t('ai_assistant.usage.noSessions', 'No sessions found for the selected period.')}
|
|
321
|
+
</p>
|
|
322
|
+
)}
|
|
323
|
+
{sessionsQuery.isSuccess && (sessionsQuery.data?.sessions ?? []).length > 0 && (
|
|
324
|
+
<>
|
|
325
|
+
<div className="overflow-x-auto rounded-lg border border-border">
|
|
326
|
+
<table className="min-w-full text-sm">
|
|
327
|
+
<thead>
|
|
328
|
+
<tr className="border-b border-border bg-muted/40">
|
|
329
|
+
<th className="px-3 py-2 text-left font-medium text-muted-foreground">
|
|
330
|
+
{t('ai_assistant.usage.col.session', 'Session')}
|
|
331
|
+
</th>
|
|
332
|
+
<th className="px-3 py-2 text-left font-medium text-muted-foreground">
|
|
333
|
+
{t('ai_assistant.usage.col.agent', 'Agent')}
|
|
334
|
+
</th>
|
|
335
|
+
<th className="px-3 py-2 text-left font-medium text-muted-foreground">
|
|
336
|
+
{t('ai_assistant.usage.col.startedAt', 'Started')}
|
|
337
|
+
</th>
|
|
338
|
+
<th className="px-3 py-2 text-right font-medium text-muted-foreground">
|
|
339
|
+
{t('ai_assistant.usage.col.inputTokens', 'Input')}
|
|
340
|
+
</th>
|
|
341
|
+
<th className="px-3 py-2 text-right font-medium text-muted-foreground">
|
|
342
|
+
{t('ai_assistant.usage.col.outputTokens', 'Output')}
|
|
343
|
+
</th>
|
|
344
|
+
<th className="px-3 py-2 text-right font-medium text-muted-foreground">
|
|
345
|
+
{t('ai_assistant.usage.col.steps', 'Steps')}
|
|
346
|
+
</th>
|
|
347
|
+
<th className="w-8" />
|
|
348
|
+
</tr>
|
|
349
|
+
</thead>
|
|
350
|
+
<tbody>
|
|
351
|
+
{(sessionsQuery.data?.sessions ?? []).map((session) => (
|
|
352
|
+
<tr
|
|
353
|
+
key={session.sessionId}
|
|
354
|
+
className="border-b border-border last:border-b-0 cursor-pointer hover:bg-muted/30 transition-colors"
|
|
355
|
+
onClick={() => setSelectedSessionId(session.sessionId)}
|
|
356
|
+
>
|
|
357
|
+
<td className="px-3 py-2 font-mono text-xs">{shortId(session.sessionId)}…</td>
|
|
358
|
+
<td className="px-3 py-2 font-mono text-xs">{session.agentId}</td>
|
|
359
|
+
<td className="px-3 py-2 text-muted-foreground text-xs">{formatDate(session.startedAt)}</td>
|
|
360
|
+
<td className="px-3 py-2 text-right tabular-nums">{formatNumber(session.inputTokens)}</td>
|
|
361
|
+
<td className="px-3 py-2 text-right tabular-nums">{formatNumber(session.outputTokens)}</td>
|
|
362
|
+
<td className="px-3 py-2 text-right tabular-nums">{session.stepCount}</td>
|
|
363
|
+
<td className="px-3 py-2 text-muted-foreground">
|
|
364
|
+
<ChevronRight size={14} />
|
|
365
|
+
</td>
|
|
366
|
+
</tr>
|
|
367
|
+
))}
|
|
368
|
+
</tbody>
|
|
369
|
+
</table>
|
|
370
|
+
</div>
|
|
371
|
+
<div className="flex items-center gap-2">
|
|
372
|
+
<Button
|
|
373
|
+
variant="secondary"
|
|
374
|
+
size="sm"
|
|
375
|
+
disabled={sessionsOffset === 0}
|
|
376
|
+
onClick={() => setSessionsOffset(Math.max(0, sessionsOffset - 50))}
|
|
377
|
+
>
|
|
378
|
+
{t('ai_assistant.usage.prev', 'Previous')}
|
|
379
|
+
</Button>
|
|
380
|
+
<span className="text-muted-foreground text-sm">
|
|
381
|
+
{sessionsOffset + 1}–{sessionsOffset + (sessionsQuery.data?.sessions.length ?? 0)}
|
|
382
|
+
{sessionsQuery.data?.total !== undefined ? ` / ${sessionsQuery.data.total}` : ''}
|
|
383
|
+
</span>
|
|
384
|
+
<Button
|
|
385
|
+
variant="secondary"
|
|
386
|
+
size="sm"
|
|
387
|
+
disabled={
|
|
388
|
+
(sessionsQuery.data?.sessions.length ?? 0) < 50 ||
|
|
389
|
+
sessionsOffset + 50 >= (sessionsQuery.data?.total ?? 0)
|
|
390
|
+
}
|
|
391
|
+
onClick={() => setSessionsOffset(sessionsOffset + 50)}
|
|
392
|
+
>
|
|
393
|
+
{t('ai_assistant.usage.next', 'Next')}
|
|
394
|
+
</Button>
|
|
395
|
+
</div>
|
|
396
|
+
</>
|
|
397
|
+
)}
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
{/* Session drill-down dialog */}
|
|
401
|
+
<Dialog
|
|
402
|
+
open={selectedSessionId !== null}
|
|
403
|
+
onOpenChange={(open) => { if (!open) setSelectedSessionId(null) }}
|
|
404
|
+
>
|
|
405
|
+
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
|
406
|
+
<DialogHeader>
|
|
407
|
+
<DialogTitle>
|
|
408
|
+
{t('ai_assistant.usage.sessionDetail', 'Session detail')}
|
|
409
|
+
{selectedSessionId && (
|
|
410
|
+
<span className="ml-2 font-mono text-sm text-muted-foreground">
|
|
411
|
+
{shortId(selectedSessionId)}…
|
|
412
|
+
</span>
|
|
413
|
+
)}
|
|
414
|
+
</DialogTitle>
|
|
415
|
+
</DialogHeader>
|
|
416
|
+
<div className="mt-4">
|
|
417
|
+
{sessionDetailQuery.isLoading && (
|
|
418
|
+
<div className="flex items-center gap-2 text-muted-foreground text-sm">
|
|
419
|
+
<Loader2 size={14} className="animate-spin" />
|
|
420
|
+
{t('ai_assistant.usage.loadingDetail', 'Loading session events...')}
|
|
421
|
+
</div>
|
|
422
|
+
)}
|
|
423
|
+
{sessionDetailQuery.isError && (
|
|
424
|
+
<p className="text-status-error-text text-sm">
|
|
425
|
+
{t('ai_assistant.usage.errorDetail', 'Failed to load session events.')}
|
|
426
|
+
</p>
|
|
427
|
+
)}
|
|
428
|
+
{sessionDetailQuery.isSuccess && (
|
|
429
|
+
<div className="overflow-x-auto rounded-lg border border-border">
|
|
430
|
+
<table className="min-w-full text-sm">
|
|
431
|
+
<thead>
|
|
432
|
+
<tr className="border-b border-border bg-muted/40">
|
|
433
|
+
<th className="px-3 py-2 text-left font-medium text-muted-foreground">
|
|
434
|
+
{t('ai_assistant.usage.col.step', 'Step')}
|
|
435
|
+
</th>
|
|
436
|
+
<th className="px-3 py-2 text-left font-medium text-muted-foreground">
|
|
437
|
+
{t('ai_assistant.usage.col.model', 'Model')}
|
|
438
|
+
</th>
|
|
439
|
+
<th className="px-3 py-2 text-right font-medium text-muted-foreground">
|
|
440
|
+
{t('ai_assistant.usage.col.inputTokens', 'Input')}
|
|
441
|
+
</th>
|
|
442
|
+
<th className="px-3 py-2 text-right font-medium text-muted-foreground">
|
|
443
|
+
{t('ai_assistant.usage.col.outputTokens', 'Output')}
|
|
444
|
+
</th>
|
|
445
|
+
<th className="px-3 py-2 text-left font-medium text-muted-foreground">
|
|
446
|
+
{t('ai_assistant.usage.col.finishReason', 'Finish')}
|
|
447
|
+
</th>
|
|
448
|
+
</tr>
|
|
449
|
+
</thead>
|
|
450
|
+
<tbody>
|
|
451
|
+
{(sessionDetailQuery.data?.events ?? []).map((event) => (
|
|
452
|
+
<tr key={event.id} className="border-b border-border last:border-b-0">
|
|
453
|
+
<td className="px-3 py-2 tabular-nums">{event.stepIndex}</td>
|
|
454
|
+
<td className="px-3 py-2 font-mono text-xs">{event.modelId}</td>
|
|
455
|
+
<td className="px-3 py-2 text-right tabular-nums">{formatNumber(event.inputTokens)}</td>
|
|
456
|
+
<td className="px-3 py-2 text-right tabular-nums">{formatNumber(event.outputTokens)}</td>
|
|
457
|
+
<td className="px-3 py-2 text-muted-foreground text-xs">{event.finishReason ?? '—'}</td>
|
|
458
|
+
</tr>
|
|
459
|
+
))}
|
|
460
|
+
</tbody>
|
|
461
|
+
</table>
|
|
462
|
+
</div>
|
|
463
|
+
)}
|
|
464
|
+
</div>
|
|
465
|
+
</DialogContent>
|
|
466
|
+
</Dialog>
|
|
467
|
+
</div>
|
|
468
|
+
)
|
|
469
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
const usageIcon = React.createElement(
|
|
4
|
+
'svg',
|
|
5
|
+
{ width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
|
|
6
|
+
React.createElement('path', { d: 'M3 3v18h18' }),
|
|
7
|
+
React.createElement('path', { d: 'M7 16l4-4 4 4 4-4' }),
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
export const metadata = {
|
|
11
|
+
requireAuth: true,
|
|
12
|
+
requireFeatures: ['ai_assistant.settings.manage'],
|
|
13
|
+
pageTitle: 'AI Usage',
|
|
14
|
+
pageTitleKey: 'ai_assistant.usage.navTitle',
|
|
15
|
+
pageGroup: 'Module Configs',
|
|
16
|
+
pageGroupKey: 'settings.sections.moduleConfigs',
|
|
17
|
+
pageOrder: 431,
|
|
18
|
+
icon: usageIcon,
|
|
19
|
+
pageContext: 'settings' as const,
|
|
20
|
+
breadcrumb: [
|
|
21
|
+
{ label: 'AI Usage', labelKey: 'ai_assistant.usage.navTitle' },
|
|
22
|
+
],
|
|
23
|
+
} as const
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
2
|
+
import { AiUsageStatsPageClient } from './AiUsageStatsPageClient'
|
|
3
|
+
|
|
4
|
+
export default async function AiUsageStatsPage() {
|
|
5
|
+
return (
|
|
6
|
+
<Page>
|
|
7
|
+
<PageBody>
|
|
8
|
+
<AiUsageStatsPageClient />
|
|
9
|
+
</PageBody>
|
|
10
|
+
</Page>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -338,6 +338,23 @@ const testTools: ModuleCli = {
|
|
|
338
338
|
},
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
const runTokenUsagePrune: ModuleCli = {
|
|
342
|
+
command: 'run-token-usage-prune',
|
|
343
|
+
async run() {
|
|
344
|
+
await ensureBootstrap()
|
|
345
|
+
const container = await createRequestContainer()
|
|
346
|
+
|
|
347
|
+
const { runTokenUsagePrune: runPrune } = await import(
|
|
348
|
+
'./workers/ai-token-usage-prune'
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
const em = container.resolve<import('@mikro-orm/postgresql').EntityManager>('em')
|
|
352
|
+
const summary = await runPrune({ em })
|
|
353
|
+
|
|
354
|
+
console.log('[ai-token-usage-prune] Prune complete:', summary)
|
|
355
|
+
},
|
|
356
|
+
}
|
|
357
|
+
|
|
341
358
|
export default [
|
|
342
359
|
mcpServe,
|
|
343
360
|
mcpServeHttp,
|
|
@@ -345,5 +362,6 @@ export default [
|
|
|
345
362
|
listTools,
|
|
346
363
|
entityGraph,
|
|
347
364
|
runPendingActionCleanup,
|
|
365
|
+
runTokenUsagePrune,
|
|
348
366
|
testTools,
|
|
349
367
|
]
|