stagent 0.6.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +21 -2
  2. package/dist/cli.js +226 -1
  3. package/docs/.coverage-gaps.json +66 -16
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/dashboard-kanban.md +13 -7
  6. package/docs/features/settings.md +15 -3
  7. package/docs/features/tables.md +122 -0
  8. package/docs/index.md +3 -2
  9. package/docs/journeys/developer.md +26 -16
  10. package/docs/journeys/personal-use.md +23 -9
  11. package/docs/journeys/power-user.md +40 -14
  12. package/docs/journeys/work-use.md +43 -15
  13. package/docs/manifest.json +27 -17
  14. package/package.json +3 -1
  15. package/src/app/api/chat/entities/search/route.ts +12 -3
  16. package/src/app/api/projects/[id]/route.ts +37 -0
  17. package/src/app/api/projects/__tests__/delete-project.test.ts +12 -0
  18. package/src/app/api/snapshots/[id]/restore/route.ts +62 -0
  19. package/src/app/api/snapshots/[id]/route.ts +44 -0
  20. package/src/app/api/snapshots/route.ts +54 -0
  21. package/src/app/api/snapshots/settings/route.ts +67 -0
  22. package/src/app/api/tables/[id]/charts/[chartId]/route.ts +89 -0
  23. package/src/app/api/tables/[id]/charts/route.ts +72 -0
  24. package/src/app/api/tables/[id]/columns/route.ts +70 -0
  25. package/src/app/api/tables/[id]/export/route.ts +94 -0
  26. package/src/app/api/tables/[id]/history/route.ts +15 -0
  27. package/src/app/api/tables/[id]/import/route.ts +111 -0
  28. package/src/app/api/tables/[id]/route.ts +86 -0
  29. package/src/app/api/tables/[id]/rows/[rowId]/history/route.ts +32 -0
  30. package/src/app/api/tables/[id]/rows/[rowId]/route.ts +51 -0
  31. package/src/app/api/tables/[id]/rows/route.ts +101 -0
  32. package/src/app/api/tables/[id]/triggers/[triggerId]/route.ts +65 -0
  33. package/src/app/api/tables/[id]/triggers/route.ts +122 -0
  34. package/src/app/api/tables/route.ts +65 -0
  35. package/src/app/api/tables/templates/route.ts +92 -0
  36. package/src/app/settings/page.tsx +2 -0
  37. package/src/app/tables/[id]/page.tsx +67 -0
  38. package/src/app/tables/page.tsx +21 -0
  39. package/src/app/tables/templates/page.tsx +19 -0
  40. package/src/components/chat/chat-table-result.tsx +139 -0
  41. package/src/components/documents/document-browser.tsx +1 -1
  42. package/src/components/projects/project-form-sheet.tsx +3 -27
  43. package/src/components/schedules/schedule-form.tsx +5 -27
  44. package/src/components/settings/data-management-section.tsx +17 -12
  45. package/src/components/settings/database-snapshots-section.tsx +469 -0
  46. package/src/components/shared/app-sidebar.tsx +2 -0
  47. package/src/components/shared/document-picker-sheet.tsx +214 -11
  48. package/src/components/tables/table-browser.tsx +234 -0
  49. package/src/components/tables/table-cell-editor.tsx +226 -0
  50. package/src/components/tables/table-chart-builder.tsx +288 -0
  51. package/src/components/tables/table-chart-view.tsx +146 -0
  52. package/src/components/tables/table-column-header.tsx +103 -0
  53. package/src/components/tables/table-column-sheet.tsx +331 -0
  54. package/src/components/tables/table-create-sheet.tsx +240 -0
  55. package/src/components/tables/table-detail-sheet.tsx +144 -0
  56. package/src/components/tables/table-detail-tabs.tsx +278 -0
  57. package/src/components/tables/table-grid.tsx +61 -0
  58. package/src/components/tables/table-history-tab.tsx +148 -0
  59. package/src/components/tables/table-import-wizard.tsx +542 -0
  60. package/src/components/tables/table-list-table.tsx +95 -0
  61. package/src/components/tables/table-relation-combobox.tsx +217 -0
  62. package/src/components/tables/table-spreadsheet.tsx +499 -0
  63. package/src/components/tables/table-template-gallery.tsx +162 -0
  64. package/src/components/tables/table-template-preview.tsx +219 -0
  65. package/src/components/tables/table-toolbar.tsx +79 -0
  66. package/src/components/tables/table-triggers-tab.tsx +446 -0
  67. package/src/components/tables/types.ts +6 -0
  68. package/src/components/tables/use-spreadsheet-keys.ts +171 -0
  69. package/src/components/tables/utils.ts +29 -0
  70. package/src/components/tasks/task-create-panel.tsx +5 -31
  71. package/src/components/tasks/task-edit-dialog.tsx +5 -27
  72. package/src/components/workflows/workflow-form-view.tsx +5 -29
  73. package/src/components/workflows/workflow-status-view.tsx +1 -1
  74. package/src/instrumentation.ts +3 -0
  75. package/src/lib/agents/__tests__/claude-agent.test.ts +5 -1
  76. package/src/lib/agents/claude-agent.ts +3 -1
  77. package/src/lib/agents/runtime/anthropic-direct.ts +29 -0
  78. package/src/lib/agents/runtime/openai-direct.ts +29 -0
  79. package/src/lib/chat/stagent-tools.ts +2 -0
  80. package/src/lib/chat/tool-catalog.ts +34 -0
  81. package/src/lib/chat/tools/table-tools.ts +955 -0
  82. package/src/lib/constants/table-status.ts +68 -0
  83. package/src/lib/data/__tests__/clear.test.ts +1 -1
  84. package/src/lib/data/clear.ts +45 -0
  85. package/src/lib/data/seed-data/__tests__/profiles.test.ts +28 -23
  86. package/src/lib/data/seed-data/conversations.ts +350 -42
  87. package/src/lib/data/seed-data/documents.ts +564 -591
  88. package/src/lib/data/seed-data/learned-context.ts +101 -22
  89. package/src/lib/data/seed-data/notifications.ts +344 -70
  90. package/src/lib/data/seed-data/profile-test-results.ts +92 -11
  91. package/src/lib/data/seed-data/profiles.ts +144 -46
  92. package/src/lib/data/seed-data/projects.ts +50 -18
  93. package/src/lib/data/seed-data/repo-imports.ts +28 -13
  94. package/src/lib/data/seed-data/schedules.ts +208 -41
  95. package/src/lib/data/seed-data/table-templates.ts +234 -0
  96. package/src/lib/data/seed-data/tasks.ts +614 -116
  97. package/src/lib/data/seed-data/usage-ledger.ts +182 -103
  98. package/src/lib/data/seed-data/user-tables.ts +203 -0
  99. package/src/lib/data/seed-data/views.ts +52 -7
  100. package/src/lib/data/seed-data/workflows.ts +231 -84
  101. package/src/lib/data/seed.ts +55 -14
  102. package/src/lib/data/tables.ts +417 -0
  103. package/src/lib/db/bootstrap.ts +227 -0
  104. package/src/lib/db/index.ts +9 -0
  105. package/src/lib/db/migrations/0019_add_tables_feature.sql +160 -0
  106. package/src/lib/db/migrations/0020_add_table_triggers.sql +19 -0
  107. package/src/lib/db/migrations/0021_add_row_history.sql +15 -0
  108. package/src/lib/db/schema.ts +368 -0
  109. package/src/lib/snapshots/auto-backup.ts +132 -0
  110. package/src/lib/snapshots/retention.ts +64 -0
  111. package/src/lib/snapshots/snapshot-manager.ts +429 -0
  112. package/src/lib/tables/computed.ts +61 -0
  113. package/src/lib/tables/context-builder.ts +139 -0
  114. package/src/lib/tables/formula-engine.ts +415 -0
  115. package/src/lib/tables/history.ts +115 -0
  116. package/src/lib/tables/import.ts +343 -0
  117. package/src/lib/tables/query-builder.ts +152 -0
  118. package/src/lib/tables/trigger-evaluator.ts +146 -0
  119. package/src/lib/tables/types.ts +141 -0
  120. package/src/lib/tables/validation.ts +119 -0
  121. package/src/lib/utils/stagent-paths.ts +20 -0
  122. package/tsconfig.json +3 -1
  123. /package/docs/features/{playbook.md → user-guide.md} +0 -0
