@cryptiklemur/lattice 1.3.0 → 1.5.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.
Files changed (109) hide show
  1. package/bun.lock +776 -2
  2. package/client/index.html +1 -13
  3. package/client/package.json +7 -1
  4. package/client/src/App.tsx +2 -0
  5. package/client/src/commands.ts +36 -0
  6. package/client/src/components/analytics/AnalyticsView.tsx +61 -0
  7. package/client/src/components/analytics/ChartCard.tsx +22 -0
  8. package/client/src/components/analytics/PeriodSelector.tsx +42 -0
  9. package/client/src/components/analytics/QuickStats.tsx +99 -0
  10. package/client/src/components/analytics/charts/CostAreaChart.tsx +83 -0
  11. package/client/src/components/analytics/charts/CostDistributionChart.tsx +62 -0
  12. package/client/src/components/analytics/charts/CostDonutChart.tsx +93 -0
  13. package/client/src/components/analytics/charts/CumulativeCostChart.tsx +62 -0
  14. package/client/src/components/analytics/charts/SessionBubbleChart.tsx +122 -0
  15. package/client/src/components/chat/AttachmentChips.tsx +116 -0
  16. package/client/src/components/chat/ChatInput.tsx +250 -73
  17. package/client/src/components/chat/ChatView.tsx +242 -10
  18. package/client/src/components/chat/CommandPalette.tsx +162 -0
  19. package/client/src/components/chat/Message.tsx +23 -2
  20. package/client/src/components/chat/PromptQuestion.tsx +271 -0
  21. package/client/src/components/chat/TodoCard.tsx +57 -0
  22. package/client/src/components/chat/ToolResultRenderer.tsx +2 -1
  23. package/client/src/components/chat/VoiceRecorder.tsx +85 -0
  24. package/client/src/components/dashboard/DashboardView.tsx +5 -0
  25. package/client/src/components/project-settings/ProjectMemory.tsx +12 -2
  26. package/client/src/components/project-settings/ProjectNotifications.tsx +48 -0
  27. package/client/src/components/project-settings/ProjectRules.tsx +10 -1
  28. package/client/src/components/project-settings/ProjectSettingsView.tsx +6 -0
  29. package/client/src/components/settings/Appearance.tsx +1 -0
  30. package/client/src/components/settings/ClaudeSettings.tsx +10 -0
  31. package/client/src/components/settings/Editor.tsx +123 -0
  32. package/client/src/components/settings/GlobalMcp.tsx +10 -1
  33. package/client/src/components/settings/GlobalMemory.tsx +19 -0
  34. package/client/src/components/settings/GlobalRules.tsx +149 -0
  35. package/client/src/components/settings/GlobalSkills.tsx +10 -0
  36. package/client/src/components/settings/Notifications.tsx +88 -0
  37. package/client/src/components/settings/SettingsView.tsx +12 -0
  38. package/client/src/components/settings/skill-shared.tsx +2 -1
  39. package/client/src/components/setup/SetupWizard.tsx +1 -1
  40. package/client/src/components/sidebar/NodeSettingsModal.tsx +23 -1
  41. package/client/src/components/sidebar/ProjectDropdown.tsx +176 -27
  42. package/client/src/components/sidebar/SettingsSidebar.tsx +11 -1
  43. package/client/src/components/sidebar/Sidebar.tsx +43 -2
  44. package/client/src/components/sidebar/UserIsland.tsx +18 -7
  45. package/client/src/components/ui/UpdatePrompt.tsx +47 -0
  46. package/client/src/components/workspace/FileBrowser.tsx +174 -0
  47. package/client/src/components/workspace/FileTree.tsx +129 -0
  48. package/client/src/components/workspace/FileViewer.tsx +211 -0
  49. package/client/src/components/workspace/NoteCard.tsx +119 -0
  50. package/client/src/components/workspace/NotesView.tsx +102 -0
  51. package/client/src/components/workspace/ScheduledTasksView.tsx +117 -0
  52. package/client/src/components/workspace/SplitPane.tsx +81 -0
  53. package/client/src/components/workspace/TabBar.tsx +185 -0
  54. package/client/src/components/workspace/TaskCard.tsx +158 -0
  55. package/client/src/components/workspace/TaskEditModal.tsx +114 -0
  56. package/client/src/components/{panels/Terminal.tsx → workspace/TerminalInstance.tsx} +50 -7
  57. package/client/src/components/workspace/TerminalView.tsx +110 -0
  58. package/client/src/components/workspace/WorkspaceView.tsx +116 -0
  59. package/client/src/hooks/useAnalytics.ts +75 -0
  60. package/client/src/hooks/useAttachments.ts +280 -0
  61. package/client/src/hooks/useEditorConfig.ts +28 -0
  62. package/client/src/hooks/useIdleDetection.ts +44 -0
  63. package/client/src/hooks/useInstallPrompt.ts +53 -0
  64. package/client/src/hooks/useNotifications.ts +54 -0
  65. package/client/src/hooks/useOnline.ts +6 -0
  66. package/client/src/hooks/useSession.ts +110 -4
  67. package/client/src/hooks/useSpinnerVerb.ts +36 -0
  68. package/client/src/hooks/useSwipeDrawer.ts +275 -0
  69. package/client/src/hooks/useVoiceRecorder.ts +123 -0
  70. package/client/src/hooks/useWorkspace.ts +48 -0
  71. package/client/src/providers/WebSocketProvider.tsx +18 -0
  72. package/client/src/router.tsx +52 -20
  73. package/client/src/stores/analytics.ts +54 -0
  74. package/client/src/stores/session.ts +136 -0
  75. package/client/src/stores/sidebar.ts +11 -2
  76. package/client/src/stores/workspace.ts +254 -0
  77. package/client/src/styles/global.css +123 -0
  78. package/client/src/utils/editorUrl.ts +62 -0
  79. package/client/vite.config.ts +54 -1
  80. package/package.json +1 -1
  81. package/server/src/analytics/engine.ts +491 -0
  82. package/server/src/daemon.ts +12 -1
  83. package/server/src/features/scheduler.ts +23 -0
  84. package/server/src/features/sticky-notes.ts +5 -3
  85. package/server/src/handlers/analytics.ts +34 -0
  86. package/server/src/handlers/attachment.ts +172 -0
  87. package/server/src/handlers/chat.ts +43 -2
  88. package/server/src/handlers/editor.ts +40 -0
  89. package/server/src/handlers/fs.ts +10 -2
  90. package/server/src/handlers/memory.ts +3 -0
  91. package/server/src/handlers/notes.ts +4 -2
  92. package/server/src/handlers/scheduler.ts +18 -1
  93. package/server/src/handlers/session.ts +14 -8
  94. package/server/src/handlers/settings.ts +37 -2
  95. package/server/src/handlers/terminal.ts +13 -6
  96. package/server/src/project/pty-worker.cjs +83 -0
  97. package/server/src/project/sdk-bridge.ts +266 -11
  98. package/server/src/project/session.ts +4 -4
  99. package/server/src/project/terminal.ts +78 -34
  100. package/shared/src/analytics.ts +24 -0
  101. package/shared/src/index.ts +1 -0
  102. package/shared/src/messages.ts +173 -4
  103. package/shared/src/models.ts +27 -1
  104. package/shared/src/project-settings.ts +1 -1
  105. package/tp.js +19 -0
  106. package/client/public/manifest.json +0 -24
  107. package/client/public/sw.js +0 -61
  108. package/client/src/components/panels/FileBrowser.tsx +0 -241
  109. package/client/src/components/panels/StickyNotes.tsx +0 -187
