@agile-vibe-coding/avc 0.1.1 → 0.2.3

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.
Files changed (289) hide show
  1. package/cli/agent-loader.js +21 -0
  2. package/cli/agents/agent-selector.md +129 -0
  3. package/cli/agents/architecture-recommender.md +418 -0
  4. package/cli/agents/database-deep-dive.md +470 -0
  5. package/cli/agents/database-recommender.md +634 -0
  6. package/cli/agents/doc-distributor.md +176 -0
  7. package/cli/agents/documentation-updater.md +203 -0
  8. package/cli/agents/epic-story-decomposer.md +280 -0
  9. package/cli/agents/feature-context-generator.md +91 -0
  10. package/cli/agents/gap-checker-epic.md +52 -0
  11. package/cli/agents/impact-checker-story.md +51 -0
  12. package/cli/agents/migration-guide-generator.md +305 -0
  13. package/cli/agents/mission-scope-generator.md +79 -0
  14. package/cli/agents/mission-scope-validator.md +112 -0
  15. package/cli/agents/project-context-extractor.md +107 -0
  16. package/cli/agents/project-documentation-creator.json +226 -0
  17. package/cli/agents/project-documentation-creator.md +595 -0
  18. package/cli/agents/question-prefiller.md +269 -0
  19. package/cli/agents/refiner-epic.md +39 -0
  20. package/cli/agents/refiner-story.md +42 -0
  21. package/cli/agents/solver-epic-api.json +15 -0
  22. package/cli/agents/solver-epic-api.md +39 -0
  23. package/cli/agents/solver-epic-backend.json +15 -0
  24. package/cli/agents/solver-epic-backend.md +39 -0
  25. package/cli/agents/solver-epic-cloud.json +15 -0
  26. package/cli/agents/solver-epic-cloud.md +39 -0
  27. package/cli/agents/solver-epic-data.json +15 -0
  28. package/cli/agents/solver-epic-data.md +39 -0
  29. package/cli/agents/solver-epic-database.json +15 -0
  30. package/cli/agents/solver-epic-database.md +39 -0
  31. package/cli/agents/solver-epic-developer.json +15 -0
  32. package/cli/agents/solver-epic-developer.md +39 -0
  33. package/cli/agents/solver-epic-devops.json +15 -0
  34. package/cli/agents/solver-epic-devops.md +39 -0
  35. package/cli/agents/solver-epic-frontend.json +15 -0
  36. package/cli/agents/solver-epic-frontend.md +39 -0
  37. package/cli/agents/solver-epic-mobile.json +15 -0
  38. package/cli/agents/solver-epic-mobile.md +39 -0
  39. package/cli/agents/solver-epic-qa.json +15 -0
  40. package/cli/agents/solver-epic-qa.md +39 -0
  41. package/cli/agents/solver-epic-security.json +15 -0
  42. package/cli/agents/solver-epic-security.md +39 -0
  43. package/cli/agents/solver-epic-solution-architect.json +15 -0
  44. package/cli/agents/solver-epic-solution-architect.md +39 -0
  45. package/cli/agents/solver-epic-test-architect.json +15 -0
  46. package/cli/agents/solver-epic-test-architect.md +39 -0
  47. package/cli/agents/solver-epic-ui.json +15 -0
  48. package/cli/agents/solver-epic-ui.md +39 -0
  49. package/cli/agents/solver-epic-ux.json +15 -0
  50. package/cli/agents/solver-epic-ux.md +39 -0
  51. package/cli/agents/solver-story-api.json +15 -0
  52. package/cli/agents/solver-story-api.md +39 -0
  53. package/cli/agents/solver-story-backend.json +15 -0
  54. package/cli/agents/solver-story-backend.md +39 -0
  55. package/cli/agents/solver-story-cloud.json +15 -0
  56. package/cli/agents/solver-story-cloud.md +39 -0
  57. package/cli/agents/solver-story-data.json +15 -0
  58. package/cli/agents/solver-story-data.md +39 -0
  59. package/cli/agents/solver-story-database.json +15 -0
  60. package/cli/agents/solver-story-database.md +39 -0
  61. package/cli/agents/solver-story-developer.json +15 -0
  62. package/cli/agents/solver-story-developer.md +39 -0
  63. package/cli/agents/solver-story-devops.json +15 -0
  64. package/cli/agents/solver-story-devops.md +39 -0
  65. package/cli/agents/solver-story-frontend.json +15 -0
  66. package/cli/agents/solver-story-frontend.md +39 -0
  67. package/cli/agents/solver-story-mobile.json +15 -0
  68. package/cli/agents/solver-story-mobile.md +39 -0
  69. package/cli/agents/solver-story-qa.json +15 -0
  70. package/cli/agents/solver-story-qa.md +39 -0
  71. package/cli/agents/solver-story-security.json +15 -0
  72. package/cli/agents/solver-story-security.md +39 -0
  73. package/cli/agents/solver-story-solution-architect.json +15 -0
  74. package/cli/agents/solver-story-solution-architect.md +39 -0
  75. package/cli/agents/solver-story-test-architect.json +15 -0
  76. package/cli/agents/solver-story-test-architect.md +39 -0
  77. package/cli/agents/solver-story-ui.json +15 -0
  78. package/cli/agents/solver-story-ui.md +39 -0
  79. package/cli/agents/solver-story-ux.json +15 -0
  80. package/cli/agents/solver-story-ux.md +39 -0
  81. package/cli/agents/story-doc-enricher.md +133 -0
  82. package/cli/agents/suggestion-business-analyst.md +88 -0
  83. package/cli/agents/suggestion-deployment-architect.md +263 -0
  84. package/cli/agents/suggestion-product-manager.md +129 -0
  85. package/cli/agents/suggestion-security-specialist.md +156 -0
  86. package/cli/agents/suggestion-technical-architect.md +269 -0
  87. package/cli/agents/suggestion-ux-researcher.md +93 -0
  88. package/cli/agents/task-subtask-decomposer.md +188 -0
  89. package/cli/agents/validator-documentation.json +152 -0
  90. package/cli/agents/validator-documentation.md +453 -0
  91. package/cli/agents/validator-epic-api.json +93 -0
  92. package/cli/agents/validator-epic-api.md +137 -0
  93. package/cli/agents/validator-epic-backend.json +93 -0
  94. package/cli/agents/validator-epic-backend.md +130 -0
  95. package/cli/agents/validator-epic-cloud.json +93 -0
  96. package/cli/agents/validator-epic-cloud.md +137 -0
  97. package/cli/agents/validator-epic-data.json +93 -0
  98. package/cli/agents/validator-epic-data.md +130 -0
  99. package/cli/agents/validator-epic-database.json +93 -0
  100. package/cli/agents/validator-epic-database.md +137 -0
  101. package/cli/agents/validator-epic-developer.json +74 -0
  102. package/cli/agents/validator-epic-developer.md +153 -0
  103. package/cli/agents/validator-epic-devops.json +74 -0
  104. package/cli/agents/validator-epic-devops.md +153 -0
  105. package/cli/agents/validator-epic-frontend.json +74 -0
  106. package/cli/agents/validator-epic-frontend.md +153 -0
  107. package/cli/agents/validator-epic-mobile.json +93 -0
  108. package/cli/agents/validator-epic-mobile.md +130 -0
  109. package/cli/agents/validator-epic-qa.json +93 -0
  110. package/cli/agents/validator-epic-qa.md +130 -0
  111. package/cli/agents/validator-epic-security.json +74 -0
  112. package/cli/agents/validator-epic-security.md +154 -0
  113. package/cli/agents/validator-epic-solution-architect.json +74 -0
  114. package/cli/agents/validator-epic-solution-architect.md +156 -0
  115. package/cli/agents/validator-epic-test-architect.json +93 -0
  116. package/cli/agents/validator-epic-test-architect.md +130 -0
  117. package/cli/agents/validator-epic-ui.json +93 -0
  118. package/cli/agents/validator-epic-ui.md +130 -0
  119. package/cli/agents/validator-epic-ux.json +93 -0
  120. package/cli/agents/validator-epic-ux.md +130 -0
  121. package/cli/agents/validator-selector.md +211 -0
  122. package/cli/agents/validator-story-api.json +104 -0
  123. package/cli/agents/validator-story-api.md +152 -0
  124. package/cli/agents/validator-story-backend.json +104 -0
  125. package/cli/agents/validator-story-backend.md +152 -0
  126. package/cli/agents/validator-story-cloud.json +104 -0
  127. package/cli/agents/validator-story-cloud.md +152 -0
  128. package/cli/agents/validator-story-data.json +104 -0
  129. package/cli/agents/validator-story-data.md +152 -0
  130. package/cli/agents/validator-story-database.json +104 -0
  131. package/cli/agents/validator-story-database.md +152 -0
  132. package/cli/agents/validator-story-developer.json +104 -0
  133. package/cli/agents/validator-story-developer.md +152 -0
  134. package/cli/agents/validator-story-devops.json +104 -0
  135. package/cli/agents/validator-story-devops.md +152 -0
  136. package/cli/agents/validator-story-frontend.json +104 -0
  137. package/cli/agents/validator-story-frontend.md +152 -0
  138. package/cli/agents/validator-story-mobile.json +104 -0
  139. package/cli/agents/validator-story-mobile.md +152 -0
  140. package/cli/agents/validator-story-qa.json +104 -0
  141. package/cli/agents/validator-story-qa.md +152 -0
  142. package/cli/agents/validator-story-security.json +104 -0
  143. package/cli/agents/validator-story-security.md +152 -0
  144. package/cli/agents/validator-story-solution-architect.json +104 -0
  145. package/cli/agents/validator-story-solution-architect.md +152 -0
  146. package/cli/agents/validator-story-test-architect.json +104 -0
  147. package/cli/agents/validator-story-test-architect.md +152 -0
  148. package/cli/agents/validator-story-ui.json +104 -0
  149. package/cli/agents/validator-story-ui.md +152 -0
  150. package/cli/agents/validator-story-ux.json +104 -0
  151. package/cli/agents/validator-story-ux.md +152 -0
  152. package/cli/ansi-colors.js +21 -0
  153. package/cli/build-docs.js +29 -8
  154. package/cli/ceremony-history.js +369 -0
  155. package/cli/command-logger.js +49 -12
  156. package/cli/components/static-output.js +63 -0
  157. package/cli/console-output-manager.js +94 -0
  158. package/cli/docs-sync.js +306 -0
  159. package/cli/epic-story-validator.js +1174 -0
  160. package/cli/evaluation-prompts.js +1008 -0
  161. package/cli/execution-context.js +195 -0
  162. package/cli/generate-summary-table.js +340 -0
  163. package/cli/index.js +0 -0
  164. package/cli/init-model-config.js +697 -0
  165. package/cli/init.js +1311 -274
  166. package/cli/kanban-server-manager.js +228 -0
  167. package/cli/llm-claude.js +83 -1
  168. package/cli/llm-gemini.js +85 -0
  169. package/cli/llm-mock.js +233 -0
  170. package/cli/llm-openai.js +233 -0
  171. package/cli/llm-provider.js +240 -3
  172. package/cli/llm-token-limits.js +102 -0
  173. package/cli/llm-verifier.js +454 -0
  174. package/cli/message-constants.js +58 -0
  175. package/cli/message-manager.js +334 -0
  176. package/cli/message-types.js +96 -0
  177. package/cli/messaging-api.js +297 -0
  178. package/cli/model-pricing.js +169 -0
  179. package/cli/model-query-engine.js +468 -0
  180. package/cli/model-recommendation-analyzer.js +495 -0
  181. package/cli/model-selector.js +269 -0
  182. package/cli/output-buffer.js +107 -0
  183. package/cli/process-manager.js +73 -2
  184. package/cli/repl-ink.js +4988 -1217
  185. package/cli/repl-old.js +4 -4
  186. package/cli/seed-processor.js +792 -0
  187. package/cli/sprint-planning-processor.js +1813 -0
  188. package/cli/template-processor.js +2102 -105
  189. package/cli/templates/project.md +25 -8
  190. package/cli/templates/vitepress-config.mts.template +5 -4
  191. package/cli/token-tracker.js +520 -0
  192. package/cli/tools/generate-story-validators.js +317 -0
  193. package/cli/tools/generate-validators.js +669 -0
  194. package/cli/update-checker.js +19 -17
  195. package/cli/update-notifier.js +4 -4
  196. package/cli/validation-router.js +605 -0
  197. package/cli/verification-tracker.js +563 -0
  198. package/kanban/README.md +386 -0
  199. package/kanban/client/README.md +205 -0
  200. package/kanban/client/components.json +20 -0
  201. package/kanban/client/dist/assets/index-CiD8PS2e.js +306 -0
  202. package/kanban/client/dist/assets/index-nLh0m82Q.css +1 -0
  203. package/kanban/client/dist/index.html +16 -0
  204. package/kanban/client/dist/vite.svg +1 -0
  205. package/kanban/client/index.html +15 -0
  206. package/kanban/client/package-lock.json +9442 -0
  207. package/kanban/client/package.json +44 -0
  208. package/kanban/client/postcss.config.js +6 -0
  209. package/kanban/client/public/vite.svg +1 -0
  210. package/kanban/client/src/App.jsx +622 -0
  211. package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
  212. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +416 -0
  213. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +616 -0
  214. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +946 -0
  215. package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
  216. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +619 -0
  217. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +704 -0
  218. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
  219. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +154 -0
  220. package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
  221. package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
  222. package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
  223. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +125 -0
  224. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +228 -0
  225. package/kanban/client/src/components/kanban/CardDetailModal.jsx +559 -0
  226. package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
  227. package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
  228. package/kanban/client/src/components/kanban/GroupingSelector.jsx +57 -0
  229. package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
  230. package/kanban/client/src/components/kanban/KanbanCard.jsx +138 -0
  231. package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
  232. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +789 -0
  233. package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
  234. package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
  235. package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
  236. package/kanban/client/src/components/settings/AgentsTab.jsx +353 -0
  237. package/kanban/client/src/components/settings/ApiKeysTab.jsx +113 -0
  238. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +98 -0
  239. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +94 -0
  240. package/kanban/client/src/components/settings/ModelPricingTab.jsx +204 -0
  241. package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
  242. package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
  243. package/kanban/client/src/components/stats/CostModal.jsx +353 -0
  244. package/kanban/client/src/components/ui/badge.jsx +27 -0
  245. package/kanban/client/src/components/ui/dialog.jsx +121 -0
  246. package/kanban/client/src/components/ui/tabs.jsx +85 -0
  247. package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
  248. package/kanban/client/src/hooks/useGrouping.js +118 -0
  249. package/kanban/client/src/hooks/useWebSocket.js +120 -0
  250. package/kanban/client/src/lib/__tests__/api.test.js +196 -0
  251. package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
  252. package/kanban/client/src/lib/api.js +401 -0
  253. package/kanban/client/src/lib/status-grouping.js +144 -0
  254. package/kanban/client/src/lib/utils.js +11 -0
  255. package/kanban/client/src/main.jsx +10 -0
  256. package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
  257. package/kanban/client/src/store/ceremonyStore.js +172 -0
  258. package/kanban/client/src/store/filterStore.js +201 -0
  259. package/kanban/client/src/store/kanbanStore.js +115 -0
  260. package/kanban/client/src/store/processStore.js +65 -0
  261. package/kanban/client/src/store/sprintPlanningStore.js +33 -0
  262. package/kanban/client/src/styles/globals.css +59 -0
  263. package/kanban/client/tailwind.config.js +77 -0
  264. package/kanban/client/vite.config.js +28 -0
  265. package/kanban/client/vitest.config.js +28 -0
  266. package/kanban/dev-start.sh +47 -0
  267. package/kanban/package.json +12 -0
  268. package/kanban/server/index.js +516 -0
  269. package/kanban/server/routes/ceremony.js +305 -0
  270. package/kanban/server/routes/costs.js +157 -0
  271. package/kanban/server/routes/processes.js +50 -0
  272. package/kanban/server/routes/settings.js +303 -0
  273. package/kanban/server/routes/websocket.js +276 -0
  274. package/kanban/server/routes/work-items.js +347 -0
  275. package/kanban/server/services/CeremonyService.js +1190 -0
  276. package/kanban/server/services/FileSystemScanner.js +95 -0
  277. package/kanban/server/services/FileWatcher.js +144 -0
  278. package/kanban/server/services/HierarchyBuilder.js +196 -0
  279. package/kanban/server/services/ProcessRegistry.js +122 -0
  280. package/kanban/server/services/WorkItemReader.js +123 -0
  281. package/kanban/server/services/WorkItemRefineService.js +510 -0
  282. package/kanban/server/start.js +49 -0
  283. package/kanban/server/utils/kanban-logger.js +132 -0
  284. package/kanban/server/utils/markdown.js +91 -0
  285. package/kanban/server/utils/status-grouping.js +107 -0
  286. package/kanban/server/workers/sponsor-call-worker.js +84 -0
  287. package/kanban/server/workers/sprint-planning-worker.js +130 -0
  288. package/package.json +18 -5
  289. package/cli/agents/documentation.md +0 -302
