stagent 0.1.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/LICENSE +191 -0
- package/README.md +399 -0
- package/components.json +21 -0
- package/dist/cli.js +171 -0
- package/drizzle.config.ts +12 -0
- package/next.config.mjs +15 -0
- package/package.json +114 -0
- package/postcss.config.mjs +8 -0
- package/public/icon-512.png +0 -0
- package/public/icon.svg +13 -0
- package/public/readme/home-workspace.png +0 -0
- package/public/readme/inbox-approvals.png +0 -0
- package/public/readme/workflow-blueprints.png +0 -0
- package/public/stagent-s-128.png +0 -0
- package/public/stagent-s-64.png +0 -0
- package/src/app/api/blueprints/[id]/instantiate/route.ts +27 -0
- package/src/app/api/blueprints/[id]/route.ts +39 -0
- package/src/app/api/blueprints/import/route.ts +68 -0
- package/src/app/api/blueprints/route.ts +29 -0
- package/src/app/api/command-palette/recent/route.ts +31 -0
- package/src/app/api/data/clear/route.ts +22 -0
- package/src/app/api/data/seed/route.ts +22 -0
- package/src/app/api/documents/[id]/file/route.ts +44 -0
- package/src/app/api/documents/[id]/route.ts +123 -0
- package/src/app/api/documents/route.ts +59 -0
- package/src/app/api/logs/stream/route.ts +101 -0
- package/src/app/api/notifications/[id]/route.ts +36 -0
- package/src/app/api/notifications/mark-all-read/route.ts +13 -0
- package/src/app/api/notifications/pending-approvals/route.ts +10 -0
- package/src/app/api/notifications/pending-approvals/stream/route.ts +101 -0
- package/src/app/api/notifications/route.ts +34 -0
- package/src/app/api/permissions/route.ts +46 -0
- package/src/app/api/profiles/[id]/route.ts +79 -0
- package/src/app/api/profiles/[id]/test/route.ts +42 -0
- package/src/app/api/profiles/import/route.ts +108 -0
- package/src/app/api/profiles/route.ts +50 -0
- package/src/app/api/projects/[id]/route.ts +72 -0
- package/src/app/api/projects/route.ts +53 -0
- package/src/app/api/schedules/[id]/route.ts +185 -0
- package/src/app/api/schedules/route.ts +117 -0
- package/src/app/api/settings/budgets/route.ts +24 -0
- package/src/app/api/settings/openai/route.ts +24 -0
- package/src/app/api/settings/route.ts +21 -0
- package/src/app/api/settings/test/route.ts +26 -0
- package/src/app/api/tasks/[id]/cancel/route.ts +21 -0
- package/src/app/api/tasks/[id]/execute/route.ts +90 -0
- package/src/app/api/tasks/[id]/logs/route.ts +95 -0
- package/src/app/api/tasks/[id]/output/route.ts +47 -0
- package/src/app/api/tasks/[id]/respond/route.ts +64 -0
- package/src/app/api/tasks/[id]/resume/route.ts +76 -0
- package/src/app/api/tasks/[id]/route.ts +77 -0
- package/src/app/api/tasks/assist/route.ts +35 -0
- package/src/app/api/tasks/route.ts +82 -0
- package/src/app/api/uploads/[id]/route.ts +81 -0
- package/src/app/api/uploads/cleanup/route.ts +7 -0
- package/src/app/api/uploads/route.ts +66 -0
- package/src/app/api/workflows/[id]/execute/route.ts +82 -0
- package/src/app/api/workflows/[id]/route.ts +133 -0
- package/src/app/api/workflows/[id]/status/route.ts +54 -0
- package/src/app/api/workflows/[id]/steps/[stepId]/retry/route.ts +22 -0
- package/src/app/api/workflows/route.ts +61 -0
- package/src/app/apple-icon.tsx +31 -0
- package/src/app/costs/page.tsx +256 -0
- package/src/app/dashboard/page.tsx +44 -0
- package/src/app/documents/[id]/page.tsx +46 -0
- package/src/app/documents/page.tsx +45 -0
- package/src/app/error.tsx +26 -0
- package/src/app/global-error.tsx +23 -0
- package/src/app/globals.css +733 -0
- package/src/app/icon.tsx +30 -0
- package/src/app/inbox/loading.tsx +15 -0
- package/src/app/inbox/page.tsx +35 -0
- package/src/app/layout.tsx +78 -0
- package/src/app/manifest.ts +32 -0
- package/src/app/monitor/page.tsx +37 -0
- package/src/app/page.tsx +162 -0
- package/src/app/profiles/[id]/edit/page.tsx +39 -0
- package/src/app/profiles/[id]/page.tsx +33 -0
- package/src/app/profiles/new/page.tsx +22 -0
- package/src/app/profiles/page.tsx +19 -0
- package/src/app/projects/[id]/page.tsx +134 -0
- package/src/app/projects/loading.tsx +17 -0
- package/src/app/projects/page.tsx +32 -0
- package/src/app/schedules/[id]/page.tsx +47 -0
- package/src/app/schedules/page.tsx +18 -0
- package/src/app/settings/loading.tsx +24 -0
- package/src/app/settings/page.tsx +27 -0
- package/src/app/tasks/[id]/page.tsx +45 -0
- package/src/app/tasks/new/page.tsx +27 -0
- package/src/app/workflows/[id]/edit/page.tsx +66 -0
- package/src/app/workflows/[id]/page.tsx +37 -0
- package/src/app/workflows/blueprints/[id]/page.tsx +40 -0
- package/src/app/workflows/blueprints/new/page.tsx +20 -0
- package/src/app/workflows/blueprints/page.tsx +11 -0
- package/src/app/workflows/new/page.tsx +36 -0
- package/src/app/workflows/page.tsx +18 -0
- package/src/components/charts/donut-ring.tsx +64 -0
- package/src/components/charts/mini-bar.tsx +75 -0
- package/src/components/charts/sparkline.tsx +107 -0
- package/src/components/costs/cost-dashboard.tsx +877 -0
- package/src/components/costs/cost-filters.tsx +179 -0
- package/src/components/dashboard/activity-feed.tsx +95 -0
- package/src/components/dashboard/greeting.tsx +30 -0
- package/src/components/dashboard/priority-queue.tsx +79 -0
- package/src/components/dashboard/quick-actions.tsx +62 -0
- package/src/components/dashboard/recent-projects.tsx +79 -0
- package/src/components/dashboard/stats-cards.tsx +114 -0
- package/src/components/documents/document-browser.tsx +235 -0
- package/src/components/documents/document-detail-view.tsx +367 -0
- package/src/components/documents/document-grid.tsx +78 -0
- package/src/components/documents/document-preview.tsx +68 -0
- package/src/components/documents/document-table.tsx +119 -0
- package/src/components/documents/document-upload-dialog.tsx +153 -0
- package/src/components/documents/types.ts +6 -0
- package/src/components/documents/utils.ts +57 -0
- package/src/components/monitoring/connection-indicator.tsx +14 -0
- package/src/components/monitoring/log-entry.tsx +79 -0
- package/src/components/monitoring/log-filters.tsx +57 -0
- package/src/components/monitoring/log-stream.tsx +144 -0
- package/src/components/monitoring/monitor-overview-wrapper.tsx +64 -0
- package/src/components/monitoring/monitor-overview.tsx +119 -0
- package/src/components/notifications/failure-action.tsx +38 -0
- package/src/components/notifications/inbox-list.tsx +165 -0
- package/src/components/notifications/message-response.tsx +196 -0
- package/src/components/notifications/notification-item.tsx +250 -0
- package/src/components/notifications/pending-approval-host.tsx +478 -0
- package/src/components/notifications/permission-action.tsx +37 -0
- package/src/components/notifications/permission-response-actions.tsx +126 -0
- package/src/components/notifications/unread-badge.tsx +35 -0
- package/src/components/profiles/profile-browser.tsx +117 -0
- package/src/components/profiles/profile-card.tsx +78 -0
- package/src/components/profiles/profile-detail-view.tsx +564 -0
- package/src/components/profiles/profile-form-view.tsx +480 -0
- package/src/components/profiles/profile-import-dialog.tsx +113 -0
- package/src/components/projects/project-card.tsx +58 -0
- package/src/components/projects/project-create-dialog.tsx +140 -0
- package/src/components/projects/project-detail.tsx +68 -0
- package/src/components/projects/project-edit-dialog.tsx +219 -0
- package/src/components/projects/project-list.tsx +108 -0
- package/src/components/schedules/schedule-create-dialog.tsx +403 -0
- package/src/components/schedules/schedule-detail-view.tsx +274 -0
- package/src/components/schedules/schedule-list.tsx +242 -0
- package/src/components/schedules/schedule-status-badge.tsx +16 -0
- package/src/components/settings/api-key-form.tsx +141 -0
- package/src/components/settings/auth-config-section.tsx +141 -0
- package/src/components/settings/auth-method-selector.tsx +67 -0
- package/src/components/settings/auth-status-badge.tsx +40 -0
- package/src/components/settings/auth-status-dot.tsx +59 -0
- package/src/components/settings/budget-guardrails-section.tsx +842 -0
- package/src/components/settings/data-management-section.tsx +141 -0
- package/src/components/settings/openai-runtime-section.tsx +104 -0
- package/src/components/settings/permissions-section.tsx +91 -0
- package/src/components/shared/app-sidebar.tsx +123 -0
- package/src/components/shared/card-skeleton.tsx +42 -0
- package/src/components/shared/command-palette.tsx +250 -0
- package/src/components/shared/confirm-dialog.tsx +52 -0
- package/src/components/shared/empty-state.tsx +24 -0
- package/src/components/shared/error-state.tsx +32 -0
- package/src/components/shared/form-section-card.tsx +33 -0
- package/src/components/shared/section-heading.tsx +14 -0
- package/src/components/shared/stagent-logo.tsx +21 -0
- package/src/components/shared/theme-toggle.tsx +46 -0
- package/src/components/tasks/ai-assist-panel.tsx +210 -0
- package/src/components/tasks/content-preview.tsx +89 -0
- package/src/components/tasks/empty-board.tsx +12 -0
- package/src/components/tasks/file-upload.tsx +120 -0
- package/src/components/tasks/kanban-board.tsx +275 -0
- package/src/components/tasks/kanban-column.tsx +75 -0
- package/src/components/tasks/skeleton-board.tsx +21 -0
- package/src/components/tasks/task-attachments.tsx +114 -0
- package/src/components/tasks/task-card.tsx +101 -0
- package/src/components/tasks/task-create-panel.tsx +360 -0
- package/src/components/tasks/task-detail-view.tsx +356 -0
- package/src/components/ui/alert-dialog.tsx +196 -0
- package/src/components/ui/badge.tsx +50 -0
- package/src/components/ui/button.tsx +71 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/components/ui/command.tsx +184 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/form.tsx +167 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/popover.tsx +89 -0
- package/src/components/ui/progress.tsx +31 -0
- package/src/components/ui/radio-group.tsx +45 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sidebar.tsx +726 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +63 -0
- package/src/components/ui/sonner.tsx +36 -0
- package/src/components/ui/switch.tsx +35 -0
- package/src/components/ui/table.tsx +116 -0
- package/src/components/ui/tabs.tsx +91 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +57 -0
- package/src/components/workflows/blueprint-editor.tsx +109 -0
- package/src/components/workflows/blueprint-gallery.tsx +155 -0
- package/src/components/workflows/blueprint-preview.tsx +240 -0
- package/src/components/workflows/loop-status-view.tsx +272 -0
- package/src/components/workflows/swarm-dashboard.tsx +185 -0
- package/src/components/workflows/workflow-form-view.tsx +1376 -0
- package/src/components/workflows/workflow-list.tsx +230 -0
- package/src/components/workflows/workflow-status-view.tsx +477 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/instrumentation.ts +7 -0
- package/src/lib/agents/claude-agent.ts +737 -0
- package/src/lib/agents/execution-manager.ts +27 -0
- package/src/lib/agents/profiles/assignment-validation.ts +75 -0
- package/src/lib/agents/profiles/builtins/code-reviewer/SKILL.md +21 -0
- package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +28 -0
- package/src/lib/agents/profiles/builtins/data-analyst/SKILL.md +25 -0
- package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/devops-engineer/SKILL.md +34 -0
- package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/document-writer/SKILL.md +16 -0
- package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/general/SKILL.md +13 -0
- package/src/lib/agents/profiles/builtins/general/profile.yaml +18 -0
- package/src/lib/agents/profiles/builtins/health-fitness-coach/SKILL.md +34 -0
- package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/learning-coach/SKILL.md +35 -0
- package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/project-manager/SKILL.md +26 -0
- package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/researcher/SKILL.md +15 -0
- package/src/lib/agents/profiles/builtins/researcher/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +34 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/technical-writer/SKILL.md +31 -0
- package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +29 -0
- package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +23 -0
- package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +24 -0
- package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +26 -0
- package/src/lib/agents/profiles/compatibility.ts +109 -0
- package/src/lib/agents/profiles/registry.ts +293 -0
- package/src/lib/agents/profiles/test-runner.ts +18 -0
- package/src/lib/agents/profiles/test-types.ts +20 -0
- package/src/lib/agents/profiles/types.ts +43 -0
- package/src/lib/agents/router.ts +56 -0
- package/src/lib/agents/runtime/catalog.ts +85 -0
- package/src/lib/agents/runtime/claude-sdk.ts +12 -0
- package/src/lib/agents/runtime/claude.ts +370 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +289 -0
- package/src/lib/agents/runtime/index.ts +167 -0
- package/src/lib/agents/runtime/openai-codex.ts +1089 -0
- package/src/lib/agents/runtime/task-assist-types.ts +8 -0
- package/src/lib/agents/runtime/types.ts +30 -0
- package/src/lib/constants/settings.ts +13 -0
- package/src/lib/constants/status-colors.ts +44 -0
- package/src/lib/constants/task-status.ts +49 -0
- package/src/lib/data/clear.ts +63 -0
- package/src/lib/data/seed-data/documents.ts +715 -0
- package/src/lib/data/seed-data/logs.ts +195 -0
- package/src/lib/data/seed-data/notifications.ts +141 -0
- package/src/lib/data/seed-data/profiles.ts +175 -0
- package/src/lib/data/seed-data/projects.ts +61 -0
- package/src/lib/data/seed-data/schedules.ts +108 -0
- package/src/lib/data/seed-data/tasks.ts +341 -0
- package/src/lib/data/seed-data/usage-ledger.ts +130 -0
- package/src/lib/data/seed-data/workflows.ts +213 -0
- package/src/lib/data/seed.ts +129 -0
- package/src/lib/db/index.ts +221 -0
- package/src/lib/db/migrations/0000_aromatic_gargoyle.sql +59 -0
- package/src/lib/db/migrations/0001_first_iron_patriot.sql +6 -0
- package/src/lib/db/migrations/0002_add_resume_count.sql +1 -0
- package/src/lib/db/migrations/0003_add_settings.sql +5 -0
- package/src/lib/db/migrations/0004_add_documents.sql +20 -0
- package/src/lib/db/migrations/0005_add_document_preprocessing.sql +4 -0
- package/src/lib/db/migrations/0006_add_agent_profile.sql +2 -0
- package/src/lib/db/migrations/0007_add_usage_metering_ledger.sql +30 -0
- package/src/lib/db/migrations/0008_add_document_version.sql +1 -0
- package/src/lib/db/migrations/meta/0000_snapshot.json +416 -0
- package/src/lib/db/migrations/meta/0001_snapshot.json +461 -0
- package/src/lib/db/migrations/meta/0002_snapshot.json +469 -0
- package/src/lib/db/migrations/meta/_journal.json +27 -0
- package/src/lib/db/schema.ts +227 -0
- package/src/lib/documents/cleanup.ts +50 -0
- package/src/lib/documents/context-builder.ts +75 -0
- package/src/lib/documents/output-scanner.ts +166 -0
- package/src/lib/documents/processor.ts +120 -0
- package/src/lib/documents/processors/image.ts +21 -0
- package/src/lib/documents/processors/office.ts +36 -0
- package/src/lib/documents/processors/pdf.ts +12 -0
- package/src/lib/documents/processors/spreadsheet.ts +18 -0
- package/src/lib/documents/processors/text.ts +8 -0
- package/src/lib/documents/registry.ts +25 -0
- package/src/lib/notifications/actionable.ts +108 -0
- package/src/lib/notifications/permissions.ts +169 -0
- package/src/lib/queries/chart-data.ts +184 -0
- package/src/lib/schedules/interval-parser.ts +110 -0
- package/src/lib/schedules/scheduler.ts +220 -0
- package/src/lib/settings/auth.ts +98 -0
- package/src/lib/settings/budget-guardrails.ts +590 -0
- package/src/lib/settings/helpers.ts +23 -0
- package/src/lib/settings/openai-auth.ts +80 -0
- package/src/lib/settings/permissions.ts +102 -0
- package/src/lib/usage/ledger.ts +489 -0
- package/src/lib/usage/pricing.ts +68 -0
- package/src/lib/utils/crypto.ts +90 -0
- package/src/lib/utils/format-timestamp.ts +46 -0
- package/src/lib/utils/session-cleanup.ts +26 -0
- package/src/lib/utils/stagent-paths.ts +18 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validators/blueprint.ts +43 -0
- package/src/lib/validators/profile.ts +64 -0
- package/src/lib/validators/project.ts +17 -0
- package/src/lib/validators/settings.ts +57 -0
- package/src/lib/validators/task.ts +30 -0
- package/src/lib/workflows/blueprints/builtins/code-review-pipeline.yaml +72 -0
- package/src/lib/workflows/blueprints/builtins/documentation-generation.yaml +62 -0
- package/src/lib/workflows/blueprints/builtins/investment-research.yaml +81 -0
- package/src/lib/workflows/blueprints/builtins/meal-planning.yaml +73 -0
- package/src/lib/workflows/blueprints/builtins/product-research.yaml +72 -0
- package/src/lib/workflows/blueprints/builtins/research-report.yaml +77 -0
- package/src/lib/workflows/blueprints/builtins/sprint-planning.yaml +77 -0
- package/src/lib/workflows/blueprints/builtins/travel-planning.yaml +80 -0
- package/src/lib/workflows/blueprints/instantiator.ts +131 -0
- package/src/lib/workflows/blueprints/registry.ts +128 -0
- package/src/lib/workflows/blueprints/template.ts +58 -0
- package/src/lib/workflows/blueprints/types.ts +38 -0
- package/src/lib/workflows/definition-validation.ts +121 -0
- package/src/lib/workflows/engine.ts +1113 -0
- package/src/lib/workflows/loop-executor.ts +270 -0
- package/src/lib/workflows/parallel.ts +55 -0
- package/src/lib/workflows/swarm.ts +97 -0
- package/src/lib/workflows/types.ts +112 -0
- package/tsconfig.json +41 -0
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import {
|
|
3
|
+
AlertTriangle,
|
|
4
|
+
ArrowRight,
|
|
5
|
+
CalendarRange,
|
|
6
|
+
Coins,
|
|
7
|
+
ShieldAlert,
|
|
8
|
+
ShieldCheck,
|
|
9
|
+
Wallet,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import { listRuntimeCatalog } from "@/lib/agents/runtime/catalog";
|
|
12
|
+
import type {
|
|
13
|
+
ProviderModelBreakdownEntry,
|
|
14
|
+
UsageAuditEntry,
|
|
15
|
+
} from "@/lib/usage/ledger";
|
|
16
|
+
import { Badge } from "@/components/ui/badge";
|
|
17
|
+
import { Button } from "@/components/ui/button";
|
|
18
|
+
import { DonutRing } from "@/components/charts/donut-ring";
|
|
19
|
+
import { MiniBar } from "@/components/charts/mini-bar";
|
|
20
|
+
import { Sparkline } from "@/components/charts/sparkline";
|
|
21
|
+
import { SectionHeading } from "@/components/shared/section-heading";
|
|
22
|
+
import {
|
|
23
|
+
Table,
|
|
24
|
+
TableBody,
|
|
25
|
+
TableCell,
|
|
26
|
+
TableHead,
|
|
27
|
+
TableHeader,
|
|
28
|
+
TableRow,
|
|
29
|
+
} from "@/components/ui/table";
|
|
30
|
+
import { EmptyState } from "@/components/shared/empty-state";
|
|
31
|
+
import { CostFilters } from "@/components/costs/cost-filters";
|
|
32
|
+
|
|
33
|
+
type BudgetHealth = "unlimited" | "ok" | "warning" | "blocked";
|
|
34
|
+
type BudgetMetric = "spend" | "tokens";
|
|
35
|
+
type BudgetWindow = "daily" | "monthly";
|
|
36
|
+
|
|
37
|
+
interface BudgetStatus {
|
|
38
|
+
id: string;
|
|
39
|
+
scopeId: string;
|
|
40
|
+
scopeLabel: string;
|
|
41
|
+
runtimeId: string | null;
|
|
42
|
+
metric: BudgetMetric;
|
|
43
|
+
window: BudgetWindow;
|
|
44
|
+
currentValue: number;
|
|
45
|
+
limitValue: number | null;
|
|
46
|
+
ratio: number | null;
|
|
47
|
+
health: BudgetHealth;
|
|
48
|
+
resetAtIso: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface CostSummary {
|
|
52
|
+
todaySpendMicros: number;
|
|
53
|
+
monthSpendMicros: number;
|
|
54
|
+
todayTokens: number;
|
|
55
|
+
monthTokens: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface RuntimeBreakdownRow {
|
|
59
|
+
runtimeId: string;
|
|
60
|
+
label: string;
|
|
61
|
+
providerId: string;
|
|
62
|
+
costMicros: number;
|
|
63
|
+
totalTokens: number;
|
|
64
|
+
runs: number;
|
|
65
|
+
share: number;
|
|
66
|
+
unknownPricingRuns: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface ModelVisualMeta {
|
|
70
|
+
share: number;
|
|
71
|
+
valueLabel: string;
|
|
72
|
+
basisLabel: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface TrendSeries {
|
|
76
|
+
spend7: number[];
|
|
77
|
+
spend30: number[];
|
|
78
|
+
tokens7: number[];
|
|
79
|
+
tokens30: number[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface FilterState {
|
|
83
|
+
dateRange: string;
|
|
84
|
+
runtimeId: string;
|
|
85
|
+
status: string;
|
|
86
|
+
activityType: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface CostDashboardProps {
|
|
90
|
+
filters: FilterState;
|
|
91
|
+
summary: CostSummary;
|
|
92
|
+
trendSeries: TrendSeries;
|
|
93
|
+
budgetStatuses: BudgetStatus[];
|
|
94
|
+
runtimeBreakdown: RuntimeBreakdownRow[];
|
|
95
|
+
modelBreakdown: ProviderModelBreakdownEntry[];
|
|
96
|
+
auditEntries: UsageAuditEntry[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const runtimeCatalog = listRuntimeCatalog();
|
|
100
|
+
const runtimeLabelMap = new Map<string, string>(
|
|
101
|
+
runtimeCatalog.map((runtime) => [runtime.id, runtime.label])
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
function formatCurrencyMicros(value: number | null | undefined) {
|
|
105
|
+
const amount = value ?? 0;
|
|
106
|
+
return new Intl.NumberFormat("en-US", {
|
|
107
|
+
style: "currency",
|
|
108
|
+
currency: "USD",
|
|
109
|
+
minimumFractionDigits: 2,
|
|
110
|
+
maximumFractionDigits: amount >= 1_000_000 ? 2 : 4,
|
|
111
|
+
}).format(amount / 1_000_000);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function formatTokenCount(value: number | null | undefined) {
|
|
115
|
+
return new Intl.NumberFormat("en-US").format(value ?? 0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function formatCompactCount(value: number | null | undefined) {
|
|
119
|
+
return new Intl.NumberFormat("en-US", {
|
|
120
|
+
notation: "compact",
|
|
121
|
+
maximumFractionDigits: 1,
|
|
122
|
+
}).format(value ?? 0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function formatPercent(value: number) {
|
|
126
|
+
return `${Math.round(value)}%`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function clampPercent(value: number) {
|
|
130
|
+
return Math.max(0, Math.min(100, value));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function formatDateTime(value: string) {
|
|
134
|
+
return new Date(value).toLocaleString(undefined, {
|
|
135
|
+
dateStyle: "medium",
|
|
136
|
+
timeStyle: "short",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function formatDateRangeLabel(range: string) {
|
|
141
|
+
switch (range) {
|
|
142
|
+
case "7d":
|
|
143
|
+
return "Last 7 days";
|
|
144
|
+
case "90d":
|
|
145
|
+
return "Last 90 days";
|
|
146
|
+
case "all":
|
|
147
|
+
return "All time";
|
|
148
|
+
default:
|
|
149
|
+
return "Last 30 days";
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function formatActivityLabel(value: UsageAuditEntry["activityType"]) {
|
|
154
|
+
switch (value) {
|
|
155
|
+
case "task_run":
|
|
156
|
+
return "Task run";
|
|
157
|
+
case "task_resume":
|
|
158
|
+
return "Task resume";
|
|
159
|
+
case "workflow_step":
|
|
160
|
+
return "Workflow step";
|
|
161
|
+
case "scheduled_firing":
|
|
162
|
+
return "Scheduled firing";
|
|
163
|
+
case "task_assist":
|
|
164
|
+
return "Task assist";
|
|
165
|
+
case "profile_test":
|
|
166
|
+
return "Profile test";
|
|
167
|
+
default:
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function formatLedgerStatusLabel(value: UsageAuditEntry["status"]) {
|
|
173
|
+
switch (value) {
|
|
174
|
+
case "unknown_pricing":
|
|
175
|
+
return "Unknown pricing";
|
|
176
|
+
default:
|
|
177
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function statusBadge(status: UsageAuditEntry["status"]) {
|
|
182
|
+
switch (status) {
|
|
183
|
+
case "completed":
|
|
184
|
+
return <Badge variant="success">Completed</Badge>;
|
|
185
|
+
case "failed":
|
|
186
|
+
return <Badge variant="destructive">Failed</Badge>;
|
|
187
|
+
case "blocked":
|
|
188
|
+
return (
|
|
189
|
+
<Badge
|
|
190
|
+
variant="outline"
|
|
191
|
+
className="border-status-warning/30 bg-status-warning/10 text-status-warning"
|
|
192
|
+
>
|
|
193
|
+
Blocked
|
|
194
|
+
</Badge>
|
|
195
|
+
);
|
|
196
|
+
case "unknown_pricing":
|
|
197
|
+
return (
|
|
198
|
+
<Badge variant="outline" className="border-border/70 text-muted-foreground">
|
|
199
|
+
Unknown pricing
|
|
200
|
+
</Badge>
|
|
201
|
+
);
|
|
202
|
+
case "cancelled":
|
|
203
|
+
return <Badge variant="secondary">Cancelled</Badge>;
|
|
204
|
+
default:
|
|
205
|
+
return <Badge variant="secondary">{formatLedgerStatusLabel(status)}</Badge>;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function budgetBadge(status: BudgetStatus) {
|
|
210
|
+
if (status.health === "blocked") {
|
|
211
|
+
return <Badge variant="destructive">Blocked</Badge>;
|
|
212
|
+
}
|
|
213
|
+
if (status.health === "warning") {
|
|
214
|
+
return (
|
|
215
|
+
<Badge
|
|
216
|
+
variant="outline"
|
|
217
|
+
className="border-status-warning/30 bg-status-warning/10 text-status-warning"
|
|
218
|
+
>
|
|
219
|
+
Warning
|
|
220
|
+
</Badge>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (status.health === "ok") {
|
|
224
|
+
return <Badge variant="success">Tracked</Badge>;
|
|
225
|
+
}
|
|
226
|
+
return <Badge variant="secondary">Unlimited</Badge>;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function formatBudgetValue(status: BudgetStatus, value: number) {
|
|
230
|
+
return status.metric === "spend"
|
|
231
|
+
? formatCurrencyMicros(value)
|
|
232
|
+
: formatTokenCount(value);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function renderEntityLink(entry: UsageAuditEntry) {
|
|
236
|
+
if (entry.taskId && entry.taskTitle) {
|
|
237
|
+
return (
|
|
238
|
+
<Link href={`/tasks/${entry.taskId}`} className="font-medium hover:underline">
|
|
239
|
+
{entry.taskTitle}
|
|
240
|
+
</Link>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
if (entry.workflowId && entry.workflowName) {
|
|
244
|
+
return (
|
|
245
|
+
<Link
|
|
246
|
+
href={`/workflows/${entry.workflowId}`}
|
|
247
|
+
className="font-medium hover:underline"
|
|
248
|
+
>
|
|
249
|
+
{entry.workflowName}
|
|
250
|
+
</Link>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
if (entry.scheduleId && entry.scheduleName) {
|
|
254
|
+
return (
|
|
255
|
+
<Link
|
|
256
|
+
href={`/schedules/${entry.scheduleId}`}
|
|
257
|
+
className="font-medium hover:underline"
|
|
258
|
+
>
|
|
259
|
+
{entry.scheduleName}
|
|
260
|
+
</Link>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return <span className="font-medium">{formatActivityLabel(entry.activityType)}</span>;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function resolveModelVisualMeta(
|
|
268
|
+
row: ProviderModelBreakdownEntry,
|
|
269
|
+
totals: { costMicros: number; totalTokens: number }
|
|
270
|
+
): ModelVisualMeta {
|
|
271
|
+
if (totals.costMicros > 0 && row.costMicros > 0) {
|
|
272
|
+
const share = clampPercent((row.costMicros / totals.costMicros) * 100);
|
|
273
|
+
return {
|
|
274
|
+
share,
|
|
275
|
+
valueLabel: formatCurrencyMicros(row.costMicros),
|
|
276
|
+
basisLabel: `${formatPercent(share)} of filtered spend`,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (totals.totalTokens > 0 && row.totalTokens > 0) {
|
|
281
|
+
const share = clampPercent((row.totalTokens / totals.totalTokens) * 100);
|
|
282
|
+
return {
|
|
283
|
+
share,
|
|
284
|
+
valueLabel: `${formatCompactCount(row.totalTokens)} tokens`,
|
|
285
|
+
basisLabel: `${formatPercent(share)} of filtered tokens`,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
share: 0,
|
|
291
|
+
valueLabel:
|
|
292
|
+
row.unknownPricingRuns === row.runs ? "Pricing unavailable" : formatCurrencyMicros(0),
|
|
293
|
+
basisLabel: "No measurable cost or token usage",
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function SummaryCard({
|
|
298
|
+
eyebrow,
|
|
299
|
+
title,
|
|
300
|
+
value,
|
|
301
|
+
detail,
|
|
302
|
+
icon: Icon,
|
|
303
|
+
}: {
|
|
304
|
+
eyebrow: string;
|
|
305
|
+
title: string;
|
|
306
|
+
value: string;
|
|
307
|
+
detail: string;
|
|
308
|
+
icon: typeof Wallet;
|
|
309
|
+
}) {
|
|
310
|
+
return (
|
|
311
|
+
<div className="surface-card rounded-3xl p-5">
|
|
312
|
+
<div className="mb-4 flex items-start justify-between gap-3">
|
|
313
|
+
<div className="space-y-1">
|
|
314
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
315
|
+
{eyebrow}
|
|
316
|
+
</p>
|
|
317
|
+
<h2 className="text-sm font-medium text-foreground">{title}</h2>
|
|
318
|
+
</div>
|
|
319
|
+
<div className="surface-card-muted rounded-2xl p-2.5">
|
|
320
|
+
<Icon className="h-4 w-4 text-muted-foreground" />
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
<div className="space-y-1">
|
|
324
|
+
<p className="text-2xl font-bold tracking-tight">{value}</p>
|
|
325
|
+
<p className="text-xs text-muted-foreground">{detail}</p>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function CostDashboard({
|
|
332
|
+
filters,
|
|
333
|
+
summary,
|
|
334
|
+
trendSeries,
|
|
335
|
+
budgetStatuses,
|
|
336
|
+
runtimeBreakdown,
|
|
337
|
+
modelBreakdown,
|
|
338
|
+
auditEntries,
|
|
339
|
+
}: CostDashboardProps) {
|
|
340
|
+
const warnings = budgetStatuses.filter((status) => status.health === "warning");
|
|
341
|
+
const blocked = budgetStatuses.filter((status) => status.health === "blocked");
|
|
342
|
+
const configuredBudgets = budgetStatuses.filter((status) => status.limitValue != null);
|
|
343
|
+
const nearestBudget = configuredBudgets
|
|
344
|
+
.slice()
|
|
345
|
+
.sort((left, right) => (right.ratio ?? 0) - (left.ratio ?? 0))[0];
|
|
346
|
+
const hasUsage =
|
|
347
|
+
summary.monthSpendMicros > 0 ||
|
|
348
|
+
summary.monthTokens > 0 ||
|
|
349
|
+
modelBreakdown.length > 0 ||
|
|
350
|
+
auditEntries.length > 0;
|
|
351
|
+
const filteredUnknownPricingRuns = modelBreakdown.reduce(
|
|
352
|
+
(total, row) => total + row.unknownPricingRuns,
|
|
353
|
+
0
|
|
354
|
+
);
|
|
355
|
+
const modelTotals = modelBreakdown.reduce(
|
|
356
|
+
(totals, row) => ({
|
|
357
|
+
costMicros: totals.costMicros + row.costMicros,
|
|
358
|
+
totalTokens: totals.totalTokens + row.totalTokens,
|
|
359
|
+
}),
|
|
360
|
+
{ costMicros: 0, totalTokens: 0 }
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<div className="mx-auto flex max-w-7xl flex-col gap-6">
|
|
365
|
+
<div className="max-w-3xl space-y-3">
|
|
366
|
+
<div className="inline-flex items-center gap-2 rounded-full border border-border/60 bg-background/55 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
|
367
|
+
<Wallet className="h-3.5 w-3.5" />
|
|
368
|
+
Governance & Analytics
|
|
369
|
+
</div>
|
|
370
|
+
<div className="space-y-2">
|
|
371
|
+
<h1 className="text-2xl font-bold tracking-tight">Cost & Usage</h1>
|
|
372
|
+
<p className="max-w-2xl text-sm text-muted-foreground">
|
|
373
|
+
Review spend, token usage, and the execution history behind each paid
|
|
374
|
+
runtime action without leaving the operational shell.
|
|
375
|
+
</p>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<CostFilters
|
|
380
|
+
dateRange={filters.dateRange}
|
|
381
|
+
runtimeId={filters.runtimeId}
|
|
382
|
+
status={filters.status}
|
|
383
|
+
activityType={filters.activityType}
|
|
384
|
+
runtimeOptions={runtimeCatalog.map((runtime) => ({
|
|
385
|
+
id: runtime.id,
|
|
386
|
+
label: runtime.label,
|
|
387
|
+
}))}
|
|
388
|
+
/>
|
|
389
|
+
|
|
390
|
+
{blocked.length > 0 ? (
|
|
391
|
+
<div className="surface-card rounded-3xl border border-status-failed/25 bg-status-failed/8 p-5">
|
|
392
|
+
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
|
393
|
+
<div className="space-y-2">
|
|
394
|
+
<div className="flex items-center gap-2 text-status-failed">
|
|
395
|
+
<ShieldAlert className="h-4 w-4" />
|
|
396
|
+
<p className="text-sm font-semibold">Provider activity is currently blocked</p>
|
|
397
|
+
</div>
|
|
398
|
+
<p className="text-sm text-muted-foreground">
|
|
399
|
+
One or more active budget windows have been exceeded. New paid work
|
|
400
|
+
will remain blocked until the affected window resets.
|
|
401
|
+
</p>
|
|
402
|
+
</div>
|
|
403
|
+
<div className="grid gap-2 lg:min-w-[320px]">
|
|
404
|
+
{blocked.slice(0, 2).map((status) => (
|
|
405
|
+
<div
|
|
406
|
+
key={status.id}
|
|
407
|
+
className="surface-card-muted flex items-start justify-between gap-3 rounded-2xl p-3"
|
|
408
|
+
>
|
|
409
|
+
<div>
|
|
410
|
+
<p className="text-sm font-medium">
|
|
411
|
+
{status.scopeLabel} {status.window} {status.metric}
|
|
412
|
+
</p>
|
|
413
|
+
<p className="text-xs text-muted-foreground">
|
|
414
|
+
{formatBudgetValue(status, status.currentValue)} of{" "}
|
|
415
|
+
{formatBudgetValue(status, status.limitValue ?? 0)} used
|
|
416
|
+
</p>
|
|
417
|
+
</div>
|
|
418
|
+
<p className="text-right text-xs text-muted-foreground">
|
|
419
|
+
Resets {formatDateTime(status.resetAtIso)}
|
|
420
|
+
</p>
|
|
421
|
+
</div>
|
|
422
|
+
))}
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
) : null}
|
|
427
|
+
|
|
428
|
+
{blocked.length === 0 && warnings.length > 0 ? (
|
|
429
|
+
<div className="surface-card rounded-3xl border border-status-warning/25 bg-status-warning/8 p-5">
|
|
430
|
+
<div className="flex items-start gap-3">
|
|
431
|
+
<AlertTriangle className="mt-0.5 h-4 w-4 text-status-warning" />
|
|
432
|
+
<div className="space-y-2">
|
|
433
|
+
<p className="text-sm font-semibold">Budget usage is approaching a cap</p>
|
|
434
|
+
<p className="text-sm text-muted-foreground">
|
|
435
|
+
{warnings[0].scopeLabel} {warnings[0].window} {warnings[0].metric} is at{" "}
|
|
436
|
+
{formatPercent((warnings[0].ratio ?? 0) * 100)} of its configured
|
|
437
|
+
limit and resets {formatDateTime(warnings[0].resetAtIso)}.
|
|
438
|
+
</p>
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
) : null}
|
|
443
|
+
|
|
444
|
+
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
|
|
445
|
+
<SummaryCard
|
|
446
|
+
eyebrow="Today"
|
|
447
|
+
title="Spend"
|
|
448
|
+
value={formatCurrencyMicros(summary.todaySpendMicros)}
|
|
449
|
+
detail="Current-day spend across governed runtimes"
|
|
450
|
+
icon={Wallet}
|
|
451
|
+
/>
|
|
452
|
+
<SummaryCard
|
|
453
|
+
eyebrow="Month"
|
|
454
|
+
title="Spend"
|
|
455
|
+
value={formatCurrencyMicros(summary.monthSpendMicros)}
|
|
456
|
+
detail="Current-month spend used so far"
|
|
457
|
+
icon={CalendarRange}
|
|
458
|
+
/>
|
|
459
|
+
<SummaryCard
|
|
460
|
+
eyebrow="Today"
|
|
461
|
+
title="Tokens"
|
|
462
|
+
value={formatCompactCount(summary.todayTokens)}
|
|
463
|
+
detail={`${formatTokenCount(summary.todayTokens)} total tokens today`}
|
|
464
|
+
icon={Coins}
|
|
465
|
+
/>
|
|
466
|
+
<SummaryCard
|
|
467
|
+
eyebrow="Month"
|
|
468
|
+
title="Tokens"
|
|
469
|
+
value={formatCompactCount(summary.monthTokens)}
|
|
470
|
+
detail={`${formatTokenCount(summary.monthTokens)} total tokens this month`}
|
|
471
|
+
icon={Coins}
|
|
472
|
+
/>
|
|
473
|
+
|
|
474
|
+
<div className="surface-card rounded-3xl p-5">
|
|
475
|
+
<div className="mb-4 flex items-start justify-between gap-3">
|
|
476
|
+
<div className="space-y-1">
|
|
477
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
478
|
+
Budgets
|
|
479
|
+
</p>
|
|
480
|
+
<h2 className="text-sm font-medium text-foreground">Guardrail state</h2>
|
|
481
|
+
</div>
|
|
482
|
+
<div className="surface-card-muted rounded-2xl p-2.5">
|
|
483
|
+
{blocked.length > 0 ? (
|
|
484
|
+
<ShieldAlert className="h-4 w-4 text-status-failed" />
|
|
485
|
+
) : (
|
|
486
|
+
<ShieldCheck className="h-4 w-4 text-status-completed" />
|
|
487
|
+
)}
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
|
|
491
|
+
{nearestBudget ? (
|
|
492
|
+
<div className="space-y-3">
|
|
493
|
+
<div className="flex items-center gap-2">
|
|
494
|
+
{budgetBadge(nearestBudget)}
|
|
495
|
+
<span className="text-xs text-muted-foreground">
|
|
496
|
+
{nearestBudget.scopeLabel} {nearestBudget.window} {nearestBudget.metric}
|
|
497
|
+
</span>
|
|
498
|
+
</div>
|
|
499
|
+
<div className="space-y-1">
|
|
500
|
+
<p className="text-2xl font-bold tracking-tight">
|
|
501
|
+
{formatBudgetValue(
|
|
502
|
+
nearestBudget,
|
|
503
|
+
Math.max((nearestBudget.limitValue ?? 0) - nearestBudget.currentValue, 0)
|
|
504
|
+
)}
|
|
505
|
+
</p>
|
|
506
|
+
<p className="text-xs text-muted-foreground">
|
|
507
|
+
Remaining before the nearest configured cap. Resets{" "}
|
|
508
|
+
{formatDateTime(nearestBudget.resetAtIso)}.
|
|
509
|
+
</p>
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
) : (
|
|
513
|
+
<div className="space-y-2">
|
|
514
|
+
<Badge variant="secondary">Unconfigured</Badge>
|
|
515
|
+
<p className="text-sm text-muted-foreground">
|
|
516
|
+
No spend or token caps are configured yet. Usage is being metered,
|
|
517
|
+
but there is no automatic stop condition.
|
|
518
|
+
</p>
|
|
519
|
+
</div>
|
|
520
|
+
)}
|
|
521
|
+
</div>
|
|
522
|
+
</div>
|
|
523
|
+
|
|
524
|
+
{hasUsage ? (
|
|
525
|
+
<>
|
|
526
|
+
<div className="grid gap-6 xl:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]">
|
|
527
|
+
<div className="surface-card rounded-3xl p-5">
|
|
528
|
+
<SectionHeading>Trend View</SectionHeading>
|
|
529
|
+
<div className="grid gap-4 lg:grid-cols-2">
|
|
530
|
+
<div className="surface-card-muted rounded-2xl p-4">
|
|
531
|
+
<div className="mb-4 flex items-center justify-between gap-3">
|
|
532
|
+
<div>
|
|
533
|
+
<p className="text-sm font-medium">Spend velocity</p>
|
|
534
|
+
<p className="text-xs text-muted-foreground">
|
|
535
|
+
7-day and 30-day spend series
|
|
536
|
+
</p>
|
|
537
|
+
</div>
|
|
538
|
+
<Badge variant="outline">{formatCurrencyMicros(summary.monthSpendMicros)}</Badge>
|
|
539
|
+
</div>
|
|
540
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
541
|
+
<div className="rounded-2xl border border-border/50 bg-background/40 p-3">
|
|
542
|
+
<p className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
543
|
+
7-day
|
|
544
|
+
</p>
|
|
545
|
+
<Sparkline
|
|
546
|
+
data={trendSeries.spend7}
|
|
547
|
+
width={160}
|
|
548
|
+
height={48}
|
|
549
|
+
color="var(--chart-1)"
|
|
550
|
+
label="7 day spend trend"
|
|
551
|
+
className="w-full"
|
|
552
|
+
/>
|
|
553
|
+
</div>
|
|
554
|
+
<div className="rounded-2xl border border-border/50 bg-background/40 p-3">
|
|
555
|
+
<p className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
556
|
+
30-day
|
|
557
|
+
</p>
|
|
558
|
+
<MiniBar
|
|
559
|
+
data={trendSeries.spend30.map((value) => ({
|
|
560
|
+
value,
|
|
561
|
+
color: "var(--chart-1)",
|
|
562
|
+
}))}
|
|
563
|
+
width={220}
|
|
564
|
+
height={48}
|
|
565
|
+
label="30 day spend trend"
|
|
566
|
+
className="w-full"
|
|
567
|
+
/>
|
|
568
|
+
</div>
|
|
569
|
+
</div>
|
|
570
|
+
</div>
|
|
571
|
+
|
|
572
|
+
<div className="surface-card-muted rounded-2xl p-4">
|
|
573
|
+
<div className="mb-4 flex items-center justify-between gap-3">
|
|
574
|
+
<div>
|
|
575
|
+
<p className="text-sm font-medium">Token velocity</p>
|
|
576
|
+
<p className="text-xs text-muted-foreground">
|
|
577
|
+
7-day and 30-day token series
|
|
578
|
+
</p>
|
|
579
|
+
</div>
|
|
580
|
+
<Badge variant="outline">{formatCompactCount(summary.monthTokens)} tokens</Badge>
|
|
581
|
+
</div>
|
|
582
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
583
|
+
<div className="rounded-2xl border border-border/50 bg-background/40 p-3">
|
|
584
|
+
<p className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
585
|
+
7-day
|
|
586
|
+
</p>
|
|
587
|
+
<Sparkline
|
|
588
|
+
data={trendSeries.tokens7}
|
|
589
|
+
width={160}
|
|
590
|
+
height={48}
|
|
591
|
+
color="var(--chart-2)"
|
|
592
|
+
label="7 day token trend"
|
|
593
|
+
className="w-full"
|
|
594
|
+
/>
|
|
595
|
+
</div>
|
|
596
|
+
<div className="rounded-2xl border border-border/50 bg-background/40 p-3">
|
|
597
|
+
<p className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
598
|
+
30-day
|
|
599
|
+
</p>
|
|
600
|
+
<MiniBar
|
|
601
|
+
data={trendSeries.tokens30.map((value) => ({
|
|
602
|
+
value,
|
|
603
|
+
color: "var(--chart-2)",
|
|
604
|
+
}))}
|
|
605
|
+
width={220}
|
|
606
|
+
height={48}
|
|
607
|
+
label="30 day token trend"
|
|
608
|
+
className="w-full"
|
|
609
|
+
/>
|
|
610
|
+
</div>
|
|
611
|
+
</div>
|
|
612
|
+
</div>
|
|
613
|
+
</div>
|
|
614
|
+
</div>
|
|
615
|
+
|
|
616
|
+
<div className="surface-card rounded-3xl p-5">
|
|
617
|
+
<SectionHeading>Runtime Breakdown</SectionHeading>
|
|
618
|
+
<div className="space-y-3">
|
|
619
|
+
{runtimeBreakdown.length > 0 ? (
|
|
620
|
+
runtimeBreakdown.map((runtime) => (
|
|
621
|
+
<div
|
|
622
|
+
key={runtime.runtimeId}
|
|
623
|
+
className="surface-card-muted flex items-center justify-between gap-4 rounded-2xl p-4"
|
|
624
|
+
>
|
|
625
|
+
<div className="flex items-center gap-4">
|
|
626
|
+
<DonutRing
|
|
627
|
+
value={runtime.share}
|
|
628
|
+
size={44}
|
|
629
|
+
strokeWidth={4}
|
|
630
|
+
color="var(--chart-1)"
|
|
631
|
+
trackColor="var(--muted)"
|
|
632
|
+
label={`${runtime.label} share of spend`}
|
|
633
|
+
/>
|
|
634
|
+
<div className="space-y-1">
|
|
635
|
+
<div className="flex items-center gap-2">
|
|
636
|
+
<p className="text-sm font-medium">{runtime.label}</p>
|
|
637
|
+
<Badge variant="outline">{runtime.providerId}</Badge>
|
|
638
|
+
</div>
|
|
639
|
+
<p className="text-xs text-muted-foreground">
|
|
640
|
+
{formatPercent(runtime.share)} of filtered spend across{" "}
|
|
641
|
+
{runtime.runs} runs
|
|
642
|
+
</p>
|
|
643
|
+
</div>
|
|
644
|
+
</div>
|
|
645
|
+
<div className="grid grid-cols-2 gap-3 text-right text-sm">
|
|
646
|
+
<div>
|
|
647
|
+
<p className="text-xs uppercase tracking-wide text-muted-foreground">
|
|
648
|
+
Spend
|
|
649
|
+
</p>
|
|
650
|
+
<p className="font-medium">
|
|
651
|
+
{formatCurrencyMicros(runtime.costMicros)}
|
|
652
|
+
</p>
|
|
653
|
+
</div>
|
|
654
|
+
<div>
|
|
655
|
+
<p className="text-xs uppercase tracking-wide text-muted-foreground">
|
|
656
|
+
Tokens
|
|
657
|
+
</p>
|
|
658
|
+
<p className="font-medium">
|
|
659
|
+
{formatCompactCount(runtime.totalTokens)}
|
|
660
|
+
</p>
|
|
661
|
+
</div>
|
|
662
|
+
{runtime.unknownPricingRuns > 0 ? (
|
|
663
|
+
<div className="col-span-2">
|
|
664
|
+
<p className="text-xs text-muted-foreground">
|
|
665
|
+
{runtime.unknownPricingRuns} run
|
|
666
|
+
{runtime.unknownPricingRuns === 1 ? "" : "s"} missing
|
|
667
|
+
pricing data
|
|
668
|
+
</p>
|
|
669
|
+
</div>
|
|
670
|
+
) : null}
|
|
671
|
+
</div>
|
|
672
|
+
</div>
|
|
673
|
+
))
|
|
674
|
+
) : (
|
|
675
|
+
<div className="surface-card-muted rounded-2xl p-4 text-sm text-muted-foreground">
|
|
676
|
+
No metered runtime activity exists for {formatDateRangeLabel(filters.dateRange).toLowerCase()}.
|
|
677
|
+
</div>
|
|
678
|
+
)}
|
|
679
|
+
</div>
|
|
680
|
+
</div>
|
|
681
|
+
</div>
|
|
682
|
+
|
|
683
|
+
<div className="surface-card rounded-3xl p-5">
|
|
684
|
+
<div className="mb-4 flex items-start justify-between gap-3">
|
|
685
|
+
<div>
|
|
686
|
+
<SectionHeading className="mb-2">Model Breakdown</SectionHeading>
|
|
687
|
+
<p className="text-sm text-muted-foreground">
|
|
688
|
+
Concentration by model for {formatDateRangeLabel(filters.dateRange).toLowerCase()}.
|
|
689
|
+
</p>
|
|
690
|
+
</div>
|
|
691
|
+
{filteredUnknownPricingRuns > 0 ? (
|
|
692
|
+
<Badge variant="outline">
|
|
693
|
+
{filteredUnknownPricingRuns} unknown-pricing row
|
|
694
|
+
{filteredUnknownPricingRuns === 1 ? "" : "s"}
|
|
695
|
+
</Badge>
|
|
696
|
+
) : null}
|
|
697
|
+
</div>
|
|
698
|
+
|
|
699
|
+
{modelBreakdown.length > 0 ? (
|
|
700
|
+
<div className="space-y-3">
|
|
701
|
+
{modelBreakdown.map((row) => {
|
|
702
|
+
const visual = resolveModelVisualMeta(row, modelTotals);
|
|
703
|
+
return (
|
|
704
|
+
<div
|
|
705
|
+
key={`${row.runtimeId}-${row.modelId ?? "unknown"}`}
|
|
706
|
+
className="surface-card-muted rounded-2xl p-4"
|
|
707
|
+
>
|
|
708
|
+
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
|
709
|
+
<div className="min-w-0 flex-1 space-y-3">
|
|
710
|
+
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
|
711
|
+
<div className="min-w-0 space-y-1">
|
|
712
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
713
|
+
<p className="text-sm font-medium">
|
|
714
|
+
{row.modelId ?? "Unknown model"}
|
|
715
|
+
</p>
|
|
716
|
+
<Badge variant="outline">
|
|
717
|
+
{runtimeLabelMap.get(row.runtimeId) ?? row.runtimeId}
|
|
718
|
+
</Badge>
|
|
719
|
+
{row.unknownPricingRuns > 0 ? (
|
|
720
|
+
<Badge variant="outline">Pricing unavailable</Badge>
|
|
721
|
+
) : null}
|
|
722
|
+
</div>
|
|
723
|
+
<p className="text-xs text-muted-foreground">
|
|
724
|
+
{row.providerId} • {row.runs} run
|
|
725
|
+
{row.runs === 1 ? "" : "s"} •{" "}
|
|
726
|
+
{formatCompactCount(row.totalTokens)} tokens
|
|
727
|
+
</p>
|
|
728
|
+
</div>
|
|
729
|
+
<div className="text-left sm:text-right">
|
|
730
|
+
<p className="text-sm font-medium">{visual.valueLabel}</p>
|
|
731
|
+
<p className="text-xs text-muted-foreground">
|
|
732
|
+
{visual.basisLabel}
|
|
733
|
+
</p>
|
|
734
|
+
</div>
|
|
735
|
+
</div>
|
|
736
|
+
|
|
737
|
+
<div className="space-y-2">
|
|
738
|
+
<div className="h-2.5 overflow-hidden rounded-full bg-background/70">
|
|
739
|
+
<div
|
|
740
|
+
className="h-full rounded-full bg-[linear-gradient(90deg,var(--chart-1),var(--chart-2))]"
|
|
741
|
+
style={{ width: `${Math.max(visual.share, visual.share > 0 ? 6 : 0)}%` }}
|
|
742
|
+
/>
|
|
743
|
+
</div>
|
|
744
|
+
<div className="flex flex-wrap items-center justify-between gap-2 text-xs text-muted-foreground">
|
|
745
|
+
<span>{formatPercent(visual.share)} of current filtered volume</span>
|
|
746
|
+
{row.unknownPricingRuns > 0 ? (
|
|
747
|
+
<span>
|
|
748
|
+
{row.unknownPricingRuns} run
|
|
749
|
+
{row.unknownPricingRuns === 1 ? "" : "s"} without price data
|
|
750
|
+
</span>
|
|
751
|
+
) : (
|
|
752
|
+
<span>Cost and token totals are both shown above</span>
|
|
753
|
+
)}
|
|
754
|
+
</div>
|
|
755
|
+
</div>
|
|
756
|
+
</div>
|
|
757
|
+
</div>
|
|
758
|
+
</div>
|
|
759
|
+
);
|
|
760
|
+
})}
|
|
761
|
+
</div>
|
|
762
|
+
) : (
|
|
763
|
+
<div className="surface-card-muted rounded-2xl p-4 text-sm text-muted-foreground">
|
|
764
|
+
No model breakdown is available for the selected window yet.
|
|
765
|
+
</div>
|
|
766
|
+
)}
|
|
767
|
+
</div>
|
|
768
|
+
|
|
769
|
+
<div className="surface-card rounded-3xl p-5">
|
|
770
|
+
<div className="mb-4 flex items-start justify-between gap-3">
|
|
771
|
+
<div>
|
|
772
|
+
<SectionHeading className="mb-2">Audit Log</SectionHeading>
|
|
773
|
+
<p className="text-sm text-muted-foreground">
|
|
774
|
+
Filtered execution history for {formatDateRangeLabel(filters.dateRange).toLowerCase()}.
|
|
775
|
+
</p>
|
|
776
|
+
</div>
|
|
777
|
+
<Badge variant="outline">{auditEntries.length} rows</Badge>
|
|
778
|
+
</div>
|
|
779
|
+
|
|
780
|
+
{auditEntries.length > 0 ? (
|
|
781
|
+
<div className="surface-scroll rounded-2xl">
|
|
782
|
+
<Table>
|
|
783
|
+
<TableHeader>
|
|
784
|
+
<TableRow>
|
|
785
|
+
<TableHead>Timestamp</TableHead>
|
|
786
|
+
<TableHead>Activity</TableHead>
|
|
787
|
+
<TableHead>Linked entity</TableHead>
|
|
788
|
+
<TableHead>Runtime</TableHead>
|
|
789
|
+
<TableHead>Tokens</TableHead>
|
|
790
|
+
<TableHead>Cost</TableHead>
|
|
791
|
+
<TableHead>Status</TableHead>
|
|
792
|
+
</TableRow>
|
|
793
|
+
</TableHeader>
|
|
794
|
+
<TableBody>
|
|
795
|
+
{auditEntries.map((entry) => (
|
|
796
|
+
<TableRow key={entry.id}>
|
|
797
|
+
<TableCell className="align-top text-xs text-muted-foreground">
|
|
798
|
+
{formatDateTime(entry.finishedAt.toISOString())}
|
|
799
|
+
</TableCell>
|
|
800
|
+
<TableCell className="align-top">
|
|
801
|
+
<div className="space-y-1">
|
|
802
|
+
<p className="font-medium">
|
|
803
|
+
{formatActivityLabel(entry.activityType)}
|
|
804
|
+
</p>
|
|
805
|
+
<p className="text-xs text-muted-foreground">
|
|
806
|
+
{entry.modelId ?? "Unknown model"}
|
|
807
|
+
</p>
|
|
808
|
+
</div>
|
|
809
|
+
</TableCell>
|
|
810
|
+
<TableCell className="align-top">
|
|
811
|
+
<div className="space-y-1">
|
|
812
|
+
{renderEntityLink(entry)}
|
|
813
|
+
{entry.projectId && entry.projectName ? (
|
|
814
|
+
<p className="text-xs text-muted-foreground">
|
|
815
|
+
<Link
|
|
816
|
+
href={`/projects/${entry.projectId}`}
|
|
817
|
+
className="hover:underline"
|
|
818
|
+
>
|
|
819
|
+
{entry.projectName}
|
|
820
|
+
</Link>
|
|
821
|
+
</p>
|
|
822
|
+
) : null}
|
|
823
|
+
</div>
|
|
824
|
+
</TableCell>
|
|
825
|
+
<TableCell className="align-top">
|
|
826
|
+
<div className="space-y-1">
|
|
827
|
+
<p className="font-medium">
|
|
828
|
+
{runtimeLabelMap.get(entry.runtimeId) ?? entry.runtimeId}
|
|
829
|
+
</p>
|
|
830
|
+
<p className="text-xs text-muted-foreground">{entry.providerId}</p>
|
|
831
|
+
</div>
|
|
832
|
+
</TableCell>
|
|
833
|
+
<TableCell className="align-top text-right">
|
|
834
|
+
{formatCompactCount(entry.totalTokens ?? 0)}
|
|
835
|
+
</TableCell>
|
|
836
|
+
<TableCell className="align-top text-right">
|
|
837
|
+
{entry.status === "unknown_pricing"
|
|
838
|
+
? "Unavailable"
|
|
839
|
+
: formatCurrencyMicros(entry.costMicros)}
|
|
840
|
+
</TableCell>
|
|
841
|
+
<TableCell className="align-top">{statusBadge(entry.status)}</TableCell>
|
|
842
|
+
</TableRow>
|
|
843
|
+
))}
|
|
844
|
+
</TableBody>
|
|
845
|
+
</Table>
|
|
846
|
+
</div>
|
|
847
|
+
) : (
|
|
848
|
+
<div className="surface-card-muted rounded-2xl p-4 text-sm text-muted-foreground">
|
|
849
|
+
No audit rows match the current filters. Adjust the runtime, status,
|
|
850
|
+
activity, or date range to widen the view.
|
|
851
|
+
</div>
|
|
852
|
+
)}
|
|
853
|
+
</div>
|
|
854
|
+
</>
|
|
855
|
+
) : (
|
|
856
|
+
<EmptyState
|
|
857
|
+
icon={Wallet}
|
|
858
|
+
heading="No usage recorded yet"
|
|
859
|
+
description="Metering is wired, but there are no paid runtime rows to visualize yet. Run a task, schedule, or workflow to populate the dashboard."
|
|
860
|
+
action={
|
|
861
|
+
<div className="flex flex-wrap items-center justify-center gap-2">
|
|
862
|
+
<Button asChild size="sm">
|
|
863
|
+
<Link href="/dashboard?create=task">
|
|
864
|
+
Create Task
|
|
865
|
+
<ArrowRight className="h-3.5 w-3.5" />
|
|
866
|
+
</Link>
|
|
867
|
+
</Button>
|
|
868
|
+
<Button asChild variant="outline" size="sm">
|
|
869
|
+
<Link href="/settings">Review Budgets</Link>
|
|
870
|
+
</Button>
|
|
871
|
+
</div>
|
|
872
|
+
}
|
|
873
|
+
/>
|
|
874
|
+
)}
|
|
875
|
+
</div>
|
|
876
|
+
);
|
|
877
|
+
}
|