@swarmclawai/swarmclaw 0.6.7 → 0.7.0
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/README.md +82 -39
- package/next.config.ts +31 -6
- package/package.json +3 -2
- package/src/app/api/agents/[id]/thread/route.ts +1 -0
- package/src/app/api/agents/route.ts +19 -5
- package/src/app/api/approvals/route.ts +22 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/eval/run/route.ts +37 -0
- package/src/app/api/eval/scenarios/route.ts +24 -0
- package/src/app/api/eval/suite/route.ts +29 -0
- package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
- package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
- package/src/app/api/memory/graph/route.ts +46 -0
- package/src/app/api/memory/route.ts +36 -5
- package/src/app/api/notifications/route.ts +3 -0
- package/src/app/api/plugins/install/route.ts +57 -5
- package/src/app/api/plugins/marketplace/route.ts +73 -22
- package/src/app/api/plugins/route.ts +61 -1
- package/src/app/api/plugins/ui/route.ts +34 -0
- package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
- package/src/app/api/sessions/[id]/restore/route.ts +36 -0
- package/src/app/api/settings/route.ts +62 -0
- package/src/app/api/setup/doctor/route.ts +22 -5
- package/src/app/api/souls/[id]/route.ts +65 -0
- package/src/app/api/souls/route.ts +70 -0
- package/src/app/api/tasks/[id]/approve/route.ts +4 -3
- package/src/app/api/tasks/[id]/route.ts +16 -3
- package/src/app/api/tasks/route.ts +10 -2
- package/src/app/api/usage/route.ts +9 -2
- package/src/app/globals.css +27 -0
- package/src/app/page.tsx +10 -5
- package/src/cli/index.js +37 -0
- package/src/components/activity/activity-feed.tsx +9 -2
- package/src/components/agents/agent-avatar.tsx +5 -1
- package/src/components/agents/agent-card.tsx +55 -9
- package/src/components/agents/agent-sheet.tsx +112 -34
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/agents/soul-library-picker.tsx +84 -13
- package/src/components/auth/access-key-gate.tsx +63 -54
- package/src/components/auth/user-picker.tsx +37 -32
- package/src/components/chat/activity-moment.tsx +2 -0
- package/src/components/chat/chat-area.tsx +11 -0
- package/src/components/chat/chat-header.tsx +69 -25
- package/src/components/chat/chat-tool-toggles.tsx +2 -2
- package/src/components/chat/checkpoint-timeline.tsx +112 -0
- package/src/components/chat/code-block.tsx +3 -1
- package/src/components/chat/exec-approval-card.tsx +8 -1
- package/src/components/chat/message-bubble.tsx +164 -4
- package/src/components/chat/message-list.tsx +46 -4
- package/src/components/chat/session-approval-card.tsx +80 -0
- package/src/components/chat/session-debug-panel.tsx +106 -84
- package/src/components/chat/streaming-bubble.tsx +6 -5
- package/src/components/chat/task-approval-card.tsx +78 -0
- package/src/components/chat/thinking-indicator.tsx +48 -12
- package/src/components/chat/tool-call-bubble.tsx +3 -0
- package/src/components/chat/tool-request-banner.tsx +39 -20
- package/src/components/chatrooms/chatroom-list.tsx +11 -4
- package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
- package/src/components/connectors/connector-list.tsx +33 -11
- package/src/components/connectors/connector-sheet.tsx +37 -7
- package/src/components/home/home-view.tsx +54 -24
- package/src/components/input/chat-input.tsx +22 -1
- package/src/components/knowledge/knowledge-list.tsx +17 -18
- package/src/components/knowledge/knowledge-sheet.tsx +9 -5
- package/src/components/layout/app-layout.tsx +87 -19
- package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
- package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
- package/src/components/memory/memory-browser.tsx +73 -45
- package/src/components/memory/memory-graph-view.tsx +203 -0
- package/src/components/memory/memory-list.tsx +20 -13
- package/src/components/plugins/plugin-list.tsx +214 -60
- package/src/components/plugins/plugin-sheet.tsx +119 -24
- package/src/components/projects/project-list.tsx +17 -9
- package/src/components/providers/provider-list.tsx +21 -6
- package/src/components/providers/provider-sheet.tsx +42 -25
- package/src/components/runs/run-list.tsx +17 -13
- package/src/components/schedules/schedule-card.tsx +10 -3
- package/src/components/schedules/schedule-list.tsx +2 -2
- package/src/components/schedules/schedule-sheet.tsx +28 -9
- package/src/components/secrets/secret-sheet.tsx +7 -2
- package/src/components/secrets/secrets-list.tsx +18 -5
- package/src/components/sessions/new-session-sheet.tsx +183 -376
- package/src/components/sessions/session-card.tsx +10 -2
- package/src/components/settings/gateway-connection-panel.tsx +9 -8
- package/src/components/shared/command-palette.tsx +13 -5
- package/src/components/shared/empty-state.tsx +20 -8
- package/src/components/shared/hint-tip.tsx +31 -0
- package/src/components/shared/notification-center.tsx +134 -86
- package/src/components/shared/profile-sheet.tsx +4 -0
- package/src/components/shared/settings/plugin-manager.tsx +360 -135
- package/src/components/shared/settings/section-capability-policy.tsx +3 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
- package/src/components/skills/clawhub-browser.tsx +1 -0
- package/src/components/skills/skill-list.tsx +31 -12
- package/src/components/skills/skill-sheet.tsx +20 -7
- package/src/components/tasks/approvals-panel.tsx +224 -0
- package/src/components/tasks/task-board.tsx +20 -12
- package/src/components/tasks/task-card.tsx +21 -7
- package/src/components/tasks/task-column.tsx +4 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +130 -1
- package/src/components/ui/dialog.tsx +1 -0
- package/src/components/ui/sheet.tsx +1 -0
- package/src/components/usage/metrics-dashboard.tsx +72 -48
- package/src/components/wallets/wallet-panel.tsx +65 -41
- package/src/components/wallets/wallet-section.tsx +9 -3
- package/src/components/webhooks/webhook-list.tsx +21 -12
- package/src/components/webhooks/webhook-sheet.tsx +13 -3
- package/src/lib/approval-display.test.ts +45 -0
- package/src/lib/approval-display.ts +62 -0
- package/src/lib/clipboard.ts +38 -0
- package/src/lib/memory.ts +8 -0
- package/src/lib/providers/claude-cli.ts +5 -3
- package/src/lib/providers/index.ts +67 -21
- package/src/lib/runtime-loop.ts +3 -2
- package/src/lib/server/approvals.ts +150 -0
- package/src/lib/server/chat-execution.ts +319 -74
- package/src/lib/server/chatroom-helpers.ts +63 -5
- package/src/lib/server/chatroom-orchestration.ts +74 -0
- package/src/lib/server/clawhub-client.ts +82 -6
- package/src/lib/server/connectors/manager.ts +27 -1
- package/src/lib/server/context-manager.ts +132 -50
- package/src/lib/server/cost.test.ts +73 -0
- package/src/lib/server/cost.ts +165 -34
- package/src/lib/server/daemon-state.ts +112 -1
- package/src/lib/server/data-dir.ts +18 -1
- package/src/lib/server/eval/runner.ts +126 -0
- package/src/lib/server/eval/scenarios.ts +218 -0
- package/src/lib/server/eval/scorer.ts +96 -0
- package/src/lib/server/eval/store.ts +37 -0
- package/src/lib/server/eval/types.ts +48 -0
- package/src/lib/server/execution-log.ts +12 -8
- package/src/lib/server/guardian.ts +34 -0
- package/src/lib/server/heartbeat-service.ts +53 -1
- package/src/lib/server/integrity-monitor.ts +208 -0
- package/src/lib/server/langgraph-checkpoint.ts +10 -0
- package/src/lib/server/link-understanding.ts +55 -0
- package/src/lib/server/llm-response-cache.test.ts +102 -0
- package/src/lib/server/llm-response-cache.ts +227 -0
- package/src/lib/server/main-agent-loop.ts +115 -16
- package/src/lib/server/main-session.ts +6 -3
- package/src/lib/server/mcp-conformance.test.ts +18 -0
- package/src/lib/server/mcp-conformance.ts +233 -0
- package/src/lib/server/memory-db.ts +193 -19
- package/src/lib/server/memory-retrieval.test.ts +56 -0
- package/src/lib/server/mmr.ts +73 -0
- package/src/lib/server/orchestrator-lg.ts +7 -1
- package/src/lib/server/orchestrator.ts +4 -3
- package/src/lib/server/plugins.ts +662 -132
- package/src/lib/server/process-manager.ts +18 -0
- package/src/lib/server/query-expansion.ts +57 -0
- package/src/lib/server/queue.ts +280 -11
- package/src/lib/server/runtime-settings.ts +9 -0
- package/src/lib/server/session-run-manager.test.ts +23 -0
- package/src/lib/server/session-run-manager.ts +32 -2
- package/src/lib/server/session-tools/canvas.ts +85 -50
- package/src/lib/server/session-tools/chatroom.ts +130 -127
- package/src/lib/server/session-tools/connector.ts +233 -454
- package/src/lib/server/session-tools/context-mgmt.ts +87 -105
- package/src/lib/server/session-tools/crud.ts +84 -7
- package/src/lib/server/session-tools/delegate.ts +351 -752
- package/src/lib/server/session-tools/discovery.ts +198 -0
- package/src/lib/server/session-tools/edit_file.ts +82 -0
- package/src/lib/server/session-tools/file-send.test.ts +39 -0
- package/src/lib/server/session-tools/file.ts +257 -425
- package/src/lib/server/session-tools/git.ts +87 -47
- package/src/lib/server/session-tools/http.ts +95 -33
- package/src/lib/server/session-tools/index.ts +217 -138
- package/src/lib/server/session-tools/memory.ts +154 -239
- package/src/lib/server/session-tools/monitor.ts +126 -0
- package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
- package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
- package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
- package/src/lib/server/session-tools/platform.ts +86 -0
- package/src/lib/server/session-tools/plugin-creator.ts +239 -0
- package/src/lib/server/session-tools/sample-ui.ts +97 -0
- package/src/lib/server/session-tools/sandbox.ts +175 -148
- package/src/lib/server/session-tools/schedule.ts +78 -0
- package/src/lib/server/session-tools/session-info.ts +104 -410
- package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
- package/src/lib/server/session-tools/shell.ts +171 -143
- package/src/lib/server/session-tools/subagent.ts +77 -77
- package/src/lib/server/session-tools/wallet.ts +182 -106
- package/src/lib/server/session-tools/web.ts +181 -327
- package/src/lib/server/storage.ts +36 -0
- package/src/lib/server/stream-agent-chat.ts +348 -242
- package/src/lib/server/task-quality-gate.test.ts +44 -0
- package/src/lib/server/task-quality-gate.ts +67 -0
- package/src/lib/server/task-validation.test.ts +78 -0
- package/src/lib/server/task-validation.ts +67 -2
- package/src/lib/server/tool-aliases.ts +68 -0
- package/src/lib/server/tool-capability-policy.ts +24 -5
- package/src/lib/server/tool-retry.ts +62 -0
- package/src/lib/server/transcript-repair.ts +72 -0
- package/src/lib/setup-defaults.ts +1 -0
- package/src/lib/tasks.ts +7 -1
- package/src/lib/tool-definitions.ts +24 -23
- package/src/lib/validation/schemas.ts +13 -0
- package/src/lib/view-routes.ts +2 -23
- package/src/stores/use-app-store.ts +23 -1
- package/src/types/index.ts +155 -10
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState, useCallback } from 'react'
|
|
3
|
+
import { useState, useCallback, useEffect } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { api } from '@/lib/api-client'
|
|
6
6
|
import { updateTask, archiveTask } from '@/lib/tasks'
|
|
@@ -20,9 +20,10 @@ interface TaskCardProps {
|
|
|
20
20
|
selectionMode?: boolean
|
|
21
21
|
selected?: boolean
|
|
22
22
|
onToggleSelect?: (id: string) => void
|
|
23
|
+
index?: number
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
export function TaskCard({ task, selectionMode, selected, onToggleSelect }: TaskCardProps) {
|
|
26
|
+
export function TaskCard({ task, selectionMode, selected, onToggleSelect, index = 0 }: TaskCardProps) {
|
|
26
27
|
const agents = useAppStore((s) => s.agents)
|
|
27
28
|
const projects = useAppStore((s) => s.projects)
|
|
28
29
|
const setEditingTaskId = useAppStore((s) => s.setEditingTaskId)
|
|
@@ -32,6 +33,15 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
|
|
|
32
33
|
const setActiveView = useAppStore((s) => s.setActiveView)
|
|
33
34
|
const [dragging, setDragging] = useState(false)
|
|
34
35
|
const [confirmArchive, setConfirmArchive] = useState(false)
|
|
36
|
+
const [allowDrag, setAllowDrag] = useState(false)
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (typeof window === 'undefined') return
|
|
40
|
+
const isCoarsePointer = typeof window.matchMedia === 'function'
|
|
41
|
+
? window.matchMedia('(pointer: coarse)').matches
|
|
42
|
+
: 'ontouchstart' in window
|
|
43
|
+
setAllowDrag(!isCoarsePointer)
|
|
44
|
+
}, [])
|
|
35
45
|
|
|
36
46
|
const tasks = useAppStore((s) => s.tasks)
|
|
37
47
|
const agent = agents[task.agentId]
|
|
@@ -85,9 +95,9 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
|
|
|
85
95
|
|
|
86
96
|
return (
|
|
87
97
|
<div
|
|
88
|
-
draggable={!selectionMode}
|
|
89
|
-
onDragStart={selectionMode ? undefined : handleDragStart}
|
|
90
|
-
onDragEnd={selectionMode ? undefined : handleDragEnd}
|
|
98
|
+
draggable={!selectionMode && allowDrag}
|
|
99
|
+
onDragStart={selectionMode || !allowDrag ? undefined : handleDragStart}
|
|
100
|
+
onDragEnd={selectionMode || !allowDrag ? undefined : handleDragEnd}
|
|
91
101
|
onClick={(e) => {
|
|
92
102
|
if (selectionMode && onToggleSelect) {
|
|
93
103
|
e.stopPropagation()
|
|
@@ -98,9 +108,13 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect }: Task
|
|
|
98
108
|
}
|
|
99
109
|
}}
|
|
100
110
|
className={`py-3 px-4 rounded-[14px] border border-l-[3px] ${borderColor} bg-surface hover:bg-surface-2 transition-all group
|
|
101
|
-
${selectionMode ? 'cursor-pointer' : 'cursor-grab active:cursor-grabbing'}
|
|
111
|
+
${selectionMode || !allowDrag ? 'cursor-pointer' : 'cursor-grab active:cursor-grabbing'} touch-pan-y
|
|
102
112
|
${dragging ? 'opacity-40 scale-[0.97]' : ''}
|
|
103
|
-
${selected ? 'border-accent-bright/40 bg-accent-bright/[0.04] ring-1 ring-accent-bright/20' : 'border-white/[0.06]'}`}
|
|
113
|
+
${selected ? 'border-accent-bright/40 bg-accent-bright/[0.04] ring-1 ring-accent-bright/20 shadow-lg' : 'border-white/[0.06] hover:border-white/[0.12] hover:scale-[1.01] hover:shadow-md'}`}
|
|
114
|
+
style={{
|
|
115
|
+
animation: 'spring-in 0.5s var(--ease-spring) both',
|
|
116
|
+
animationDelay: `${Math.min(index * 0.05, 0.4)}s`
|
|
117
|
+
}}
|
|
104
118
|
>
|
|
105
119
|
<div className="flex items-start gap-3 mb-3">
|
|
106
120
|
{/* Selection checkbox */}
|
|
@@ -68,7 +68,7 @@ export function TaskColumn({ status, tasks, onDrop, selectionMode, selectedIds,
|
|
|
68
68
|
|
|
69
69
|
return (
|
|
70
70
|
<div
|
|
71
|
-
className={`flex-1 min-w-[240px] max-w-[320px] flex flex-col rounded-[16px] transition-colors duration-150 ${
|
|
71
|
+
className={`flex-1 min-w-[240px] max-w-[320px] min-h-0 flex flex-col rounded-[16px] transition-colors duration-150 ${
|
|
72
72
|
dragOver ? 'bg-accent-bright/[0.04] ring-1 ring-accent-bright/20' : ''
|
|
73
73
|
}`}
|
|
74
74
|
onDragOver={handleDragOver}
|
|
@@ -109,11 +109,12 @@ export function TaskColumn({ status, tasks, onDrop, selectionMode, selectedIds,
|
|
|
109
109
|
</div>
|
|
110
110
|
)}
|
|
111
111
|
|
|
112
|
-
<div className="flex flex-col gap-3 flex-1 overflow-y-auto pr-1 px-1 pb-2">
|
|
113
|
-
{tasks.map((task) => (
|
|
112
|
+
<div className="flex flex-col gap-3 flex-1 min-h-0 overflow-y-auto overscroll-y-contain touch-pan-y pr-1 px-1 pb-2">
|
|
113
|
+
{tasks.map((task, idx) => (
|
|
114
114
|
<TaskCard
|
|
115
115
|
key={task.id}
|
|
116
116
|
task={task}
|
|
117
|
+
index={idx}
|
|
117
118
|
selectionMode={selectionMode}
|
|
118
119
|
selected={selectedIds?.has(task.id)}
|
|
119
120
|
onToggleSelect={onToggleSelect}
|
|
@@ -62,7 +62,7 @@ export function TaskList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
return (
|
|
65
|
-
<div className="flex-1 flex flex-col overflow-y-auto">
|
|
65
|
+
<div className="flex-1 min-h-0 flex flex-col overflow-y-auto overscroll-y-contain touch-pan-y">
|
|
66
66
|
{/* Search + clear */}
|
|
67
67
|
{sorted.length > 0 && (
|
|
68
68
|
<div className="px-3 py-2 shrink-0 flex flex-col gap-2">
|
|
@@ -10,7 +10,7 @@ import { AgentPickerList } from '@/components/shared/agent-picker-list'
|
|
|
10
10
|
import { DirBrowser } from '@/components/shared/dir-browser'
|
|
11
11
|
import { SheetFooter } from '@/components/shared/sheet-footer'
|
|
12
12
|
import { inputClass } from '@/components/shared/form-styles'
|
|
13
|
-
import type { BoardTask, TaskComment } from '@/types'
|
|
13
|
+
import type { BoardTask, TaskComment, TaskQualityGateConfig } from '@/types'
|
|
14
14
|
import { SectionLabel } from '@/components/shared/section-label'
|
|
15
15
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
16
16
|
|
|
@@ -22,6 +22,16 @@ function fmtTime(ts: number) {
|
|
|
22
22
|
return d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function normalizeGateNumber(value: unknown, fallback: number, min: number, max: number): number {
|
|
26
|
+
const parsed = typeof value === 'number'
|
|
27
|
+
? value
|
|
28
|
+
: typeof value === 'string'
|
|
29
|
+
? Number.parseInt(value, 10)
|
|
30
|
+
: Number.NaN
|
|
31
|
+
if (!Number.isFinite(parsed)) return fallback
|
|
32
|
+
return Math.max(min, Math.min(max, Math.trunc(parsed)))
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
export function TaskSheet() {
|
|
26
36
|
const open = useAppStore((s) => s.taskSheetOpen)
|
|
27
37
|
const setOpen = useAppStore((s) => s.setTaskSheetOpen)
|
|
@@ -59,6 +69,12 @@ export function TaskSheet() {
|
|
|
59
69
|
const [dueAt, setDueAt] = useState<string>('')
|
|
60
70
|
const [customFields, setCustomFields] = useState<Record<string, string | number | boolean>>({})
|
|
61
71
|
const [priority, setPriority] = useState<'low' | 'medium' | 'high' | 'critical' | ''>('')
|
|
72
|
+
const [qualityGateEnabled, setQualityGateEnabled] = useState(true)
|
|
73
|
+
const [qualityGateMinResultChars, setQualityGateMinResultChars] = useState(80)
|
|
74
|
+
const [qualityGateMinEvidenceItems, setQualityGateMinEvidenceItems] = useState(2)
|
|
75
|
+
const [qualityGateRequireVerification, setQualityGateRequireVerification] = useState(false)
|
|
76
|
+
const [qualityGateRequireArtifact, setQualityGateRequireArtifact] = useState(false)
|
|
77
|
+
const [qualityGateRequireReport, setQualityGateRequireReport] = useState(false)
|
|
62
78
|
|
|
63
79
|
const editing = editingId ? tasks[editingId] : null
|
|
64
80
|
const agentList = Object.values(agents).sort((a, b) => a.name.localeCompare(b.name))
|
|
@@ -68,6 +84,12 @@ export function TaskSheet() {
|
|
|
68
84
|
loadAgents()
|
|
69
85
|
loadProjects()
|
|
70
86
|
loadSettings()
|
|
87
|
+
const defaultGateEnabled = appSettings.taskQualityGateEnabled ?? true
|
|
88
|
+
const defaultGateMinResult = normalizeGateNumber(appSettings.taskQualityGateMinResultChars, 80, 10, 2000)
|
|
89
|
+
const defaultGateMinEvidence = normalizeGateNumber(appSettings.taskQualityGateMinEvidenceItems, 2, 0, 8)
|
|
90
|
+
const defaultGateRequireVerification = appSettings.taskQualityGateRequireVerification ?? false
|
|
91
|
+
const defaultGateRequireArtifact = appSettings.taskQualityGateRequireArtifact ?? false
|
|
92
|
+
const defaultGateRequireReport = appSettings.taskQualityGateRequireReport ?? false
|
|
71
93
|
if (editing) {
|
|
72
94
|
setTitle(editing.title)
|
|
73
95
|
setDescription(editing.description)
|
|
@@ -83,6 +105,13 @@ export function TaskSheet() {
|
|
|
83
105
|
setDueAt(editing.dueAt ? new Date(editing.dueAt).toISOString().slice(0, 10) : '')
|
|
84
106
|
setCustomFields(editing.customFields || {})
|
|
85
107
|
setPriority(editing.priority || '')
|
|
108
|
+
const gate = (editing.qualityGate || null) as TaskQualityGateConfig | null
|
|
109
|
+
setQualityGateEnabled(gate?.enabled ?? defaultGateEnabled)
|
|
110
|
+
setQualityGateMinResultChars(normalizeGateNumber(gate?.minResultChars, defaultGateMinResult, 10, 2000))
|
|
111
|
+
setQualityGateMinEvidenceItems(normalizeGateNumber(gate?.minEvidenceItems, defaultGateMinEvidence, 0, 8))
|
|
112
|
+
setQualityGateRequireVerification(gate?.requireVerification ?? defaultGateRequireVerification)
|
|
113
|
+
setQualityGateRequireArtifact(gate?.requireArtifact ?? defaultGateRequireArtifact)
|
|
114
|
+
setQualityGateRequireReport(gate?.requireReport ?? defaultGateRequireReport)
|
|
86
115
|
} else {
|
|
87
116
|
setTitle('')
|
|
88
117
|
setDescription('')
|
|
@@ -98,6 +127,12 @@ export function TaskSheet() {
|
|
|
98
127
|
setDueAt('')
|
|
99
128
|
setCustomFields({})
|
|
100
129
|
setPriority('')
|
|
130
|
+
setQualityGateEnabled(defaultGateEnabled)
|
|
131
|
+
setQualityGateMinResultChars(defaultGateMinResult)
|
|
132
|
+
setQualityGateMinEvidenceItems(defaultGateMinEvidence)
|
|
133
|
+
setQualityGateRequireVerification(defaultGateRequireVerification)
|
|
134
|
+
setQualityGateRequireArtifact(defaultGateRequireArtifact)
|
|
135
|
+
setQualityGateRequireReport(defaultGateRequireReport)
|
|
101
136
|
}
|
|
102
137
|
}
|
|
103
138
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -116,6 +151,17 @@ export function TaskSheet() {
|
|
|
116
151
|
}
|
|
117
152
|
|
|
118
153
|
const handleSave = async () => {
|
|
154
|
+
const qualityGate: TaskQualityGateConfig | null = qualityGateEnabled
|
|
155
|
+
? {
|
|
156
|
+
enabled: true,
|
|
157
|
+
minResultChars: qualityGateMinResultChars,
|
|
158
|
+
minEvidenceItems: qualityGateMinEvidenceItems,
|
|
159
|
+
requireVerification: qualityGateRequireVerification,
|
|
160
|
+
requireArtifact: qualityGateRequireArtifact,
|
|
161
|
+
requireReport: qualityGateRequireReport,
|
|
162
|
+
}
|
|
163
|
+
: null
|
|
164
|
+
|
|
119
165
|
// projectId uses null (not undefined) so the API can distinguish "clear" from "not sent"
|
|
120
166
|
// projectId uses null (not undefined) so the API can distinguish "clear" from "not sent"
|
|
121
167
|
const payload = {
|
|
@@ -124,6 +170,7 @@ export function TaskSheet() {
|
|
|
124
170
|
tags, blockedBy, dueAt: dueAt ? new Date(dueAt).getTime() : null,
|
|
125
171
|
customFields: Object.keys(customFields).length > 0 ? customFields : undefined,
|
|
126
172
|
priority: priority || undefined,
|
|
173
|
+
qualityGate,
|
|
127
174
|
} as Partial<BoardTask> & { title: string; description: string; agentId: string }
|
|
128
175
|
try {
|
|
129
176
|
if (editing) {
|
|
@@ -363,6 +410,19 @@ export function TaskSheet() {
|
|
|
363
410
|
</div>
|
|
364
411
|
)}
|
|
365
412
|
|
|
413
|
+
{editing.qualityGate?.enabled && (
|
|
414
|
+
<div className="mb-8">
|
|
415
|
+
<SectionLabel>Quality Gate</SectionLabel>
|
|
416
|
+
<div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface space-y-1.5 text-[12px] text-text-2">
|
|
417
|
+
<p>Min result chars: {editing.qualityGate.minResultChars ?? 80}</p>
|
|
418
|
+
<p>Min evidence signals: {editing.qualityGate.minEvidenceItems ?? 2}</p>
|
|
419
|
+
<p>Verification required: {(editing.qualityGate.requireVerification ?? false) ? 'Yes' : 'No'}</p>
|
|
420
|
+
<p>Artifact required: {(editing.qualityGate.requireArtifact ?? false) ? 'Yes' : 'No'}</p>
|
|
421
|
+
<p>Task report required: {(editing.qualityGate.requireReport ?? false) ? 'Yes' : 'No'}</p>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
)}
|
|
425
|
+
|
|
366
426
|
{/* Images (thumbnails only, no remove/upload) */}
|
|
367
427
|
{editing.images && editing.images.length > 0 && (
|
|
368
428
|
<div className="mb-8">
|
|
@@ -797,6 +857,75 @@ export function TaskSheet() {
|
|
|
797
857
|
/>
|
|
798
858
|
</div>
|
|
799
859
|
|
|
860
|
+
<div className="mb-8">
|
|
861
|
+
<SectionLabel>Quality Gate</SectionLabel>
|
|
862
|
+
<p className="text-[12px] text-text-3 mb-3">
|
|
863
|
+
Checks that must pass before this task can be marked completed.
|
|
864
|
+
</p>
|
|
865
|
+
<div className="p-4 rounded-[14px] border border-white/[0.06] bg-surface">
|
|
866
|
+
<button
|
|
867
|
+
onClick={() => setQualityGateEnabled((prev) => !prev)}
|
|
868
|
+
className={`relative w-10 h-[22px] rounded-full transition-colors duration-200 cursor-pointer ${qualityGateEnabled ? 'bg-accent' : 'bg-white/[0.12]'}`}
|
|
869
|
+
>
|
|
870
|
+
<span className={`absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform duration-200 ${qualityGateEnabled ? 'translate-x-[18px]' : ''}`} />
|
|
871
|
+
</button>
|
|
872
|
+
<span className="ml-2 text-[12px] text-text-2">{qualityGateEnabled ? 'Enabled' : 'Disabled'}</span>
|
|
873
|
+
|
|
874
|
+
{qualityGateEnabled && (
|
|
875
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-4">
|
|
876
|
+
<div>
|
|
877
|
+
<label className="block text-[11px] text-text-3 mb-1.5">Min Result Chars</label>
|
|
878
|
+
<input
|
|
879
|
+
type="number"
|
|
880
|
+
min={10}
|
|
881
|
+
max={2000}
|
|
882
|
+
value={qualityGateMinResultChars}
|
|
883
|
+
onChange={(e) => setQualityGateMinResultChars(normalizeGateNumber(e.target.value, 80, 10, 2000))}
|
|
884
|
+
className={inputClass}
|
|
885
|
+
style={{ fontFamily: 'inherit' }}
|
|
886
|
+
/>
|
|
887
|
+
</div>
|
|
888
|
+
<div>
|
|
889
|
+
<label className="block text-[11px] text-text-3 mb-1.5">Min Evidence Signals</label>
|
|
890
|
+
<input
|
|
891
|
+
type="number"
|
|
892
|
+
min={0}
|
|
893
|
+
max={8}
|
|
894
|
+
value={qualityGateMinEvidenceItems}
|
|
895
|
+
onChange={(e) => setQualityGateMinEvidenceItems(normalizeGateNumber(e.target.value, 2, 0, 8))}
|
|
896
|
+
className={inputClass}
|
|
897
|
+
style={{ fontFamily: 'inherit' }}
|
|
898
|
+
/>
|
|
899
|
+
</div>
|
|
900
|
+
<label className="flex items-center gap-2 text-[12px] text-text-2">
|
|
901
|
+
<input
|
|
902
|
+
type="checkbox"
|
|
903
|
+
checked={qualityGateRequireVerification}
|
|
904
|
+
onChange={(e) => setQualityGateRequireVerification(e.target.checked)}
|
|
905
|
+
/>
|
|
906
|
+
Require verification evidence (tests/lint/build)
|
|
907
|
+
</label>
|
|
908
|
+
<label className="flex items-center gap-2 text-[12px] text-text-2">
|
|
909
|
+
<input
|
|
910
|
+
type="checkbox"
|
|
911
|
+
checked={qualityGateRequireArtifact}
|
|
912
|
+
onChange={(e) => setQualityGateRequireArtifact(e.target.checked)}
|
|
913
|
+
/>
|
|
914
|
+
Require artifact evidence (upload URL or task artifacts)
|
|
915
|
+
</label>
|
|
916
|
+
<label className="flex items-center gap-2 text-[12px] text-text-2 md:col-span-2">
|
|
917
|
+
<input
|
|
918
|
+
type="checkbox"
|
|
919
|
+
checked={qualityGateRequireReport}
|
|
920
|
+
onChange={(e) => setQualityGateRequireReport(e.target.checked)}
|
|
921
|
+
/>
|
|
922
|
+
Require generated task report
|
|
923
|
+
</label>
|
|
924
|
+
</div>
|
|
925
|
+
)}
|
|
926
|
+
</div>
|
|
927
|
+
</div>
|
|
928
|
+
|
|
800
929
|
{/* Custom Fields */}
|
|
801
930
|
{appSettings.taskCustomFieldDefs && appSettings.taskCustomFieldDefs.length > 0 && (
|
|
802
931
|
<div className="mb-8">
|
|
@@ -64,6 +64,7 @@ function DialogContent({
|
|
|
64
64
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
|
65
65
|
className
|
|
66
66
|
)}
|
|
67
|
+
style={{ animation: 'spring-in 0.4s var(--ease-spring)' }}
|
|
67
68
|
{...props}
|
|
68
69
|
>
|
|
69
70
|
{children}
|
|
@@ -9,6 +9,7 @@ import { useAppStore } from '@/stores/use-app-store'
|
|
|
9
9
|
import { useWs } from '@/hooks/use-ws'
|
|
10
10
|
import { api } from '@/lib/api-client'
|
|
11
11
|
import type { BoardTask } from '@/types'
|
|
12
|
+
import { HintTip } from '@/components/shared/hint-tip'
|
|
12
13
|
|
|
13
14
|
type Range = '24h' | '7d' | '30d'
|
|
14
15
|
|
|
@@ -66,11 +67,9 @@ function formatDuration(ms: number): string {
|
|
|
66
67
|
|
|
67
68
|
function formatBucketLabel(bucket: string, range: Range): string {
|
|
68
69
|
if (range === '24h') {
|
|
69
|
-
// "2026-03-01T14" → "14:00"
|
|
70
70
|
const hour = bucket.split('T')[1]
|
|
71
71
|
return hour ? `${hour}:00` : bucket
|
|
72
72
|
}
|
|
73
|
-
// "2026-03-01" → "Mar 1"
|
|
74
73
|
const parts = bucket.split('-')
|
|
75
74
|
if (parts.length === 3) {
|
|
76
75
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
|
@@ -118,11 +117,8 @@ export function MetricsDashboard() {
|
|
|
118
117
|
try {
|
|
119
118
|
const res = await api<UsageResponse>('GET', `/usage?range=${range}`)
|
|
120
119
|
setData(res)
|
|
121
|
-
} catch {
|
|
122
|
-
|
|
123
|
-
} finally {
|
|
124
|
-
setLoading(false)
|
|
125
|
-
}
|
|
120
|
+
} catch { /* ignore */ }
|
|
121
|
+
setLoading(false)
|
|
126
122
|
}, [range])
|
|
127
123
|
|
|
128
124
|
useEffect(() => {
|
|
@@ -135,7 +131,6 @@ export function MetricsDashboard() {
|
|
|
135
131
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
136
132
|
}, [])
|
|
137
133
|
|
|
138
|
-
// --- Task metrics ---
|
|
139
134
|
const [taskMetrics, setTaskMetrics] = useState<{
|
|
140
135
|
wip: number; completedCount: number; avgCycleMs: number
|
|
141
136
|
velocity: { bucket: string; count: number }[]
|
|
@@ -156,7 +151,6 @@ export function MetricsDashboard() {
|
|
|
156
151
|
|
|
157
152
|
const completionRate = computeCompletionRate(tasks)
|
|
158
153
|
|
|
159
|
-
// Prepare chart data
|
|
160
154
|
const timeSeriesFormatted = (data?.timeSeries ?? []).map((pt) => ({
|
|
161
155
|
...pt,
|
|
162
156
|
label: formatBucketLabel(pt.bucket, range),
|
|
@@ -190,13 +184,13 @@ export function MetricsDashboard() {
|
|
|
190
184
|
|
|
191
185
|
return (
|
|
192
186
|
<div className="flex-1 flex flex-col h-full overflow-y-auto">
|
|
193
|
-
<div className="px-8 pt-6 pb-4 shrink-0">
|
|
187
|
+
<div className="px-8 pt-6 pb-4 shrink-0" style={{ animation: 'fade-up 0.5s var(--ease-spring)' }}>
|
|
194
188
|
<h1 className="font-display text-[28px] font-700 tracking-[-0.03em]">Usage</h1>
|
|
195
189
|
<p className="text-[13px] text-text-3 mt-1">Token usage, cost tracking & agent performance</p>
|
|
196
190
|
</div>
|
|
197
191
|
|
|
198
192
|
{/* Range tabs */}
|
|
199
|
-
<div className="px-8 pb-4 shrink-0">
|
|
193
|
+
<div className="px-8 pb-4 shrink-0" style={{ animation: 'fade-up 0.5s var(--ease-spring) 0.05s both' }}>
|
|
200
194
|
<div className="flex gap-1 bg-surface-2 rounded-[10px] p-1 w-fit">
|
|
201
195
|
{RANGES.map((r) => (
|
|
202
196
|
<button
|
|
@@ -207,6 +201,7 @@ export function MetricsDashboard() {
|
|
|
207
201
|
? 'bg-accent-soft text-accent-bright'
|
|
208
202
|
: 'text-text-3 hover:text-text-2'
|
|
209
203
|
}`}
|
|
204
|
+
style={range === r ? { animation: 'spring-in 0.3s var(--ease-spring)' } : undefined}
|
|
210
205
|
>
|
|
211
206
|
{RANGE_LABELS[r]}
|
|
212
207
|
</button>
|
|
@@ -225,31 +220,33 @@ export function MetricsDashboard() {
|
|
|
225
220
|
<div className="px-8 pb-8 space-y-6">
|
|
226
221
|
{/* Stats cards */}
|
|
227
222
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
228
|
-
<StatCard label="Total Tokens" value={formatTokens(data?.totalTokens ?? 0)} />
|
|
229
|
-
<StatCard label="Total Cost" value={formatCost(data?.totalCost ?? 0)} />
|
|
230
|
-
<StatCard label="Requests" value={String(data?.records.length ?? 0)} />
|
|
231
|
-
<StatCard label="Completion Rate" value={`${completionRate}%`} />
|
|
223
|
+
<StatCard label="Total Tokens" value={formatTokens(data?.totalTokens ?? 0)} index={0} />
|
|
224
|
+
<StatCard label="Total Cost" value={formatCost(data?.totalCost ?? 0)} index={1} />
|
|
225
|
+
<StatCard label="Requests" value={String(data?.records.length ?? 0)} index={2} />
|
|
226
|
+
<StatCard label="Completion Rate" value={`${completionRate}%`} index={3} />
|
|
232
227
|
</div>
|
|
233
228
|
|
|
234
229
|
{/* Token usage over time */}
|
|
235
|
-
<
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
<
|
|
239
|
-
<
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
230
|
+
<div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.2s both' }}>
|
|
231
|
+
<ChartCard title="Token Usage Over Time">
|
|
232
|
+
{timeSeriesFormatted.length > 0 ? (
|
|
233
|
+
<ResponsiveContainer width="100%" height={280}>
|
|
234
|
+
<LineChart data={timeSeriesFormatted} margin={{ top: 5, right: 20, bottom: 5, left: 0 }}>
|
|
235
|
+
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.06)" />
|
|
236
|
+
<XAxis dataKey="label" tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} />
|
|
237
|
+
<YAxis tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} tickFormatter={formatTokens} />
|
|
238
|
+
<Tooltip {...tooltipStyle} formatter={(value: number | undefined) => [formatTokens(value ?? 0), 'Tokens']} />
|
|
239
|
+
<Line type="monotone" dataKey="tokens" stroke="#818CF8" strokeWidth={2} dot={false} activeDot={{ r: 4, fill: '#818CF8' }} />
|
|
240
|
+
</LineChart>
|
|
241
|
+
</ResponsiveContainer>
|
|
242
|
+
) : (
|
|
243
|
+
<EmptyChart />
|
|
244
|
+
)}
|
|
245
|
+
</ChartCard>
|
|
246
|
+
</div>
|
|
250
247
|
|
|
251
248
|
{/* Cost by provider + cost by agent */}
|
|
252
|
-
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
249
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.25s both' }}>
|
|
253
250
|
<ChartCard title="Cost by Provider">
|
|
254
251
|
{providerData.length > 0 ? (
|
|
255
252
|
<ResponsiveContainer width="100%" height={280}>
|
|
@@ -293,16 +290,16 @@ export function MetricsDashboard() {
|
|
|
293
290
|
|
|
294
291
|
{/* Task KPIs */}
|
|
295
292
|
{taskMetrics && (
|
|
296
|
-
|
|
293
|
+
<div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.3s both' }}>
|
|
297
294
|
<h3 className="font-display text-[16px] font-700 text-text mt-2">Task Performance</h3>
|
|
298
|
-
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
299
|
-
<StatCard label="Tasks Completed" value={String(taskMetrics.completedCount)} />
|
|
300
|
-
<StatCard label="Avg Cycle Time" value={formatDuration(taskMetrics.avgCycleMs)} />
|
|
301
|
-
<StatCard label="WIP" value={String(taskMetrics.wip)} />
|
|
302
|
-
<StatCard label="Completion Rate" value={`${completionRate}%`} />
|
|
295
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mt-4">
|
|
296
|
+
<StatCard label="Tasks Completed" value={String(taskMetrics.completedCount)} index={0} />
|
|
297
|
+
<StatCard label="Avg Cycle Time" value={formatDuration(taskMetrics.avgCycleMs)} index={1} />
|
|
298
|
+
<StatCard label="WIP" value={String(taskMetrics.wip)} index={2} />
|
|
299
|
+
<StatCard label="Completion Rate" value={`${completionRate}%`} index={3} />
|
|
303
300
|
</div>
|
|
304
301
|
|
|
305
|
-
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
302
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
|
|
306
303
|
<ChartCard title="Task Velocity">
|
|
307
304
|
{taskMetrics.velocity.length > 0 ? (
|
|
308
305
|
<ResponsiveContainer width="100%" height={280}>
|
|
@@ -345,20 +342,44 @@ export function MetricsDashboard() {
|
|
|
345
342
|
)}
|
|
346
343
|
</ChartCard>
|
|
347
344
|
</div>
|
|
348
|
-
|
|
345
|
+
</div>
|
|
349
346
|
)}
|
|
350
347
|
|
|
348
|
+
{/* Latency by Provider */}
|
|
349
|
+
<div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.35s both' }}>
|
|
350
|
+
<ChartCard title="Average Latency by Provider (ms)">
|
|
351
|
+
{providerData.some(p => (data?.providerHealth?.[p.name]?.avgLatencyMs ?? 0) > 0) ? (
|
|
352
|
+
<ResponsiveContainer width="100%" height={280}>
|
|
353
|
+
<BarChart data={providerData.map(p => ({ ...p, latency: Math.round(data?.providerHealth?.[p.name]?.avgLatencyMs || 0) }))} layout="vertical" margin={{ top: 5, right: 30, bottom: 5, left: 40 }}>
|
|
354
|
+
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.06)" horizontal={false} />
|
|
355
|
+
<XAxis type="number" tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} />
|
|
356
|
+
<YAxis dataKey="name" type="category" tick={{ fill: '#888', fontSize: 11 }} axisLine={false} tickLine={false} width={80} />
|
|
357
|
+
<Tooltip {...tooltipStyle} cursor={{ fill: 'rgba(255,255,255,0.04)' }} />
|
|
358
|
+
<Bar dataKey="latency" radius={[0, 4, 4, 0]}>
|
|
359
|
+
{providerData.map((_entry, index) => (
|
|
360
|
+
<Cell key={`cell-${index}`} fill={CHART_COLORS[index % CHART_COLORS.length]} />
|
|
361
|
+
))}
|
|
362
|
+
</Bar>
|
|
363
|
+
</BarChart>
|
|
364
|
+
</ResponsiveContainer>
|
|
365
|
+
) : (
|
|
366
|
+
<EmptyChart />
|
|
367
|
+
)}
|
|
368
|
+
</ChartCard>
|
|
369
|
+
</div>
|
|
370
|
+
|
|
351
371
|
{/* Provider Health */}
|
|
352
372
|
{data?.providerHealth && Object.keys(data.providerHealth).length > 0 && (
|
|
353
|
-
<div>
|
|
354
|
-
<h3 className="font-display text-[14px] font-600 text-text-2 mb-3">Provider Health
|
|
373
|
+
<div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.4s both' }}>
|
|
374
|
+
<h3 className="font-display text-[14px] font-600 text-text-2 mb-3 flex items-center gap-2">Provider Health <HintTip text="API reliability and performance across your configured providers" /></h3>
|
|
355
375
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
356
376
|
{Object.entries(data.providerHealth)
|
|
357
377
|
.sort(([, a], [, b]) => b.totalRequests - a.totalRequests)
|
|
358
|
-
.map(([name, h]) => (
|
|
378
|
+
.map(([name, h], idx) => (
|
|
359
379
|
<div
|
|
360
380
|
key={name}
|
|
361
|
-
className="bg-surface-2 rounded-[12px] p-4 border border-white/[0.04] flex flex-col gap-3"
|
|
381
|
+
className="bg-surface-2 rounded-[12px] p-4 border border-white/[0.04] flex flex-col gap-3 hover:bg-surface transition-all hover:scale-[1.02]"
|
|
382
|
+
style={{ animation: 'spring-in 0.5s var(--ease-spring) both', animationDelay: `${0.45 + idx * 0.03}s` }}
|
|
362
383
|
>
|
|
363
384
|
<div className="flex items-center justify-between">
|
|
364
385
|
<p className="text-[14px] font-600 text-text">{name}</p>
|
|
@@ -367,13 +388,13 @@ export function MetricsDashboard() {
|
|
|
367
388
|
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-[12px]">
|
|
368
389
|
<span className="text-text-3">Requests</span>
|
|
369
390
|
<span className="text-text font-500 text-right">{h.totalRequests}</span>
|
|
370
|
-
<span className="text-text-3">Error Rate
|
|
391
|
+
<span className="text-text-3 flex items-center gap-1">Error Rate <HintTip text="Percentage of API calls that failed" /></span>
|
|
371
392
|
<span className={`font-500 text-right ${errorRateColor(h.errorRate)}`}>
|
|
372
393
|
{(h.errorRate * 100).toFixed(1)}%
|
|
373
394
|
</span>
|
|
374
395
|
{h.avgLatencyMs > 0 && (
|
|
375
396
|
<>
|
|
376
|
-
<span className="text-text-3">Avg Latency
|
|
397
|
+
<span className="text-text-3 flex items-center gap-1">Avg Latency <HintTip text="Average response time from the provider" /></span>
|
|
377
398
|
<span className="text-text font-500 text-right">{Math.round(h.avgLatencyMs)}ms</span>
|
|
378
399
|
</>
|
|
379
400
|
)}
|
|
@@ -401,9 +422,12 @@ export function MetricsDashboard() {
|
|
|
401
422
|
)
|
|
402
423
|
}
|
|
403
424
|
|
|
404
|
-
function StatCard({ label, value }: { label: string; value: string }) {
|
|
425
|
+
function StatCard({ label, value, index = 0 }: { label: string; value: string; index?: number }) {
|
|
405
426
|
return (
|
|
406
|
-
<div
|
|
427
|
+
<div
|
|
428
|
+
className="bg-surface-2 rounded-[12px] p-4 border border-white/[0.04] hover:bg-surface transition-all hover:scale-[1.02]"
|
|
429
|
+
style={{ animation: 'spring-in 0.6s var(--ease-spring) both', animationDelay: `${0.1 + index * 0.05}s` }}
|
|
430
|
+
>
|
|
407
431
|
<p className="text-[11px] font-500 text-text-3 uppercase tracking-[0.05em] mb-1">{label}</p>
|
|
408
432
|
<p className="text-[22px] font-display font-700 tracking-[-0.02em] text-text">{value}</p>
|
|
409
433
|
</div>
|
|
@@ -412,7 +436,7 @@ function StatCard({ label, value }: { label: string; value: string }) {
|
|
|
412
436
|
|
|
413
437
|
function ChartCard({ title, children }: { title: string; children: React.ReactNode }) {
|
|
414
438
|
return (
|
|
415
|
-
<div className="bg-surface-2 rounded-[12px] p-5 border border-white/[0.04]">
|
|
439
|
+
<div className="bg-surface-2 rounded-[12px] p-5 border border-white/[0.04] hover:border-white/[0.1] transition-colors">
|
|
416
440
|
<h3 className="font-display text-[14px] font-600 text-text-2 mb-4">{title}</h3>
|
|
417
441
|
{children}
|
|
418
442
|
</div>
|