@@ -0,0 +1,353 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { X, BarChart2, DollarSign, ChevronDown, ChevronRight } from 'lucide-react';
3
+ import {
4
+ BarChart,
5
+ Bar,
6
+ XAxis,
7
+ YAxis,
8
+ Tooltip,
9
+ ResponsiveContainer,
10
+ } from 'recharts';
11
+ import { getCostHistory } from '../../lib/api';
12
+
13
+ const RANGE_TABS = [
14
+ { label: 'Today', value: 'today' },
15
+ { label: '7 days', value: 7 },
16
+ { label: '30 days', value: 30 },
17
+ { label: '90 days', value: 90 },
18
+ ];
19
+
20
+ function formatCostLabel(cost) {
21
+ if (cost === 0) return '$0.00';
22
+ if (cost < 0.01) return '< $0.01';
23
+ return `$${cost.toFixed(2)}`;
24
+ }
25
+
26
+ function formatCostDetail(cost) {
27
+ if (cost === 0) return '$0.0000';
28
+ return `$${cost.toFixed(4)}`;
29
+ }
30
+
31
+ function formatTokens(n) {
32
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
33
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
34
+ return String(n);
35
+ }
36
+
37
+ function formatDateLabel(dateStr) {
38
+ const d = new Date(dateStr + 'T00:00:00');
39
+ return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
40
+ }
41
+
42
+ function formatCeremonyName(name) {
43
+ return name
44
+ .replace(/-/g, ' ')
45
+ .replace(/\b\w/g, (c) => c.toUpperCase());
46
+ }
47
+
48
+ /** Strip parent prefix from stage name, e.g. "sponsor-call-validation" → "Validation" */
49
+ function formatStageName(name, parentName) {
50
+ if (name === parentName) return 'Documentation';
51
+ const prefix = `${parentName}-`;
52
+ if (name.startsWith(prefix)) return formatCeremonyName(name.slice(prefix.length));
53
+ return formatCeremonyName(name);
54
+ }
55
+
56
+ export function CostModal({ onClose }) {
57
+ const [rangeMode, setRangeMode] = useState('today');
58
+ const [customFrom, setCustomFrom] = useState('');
59
+ const [customTo, setCustomTo] = useState('');
60
+ const [data, setData] = useState(null);
61
+ const [loading, setLoading] = useState(true);
62
+ const [expanded, setExpanded] = useState({});
63
+
64
+ // Fetch data when range changes
65
+ useEffect(() => {
66
+ setLoading(true);
67
+ setData(null);
68
+
69
+ let rangeArg;
70
+ if (rangeMode === 'today') {
71
+ const today = new Date().toISOString().split('T')[0];
72
+ rangeArg = { from: today, to: today };
73
+ } else if (rangeMode === 'custom') {
74
+ if (!customFrom || !customTo) {
75
+ setLoading(false);
76
+ return;
77
+ }
78
+ rangeArg = { from: customFrom, to: customTo };
79
+ } else {
80
+ rangeArg = parseInt(rangeMode, 10);
81
+ }
82
+
83
+ getCostHistory(rangeArg)
84
+ .then((d) => {
85
+ setData(d);
86
+ setLoading(false);
87
+ // Auto-expand parents that have stages
88
+ const init = {};
89
+ (d.ceremonies || []).forEach((c) => {
90
+ if (c.stages && c.stages.length > 0) init[c.name] = true;
91
+ });
92
+ setExpanded(init);
93
+ })
94
+ .catch(() => { setData({ daily: [], ceremonies: [] }); setLoading(false); });
95
+ }, [rangeMode, customFrom, customTo]);
96
+
97
+ // Close on Escape
98
+ useEffect(() => {
99
+ const handler = (e) => { if (e.key === 'Escape') onClose(); };
100
+ document.addEventListener('keydown', handler);
101
+ return () => document.removeEventListener('keydown', handler);
102
+ }, [onClose]);
103
+
104
+ const toggleExpanded = (name) => setExpanded((prev) => ({ ...prev, [name]: !prev[name] }));
105
+
106
+ // Totals come from parent nodes only — stages are already rolled up into them
107
+ const totalCost = data?.ceremonies.reduce((s, c) => s + c.cost, 0) ?? 0;
108
+ const totalTokens = data?.ceremonies.reduce((s, c) => s + c.tokens, 0) ?? 0;
109
+ const totalCalls = data?.ceremonies.reduce((s, c) => s + c.calls, 0) ?? 0;
110
+ const hasData = data && (data.daily.length > 0 || data.ceremonies.length > 0);
111
+
112
+ return (
113
+ <div className="fixed inset-0 z-[65] flex items-center justify-center p-4">
114
+ {/* Backdrop */}
115
+ <div
116
+ className="absolute inset-0 bg-black/40"
117
+ onClick={onClose}
118
+ aria-hidden="true"
119
+ />
120
+
121
+ {/* Panel */}
122
+ <div
123
+ className="relative bg-white rounded-xl shadow-2xl w-full max-w-2xl flex flex-col"
124
+ style={{ height: '90vh', maxHeight: '900px' }}
125
+ onClick={(e) => e.stopPropagation()}
126
+ >
127
+ {/* Header */}
128
+ <div className="flex items-center justify-between px-6 py-4 border-b border-slate-200 flex-shrink-0">
129
+ <div className="flex items-center gap-2">
130
+ <DollarSign className="w-5 h-5 text-slate-500" />
131
+ <h2 className="text-lg font-semibold text-slate-900">LLM Cost Tracker</h2>
132
+ </div>
133
+ <button
134
+ onClick={onClose}
135
+ className="text-slate-400 hover:text-slate-600 transition-colors"
136
+ aria-label="Close"
137
+ >
138
+ <X className="w-5 h-5" />
139
+ </button>
140
+ </div>
141
+
142
+ {/* Scrollable body */}
143
+ <div className="overflow-y-auto flex-1 px-6 py-4 flex flex-col gap-5">
144
+ {/* Time range tabs — always 2 rows */}
145
+ <div className="flex flex-col gap-1.5">
146
+ {/* Row 1: preset buttons + Custom */}
147
+ <div className="flex items-center gap-2">
148
+ {RANGE_TABS.map((tab) => (
149
+ <button
150
+ key={tab.value}
151
+ onClick={() => setRangeMode(String(tab.value))}
152
+ className={`px-3 py-1 rounded text-sm font-medium transition-colors ${
153
+ rangeMode === String(tab.value)
154
+ ? 'bg-blue-600 text-white'
155
+ : 'bg-slate-100 text-slate-600 hover:bg-slate-200'
156
+ }`}
157
+ >
158
+ {tab.label}
159
+ </button>
160
+ ))}
161
+ <button
162
+ onClick={() => setRangeMode('custom')}
163
+ className={`px-3 py-1 rounded text-sm font-medium transition-colors ${
164
+ rangeMode === 'custom'
165
+ ? 'bg-blue-600 text-white'
166
+ : 'bg-slate-100 text-slate-600 hover:bg-slate-200'
167
+ }`}
168
+ >
169
+ Custom
170
+ </button>
171
+ </div>
172
+
173
+ {/* Row 2: date inputs — always rendered to keep height constant */}
174
+ <div className={`flex items-center gap-2 ${rangeMode !== 'custom' ? 'invisible' : ''}`}>
175
+ <input
176
+ type="date"
177
+ value={customFrom}
178
+ onChange={(e) => setCustomFrom(e.target.value)}
179
+ className="text-sm border border-slate-300 rounded px-2 py-0.5 text-slate-700"
180
+ />
181
+ <span className="text-slate-400 text-sm">to</span>
182
+ <input
183
+ type="date"
184
+ value={customTo}
185
+ onChange={(e) => setCustomTo(e.target.value)}
186
+ className="text-sm border border-slate-300 rounded px-2 py-0.5 text-slate-700"
187
+ />
188
+ </div>
189
+ </div>
190
+
191
+ {/* Loading */}
192
+ {loading && (
193
+ <div className="flex-1 flex items-center justify-center">
194
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
195
+ </div>
196
+ )}
197
+
198
+ {/* No data */}
199
+ {!loading && !hasData && (
200
+ <div className="flex-1 flex flex-col items-center justify-center text-slate-400 gap-3">
201
+ <BarChart2 className="w-10 h-10" />
202
+ <p className="text-sm">No usage data for this period.</p>
203
+ <p className="text-xs text-slate-300">Run a ceremony to start tracking costs.</p>
204
+ </div>
205
+ )}
206
+
207
+ {/* Content */}
208
+ {!loading && hasData && (
209
+ <>
210
+ {/* Stat chips */}
211
+ <div className="grid grid-cols-3 gap-3">
212
+ <div className="bg-slate-50 rounded-lg p-3">
213
+ <p className="text-xs text-slate-500 mb-1">Total Cost</p>
214
+ <p className="text-xl font-bold text-slate-900">{formatCostLabel(totalCost)}</p>
215
+ <p className="text-xs text-slate-400 mt-0.5">this period</p>
216
+ </div>
217
+ <div className="bg-slate-50 rounded-lg p-3">
218
+ <p className="text-xs text-slate-500 mb-1">Total Tokens</p>
219
+ <p className="text-xl font-bold text-slate-900">{formatTokens(totalTokens)}</p>
220
+ <p className="text-xs text-slate-400 mt-0.5">this period</p>
221
+ </div>
222
+ <div className="bg-slate-50 rounded-lg p-3">
223
+ <p className="text-xs text-slate-500 mb-1">API Calls</p>
224
+ <p className="text-xl font-bold text-slate-900">{totalCalls.toLocaleString()}</p>
225
+ <p className="text-xs text-slate-400 mt-0.5">this period</p>
226
+ </div>
227
+ </div>
228
+
229
+ {/* Bar chart */}
230
+ {data.daily.length > 0 && (
231
+ <div>
232
+ <p className="text-xs font-medium text-slate-500 mb-2 uppercase tracking-wide">Daily Cost</p>
233
+ <ResponsiveContainer width="100%" height={180}>
234
+ <BarChart data={data.daily} margin={{ top: 0, right: 0, bottom: 0, left: 0 }}>
235
+ <XAxis
236
+ dataKey="date"
237
+ tickFormatter={formatDateLabel}
238
+ tick={{ fontSize: 11, fill: '#94a3b8' }}
239
+ axisLine={false}
240
+ tickLine={false}
241
+ />
242
+ <YAxis
243
+ tickFormatter={(v) => `$${v.toFixed(2)}`}
244
+ tick={{ fontSize: 11, fill: '#94a3b8' }}
245
+ width={56}
246
+ axisLine={false}
247
+ tickLine={false}
248
+ />
249
+ <Tooltip
250
+ formatter={(v) => [`$${v.toFixed(4)}`, 'Cost']}
251
+ labelFormatter={(label) => formatDateLabel(label)}
252
+ contentStyle={{ fontSize: 12 }}
253
+ />
254
+ <Bar dataKey="cost" fill="#3b82f6" radius={[3, 3, 0, 0]} />
255
+ </BarChart>
256
+ </ResponsiveContainer>
257
+ </div>
258
+ )}
259
+
260
+ {/* Ceremony breakdown — hierarchical */}
261
+ {data.ceremonies.length > 0 && (
262
+ <div>
263
+ <p className="text-xs font-medium text-slate-500 mb-2 uppercase tracking-wide">By Ceremony</p>
264
+ <div className="overflow-x-auto">
265
+ <table className="w-full text-sm">
266
+ <thead>
267
+ <tr className="text-left text-xs text-slate-400 border-b border-slate-100">
268
+ <th className="pb-2 font-medium">Ceremony / Stage</th>
269
+ <th className="pb-2 font-medium text-right">Calls</th>
270
+ <th className="pb-2 font-medium text-right">Tokens</th>
271
+ <th className="pb-2 font-medium text-right">Cost</th>
272
+ <th className="pb-2 font-medium pl-4">Share</th>
273
+ </tr>
274
+ </thead>
275
+ <tbody>
276
+ {data.ceremonies.map((c) => {
277
+ const pct = totalCost > 0 ? (c.cost / totalCost) * 100 : 0;
278
+ const hasStages = c.stages && c.stages.length > 0;
279
+ const isOpen = expanded[c.name];
280
+
281
+ return [
282
+ /* Parent row */
283
+ <tr
284
+ key={c.name}
285
+ className={`border-b border-slate-100 ${hasStages ? 'cursor-pointer hover:bg-slate-50' : ''}`}
286
+ onClick={hasStages ? () => toggleExpanded(c.name) : undefined}
287
+ >
288
+ <td className="py-2 text-slate-800 font-semibold">
289
+ <div className="flex items-center gap-1.5">
290
+ {hasStages
291
+ ? (isOpen
292
+ ? <ChevronDown className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />
293
+ : <ChevronRight className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />)
294
+ : <span className="w-3.5 flex-shrink-0" />
295
+ }
296
+ {formatCeremonyName(c.name)}
297
+ </div>
298
+ </td>
299
+ <td className="py-2 text-right text-slate-500">{c.calls}</td>
300
+ <td className="py-2 text-right text-slate-500">{formatTokens(c.tokens)}</td>
301
+ <td className="py-2 text-right text-slate-800 font-semibold">{formatCostDetail(c.cost)}</td>
302
+ <td className="py-2 pl-4">
303
+ <div className="flex items-center gap-2">
304
+ <div className="w-20 bg-slate-100 rounded-full h-1.5 flex-shrink-0">
305
+ <div
306
+ className="bg-blue-500 h-1.5 rounded-full"
307
+ style={{ width: `${pct}%` }}
308
+ />
309
+ </div>
310
+ <span className="text-xs text-slate-400">{Math.round(pct)}%</span>
311
+ </div>
312
+ </td>
313
+ </tr>,
314
+
315
+ /* Stage rows — shown when expanded */
316
+ ...(isOpen && hasStages ? c.stages.map((s) => {
317
+ const stagePct = c.cost > 0 ? (s.cost / c.cost) * 100 : 0;
318
+ return (
319
+ <tr key={`${c.name}/${s.name}`} className="border-b border-slate-50 bg-slate-50/50">
320
+ <td className="py-1.5 text-slate-500 pl-7">
321
+ {formatStageName(s.name, c.name)}
322
+ </td>
323
+ <td className="py-1.5 text-right text-slate-400 text-xs">{s.calls}</td>
324
+ <td className="py-1.5 text-right text-slate-400 text-xs">{formatTokens(s.tokens)}</td>
325
+ <td className="py-1.5 text-right text-slate-500 text-xs">{formatCostDetail(s.cost)}</td>
326
+ <td className="py-1.5 pl-4">
327
+ <div className="flex items-center gap-2">
328
+ <div className="w-20 bg-slate-100 rounded-full h-1 flex-shrink-0">
329
+ <div
330
+ className="bg-blue-300 h-1 rounded-full"
331
+ style={{ width: `${stagePct}%` }}
332
+ />
333
+ </div>
334
+ <span className="text-[10px] text-slate-300">{Math.round(stagePct)}%</span>
335
+ </div>
336
+ </td>
337
+ </tr>
338
+ );
339
+ }) : []),
340
+ ];
341
+ })}
342
+ </tbody>
343
+ </table>
344
+ </div>
345
+ </div>
346
+ )}
347
+ </>
348
+ )}
349
+ </div>
350
+ </div>
351
+ </div>
352
+ );
353
+ }
@@ -0,0 +1,27 @@
1
+ import { cn } from '../../lib/utils';
2
+
3
+ /**
4
+ * Badge Component (shadcn/ui style)
5
+ */
6
+ export function Badge({ variant = 'default', className, children }) {
7
+ return (
8
+ <div
9
+ className={cn(
10
+ 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold',
11
+ 'transition-colors focus:outline-none focus:ring-2 focus:ring-slate-950 focus:ring-offset-2',
12
+ {
13
+ 'border-transparent bg-slate-900 text-slate-50 hover:bg-slate-900/80':
14
+ variant === 'default',
15
+ 'border-transparent bg-slate-100 text-slate-900 hover:bg-slate-100/80':
16
+ variant === 'secondary',
17
+ 'border-transparent bg-red-100 text-red-900 hover:bg-red-100/80':
18
+ variant === 'destructive',
19
+ 'border-slate-200 text-slate-900': variant === 'outline',
20
+ },
21
+ className
22
+ )}
23
+ >
24
+ {children}
25
+ </div>
26
+ );
27
+ }
@@ -0,0 +1,121 @@
1
+ import { useEffect } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { X } from 'lucide-react';
4
+ import { cn } from '../../lib/utils';
5
+
6
+ /**
7
+ * Dialog Component (shadcn/ui style)
8
+ * Modal dialog with backdrop and animations
9
+ */
10
+ export function Dialog({ open, onOpenChange, children }) {
11
+ // Close on Escape key
12
+ useEffect(() => {
13
+ const handleEscape = (e) => {
14
+ if (e.key === 'Escape' && open) {
15
+ onOpenChange(false);
16
+ }
17
+ };
18
+
19
+ document.addEventListener('keydown', handleEscape);
20
+ return () => document.removeEventListener('keydown', handleEscape);
21
+ }, [open, onOpenChange]);
22
+
23
+ // Prevent body scroll when dialog is open
24
+ useEffect(() => {
25
+ if (open) {
26
+ document.body.style.overflow = 'hidden';
27
+ } else {
28
+ document.body.style.overflow = 'unset';
29
+ }
30
+
31
+ return () => {
32
+ document.body.style.overflow = 'unset';
33
+ };
34
+ }, [open]);
35
+
36
+ return (
37
+ <AnimatePresence>
38
+ {open && (
39
+ <>
40
+ {/* Backdrop */}
41
+ <motion.div
42
+ initial={{ opacity: 0 }}
43
+ animate={{ opacity: 1 }}
44
+ exit={{ opacity: 0 }}
45
+ onClick={() => onOpenChange(false)}
46
+ className="fixed inset-0 bg-black/50 z-50"
47
+ />
48
+
49
+ {/* Dialog Container */}
50
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
51
+ {children}
52
+ </div>
53
+ </>
54
+ )}
55
+ </AnimatePresence>
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Dialog Content
61
+ */
62
+ export function DialogContent({ className, children, onClose }) {
63
+ return (
64
+ <motion.div
65
+ initial={{ opacity: 0, scale: 0.95, y: 20 }}
66
+ animate={{ opacity: 1, scale: 1, y: 0 }}
67
+ exit={{ opacity: 0, scale: 0.95, y: 20 }}
68
+ transition={{ duration: 0.2 }}
69
+ onClick={(e) => e.stopPropagation()}
70
+ className={cn(
71
+ 'relative bg-white rounded-lg shadow-xl max-w-4xl w-full h-[90vh] overflow-hidden',
72
+ 'flex flex-col',
73
+ className
74
+ )}
75
+ >
76
+ {/* Close button */}
77
+ <button
78
+ onClick={onClose}
79
+ className="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100 transition-opacity z-10"
80
+ >
81
+ <X className="h-5 w-5" />
82
+ <span className="sr-only">Close</span>
83
+ </button>
84
+
85
+ {children}
86
+ </motion.div>
87
+ );
88
+ }
89
+
90
+ /**
91
+ * Dialog Header
92
+ */
93
+ export function DialogHeader({ className, children }) {
94
+ return (
95
+ <div className={cn('flex flex-col space-y-1.5 px-6 pt-6 pb-4', className)}>
96
+ {children}
97
+ </div>
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Dialog Title
103
+ */
104
+ export function DialogTitle({ className, children }) {
105
+ return (
106
+ <h2 className={cn('text-2xl font-semibold leading-none tracking-tight', className)}>
107
+ {children}
108
+ </h2>
109
+ );
110
+ }
111
+
112
+ /**
113
+ * Dialog Description
114
+ */
115
+ export function DialogDescription({ className, children }) {
116
+ return (
117
+ <p className={cn('text-sm text-slate-600', className)}>
118
+ {children}
119
+ </p>
120
+ );
121
+ }
@@ -0,0 +1,85 @@
1
+ import { createContext, useContext, useEffect, useState } from 'react';
2
+ import { cn } from '../../lib/utils';
3
+
4
+ /**
5
+ * Tabs Component (shadcn/ui style)
6
+ * Tabbed interface with keyboard navigation
7
+ */
8
+
9
+ const TabsContext = createContext();
10
+
11
+ export function Tabs({ defaultValue, value, onValueChange, children, className }) {
12
+ const [selectedTab, setSelectedTab] = useState(value || defaultValue);
13
+
14
+ // Sync internal state when controlled value prop changes
15
+ useEffect(() => {
16
+ if (value !== undefined) {
17
+ setSelectedTab(value);
18
+ }
19
+ }, [value]);
20
+
21
+ const handleTabChange = (newValue) => {
22
+ setSelectedTab(newValue);
23
+ onValueChange?.(newValue);
24
+ };
25
+
26
+ return (
27
+ <TabsContext.Provider value={{ selectedTab, setSelectedTab: handleTabChange }}>
28
+ <div className={cn('w-full', className)}>{children}</div>
29
+ </TabsContext.Provider>
30
+ );
31
+ }
32
+
33
+ export function TabsList({ className, children }) {
34
+ return (
35
+ <div
36
+ className={cn(
37
+ 'inline-flex h-10 items-center justify-start rounded-md bg-slate-100 p-1 text-slate-600',
38
+ className
39
+ )}
40
+ >
41
+ {children}
42
+ </div>
43
+ );
44
+ }
45
+
46
+ export function TabsTrigger({ value, children, className }) {
47
+ const { selectedTab, setSelectedTab } = useContext(TabsContext);
48
+ const isSelected = selectedTab === value;
49
+
50
+ return (
51
+ <button
52
+ onClick={() => setSelectedTab(value)}
53
+ className={cn(
54
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5',
55
+ 'text-sm font-medium ring-offset-white transition-all',
56
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2',
57
+ 'disabled:pointer-events-none disabled:opacity-50',
58
+ isSelected
59
+ ? 'bg-white text-slate-900 shadow-sm'
60
+ : 'text-slate-600 hover:bg-slate-200',
61
+ className
62
+ )}
63
+ >
64
+ {children}
65
+ </button>
66
+ );
67
+ }
68
+
69
+ export function TabsContent({ value, children, className }) {
70
+ const { selectedTab } = useContext(TabsContext);
71
+
72
+ if (selectedTab !== value) return null;
73
+
74
+ return (
75
+ <div
76
+ className={cn(
77
+ 'mt-2 ring-offset-white focus-visible:outline-none focus-visible:ring-2',
78
+ 'focus-visible:ring-slate-950 focus-visible:ring-offset-2',
79
+ className
80
+ )}
81
+ >
82
+ {children}
83
+ </div>
84
+ );
85
+ }