@growthub/cli 0.10.1 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +307 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +372 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/receipts/route.js +47 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +402 -49
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +1371 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +1348 -21
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +7 -21
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/ownership/ownership-panel.jsx +222 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/ownership/page.jsx +19 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/settings-shell.jsx +2 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +116 -24
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +497 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/growthub.config.json +20 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +19 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +15 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper-apply.js +473 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +583 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json +34 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +3 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +9 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/workers/custom-workspace-operator/CLAUDE.md +38 -0
- package/dist/index.js +2935 -2073
- package/package.json +1 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/workspace/helper/receipts
|
|
3
|
+
*
|
|
4
|
+
* Returns the last N workspace helper apply receipts.
|
|
5
|
+
* Used by the review UI to display accepted proposal history and seed
|
|
6
|
+
* the fine-tune feedback loop.
|
|
7
|
+
*
|
|
8
|
+
* Query params:
|
|
9
|
+
* limit — max records to return (default 25, max 100)
|
|
10
|
+
* type — filter by proposal type (optional)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { NextResponse } from "next/server";
|
|
14
|
+
import { readWorkspaceSourceRecords } from "@/lib/workspace-config";
|
|
15
|
+
|
|
16
|
+
const HELPER_APPLY_SOURCE_KEY = "helper:apply:receipts";
|
|
17
|
+
|
|
18
|
+
async function GET(request) {
|
|
19
|
+
const { searchParams } = new URL(request.url);
|
|
20
|
+
const limitRaw = parseInt(searchParams.get("limit") || "25", 10);
|
|
21
|
+
const limit = Number.isFinite(limitRaw) ? Math.min(Math.max(limitRaw, 1), 100) : 25;
|
|
22
|
+
const typeFilter = searchParams.get("type") || "";
|
|
23
|
+
|
|
24
|
+
let records = [];
|
|
25
|
+
try {
|
|
26
|
+
const existing = await readWorkspaceSourceRecords(HELPER_APPLY_SOURCE_KEY);
|
|
27
|
+
records = Array.isArray(existing?.records) ? existing.records : [];
|
|
28
|
+
} catch {
|
|
29
|
+
records = [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (typeFilter) {
|
|
33
|
+
records = records.filter((r) => r.type === typeFilter);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const page = records.slice(-limit).reverse();
|
|
37
|
+
|
|
38
|
+
return NextResponse.json({
|
|
39
|
+
ok: true,
|
|
40
|
+
totalCount: records.length,
|
|
41
|
+
recordCount: page.length,
|
|
42
|
+
receipts: page,
|
|
43
|
+
records: page,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { GET };
|
|
@@ -43,6 +43,9 @@ import {
|
|
|
43
43
|
Zap,
|
|
44
44
|
} from "lucide-react";
|
|
45
45
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
46
|
+
import { HelperSidecar } from "./HelperSidecar.jsx";
|
|
47
|
+
import { WorkspaceRail } from "../../workspace-rail.jsx";
|
|
48
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
46
49
|
import {
|
|
47
50
|
OBJECT_TYPE_PRESETS,
|
|
48
51
|
addTableField,
|
|
@@ -302,8 +305,8 @@ function ObjectViewPicker({ tables, selectedTable, saving, onSelectSource, onSav
|
|
|
302
305
|
{favoriteObjects.length > 0 && (
|
|
303
306
|
<div className="dm-picker-section">
|
|
304
307
|
<p>Favorites</p>
|
|
305
|
-
{favoriteObjects.map((table) => (
|
|
306
|
-
<button key={`favorite-${table.source}`} type="button" className="dm-picker-row" onClick={() => onSelectSource(table.source)}>
|
|
308
|
+
{favoriteObjects.map((table, favIdx) => (
|
|
309
|
+
<button key={`favorite-${table.id || table.source}-${favIdx}`} type="button" className="dm-picker-row" onClick={() => onSelectSource(table.source)}>
|
|
307
310
|
<Pin size={14} />
|
|
308
311
|
<span>{table.label}</span>
|
|
309
312
|
</button>
|
|
@@ -325,8 +328,8 @@ function ObjectViewPicker({ tables, selectedTable, saving, onSelectSource, onSav
|
|
|
325
328
|
<div className="dm-picker-section">
|
|
326
329
|
<p>Objects</p>
|
|
327
330
|
<div className="dm-picker-scroll">
|
|
328
|
-
{objects.map((table) => (
|
|
329
|
-
<div key={table.source} className={`dm-picker-item${selectedTable?.source === table.source ? " active" : ""}`}>
|
|
331
|
+
{objects.map((table, objIdx) => (
|
|
332
|
+
<div key={`${table.id || table.source}:${objIdx}`} className={`dm-picker-item${selectedTable?.source === table.source ? " active" : ""}`}>
|
|
330
333
|
<button type="button" className="dm-picker-row" onClick={() => {
|
|
331
334
|
onSelectSource(table.source);
|
|
332
335
|
setOpen(false);
|
|
@@ -418,36 +421,9 @@ function SaveToast({ saving, message }) {
|
|
|
418
421
|
return <span className={`dm-toast ${message.startsWith("Error") ? "error" : "ok"}`}>{message}</span>;
|
|
419
422
|
}
|
|
420
423
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
return (
|
|
425
|
-
<aside className="workspace-rail" aria-label="Workspace navigation">
|
|
426
|
-
<div className="workspace-brand">
|
|
427
|
-
<span
|
|
428
|
-
className="workspace-mark"
|
|
429
|
-
style={{
|
|
430
|
-
background: branding.logoUrl ? undefined : branding.accent || undefined,
|
|
431
|
-
color: branding.logoUrl ? undefined : textColorForAccent(branding.accent),
|
|
432
|
-
}}
|
|
433
|
-
>
|
|
434
|
-
{branding.logoUrl ? <img src={branding.logoUrl} alt="" /> : workspaceName.slice(0, 1).toUpperCase()}
|
|
435
|
-
</span>
|
|
436
|
-
<span>{workspaceName}</span>
|
|
437
|
-
</div>
|
|
438
|
-
<nav className="workspace-nav">
|
|
439
|
-
<Link href="/">Dashboards</Link>
|
|
440
|
-
<Link className="active" href="/data-model">Data Model</Link>
|
|
441
|
-
<span className="workspace-nav-static">Management</span>
|
|
442
|
-
<Link className="workspace-nav-bottom" href="/settings/general">Workspace Settings</Link>
|
|
443
|
-
</nav>
|
|
444
|
-
<div className="workspace-rail-status">
|
|
445
|
-
<span className="status-dot" />
|
|
446
|
-
{authority || "local-catalog"}
|
|
447
|
-
</div>
|
|
448
|
-
</aside>
|
|
449
|
-
);
|
|
450
|
-
}
|
|
424
|
+
// NavRail extracted to `app/workspace-rail.jsx` (shared across all
|
|
425
|
+
// governed-workspace pages). The legacy local definition has been
|
|
426
|
+
// removed — every surface now renders <WorkspaceRail />.
|
|
451
427
|
|
|
452
428
|
// ─── Object list (sidebar lives in ./ObjectSidebar.jsx) ───────────────────────
|
|
453
429
|
|
|
@@ -1393,7 +1369,7 @@ function DataModelRecordDrawer({ table, tables, workspaceConfig, rowIndex, row,
|
|
|
1393
1369
|
);
|
|
1394
1370
|
}
|
|
1395
1371
|
|
|
1396
|
-
function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave }) {
|
|
1372
|
+
function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave, onOpenThread }) {
|
|
1397
1373
|
const [selectedRow, setSelectedRow] = useState(null);
|
|
1398
1374
|
const [fieldName, setFieldName] = useState("");
|
|
1399
1375
|
const [fieldType, setFieldType] = useState("text");
|
|
@@ -1853,9 +1829,27 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave
|
|
|
1853
1829
|
</td>
|
|
1854
1830
|
{visibleColumns.map((column) => {
|
|
1855
1831
|
const relation = relationForColumn(table, column);
|
|
1832
|
+
// The Helper Threads object is a normal custom-typed
|
|
1833
|
+
// governed object. We opt the "open" column into a
|
|
1834
|
+
// Reopen link based on the stable well-known object id
|
|
1835
|
+
// so we don't need a dedicated object type.
|
|
1836
|
+
const isHelperThreadOpenCol = table.objectId === "helper-threads" && column === "open";
|
|
1856
1837
|
return (
|
|
1857
1838
|
<td key={column}>
|
|
1858
|
-
{
|
|
1839
|
+
{isHelperThreadOpenCol ? (
|
|
1840
|
+
<button
|
|
1841
|
+
type="button"
|
|
1842
|
+
className="dm-thread-open-link"
|
|
1843
|
+
data-helper-thread-open=""
|
|
1844
|
+
data-thread-id={row?.id || ""}
|
|
1845
|
+
onClick={(event) => {
|
|
1846
|
+
event.stopPropagation();
|
|
1847
|
+
if (typeof onOpenThread === "function") onOpenThread(row);
|
|
1848
|
+
}}
|
|
1849
|
+
>
|
|
1850
|
+
<Zap size={11} />Reopen
|
|
1851
|
+
</button>
|
|
1852
|
+
) : relation ? (
|
|
1859
1853
|
<RelationPickerOrSelect
|
|
1860
1854
|
table={table}
|
|
1861
1855
|
tables={tables}
|
|
@@ -2081,6 +2075,93 @@ function AddObjectSidebar({ open, saving, onClose, onCreate, allTables }) {
|
|
|
2081
2075
|
);
|
|
2082
2076
|
}
|
|
2083
2077
|
|
|
2078
|
+
|
|
2079
|
+
// ─── Command Palette ──────────────────────────────────────────────────────────
|
|
2080
|
+
|
|
2081
|
+
function DataModelCommandPalette({ commands, onClose }) {
|
|
2082
|
+
const [query, setQuery] = useState("");
|
|
2083
|
+
const [highlight, setHighlight] = useState(0);
|
|
2084
|
+
const inputRef = useRef(null);
|
|
2085
|
+
useEffect(() => { inputRef.current?.focus(); }, []);
|
|
2086
|
+
const filtered = useMemo(() => {
|
|
2087
|
+
const q = query.trim().toLowerCase();
|
|
2088
|
+
if (!q) return commands;
|
|
2089
|
+
return commands.filter((c) =>
|
|
2090
|
+
`${c.label} ${c.group || ""} ${(c.aliases || []).join(" ")}`.toLowerCase().includes(q)
|
|
2091
|
+
);
|
|
2092
|
+
}, [commands, query]);
|
|
2093
|
+
useEffect(() => {
|
|
2094
|
+
setHighlight((v) => Math.min(v, Math.max(0, filtered.length - 1)));
|
|
2095
|
+
}, [filtered.length]);
|
|
2096
|
+
const handleKey = (e) => {
|
|
2097
|
+
if (e.key === "ArrowDown") { e.preventDefault(); setHighlight((v) => Math.min(filtered.length - 1, v + 1)); }
|
|
2098
|
+
else if (e.key === "ArrowUp") { e.preventDefault(); setHighlight((v) => Math.max(0, v - 1)); }
|
|
2099
|
+
else if (e.key === "Enter") {
|
|
2100
|
+
e.preventDefault();
|
|
2101
|
+
const cmd = filtered[highlight];
|
|
2102
|
+
if (cmd && !cmd.disabled) { cmd.run(); onClose(); }
|
|
2103
|
+
} else if (e.key === "Escape") { e.preventDefault(); onClose(); }
|
|
2104
|
+
};
|
|
2105
|
+
const groups = useMemo(() => {
|
|
2106
|
+
const map = new Map();
|
|
2107
|
+
filtered.forEach((c) => {
|
|
2108
|
+
const key = c.group || "General";
|
|
2109
|
+
if (!map.has(key)) map.set(key, []);
|
|
2110
|
+
map.get(key).push(c);
|
|
2111
|
+
});
|
|
2112
|
+
return Array.from(map.entries());
|
|
2113
|
+
}, [filtered]);
|
|
2114
|
+
return (
|
|
2115
|
+
<div className="workspace-command-palette" role="dialog" aria-modal="true" aria-label="Command palette" data-palette="">
|
|
2116
|
+
<div className="workspace-overlay-backdrop" onClick={onClose} aria-hidden="true" />
|
|
2117
|
+
<section className="workspace-command-palette-panel" onKeyDown={handleKey}>
|
|
2118
|
+
<header className="workspace-command-palette-input">
|
|
2119
|
+
<span aria-hidden="true">⌘</span>
|
|
2120
|
+
<input
|
|
2121
|
+
ref={inputRef}
|
|
2122
|
+
value={query}
|
|
2123
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
2124
|
+
placeholder="Type a command or ask helper…"
|
|
2125
|
+
aria-label="Command palette search"
|
|
2126
|
+
/>
|
|
2127
|
+
<kbd>esc</kbd>
|
|
2128
|
+
</header>
|
|
2129
|
+
<div className="workspace-command-palette-list" role="listbox">
|
|
2130
|
+
{filtered.length === 0 ? <p className="workspace-panel-hint">No matching commands.</p> : null}
|
|
2131
|
+
{groups.map(([group, items]) => (
|
|
2132
|
+
<div key={group} className="workspace-command-palette-group">
|
|
2133
|
+
<p className="workspace-panel-label">{group}</p>
|
|
2134
|
+
{items.map((cmd) => {
|
|
2135
|
+
const gi = filtered.indexOf(cmd);
|
|
2136
|
+
const isHL = gi === highlight;
|
|
2137
|
+
return (
|
|
2138
|
+
<button
|
|
2139
|
+
key={cmd.id}
|
|
2140
|
+
type="button"
|
|
2141
|
+
role="option"
|
|
2142
|
+
aria-selected={isHL}
|
|
2143
|
+
className={"workspace-command-palette-item" + (isHL ? " active" : "") + (cmd.disabled ? " disabled" : "")}
|
|
2144
|
+
disabled={cmd.disabled}
|
|
2145
|
+
onMouseEnter={() => setHighlight(gi)}
|
|
2146
|
+
onClick={() => { if (!cmd.disabled) { cmd.run(); onClose(); } }}
|
|
2147
|
+
>
|
|
2148
|
+
<span aria-hidden="true"><Zap size={14} /></span>
|
|
2149
|
+
<span className="workspace-command-palette-label">{cmd.label}</span>
|
|
2150
|
+
{cmd.shortcut ? <kbd>{cmd.shortcut}</kbd> : null}
|
|
2151
|
+
</button>
|
|
2152
|
+
);
|
|
2153
|
+
})}
|
|
2154
|
+
</div>
|
|
2155
|
+
))}
|
|
2156
|
+
</div>
|
|
2157
|
+
<footer className="workspace-command-palette-footer">
|
|
2158
|
+
<span>↑ ↓ navigate</span><span>↵ run</span><span>esc close</span>
|
|
2159
|
+
</footer>
|
|
2160
|
+
</section>
|
|
2161
|
+
</div>
|
|
2162
|
+
);
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2084
2165
|
// ─── Page ─────────────────────────────────────────────────────────────────────
|
|
2085
2166
|
|
|
2086
2167
|
// Auto-save tempo: hold local edits in memory + localStorage, only PATCH the
|
|
@@ -2098,9 +2179,44 @@ export default function DataModelShell() {
|
|
|
2098
2179
|
const [message, setMessage] = useState("");
|
|
2099
2180
|
const [selectedSource, setSelectedSource] = useState("");
|
|
2100
2181
|
const [addOpen, setAddOpen] = useState(false);
|
|
2182
|
+
const [helperOpen, setHelperOpen] = useState(false);
|
|
2183
|
+
const [helperIntent, setHelperIntent] = useState("create_object");
|
|
2184
|
+
const [helperInitialPrompt, setHelperInitialPrompt] = useState("");
|
|
2185
|
+
const [helperInitialThread, setHelperInitialThread] = useState(null);
|
|
2186
|
+
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
|
2101
2187
|
const pendingPatchRef = useRef({});
|
|
2102
2188
|
const saveTimerRef = useRef(null);
|
|
2103
2189
|
|
|
2190
|
+
// Cross-page rail entrypoints. Settings / integrations pages render
|
|
2191
|
+
// <WorkspaceRail> without an in-process helper handler — clicking the
|
|
2192
|
+
// pill or a chat thread there navigates to `/data-model?helper=open`
|
|
2193
|
+
// or `/data-model?thread=<id>`. We consume those query params here
|
|
2194
|
+
// exactly once per change and strip them so refreshes are idempotent.
|
|
2195
|
+
const router = useRouter();
|
|
2196
|
+
const searchParams = useSearchParams();
|
|
2197
|
+
useEffect(() => {
|
|
2198
|
+
if (!workspaceConfig) return;
|
|
2199
|
+
const helperParam = searchParams?.get("helper");
|
|
2200
|
+
const threadParam = searchParams?.get("thread");
|
|
2201
|
+
if (!helperParam && !threadParam) return;
|
|
2202
|
+
if (threadParam) {
|
|
2203
|
+
const ht = (workspaceConfig?.dataModel?.objects || []).find((o) => o?.id === "helper-threads");
|
|
2204
|
+
const row = (ht?.rows || []).find((r) => r?.id === threadParam);
|
|
2205
|
+
if (row) {
|
|
2206
|
+
setHelperInitialThread(row);
|
|
2207
|
+
setHelperOpen(true);
|
|
2208
|
+
}
|
|
2209
|
+
} else if (helperParam === "open") {
|
|
2210
|
+
setHelperInitialThread(null);
|
|
2211
|
+
setHelperOpen(true);
|
|
2212
|
+
}
|
|
2213
|
+
const next = new URLSearchParams(searchParams.toString());
|
|
2214
|
+
next.delete("helper");
|
|
2215
|
+
next.delete("thread");
|
|
2216
|
+
const query = next.toString();
|
|
2217
|
+
router.replace(query ? `/data-model?${query}` : "/data-model", { scroll: false });
|
|
2218
|
+
}, [workspaceConfig, searchParams, router]);
|
|
2219
|
+
|
|
2104
2220
|
const load = useCallback(async () => {
|
|
2105
2221
|
setLoading(true);
|
|
2106
2222
|
setError("");
|
|
@@ -2119,6 +2235,40 @@ export default function DataModelShell() {
|
|
|
2119
2235
|
|
|
2120
2236
|
useEffect(() => { load(); }, [load]);
|
|
2121
2237
|
|
|
2238
|
+
// Cmd+K opens command palette. Slash opens it too, but only when no
|
|
2239
|
+
// editable element is focused — matches the dashboard builder.
|
|
2240
|
+
useEffect(() => {
|
|
2241
|
+
const handler = (e) => {
|
|
2242
|
+
if ((e.metaKey || e.ctrlKey) && (e.key === "k" || e.key === "K")) {
|
|
2243
|
+
e.preventDefault();
|
|
2244
|
+
setCommandPaletteOpen((v) => !v);
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
if (e.key === "/" && !commandPaletteOpen && !addOpen && !helperOpen) {
|
|
2248
|
+
const t = e.target;
|
|
2249
|
+
const editable = t instanceof HTMLElement && (
|
|
2250
|
+
t.tagName === "INPUT" ||
|
|
2251
|
+
t.tagName === "TEXTAREA" ||
|
|
2252
|
+
t.tagName === "SELECT" ||
|
|
2253
|
+
t.isContentEditable
|
|
2254
|
+
);
|
|
2255
|
+
if (!editable) {
|
|
2256
|
+
e.preventDefault();
|
|
2257
|
+
setCommandPaletteOpen(true);
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
if (e.key === "Escape" && commandPaletteOpen) setCommandPaletteOpen(false);
|
|
2262
|
+
};
|
|
2263
|
+
const railOpen = () => setCommandPaletteOpen(true);
|
|
2264
|
+
window.addEventListener("keydown", handler);
|
|
2265
|
+
window.addEventListener("growthub:open-command-palette", railOpen);
|
|
2266
|
+
return () => {
|
|
2267
|
+
window.removeEventListener("keydown", handler);
|
|
2268
|
+
window.removeEventListener("growthub:open-command-palette", railOpen);
|
|
2269
|
+
};
|
|
2270
|
+
}, [commandPaletteOpen, addOpen, helperOpen]);
|
|
2271
|
+
|
|
2122
2272
|
const tables = useMemo(
|
|
2123
2273
|
() => (workspaceConfig ? listWorkspaceDataModelTables(workspaceConfig) : []),
|
|
2124
2274
|
[workspaceConfig],
|
|
@@ -2211,15 +2361,162 @@ export default function DataModelShell() {
|
|
|
2211
2361
|
setAddOpen(false);
|
|
2212
2362
|
}, [save]);
|
|
2213
2363
|
|
|
2364
|
+
const INTENT_FOR_TYPE = {
|
|
2365
|
+
people: "edit_view",
|
|
2366
|
+
tasks: "edit_view",
|
|
2367
|
+
"api-registry": "register_api",
|
|
2368
|
+
"sandbox-environment": "create_object",
|
|
2369
|
+
"data-source": "explain",
|
|
2370
|
+
custom: "create_object",
|
|
2371
|
+
};
|
|
2372
|
+
|
|
2373
|
+
// Starter prompt seeded into the textarea when the user asks the helper
|
|
2374
|
+
// about a specific Data Model object. Non-technical users see context-
|
|
2375
|
+
// appropriate guidance instead of an empty box.
|
|
2376
|
+
const STARTER_PROMPT_FOR_TYPE = {
|
|
2377
|
+
people: (name) => `Improve the "${name}" people list. Suggest fields and a view layout that fit a sales / outreach workflow.`,
|
|
2378
|
+
tasks: (name) => `Improve the "${name}" tasks board. Suggest status fields, owners, and a sensible view layout.`,
|
|
2379
|
+
"api-registry": (name) => `Register a new API integration for "${name}". Draft the row with integration label, base URL, endpoint, auth header, and method.`,
|
|
2380
|
+
"sandbox-environment": (name) => `Configure the "${name}" sandbox environment. Suggest runtime, prompt, instructions, and lifecycle status fields.`,
|
|
2381
|
+
"data-source": (name) => `Explain how the "${name}" data source is wired up and what changes would make it more reliable.`,
|
|
2382
|
+
custom: (name) => `Improve the "${name}" object. Suggest fields, relations, and starter rows that fit my use case.`,
|
|
2383
|
+
};
|
|
2384
|
+
|
|
2385
|
+
const openHelperForTable = (table) => {
|
|
2386
|
+
const intent = INTENT_FOR_TYPE[table?.objectType] || "create_object";
|
|
2387
|
+
const fill = STARTER_PROMPT_FOR_TYPE[table?.objectType];
|
|
2388
|
+
setHelperIntent(intent);
|
|
2389
|
+
setHelperInitialPrompt(fill ? fill(table?.label || table?.source || "this object") : "");
|
|
2390
|
+
setHelperInitialThread(null);
|
|
2391
|
+
setHelperOpen(true);
|
|
2392
|
+
};
|
|
2393
|
+
|
|
2394
|
+
const openHelperWith = (intent, prompt) => {
|
|
2395
|
+
setHelperIntent(intent);
|
|
2396
|
+
setHelperInitialPrompt(prompt || "");
|
|
2397
|
+
setHelperInitialThread(null);
|
|
2398
|
+
setHelperOpen(true);
|
|
2399
|
+
};
|
|
2400
|
+
|
|
2401
|
+
// Reopen a helper thread row from the Helper Threads Data Model object.
|
|
2402
|
+
// The row already holds the full prior turn (intent, prompt, proposals,
|
|
2403
|
+
// warnings, receipts) — passing it through initialThread rehydrates the
|
|
2404
|
+
// sidecar state so the user reads the conversation exactly where it ended.
|
|
2405
|
+
const openHelperThreadFromRow = (row) => {
|
|
2406
|
+
if (!row || !row.id) return;
|
|
2407
|
+
const proposals = Array.isArray(row.proposals) ? row.proposals : [];
|
|
2408
|
+
const warnings = Array.isArray(row.warnings) ? row.warnings : [];
|
|
2409
|
+
const result = {
|
|
2410
|
+
summary: row.summary || "",
|
|
2411
|
+
proposals,
|
|
2412
|
+
warnings,
|
|
2413
|
+
receipts: row.receipts || null,
|
|
2414
|
+
threadId: row.id,
|
|
2415
|
+
};
|
|
2416
|
+
setHelperIntent(row.intent || "explain");
|
|
2417
|
+
setHelperInitialPrompt(typeof row.prompt === "string" ? row.prompt : "");
|
|
2418
|
+
setHelperInitialThread({
|
|
2419
|
+
id: row.id,
|
|
2420
|
+
intent: row.intent || "explain",
|
|
2421
|
+
prompt: typeof row.prompt === "string" ? row.prompt : "",
|
|
2422
|
+
result,
|
|
2423
|
+
});
|
|
2424
|
+
setHelperOpen(true);
|
|
2425
|
+
};
|
|
2426
|
+
|
|
2427
|
+
const paletteCommands = [
|
|
2428
|
+
{
|
|
2429
|
+
id: "helper.build_dashboard", group: "Ask helper", label: "Ask helper — build a dashboard",
|
|
2430
|
+
run: () => openHelperWith("build_dashboard", "Draft a dashboard for a local agency with pipeline stages, weekly revenue, and a leaderboard widget.")
|
|
2431
|
+
},
|
|
2432
|
+
{
|
|
2433
|
+
id: "helper.create_object", group: "Ask helper", label: "Ask helper — create a custom object",
|
|
2434
|
+
run: () => openHelperWith("create_object", "Create a custom object for tracking client engagements: name, owner, status, value, next step.")
|
|
2435
|
+
},
|
|
2436
|
+
{
|
|
2437
|
+
id: "helper.register_api", group: "Ask helper", label: "Ask helper — register an API",
|
|
2438
|
+
run: () => openHelperWith("register_api", "Register an API integration: integration label, base URL, endpoint, auth header, and method.")
|
|
2439
|
+
},
|
|
2440
|
+
{
|
|
2441
|
+
id: "helper.repair", group: "Ask helper", label: "Ask helper — repair workspace",
|
|
2442
|
+
run: () => openHelperWith("repair", "Inspect this workspace for missing references, broken bindings, or incomplete views. Propose the smallest fix for each issue.")
|
|
2443
|
+
},
|
|
2444
|
+
{
|
|
2445
|
+
id: "helper.explain", group: "Ask helper", label: "Ask helper — explain this workspace",
|
|
2446
|
+
run: () => openHelperWith("explain", "Explain what this workspace contains and how the objects, dashboards, and bindings relate to each other.")
|
|
2447
|
+
},
|
|
2448
|
+
{
|
|
2449
|
+
id: "object.new", group: "Data Model", label: "New object",
|
|
2450
|
+
run: () => setAddOpen(true)
|
|
2451
|
+
},
|
|
2452
|
+
{
|
|
2453
|
+
id: "nav.dashboards", group: "Navigation", label: "Go to Dashboards",
|
|
2454
|
+
run: () => { window.location.href = "/"; }
|
|
2455
|
+
},
|
|
2456
|
+
{
|
|
2457
|
+
id: "nav.settings", group: "Navigation", label: "Go to Settings",
|
|
2458
|
+
run: () => { window.location.href = "/settings/general"; }
|
|
2459
|
+
},
|
|
2460
|
+
];
|
|
2461
|
+
|
|
2214
2462
|
return (
|
|
2215
2463
|
<main className="workspace-builder workspace-settings-page">
|
|
2216
|
-
<
|
|
2464
|
+
<WorkspaceRail
|
|
2465
|
+
authority={authority}
|
|
2466
|
+
workspaceConfig={workspaceConfig}
|
|
2467
|
+
helperOpen={helperOpen}
|
|
2468
|
+
onOpenHelper={() => {
|
|
2469
|
+
if (helperOpen) { setHelperOpen(false); return; }
|
|
2470
|
+
// Rail pill ALWAYS opens a fresh thread (empty state, chip
|
|
2471
|
+
// stack visible). Reopening a specific conversation goes
|
|
2472
|
+
// through onOpenThread from the Chat tab.
|
|
2473
|
+
setHelperInitialThread(null);
|
|
2474
|
+
setHelperIntent("create_object");
|
|
2475
|
+
setHelperInitialPrompt("");
|
|
2476
|
+
setHelperOpen(true);
|
|
2477
|
+
}}
|
|
2478
|
+
onOpenThread={(row) => {
|
|
2479
|
+
setHelperInitialThread(row);
|
|
2480
|
+
setHelperOpen(true);
|
|
2481
|
+
}}
|
|
2482
|
+
onConfigChange={(next) => {
|
|
2483
|
+
if (typeof setWorkspaceConfig === "function") setWorkspaceConfig(next);
|
|
2484
|
+
}}
|
|
2485
|
+
/>
|
|
2217
2486
|
|
|
2218
2487
|
<section className="workspace-surface">
|
|
2219
2488
|
<header className="workspace-toolbar">
|
|
2220
|
-
|
|
2489
|
+
{selectedTable ? (
|
|
2490
|
+
<div className="workspace-toolbar-object">
|
|
2491
|
+
<div className="workspace-toolbar-object-title">
|
|
2492
|
+
<span className="workspace-toolbar-object-icon" aria-hidden="true">
|
|
2493
|
+
<LucideIcon
|
|
2494
|
+
name={selectedTable.icon || OBJECT_TYPE_PRESETS[selectedTable.objectType]?.icon || "Database"}
|
|
2495
|
+
size={16}
|
|
2496
|
+
/>
|
|
2497
|
+
</span>
|
|
2498
|
+
<h1>{selectedTable.label}</h1>
|
|
2499
|
+
</div>
|
|
2500
|
+
<p className="workspace-toolbar-object-meta">
|
|
2501
|
+
{(selectedTable.columns?.length || 0)} {(selectedTable.columns?.length || 0) === 1 ? "Field" : "Fields"}
|
|
2502
|
+
{" · "}
|
|
2503
|
+
{(selectedTable.rows?.length || 0)} {(selectedTable.rows?.length || 0) === 1 ? "Record" : "Records"}
|
|
2504
|
+
</p>
|
|
2505
|
+
</div>
|
|
2506
|
+
) : (
|
|
2507
|
+
<div><p>Workspace</p><h1>Data Model</h1></div>
|
|
2508
|
+
)}
|
|
2221
2509
|
<div className="workspace-toolbar-actions">
|
|
2222
2510
|
<SaveToast saving={saving} message={message} />
|
|
2511
|
+
{selectedTable && (
|
|
2512
|
+
<ObjectViewPicker
|
|
2513
|
+
tables={tables}
|
|
2514
|
+
selectedTable={selectedTable}
|
|
2515
|
+
saving={saving}
|
|
2516
|
+
onSelectSource={setSelectedSource}
|
|
2517
|
+
onSave={save}
|
|
2518
|
+
/>
|
|
2519
|
+
)}
|
|
2223
2520
|
<button type="button" className="dm-btn-primary" onClick={() => setAddOpen(true)}>
|
|
2224
2521
|
<Plus size={14} />New object
|
|
2225
2522
|
</button>
|
|
@@ -2234,6 +2531,55 @@ export default function DataModelShell() {
|
|
|
2234
2531
|
allTables={tables}
|
|
2235
2532
|
/>
|
|
2236
2533
|
|
|
2534
|
+
<HelperSidecar
|
|
2535
|
+
open={helperOpen}
|
|
2536
|
+
onClose={() => setHelperOpen(false)}
|
|
2537
|
+
workspaceConfig={workspaceConfig}
|
|
2538
|
+
initialIntent={helperIntent}
|
|
2539
|
+
initialPrompt={helperInitialPrompt}
|
|
2540
|
+
initialThread={helperInitialThread}
|
|
2541
|
+
onOpenArtifact={(target) => {
|
|
2542
|
+
// Close the chat and route the user to the artifact they
|
|
2543
|
+
// just created — data-model object/row stays in-page, a
|
|
2544
|
+
// dashboard navigates to the workspace home with a query
|
|
2545
|
+
// param the builder reads to focus it.
|
|
2546
|
+
if (!target) return;
|
|
2547
|
+
if (target.surface === "data-model" && target.source) {
|
|
2548
|
+
setSelectedSource(target.source);
|
|
2549
|
+
setHelperOpen(false);
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
if (target.surface === "dashboard" && target.dashboardId) {
|
|
2553
|
+
setHelperOpen(false);
|
|
2554
|
+
router.push(`/?dashboard=${encodeURIComponent(target.dashboardId)}`);
|
|
2555
|
+
}
|
|
2556
|
+
}}
|
|
2557
|
+
onApplied={(updatedConfig) => {
|
|
2558
|
+
// Anchor the user on the most recently created/updated Data Model
|
|
2559
|
+
// object so a helper-driven object.create lands on the surface
|
|
2560
|
+
// instead of needing a manual click.
|
|
2561
|
+
setWorkspaceConfig(updatedConfig);
|
|
2562
|
+
const nextObjects = updatedConfig?.dataModel?.objects || [];
|
|
2563
|
+
const prevIds = new Set(
|
|
2564
|
+
(workspaceConfig?.dataModel?.objects || []).map((o) => o?.id).filter(Boolean)
|
|
2565
|
+
);
|
|
2566
|
+
const newlyCreated = nextObjects.find((o) => o?.id && !prevIds.has(o.id));
|
|
2567
|
+
const nextSource = (newlyCreated?.label || newlyCreated?.id)
|
|
2568
|
+
? (newlyCreated.label || newlyCreated.id)
|
|
2569
|
+
: selectedSource;
|
|
2570
|
+
if (nextSource && nextSource !== selectedSource) {
|
|
2571
|
+
setSelectedSource(nextSource);
|
|
2572
|
+
}
|
|
2573
|
+
}}
|
|
2574
|
+
/>
|
|
2575
|
+
|
|
2576
|
+
{commandPaletteOpen && (
|
|
2577
|
+
<DataModelCommandPalette
|
|
2578
|
+
commands={paletteCommands}
|
|
2579
|
+
onClose={() => setCommandPaletteOpen(false)}
|
|
2580
|
+
/>
|
|
2581
|
+
)}
|
|
2582
|
+
|
|
2237
2583
|
{loading && <div className="dm-loading">Loading workspace…</div>}
|
|
2238
2584
|
|
|
2239
2585
|
{error && (
|
|
@@ -2248,13 +2594,8 @@ export default function DataModelShell() {
|
|
|
2248
2594
|
{!loading && !error && tables.length > 0 && (
|
|
2249
2595
|
selectedTable && (
|
|
2250
2596
|
<section className="dm-detail-v2 dm-detail-v3">
|
|
2251
|
-
<
|
|
2252
|
-
|
|
2253
|
-
<ObjectViewPicker tables={tables} selectedTable={selectedTable} saving={saving} onSelectSource={setSelectedSource} onSave={save} />
|
|
2254
|
-
</div>
|
|
2255
|
-
<SourceValidationBanner table={selectedTable} />
|
|
2256
|
-
</div>
|
|
2257
|
-
<DataModelTableSurface workspaceConfig={workspaceConfig} table={selectedTable} tables={tables} saving={saving} onSave={save} />
|
|
2597
|
+
<SourceValidationBanner table={selectedTable} />
|
|
2598
|
+
<DataModelTableSurface workspaceConfig={workspaceConfig} table={selectedTable} tables={tables} saving={saving} onSave={save} onOpenThread={openHelperThreadFromRow} />
|
|
2258
2599
|
</section>
|
|
2259
2600
|
)
|
|
2260
2601
|
)}
|
|
@@ -2263,10 +2604,22 @@ export default function DataModelShell() {
|
|
|
2263
2604
|
<div className="dm-page-empty">
|
|
2264
2605
|
<Database size={32} />
|
|
2265
2606
|
<strong>No objects yet</strong>
|
|
2266
|
-
<p>Create
|
|
2267
|
-
<
|
|
2268
|
-
<
|
|
2269
|
-
|
|
2607
|
+
<p>Create your first Data Source, API Registry, People list, or custom object to get started.</p>
|
|
2608
|
+
<div className="dm-page-empty-actions">
|
|
2609
|
+
<button type="button" className="dm-btn-primary" onClick={() => setAddOpen(true)}>
|
|
2610
|
+
<Plus size={14} />New object
|
|
2611
|
+
</button>
|
|
2612
|
+
<button
|
|
2613
|
+
type="button"
|
|
2614
|
+
className="dm-btn-outline"
|
|
2615
|
+
onClick={() => openHelperWith(
|
|
2616
|
+
"create_object",
|
|
2617
|
+
"I run a local agency. Create my first business object: a client list with name, owner, status, deal value, and next step. Then suggest a starter dashboard."
|
|
2618
|
+
)}
|
|
2619
|
+
>
|
|
2620
|
+
<Zap size={14} />Try the helper
|
|
2621
|
+
</button>
|
|
2622
|
+
</div>
|
|
2270
2623
|
</div>
|
|
2271
2624
|
)}
|
|
2272
2625
|
</section>
|