package/client/index.html CHANGED
@@ -2,12 +2,11 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
6
6
  <meta name="theme-color" content="#0d0d0d" />
7
7
  <meta name="apple-mobile-web-app-capable" content="yes" />
8
8
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
9
9
  <meta name="apple-mobile-web-app-title" content="Lattice" />
10
- <link rel="manifest" href="/manifest.json" />
11
10
  <link rel="apple-touch-icon" href="/icons/icon-192.svg" />
12
11
  <title>Lattice</title>
13
12
  <link rel="preconnect" href="https://fonts.googleapis.com" />
@@ -17,16 +16,5 @@
17
16
  <body>
18
17
  <div id="root"></div>
19
18
  <script type="module" src="/src/main.tsx"></script>
20
- <script>
21
- if ("serviceWorker" in navigator) {
22
- window.addEventListener("load", function () {
23
- navigator.serviceWorker.register("/sw.js").then(function (reg) {
24
- console.log("[lattice] Service worker registered:", reg.scope);
25
- }).catch(function (err) {
26
- console.warn("[lattice] Service worker registration failed:", err);
27
- });
28
- });
29
- }
30
- </script>
31
19
  </body>
32
20
  </html>
@@ -18,13 +18,18 @@
18
18
  "@tanstack/react-store": "^0.9.2",
19
19
  "@tanstack/react-virtual": "^3.13.23",
20
20
  "@xterm/addon-fit": "^0.11.0",
21
+ "@xterm/addon-search": "^0.16.0",
21
22
  "@xterm/addon-web-links": "^0.12.0",
22
23
  "@xterm/xterm": "^6.0.0",
24
+ "cronstrue": "^3.14.0",
23
25
  "daisyui": "^5.5.19",
24
26
  "lucide-react": "^0.577.0",
25
27
  "react": "^19",
26
28
  "react-dom": "^19",