@@ -0,0 +1,226 @@
1
+ "use client";
2
+
3
+ import { useRef, useEffect } from "react";
4
+ import { Input } from "@/components/ui/input";
5
+ import { Checkbox } from "@/components/ui/checkbox";
6
+ import {
7
+ Select,
8
+ SelectContent,
9
+ SelectItem,
10
+ SelectTrigger,
11
+ SelectValue,
12
+ } from "@/components/ui/select";
13
+ import { TableRelationCombobox } from "./table-relation-combobox";
14
+ import type { ColumnDef } from "@/lib/tables/types";
15
+
16
+ interface CellEditorProps {
17
+ column: ColumnDef;
18
+ value: unknown;
19
+ onChange: (value: unknown) => void;
20
+ onConfirm: () => void;
21
+ onCancel: () => void;
22
+ }
23
+
24
+ export function CellEditor({
25
+ column,
26
+ value,
27
+ onChange,
28
+ onConfirm,
29
+ onCancel,
30
+ }: CellEditorProps) {
31
+ const inputRef = useRef<HTMLInputElement>(null);
32
+
33
+ useEffect(() => {
34
+ // Auto-focus on mount
35
+ requestAnimationFrame(() => {
36
+ inputRef.current?.focus();
37
+ inputRef.current?.select();
38
+ });
39
+ }, []);
40
+
41
+ const strValue = value == null ? "" : String(value);
42
+
43
+ switch (column.dataType) {
44
+ case "boolean":
45
+ // Booleans toggle directly, no edit mode needed
46
+ return null;
47
+
48
+ case "select": {
49
+ const options = column.config?.options ?? [];
50
+ return (
51
+ <Select
52
+ value={strValue}
53
+ onValueChange={(v) => {
54
+ onChange(v);
55
+ onConfirm();
56
+ }}
57
+ open
58
+ >
59
+ <SelectTrigger className="h-8 border-0 shadow-none focus:ring-2 focus:ring-primary text-sm">
60
+ <SelectValue />
61
+ </SelectTrigger>
62
+ <SelectContent>
63
+ {options.map((opt) => (
64
+ <SelectItem key={opt} value={opt}>
65
+ {opt}
66
+ </SelectItem>
67
+ ))}
68
+ </SelectContent>
69
+ </Select>
70
+ );
71
+ }
72
+
73
+ case "number":
74
+ return (
75
+ <Input
76
+ ref={inputRef}
77
+ type="number"
78
+ value={strValue}
79
+ onChange={(e) => onChange(e.target.value === "" ? null : Number(e.target.value))}
80
+ onBlur={onConfirm}
81
+ onKeyDown={(e) => {
82
+ if (e.key === "Escape") onCancel();
83
+ }}
84
+ className="h-8 border-0 shadow-none focus-visible:ring-2 focus-visible:ring-primary rounded-none text-sm"
85
+ />
86
+ );
87
+
88
+ case "date":
89
+ return (
90
+ <Input
91
+ ref={inputRef}
92
+ type="date"
93
+ value={strValue}
94
+ onChange={(e) => onChange(e.target.value)}
95
+ onBlur={onConfirm}
96
+ onKeyDown={(e) => {
97
+ if (e.key === "Escape") onCancel();
98
+ }}
99
+ className="h-8 border-0 shadow-none focus-visible:ring-2 focus-visible:ring-primary rounded-none text-sm"
100
+ />
101
+ );
102
+
103
+ case "relation": {
104
+ const targetId = column.config?.targetTableId;
105
+ const dispCol = column.config?.displayColumn ?? "name";
106
+ if (!targetId) return null;
107
+ return (
108
+ <TableRelationCombobox
109
+ targetTableId={targetId}
110
+ displayColumn={dispCol}
111
+ value={strValue || null}
112
+ onChange={(v) => {
113
+ onChange(v);
114
+ onConfirm();
115
+ }}
116
+ />
117
+ );
118
+ }
119
+
120
+ case "computed":
121
+ // Computed columns are read-only
122
+ return null;
123
+
124
+ case "url":
125
+ case "email":
126
+ case "text":
127
+ default:
128
+ return (
129
+ <Input
130
+ ref={inputRef}
131
+ type={column.dataType === "email" ? "email" : column.dataType === "url" ? "url" : "text"}
132
+ value={strValue}
133
+ onChange={(e) => onChange(e.target.value)}
134
+ onBlur={onConfirm}
135
+ onKeyDown={(e) => {
136
+ if (e.key === "Escape") onCancel();
137
+ }}
138
+ className="h-8 border-0 shadow-none focus-visible:ring-2 focus-visible:ring-primary rounded-none text-sm"
139
+ />
140
+ );
141
+ }
142
+ }
143
+
144
+ // ── Display renderers ────────────────────────────────────────────────
145
+
146
+ interface CellDisplayProps {
147
+ column: ColumnDef;
148
+ value: unknown;
149
+ onToggleBoolean?: (newValue: boolean) => void;
150
+ }
151
+
152
+ export function CellDisplay({ column, value, onToggleBoolean }: CellDisplayProps) {
153
+ if (value == null || value === "") {
154
+ return <span className="text-muted-foreground/40 text-sm">—</span>;
155
+ }
156
+
157
+ switch (column.dataType) {
158
+ case "boolean":
159
+ return (
160
+ <Checkbox
161
+ checked={!!value}
162
+ onCheckedChange={(checked) => onToggleBoolean?.(!!checked)}
163
+ className="ml-1"
164
+ />
165
+ );
166
+
167
+ case "select":
168
+ return (
169
+ <span className="inline-flex items-center rounded-md bg-muted px-2 py-0.5 text-xs font-medium">
170
+ {String(value)}
171
+ </span>
172
+ );
173
+
174
+ case "number": {
175
+ const num = Number(value);
176
+ return (
177
+ <span className="tabular-nums text-sm">
178
+ {isNaN(num) ? String(value) : num.toLocaleString()}
179
+ </span>
180
+ );
181
+ }
182
+
183
+ case "date": {
184
+ const str = String(value);
185
+ try {
186
+ return <span className="text-sm">{new Date(str).toLocaleDateString()}</span>;
187
+ } catch {
188
+ return <span className="text-sm">{str}</span>;
189
+ }
190
+ }
191
+
192
+ case "url":
193
+ return (
194
+ <a
195
+ href={String(value)}
196
+ target="_blank"
197
+ rel="noopener noreferrer"
198
+ className="text-sm text-primary underline underline-offset-2 hover:text-primary/80"
199
+ onClick={(e) => e.stopPropagation()}
200
+ >
201
+ {String(value).replace(/^https?:\/\//, "").slice(0, 40)}
202
+ </a>
203
+ );
204
+
205
+ case "email":
206
+ return (
207
+ <a
208
+ href={`mailto:${value}`}
209
+ className="text-sm text-primary underline underline-offset-2 hover:text-primary/80"
210
+ onClick={(e) => e.stopPropagation()}
211
+ >
212
+ {String(value)}
213
+ </a>
214
+ );
215
+
216
+ case "computed":
217
+ return (
218
+ <span className="text-sm text-muted-foreground italic">
219
+ {String(value)}
220
+ </span>
221
+ );
222
+
223
+ default:
224
+ return <span className="text-sm">{String(value)}</span>;
225
+ }
226
+ }
@@ -0,0 +1,288 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Input } from "@/components/ui/input";
6
+ import { Label } from "@/components/ui/label";
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ } from "@/components/ui/select";
14
+ import {
15
+ Sheet,
16
+ SheetContent,
17
+ SheetHeader,
18
+ SheetTitle,
19
+ SheetFooter,
20
+ } from "@/components/ui/sheet";
21
+ import { BarChart3, LineChart, PieChart, ScatterChart } from "lucide-react";
22
+ import { toast } from "sonner";
23
+ import { cn } from "@/lib/utils";
24
+
25
+ type ChartType = "bar" | "line" | "pie" | "scatter";
26
+ type Aggregation = "sum" | "avg" | "count" | "min" | "max";
27
+
28
+ export interface EditChartData {
29
+ id: string;
30
+ name: string;
31
+ config: {
32
+ type: ChartType;
33
+ xColumn: string;
34
+ yColumn?: string;
35
+ aggregation?: Aggregation;
36
+ };
37
+ }
38
+
39
+ interface TableChartBuilderProps {
40
+ tableId: string;
41
+ columns: Array<{ name: string; displayName: string; dataType: string }>;
42
+ open: boolean;
43
+ onOpenChange: (open: boolean) => void;
44
+ onChartSaved: () => void;
45
+ editChart?: EditChartData | null;
46
+ }
47
+
48
+ const CHART_TYPES: Array<{
49
+ value: ChartType;
50
+ label: string;
51
+ icon: typeof BarChart3;
52
+ }> = [
53
+ { value: "bar", label: "Bar", icon: BarChart3 },
54
+ { value: "line", label: "Line", icon: LineChart },
55
+ { value: "pie", label: "Pie", icon: PieChart },
56
+ { value: "scatter", label: "Scatter", icon: ScatterChart },
57
+ ];
58
+
59
+ const AGGREGATIONS: Array<{ value: Aggregation; label: string }> = [
60
+ { value: "count", label: "Count" },
61
+ { value: "sum", label: "Sum" },
62
+ { value: "avg", label: "Average" },
63
+ { value: "min", label: "Min" },
64
+ { value: "max", label: "Max" },
65
+ ];
66
+
67
+ const NUMERIC_TYPES = new Set(["number", "currency", "percent", "integer", "float"]);
68
+
69
+ export function TableChartBuilder({
70
+ tableId,
71
+ columns,
72
+ open,
73
+ onOpenChange,
74
+ onChartSaved,
75
+ editChart,
76
+ }: TableChartBuilderProps) {
77
+ const [chartType, setChartType] = useState<ChartType>("bar");
78
+ const [title, setTitle] = useState("");
79
+ const [xColumn, setXColumn] = useState("");
80
+ const [yColumn, setYColumn] = useState("");
81
+ const [aggregation, setAggregation] = useState<Aggregation>("count");
82
+ const [saving, setSaving] = useState(false);
83
+
84
+ const isEditing = !!editChart;
85
+ const numericColumns = columns.filter((c) => NUMERIC_TYPES.has(c.dataType));
86
+
87
+ // Populate form when editing
88
+ useEffect(() => {
89
+ if (editChart) {
90
+ setChartType(editChart.config.type);
91
+ setTitle(editChart.name);
92
+ setXColumn(editChart.config.xColumn);
93
+ setYColumn(editChart.config.yColumn ?? "");
94
+ setAggregation(editChart.config.aggregation ?? "count");
95
+ } else {
96
+ resetForm();
97
+ }
98
+ }, [editChart]);
99
+
100
+ function resetForm() {
101
+ setChartType("bar");
102
+ setTitle("");
103
+ setXColumn("");
104
+ setYColumn("");
105
+ setAggregation("count");
106
+ }
107
+
108
+ async function handleSave() {
109
+ if (!title.trim()) {
110
+ toast.error("Chart title is required");
111
+ return;
112
+ }
113
+ if (!xColumn) {
114
+ toast.error("X-axis column is required");
115
+ return;
116
+ }
117
+ if (aggregation !== "count" && !yColumn) {
118
+ toast.error("Y-axis column is required for this aggregation");
119
+ return;
120
+ }
121
+
122
+ setSaving(true);
123
+ try {
124
+ const payload = {
125
+ type: chartType,
126
+ title: title.trim(),
127
+ xColumn,
128
+ yColumn: yColumn || null,
129
+ aggregation,
130
+ };
131
+
132
+ const url = isEditing
133
+ ? `/api/tables/${tableId}/charts/${editChart.id}`
134
+ : `/api/tables/${tableId}/charts`;
135
+
136
+ const res = await fetch(url, {
137
+ method: isEditing ? "PATCH" : "POST",
138
+ headers: { "Content-Type": "application/json" },
139
+ body: JSON.stringify(payload),
140
+ });
141
+
142
+ if (res.ok) {
143
+ toast.success(isEditing ? `Chart "${title}" updated` : `Chart "${title}" created`);
144
+ } else {
145
+ const data = await res.json().catch(() => null);
146
+ toast.error(data?.error ?? `Failed to ${isEditing ? "update" : "create"} chart`);
147
+ return;
148
+ }
149
+
150
+ onOpenChange(false);
151
+ onChartSaved();
152
+ if (!isEditing) resetForm();
153
+ } catch {
154
+ toast.error(`Failed to ${isEditing ? "update" : "create"} chart`);
155
+ } finally {
156
+ setSaving(false);
157
+ }
158
+ }
159
+
160
+ return (
161
+ <Sheet open={open} onOpenChange={onOpenChange}>
162
+ <SheetContent side="right" className="w-[420px] sm:max-w-[420px]">
163
+ <SheetHeader>
164
+ <SheetTitle>{isEditing ? "Edit Chart" : "Create Chart"}</SheetTitle>
165
+ </SheetHeader>
166
+
167
+ <div className="px-6 pb-6 space-y-5 overflow-y-auto">
168
+ {/* Chart type selector */}
169
+ <div className="space-y-2">
170
+ <Label>Chart Type</Label>
171
+ <div className="grid grid-cols-4 gap-2">
172
+ {CHART_TYPES.map((ct) => {
173
+ const Icon = ct.icon;
174
+ return (
175
+ <button
176
+ key={ct.value}
177
+ type="button"
178
+ onClick={() => setChartType(ct.value)}
179
+ className={cn(
180
+ "flex flex-col items-center gap-1.5 rounded-lg border p-3 text-xs transition-colors",
181
+ "hover:bg-accent hover:text-accent-foreground",
182
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
183
+ chartType === ct.value
184
+ ? "border-primary bg-primary/5 text-primary"
185
+ : "border-border text-muted-foreground"
186
+ )}
187
+ >
188
+ <Icon className="h-5 w-5" />
189
+ {ct.label}
190
+ </button>
191
+ );
192
+ })}
193
+ </div>
194
+ </div>
195
+
196
+ {/* Title */}
197
+ <div className="space-y-2">
198
+ <Label htmlFor="chart-title">Title</Label>
199
+ <Input
200
+ id="chart-title"
201
+ placeholder="e.g. Revenue by Month"
202
+ value={title}
203
+ onChange={(e) => setTitle(e.target.value)}
204
+ />
205
+ </div>
206
+
207
+ {/* X-axis column */}
208
+ <div className="space-y-2">
209
+ <Label>X-Axis Column</Label>
210
+ <Select value={xColumn} onValueChange={setXColumn}>
211
+ <SelectTrigger>
212
+ <SelectValue placeholder="Select column" />
213
+ </SelectTrigger>
214
+ <SelectContent>
215
+ {columns.map((col) => (
216
+ <SelectItem key={col.name} value={col.name}>
217
+ {col.displayName}
218
+ </SelectItem>
219
+ ))}
220
+ </SelectContent>
221
+ </Select>
222
+ </div>
223
+
224
+ {/* Aggregation */}
225
+ <div className="space-y-2">
226
+ <Label>Aggregation</Label>
227
+ <Select
228
+ value={aggregation}
229
+ onValueChange={(v) => setAggregation(v as Aggregation)}
230
+ >
231
+ <SelectTrigger>
232
+ <SelectValue />
233
+ </SelectTrigger>
234
+ <SelectContent>
235
+ {AGGREGATIONS.map((agg) => (
236
+ <SelectItem key={agg.value} value={agg.value}>
237
+ {agg.label}
238
+ </SelectItem>
239
+ ))}
240
+ </SelectContent>
241
+ </Select>
242
+ </div>
243
+
244
+ {/* Y-axis column (only for non-count aggregations) */}
245
+ {aggregation !== "count" && (
246
+ <div className="space-y-2">
247
+ <Label>Y-Axis Column (numeric)</Label>
248
+ <Select value={yColumn} onValueChange={setYColumn}>
249
+ <SelectTrigger>
250
+ <SelectValue placeholder="Select numeric column" />
251
+ </SelectTrigger>
252
+ <SelectContent>
253
+ {numericColumns.length === 0 ? (
254
+ <SelectItem value="_none" disabled>
255
+ No numeric columns available
256
+ </SelectItem>
257
+ ) : (
258
+ numericColumns.map((col) => (
259
+ <SelectItem key={col.name} value={col.name}>
260
+ {col.displayName}
261
+ </SelectItem>
262
+ ))
263
+ )}
264
+ </SelectContent>
265
+ </Select>
266
+ {numericColumns.length === 0 && (
267
+ <p className="text-xs text-muted-foreground">
268
+ Add a numeric column to use sum, average, min, or max aggregations.
269
+ </p>
270
+ )}
271
+ </div>
272
+ )}
273
+ </div>
274
+
275
+ <SheetFooter className="px-6">
276
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
277
+ Cancel
278
+ </Button>
279
+ <Button onClick={handleSave} disabled={saving}>
280
+ {saving
281
+ ? isEditing ? "Updating..." : "Creating..."
282
+ : isEditing ? "Update Chart" : "Create Chart"}
283
+ </Button>
284
+ </SheetFooter>
285
+ </SheetContent>
286
+ </Sheet>
287
+ );
288
+ }
@@ -0,0 +1,146 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import {
5
+ BarChart,
6
+ Bar,
7
+ LineChart,
8
+ Line,
9
+ PieChart,
10
+ Pie,
11
+ ScatterChart,
12
+ Scatter,
13
+ XAxis,
14
+ YAxis,
15
+ CartesianGrid,
16
+ Tooltip,
17
+ Legend,
18
+ ResponsiveContainer,
19
+ Cell,
20
+ } from "recharts";
21
+
22
+ interface ChartConfig {
23
+ type: "bar" | "line" | "pie" | "scatter";
24
+ xColumn: string;
25
+ yColumn?: string;
26
+ aggregation?: "sum" | "avg" | "count" | "min" | "max";
27
+ }
28
+
29
+ interface TableChartViewProps {
30
+ config: ChartConfig;
31
+ title: string;
32
+ rows: Array<{ data: Record<string, unknown> }>;
33
+ }
34
+
35
+ const CHART_COLORS = [
36
+ "oklch(0.65 0.18 250)",
37
+ "oklch(0.70 0.16 180)",
38
+ "oklch(0.68 0.17 320)",
39
+ "oklch(0.72 0.14 80)",
40
+ "oklch(0.60 0.20 30)",
41
+ "oklch(0.75 0.12 140)",
42
+ ];
43
+
44
+ export function TableChartView({ config, title, rows }: TableChartViewProps) {
45
+ const chartData = useMemo(() => {
46
+ if (config.type === "pie" || config.aggregation === "count") {
47
+ // Aggregate by xColumn values
48
+ const counts = new Map<string, number>();
49
+ for (const row of rows) {
50
+ const key = String(row.data[config.xColumn] ?? "Unknown");
51
+ counts.set(key, (counts.get(key) ?? 0) + 1);
52
+ }
53
+ return Array.from(counts.entries()).map(([name, value]) => ({ name, value }));
54
+ }
55
+
56
+ if (config.aggregation && config.yColumn) {
57
+ // Group by xColumn, aggregate yColumn
58
+ const groups = new Map<string, number[]>();
59
+ for (const row of rows) {
60
+ const key = String(row.data[config.xColumn] ?? "Unknown");
61
+ const val = Number(row.data[config.yColumn]);
62
+ if (!isNaN(val)) {
63
+ if (!groups.has(key)) groups.set(key, []);
64
+ groups.get(key)!.push(val);
65
+ }
66
+ }
67
+
68
+ return Array.from(groups.entries()).map(([name, values]) => {
69
+ let value: number;
70
+ switch (config.aggregation) {
71
+ case "sum": value = values.reduce((a, b) => a + b, 0); break;
72
+ case "avg": value = values.reduce((a, b) => a + b, 0) / values.length; break;
73
+ case "min": value = Math.min(...values); break;
74
+ case "max": value = Math.max(...values); break;
75
+ default: value = values.length;
76
+ }
77
+ return { name, value: Math.round(value * 100) / 100 };
78
+ });
79
+ }
80
+
81
+ // Raw x/y mapping
82
+ return rows.map((row) => ({
83
+ name: String(row.data[config.xColumn] ?? ""),
84
+ value: config.yColumn ? Number(row.data[config.yColumn]) || 0 : 1,
85
+ }));
86
+ }, [config, rows]);
87
+
88
+ if (chartData.length === 0) {
89
+ return <p className="text-sm text-muted-foreground p-4">No data to chart.</p>;
90
+ }
91
+
92
+ return (
93
+ <div className="space-y-2">
94
+ <h3 className="text-sm font-medium">{title}</h3>
95
+ <div className="h-[300px] w-full">
96
+ <ResponsiveContainer width="100%" height="100%">
97
+ {config.type === "bar" ? (
98
+ <BarChart data={chartData}>
99
+ <CartesianGrid strokeDasharray="3 3" className="opacity-30" />
100
+ <XAxis dataKey="name" tick={{ fontSize: 12 }} />
101
+ <YAxis tick={{ fontSize: 12 }} />
102
+ <Tooltip />
103
+ <Legend />
104
+ <Bar dataKey="value" name={config.yColumn ?? config.aggregation ?? "value"} fill="oklch(0.65 0.18 250)" radius={[4, 4, 0, 0]} />
105
+ </BarChart>
106
+ ) : config.type === "line" ? (
107
+ <LineChart data={chartData}>
108
+ <CartesianGrid strokeDasharray="3 3" className="opacity-30" />
109
+ <XAxis dataKey="name" tick={{ fontSize: 12 }} />
110
+ <YAxis tick={{ fontSize: 12 }} />
111
+ <Tooltip />
112
+ <Legend />
113
+ <Line type="monotone" dataKey="value" name={config.yColumn ?? config.aggregation ?? "value"} stroke="oklch(0.65 0.18 250)" strokeWidth={2} dot={{ r: 3 }} />
114
+ </LineChart>
115
+ ) : config.type === "pie" ? (
116
+ <PieChart>
117
+ <Tooltip />
118
+ <Legend />
119
+ <Pie
120
+ data={chartData}
121
+ cx="50%"
122
+ cy="50%"
123
+ outerRadius={100}
124
+ dataKey="value"
125
+ nameKey="name"
126
+ label={({ name, percent }: { name?: string; percent?: number }) => `${name ?? ""} ${((percent ?? 0) * 100).toFixed(0)}%`}
127
+ >
128
+ {chartData.map((_, i) => (
129
+ <Cell key={i} fill={CHART_COLORS[i % CHART_COLORS.length]} />
130
+ ))}
131
+ </Pie>
132
+ </PieChart>
133
+ ) : (
134
+ <ScatterChart>
135
+ <CartesianGrid strokeDasharray="3 3" className="opacity-30" />
136
+ <XAxis dataKey="name" tick={{ fontSize: 12 }} name={config.xColumn} />
137
+ <YAxis dataKey="value" tick={{ fontSize: 12 }} name={config.yColumn ?? "value"} />
138
+ <Tooltip />
139
+ <Scatter data={chartData} fill="oklch(0.65 0.18 250)" />
140
+ </ScatterChart>
141
+ )}
142
+ </ResponsiveContainer>
143
+ </div>
144
+ </div>
145
+ );
146
+ }