27
29
  "react-markdown": "^10.1.0",
30
+ "recharts": "2.15.3",
31
+ "remark-gfm": "^4.0.1",
32
+ "shiki": "^4.0.2",
28
33
  "tailwindcss": "^4.2.2"
29
34
  },
30
35
  "devDependencies": {
@@ -32,6 +37,7 @@
32
37
  "@types/react-dom": "^19",
33
38
  "@vitejs/plugin-react": "^6",
34
39
  "typescript": "^5.9",
35
- "vite": "^8"
40
+ "vite": "^8",
41
+ "vite-plugin-pwa": "^1.2.0"
36
42
  }
37
43
  }
@@ -4,6 +4,7 @@ import { WebSocketProvider } from "./providers/WebSocketProvider";
4
4
  import { ErrorBoundary } from "./components/ui/ErrorBoundary";
5
5
  import { Toast, useToastState } from "./components/ui/Toast";
6
6
  import { CommandPalette } from "./components/ui/CommandPalette";
7
+ import { UpdatePrompt } from "./components/ui/UpdatePrompt";
7
8
 
8
9
  function AppInner() {
9
10
  var { items, dismiss } = useToastState();
@@ -13,6 +14,7 @@ function AppInner() {
13
14
  <RouterProvider router={router} />
14
15
  <CommandPalette />
15
16
  <Toast items={items} onDismiss={dismiss} />
17
+ <UpdatePrompt />
16
18
  </>
17
19
  );
18
20
  }
@@ -0,0 +1,36 @@
1
+ export type CommandHandler = "client" | "passthrough";
2
+
3
+ export interface SlashCommand {
4
+ name: string;
5
+ description: string;
6
+ aliases?: string[];
7
+ args?: string;
8
+ category: "command" | "skill";
9
+ handler: CommandHandler;
10
+ }
11
+
12
+ export var builtinCommands: SlashCommand[] = [
13
+ { name: "clear", description: "Clear conversation, start new session", aliases: ["reset", "new"], category: "command", handler: "client" },
14
+ { name: "compact", description: "Compact conversation context", args: "[instructions]", category: "command", handler: "passthrough" },
15
+ { name: "cost", description: "Show token usage and estimated cost", category: "command", handler: "client" },
16
+ { name: "model", description: "Switch Claude model", args: "[model]", category: "command", handler: "client" },
17
+ { name: "effort", description: "Set effort level", args: "[low|medium|high|max]", category: "command", handler: "client" },
18
+ { name: "help", description: "Show available commands", category: "command", handler: "client" },
19
+ { name: "fast", description: "Toggle fast mode", args: "[on|off]", category: "command", handler: "client" },
20
+ { name: "copy", description: "Copy last assistant response", category: "command", handler: "client" },
21
+ { name: "export", description: "Export conversation as text file", category: "command", handler: "client" },
22
+ { name: "rename", description: "Rename current session", args: "[name]", category: "command", handler: "client" },
23
+ { name: "context", description: "Show context breakdown", category: "command", handler: "client" },
24
+ { name: "theme", description: "Open appearance settings", category: "command", handler: "client" },
25
+ { name: "config", description: "Open settings", aliases: ["settings"], category: "command", handler: "client" },
26
+ { name: "permissions", description: "Open permissions settings", aliases: ["allowed-tools"], category: "command", handler: "client" },
27
+ { name: "memory", description: "Open memory settings", category: "command", handler: "client" },
28
+ { name: "skills", description: "Open skills settings", category: "command", handler: "client" },
29
+ { name: "plan", description: "Enter plan mode", category: "command", handler: "client" },
30
+ { name: "diff", description: "Show last git diff", category: "command", handler: "passthrough" },
31
+ { name: "init", description: "Generate CLAUDE.md", category: "command", handler: "passthrough" },
32
+ { name: "review", description: "Review code", category: "command", handler: "passthrough" },
33
+ { name: "pr-comments", description: "Fetch PR comments", args: "[PR]", category: "command", handler: "passthrough" },
34
+ { name: "security-review", description: "Security review of recent changes", category: "command", handler: "passthrough" },
35
+ { name: "btw", description: "Ask a side question", args: "<question>", category: "command", handler: "passthrough" },
36
+ ];
@@ -0,0 +1,61 @@
1
+ import { useAnalytics } from "../../hooks/useAnalytics";
2
+ import { PeriodSelector } from "./PeriodSelector";
3
+ import { ChartCard } from "./ChartCard";
4
+ import { CostAreaChart } from "./charts/CostAreaChart";
5
+ import { CumulativeCostChart } from "./charts/CumulativeCostChart";
6
+ import { CostDonutChart } from "./charts/CostDonutChart";
7
+ import { CostDistributionChart } from "./charts/CostDistributionChart";
8
+ import { SessionBubbleChart } from "./charts/SessionBubbleChart";
9
+
10
+ export function AnalyticsView() {
11
+ var analytics = useAnalytics();
12
+
13
+ return (
14
+ <div className="flex flex-col h-full overflow-hidden bg-base-100 bg-lattice-grid">
15
+ <div className="flex items-center justify-between px-6 py-4 border-b border-base-300 flex-shrink-0">
16
+ <h1 className="text-[16px] font-mono font-bold text-base-content">Analytics</h1>
17
+ <PeriodSelector value={analytics.period} onChange={analytics.setPeriod} />
18
+ </div>
19
+
20
+ <div className="flex-1 overflow-y-auto px-6 py-4">
21
+ {analytics.loading && !analytics.data && (
22
+ <div className="text-center text-base-content/30 py-20 font-mono text-[13px]">Loading analytics...</div>
23
+ )}
24
+
25
+ {analytics.error && (
26
+ <div className="text-center text-error/60 py-20 font-mono text-[13px]">{analytics.error}</div>
27
+ )}
28
+
29
+ {analytics.data && (
30
+ <div className="flex flex-col gap-4 max-w-[1200px] mx-auto pb-8">
31
+ <ChartCard title="Cost Over Time">
32
+ <CostAreaChart data={analytics.data.costOverTime} />
33
+ </ChartCard>
34
+
35
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
36
+ <ChartCard title="Cost Breakdown">
37
+ <CostDonutChart modelUsage={analytics.data.modelUsage} totalCost={analytics.data.totalCost} />
38
+ </ChartCard>
39
+ <ChartCard title="Cumulative Cost">
40
+ <CumulativeCostChart data={analytics.data.cumulativeCost} />
41
+ </ChartCard>
42
+ </div>
43
+
44
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
45
+ <ChartCard title="Cost Distribution">
46
+ <CostDistributionChart data={analytics.data.costDistribution} />
47
+ </ChartCard>
48
+ <ChartCard title="Session Costs">
49
+ <SessionBubbleChart data={analytics.data.sessionBubbles} />
50
+ </ChartCard>
51
+ </div>
52
+ </div>
53
+ )}
54
+
55
+ {!analytics.loading && !analytics.error && !analytics.data && (
56
+ <div className="text-center text-base-content/30 py-20 font-mono text-[13px]">No analytics data yet</div>
57
+ )}
58
+ </div>
59
+ </div>
60
+ );
61
+ }
@@ -0,0 +1,22 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ interface ChartCardProps {
4
+ title: string;
5
+ children: ReactNode;
6
+ className?: string;
7
+ action?: ReactNode;
8
+ }
9
+
10
+ export function ChartCard({ title, children, className, action }: ChartCardProps) {
11
+ return (
12
+ <div className={["rounded-xl border border-base-content/8 bg-base-300/50 p-4", className].filter(Boolean).join(" ")}>
13
+ <div className="flex items-center justify-between mb-4">
14
+ <span className="text-[10px] font-mono font-bold uppercase tracking-widest text-base-content/35">
15
+ {title}
16
+ </span>
17
+ {action && <div>{action}</div>}
18
+ </div>
19
+ {children}
20
+ </div>
21
+ );
22
+ }
@@ -0,0 +1,42 @@
1
+ type Period = "24h" | "7d" | "30d" | "90d" | "all";
2
+
3
+ var PERIODS: Array<{ value: Period; label: string }> = [
4
+ { value: "24h", label: "24h" },
5
+ { value: "7d", label: "7d" },
6
+ { value: "30d", label: "30d" },
7
+ { value: "90d", label: "90d" },
8
+ { value: "all", label: "All" },
9
+ ];
10
+
11
+ interface PeriodSelectorProps {
12
+ value: Period;
13
+ onChange: (period: Period) => void;
14
+ }
15
+
16
+ export function PeriodSelector({ value, onChange }: PeriodSelectorProps) {
17
+ return (
18
+ <div role="radiogroup" aria-label="Time period" className="flex items-center gap-1">
19
+ {PERIODS.map(function (period) {
20
+ var isActive = period.value === value;
21
+ return (
22
+ <button
23
+ key={period.value}
24
+ role="radio"
25
+ aria-checked={isActive}
26
+ onClick={function () { onChange(period.value); }}
27
+ className={[
28
+ "px-2.5 py-1 rounded-md border text-[10px] font-mono font-bold uppercase tracking-widest transition-colors",
29
+ isActive
30
+ ? "bg-primary/15 text-primary border-primary/30"
31
+ : "text-base-content/35 border-base-content/8 hover:text-base-content/60 hover:border-base-content/20",
32
+ ].join(" ")}
33
+ >
34
+ {period.label}
35
+ </button>
36
+ );
37
+ })}
38
+ </div>
39
+ );
40
+ }
41
+
42
+ export type { Period };
@@ -0,0 +1,99 @@
1
+ import { LineChart, Line, ResponsiveContainer } from "recharts";
2
+ import { useAnalytics } from "../../hooks/useAnalytics";
3
+
4
+ function formatTokens(n: number): string {
5
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
6
+ if (n >= 1_000) return Math.round(n / 1_000) + "k";
7
+ return String(n);
8
+ }
9
+
10
+ interface SparklineProps {
11
+ data: Array<{ v: number }>;
12
+ stroke: string;
13
+ }
14
+
15
+ function Sparkline({ data, stroke }: SparklineProps) {
16
+ return (
17
+ <ResponsiveContainer width={60} height={20}>
18
+ <LineChart data={data} margin={{ top: 2, right: 2, left: 2, bottom: 2 }}>
19
+ <Line
20
+ type="monotone"
21
+ dataKey="v"
22
+ stroke={stroke}
23
+ strokeWidth={1.5}
24
+ dot={false}
25
+ isAnimationActive={false}
26
+ />
27
+ </LineChart>
28
+ </ResponsiveContainer>
29
+ );
30
+ }
31
+
32
+ export function QuickStats() {
33
+ var analytics = useAnalytics();
34
+
35
+ if (!analytics.data) {
36
+ return (
37
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
38
+ {[0, 1, 2, 3].map(function (i) {
39
+ return (
40
+ <div key={i} className="bg-base-content/[0.03] border border-base-content/8 rounded-xl p-3.5 animate-pulse">
41
+ <div className="h-2.5 w-16 bg-base-content/10 rounded mb-3" />
42
+ <div className="h-6 w-12 bg-base-content/10 rounded" />
43
+ </div>
44
+ );
45
+ })}
46
+ </div>
47
+ );
48
+ }
49
+
50
+ var d = analytics.data;
51
+
52
+ var costSparkData = d.costOverTime.slice(-7).map(function (e) { return { v: e.total }; });
53
+ var sessionsSparkData = d.sessionsOverTime.slice(-7).map(function (e) { return { v: e.count }; });
54
+ var tokensSparkData = d.tokensOverTime.slice(-7).map(function (e) { return { v: e.input + e.output }; });
55
+
56
+ var totalTokens = d.totalTokens.input + d.totalTokens.output;
57
+ var cacheHitPct = Math.round(d.cacheHitRate * 100);
58
+
59
+ return (
60
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
61
+ <div className="bg-base-content/[0.03] border border-base-content/8 rounded-xl p-3.5">
62
+ <div className="flex items-center justify-between mb-1">
63
+ <span className="text-[10px] font-mono font-semibold uppercase tracking-wider text-base-content/35">Cost</span>
64
+ {costSparkData.length > 1 && <Sparkline data={costSparkData} stroke="oklch(55% 0.25 280)" />}
65
+ </div>
66
+ <div className="text-[22px] font-mono text-base-content/85">${d.totalCost.toFixed(2)}</div>
67
+ </div>
68
+
69
+ <div className="bg-base-content/[0.03] border border-base-content/8 rounded-xl p-3.5">
70
+ <div className="flex items-center justify-between mb-1">
71
+ <span className="text-[10px] font-mono font-semibold uppercase tracking-wider text-base-content/35">Sessions</span>
72
+ {sessionsSparkData.length > 1 && <Sparkline data={sessionsSparkData} stroke="#22c55e" />}
73
+ </div>
74
+ <div className="text-[22px] font-mono text-base-content/85">{d.totalSessions}</div>
75
+ </div>
76
+
77
+ <div className="bg-base-content/[0.03] border border-base-content/8 rounded-xl p-3.5">
78
+ <div className="flex items-center justify-between mb-1">
79
+ <span className="text-[10px] font-mono font-semibold uppercase tracking-wider text-base-content/35">Tokens</span>
80
+ {tokensSparkData.length > 1 && <Sparkline data={tokensSparkData} stroke="#f59e0b" />}
81
+ </div>
82
+ <div className="text-[22px] font-mono text-base-content/85">{formatTokens(totalTokens)}</div>
83
+ </div>
84
+
85
+ <div className="bg-base-content/[0.03] border border-base-content/8 rounded-xl p-3.5">
86
+ <div className="mb-1">
87
+ <span className="text-[10px] font-mono font-semibold uppercase tracking-wider text-base-content/35">Cache Hit</span>
88
+ </div>
89
+ <div className="text-[22px] font-mono text-base-content/85 mb-2">{cacheHitPct}%</div>
90
+ <div className="w-full h-1 rounded-full bg-base-content/10 overflow-hidden">
91
+ <div
92
+ className="h-full rounded-full bg-primary transition-all duration-300"
93
+ style={{ width: cacheHitPct + "%" }}
94
+ />
95
+ </div>
96
+ </div>
97
+ </div>
98
+ );
99
+ }
@@ -0,0 +1,83 @@
1
+ import {
2
+ AreaChart,
3
+ Area,
4
+ XAxis,
5
+ YAxis,
6
+ CartesianGrid,
7
+ Tooltip,
8
+ ResponsiveContainer,
9
+ } from "recharts";
10
+
11
+ var TICK_STYLE = {
12
+ fontSize: 10,
13
+ fontFamily: "var(--font-mono)",
14
+ fill: "oklch(0.9 0.02 280 / 0.3)",
15
+ };
16
+
17
+ var GRID_COLOR = "oklch(0.9 0.02 280 / 0.06)";
18
+
19
+ interface CostAreaDatum {
20
+ date: string;
21
+ total: number;
22
+ opus: number;
23
+ sonnet: number;
24
+ haiku: number;
25
+ other: number;
26
+ }
27
+
28
+ interface CostAreaChartProps {
29
+ data: CostAreaDatum[];
30
+ }
31
+
32
+ function CustomTooltip({ active, payload, label }: { active?: boolean; payload?: Array<{ name: string; value: number; color: string }>; label?: string }) {
33
+ if (!active || !payload || payload.length === 0) return null;
34
+ return (
35
+ <div className="rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg">
36
+ <p className="text-[10px] font-mono text-base-content/50 mb-1">{label}</p>
37
+ {payload.map(function (entry) {
38
+ return (
39
+ <div key={entry.name} className="flex items-center gap-2 text-[11px] font-mono">
40
+ <span className="inline-block w-2 h-2 rounded-full" style={{ background: entry.color }} />
41
+ <span className="text-base-content/60 capitalize">{entry.name}</span>
42
+ <span className="text-base-content ml-auto pl-4">${entry.value.toFixed(4)}</span>
43
+ </div>
44
+ );
45
+ })}
46
+ </div>
47
+ );
48
+ }
49
+
50
+ export function CostAreaChart({ data }: CostAreaChartProps) {
51
+ return (
52
+ <ResponsiveContainer width="100%" height={200}>
53
+ <AreaChart data={data} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
54
+ <defs>
55
+ <linearGradient id="opusGrad" x1="0" y1="0" x2="0" y2="1">
56
+ <stop offset="5%" stopColor="#a855f7" stopOpacity={0.4} />
57
+ <stop offset="95%" stopColor="#a855f7" stopOpacity={0.05} />
58
+ </linearGradient>
59
+ <linearGradient id="sonnetGrad" x1="0" y1="0" x2="0" y2="1">
60
+ <stop offset="5%" stopColor="oklch(55% 0.25 280)" stopOpacity={0.4} />
61
+ <stop offset="95%" stopColor="oklch(55% 0.25 280)" stopOpacity={0.05} />
62
+ </linearGradient>
63
+ <linearGradient id="haikuGrad" x1="0" y1="0" x2="0" y2="1">
64
+ <stop offset="5%" stopColor="#22c55e" stopOpacity={0.4} />
65
+ <stop offset="95%" stopColor="#22c55e" stopOpacity={0.05} />
66
+ </linearGradient>
67
+ <linearGradient id="otherGrad" x1="0" y1="0" x2="0" y2="1">
68
+ <stop offset="5%" stopColor="#f59e0b" stopOpacity={0.4} />
69
+ <stop offset="95%" stopColor="#f59e0b" stopOpacity={0.05} />
70
+ </linearGradient>
71
+ </defs>
72
+ <CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} vertical={false} />
73
+ <XAxis dataKey="date" tick={TICK_STYLE} axisLine={false} tickLine={false} />
74
+ <YAxis tick={TICK_STYLE} axisLine={false} tickLine={false} tickFormatter={function (v) { return "$" + v.toFixed(2); }} />
75
+ <Tooltip content={<CustomTooltip />} />
76
+ <Area type="monotone" dataKey="opus" stackId="1" stroke="#a855f7" fill="url(#opusGrad)" strokeWidth={1.5} />
77
+ <Area type="monotone" dataKey="sonnet" stackId="1" stroke="oklch(55% 0.25 280)" fill="url(#sonnetGrad)" strokeWidth={1.5} />
78
+ <Area type="monotone" dataKey="haiku" stackId="1" stroke="#22c55e" fill="url(#haikuGrad)" strokeWidth={1.5} />
79
+ <Area type="monotone" dataKey="other" stackId="1" stroke="#f59e0b" fill="url(#otherGrad)" strokeWidth={1.5} />
80
+ </AreaChart>
81
+ </ResponsiveContainer>
82
+ );
83
+ }
@@ -0,0 +1,62 @@
1
+ import {
2
+ AreaChart,
3
+ Area,
4
+ XAxis,
5
+ YAxis,
6
+ CartesianGrid,
7
+ Tooltip,
8
+ ResponsiveContainer,
9
+ } from "recharts";
10
+
11
+ var TICK_STYLE = {
12
+ fontSize: 10,
13
+ fontFamily: "var(--font-mono)",
14
+ fill: "oklch(0.9 0.02 280 / 0.3)",
15
+ };
16
+
17
+ var GRID_COLOR = "oklch(0.9 0.02 280 / 0.06)";
18
+
19
+ interface DistributionDatum {
20
+ bucket: string;
21
+ count: number;
22
+ }
23
+
24
+ interface CostDistributionChartProps {
25
+ data: DistributionDatum[];
26
+ }
27
+
28
+ function CustomTooltip({ active, payload, label }: { active?: boolean; payload?: Array<{ value: number }>; label?: string }) {
29
+ if (!active || !payload || payload.length === 0) return null;
30
+ return (
31
+ <div className="rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg">
32
+ <p className="text-[10px] font-mono text-base-content/50 mb-1">{label}</p>
33
+ <p className="text-[11px] font-mono text-base-content">{payload[0].value} sessions</p>
34
+ </div>
35
+ );
36
+ }
37
+
38
+ export function CostDistributionChart({ data }: CostDistributionChartProps) {
39
+ return (
40
+ <ResponsiveContainer width="100%" height={200}>
41
+ <AreaChart data={data} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
42
+ <defs>
43
+ <linearGradient id="distGrad" x1="0" y1="0" x2="0" y2="1">
44
+ <stop offset="5%" stopColor="oklch(55% 0.25 280)" stopOpacity={0.35} />
45
+ <stop offset="95%" stopColor="oklch(55% 0.25 280)" stopOpacity={0.02} />
46
+ </linearGradient>
47
+ </defs>
48
+ <CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} vertical={false} />
49
+ <XAxis dataKey="bucket" tick={TICK_STYLE} axisLine={false} tickLine={false} />
50
+ <YAxis tick={TICK_STYLE} axisLine={false} tickLine={false} allowDecimals={false} />
51
+ <Tooltip content={<CustomTooltip />} />
52
+ <Area
53
+ type="monotone"
54
+ dataKey="count"
55
+ stroke="oklch(55% 0.25 280)"
56
+ fill="url(#distGrad)"
57
+ strokeWidth={2}
58
+ />
59
+ </AreaChart>
60
+ </ResponsiveContainer>
61
+ );
62
+ }
@@ -0,0 +1,93 @@
1
+ import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from "recharts";
2
+
3
+ var MODEL_COLORS: Record<string, string> = {
4
+ opus: "#a855f7",
5
+ sonnet: "oklch(55% 0.25 280)",
6
+ haiku: "#22c55e",
7
+ other: "#f59e0b",
8
+ };
9
+
10
+ function getModelColor(model: string): string {
11
+ var key = model.toLowerCase();
12
+ if (key.includes("opus")) return MODEL_COLORS.opus;
13
+ if (key.includes("sonnet")) return MODEL_COLORS.sonnet;
14
+ if (key.includes("haiku")) return MODEL_COLORS.haiku;
15
+ return MODEL_COLORS.other;
16
+ }
17
+
18
+ interface ModelUsage {
19
+ model: string;
20
+ cost: number;
21
+ percentage: number;
22
+ }
23
+
24
+ interface CostDonutChartProps {
25
+ modelUsage: ModelUsage[];
26
+ totalCost: number;
27
+ }
28
+
29
+ function CustomTooltip({ active, payload }: { active?: boolean; payload?: Array<{ name: string; value: number; payload: ModelUsage }> }) {
30
+ if (!active || !payload || payload.length === 0) return null;
31
+ var entry = payload[0];
32
+ return (
33
+ <div className="rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg">
34
+ <p className="text-[10px] font-mono text-base-content/50 mb-1">{entry.name}</p>
35
+ <p className="text-[11px] font-mono text-base-content">${entry.value.toFixed(4)}</p>
36
+ <p className="text-[10px] font-mono text-base-content/50">{entry.payload.percentage.toFixed(1)}%</p>
37
+ </div>
38
+ );
39
+ }
40
+
41
+ function CenterLabel({ totalCost }: { totalCost: number }) {
42
+ return (
43
+ <text x="50%" y="50%" textAnchor="middle" dominantBaseline="middle">
44
+ <tspan x="50%" dy="-0.4em" style={{ fontSize: 11, fontFamily: "var(--font-mono)", fill: "oklch(0.9 0.02 280 / 0.4)" }}>
45
+ TOTAL
46
+ </tspan>
47
+ <tspan x="50%" dy="1.4em" style={{ fontSize: 14, fontFamily: "var(--font-mono)", fill: "oklch(0.9 0.02 280 / 0.9)", fontWeight: 700 }}>
48
+ ${totalCost.toFixed(2)}
49
+ </tspan>
50
+ </text>
51
+ );
52
+ }
53
+
54
+ export function CostDonutChart({ modelUsage, totalCost }: CostDonutChartProps) {
55
+ return (
56
+ <div>
57
+ <ResponsiveContainer width="100%" height={200}>
58
+ <PieChart>
59
+ <Pie
60
+ data={modelUsage}
61
+ dataKey="cost"
62
+ nameKey="model"
63
+ innerRadius={50}
64
+ outerRadius={80}
65
+ paddingAngle={2}
66
+ startAngle={90}
67
+ endAngle={-270}
68
+ >
69
+ {modelUsage.map(function (entry, index) {
70
+ return <Cell key={entry.model + index} fill={getModelColor(entry.model)} />;
71
+ })}
72
+ </Pie>
73
+ <Tooltip content={<CustomTooltip />} />
74
+ <CenterLabel totalCost={totalCost} />
75
+ </PieChart>
76
+ </ResponsiveContainer>
77
+ <div className="flex flex-wrap justify-center gap-3 mt-2">
78
+ {modelUsage.map(function (entry) {
79
+ return (
80
+ <div key={entry.model} className="flex items-center gap-1.5 text-[10px] font-mono text-base-content/50">
81
+ <span
82
+ className="inline-block w-2 h-2 rounded-full flex-shrink-0"
83
+ style={{ background: getModelColor(entry.model) }}
84
+ />
85
+ <span className="capitalize">{entry.model}</span>
86
+ <span className="text-base-content/30">{entry.percentage.toFixed(1)}%</span>
87
+ </div>
88
+ );
89
+ })}
90
+ </div>
91
+ </div>
92
+ );
93
+ }
@@ -0,0 +1,62 @@
1
+ import {
2
+ AreaChart,
3
+ Area,
4
+ XAxis,
5
+ YAxis,
6
+ CartesianGrid,
7
+ Tooltip,
8
+ ResponsiveContainer,
9
+ } from "recharts";
10
+
11
+ var TICK_STYLE = {
12
+ fontSize: 10,
13
+ fontFamily: "var(--font-mono)",
14
+ fill: "oklch(0.9 0.02 280 / 0.3)",
15
+ };
16
+
17
+ var GRID_COLOR = "oklch(0.9 0.02 280 / 0.06)";
18
+
19
+ interface CumulativeDatum {
20
+ date: string;
21
+ total: number;
22
+ }
23
+
24
+ interface CumulativeCostChartProps {
25
+ data: CumulativeDatum[];
26
+ }
27
+
28
+ function CustomTooltip({ active, payload, label }: { active?: boolean; payload?: Array<{ value: number }>; label?: string }) {
29
+ if (!active || !payload || payload.length === 0) return null;
30
+ return (
31
+ <div className="rounded-lg border border-base-content/8 bg-base-200 px-3 py-2 shadow-lg">
32
+ <p className="text-[10px] font-mono text-base-content/50 mb-1">{label}</p>
33
+ <p className="text-[11px] font-mono text-base-content">${payload[0].value.toFixed(4)}</p>
34
+ </div>
35
+ );
36
+ }
37
+
38
+ export function CumulativeCostChart({ data }: CumulativeCostChartProps) {
39
+ return (
40
+ <ResponsiveContainer width="100%" height={200}>
41
+ <AreaChart data={data} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
42
+ <defs>
43
+ <linearGradient id="cumulativeGrad" x1="0" y1="0" x2="0" y2="1">
44
+ <stop offset="5%" stopColor="oklch(55% 0.25 280)" stopOpacity={0.3} />
45
+ <stop offset="95%" stopColor="oklch(55% 0.25 280)" stopOpacity={0} />
46
+ </linearGradient>
47
+ </defs>
48
+ <CartesianGrid strokeDasharray="3 3" stroke={GRID_COLOR} vertical={false} />
49
+ <XAxis dataKey="date" tick={TICK_STYLE} axisLine={false} tickLine={false} />
50
+ <YAxis tick={TICK_STYLE} axisLine={false} tickLine={false} tickFormatter={function (v) { return "$" + v.toFixed(2); }} />
51
+ <Tooltip content={<CustomTooltip />} />
52
+ <Area
53
+ type="monotone"
54
+ dataKey="total"
55
+ stroke="oklch(55% 0.25 280)"
56
+ fill="url(#cumulativeGrad)"
57
+ strokeWidth={2}
58
+ />
59
+ </AreaChart>
60
+ </ResponsiveContainer>
61
+ );
62
+ }