@burdenoff/microfe-bigconsole 2026.929.2 → 2026.930.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/dist/bigconsole/assistant/assistantRunPersistence.js +111 -0
- package/dist/bigconsole/assistant/assistantRunPersistence.js.map +1 -0
- package/dist/bigconsole/assistant/assistantRunStore.js +87 -48
- package/dist/bigconsole/assistant/assistantRunStore.js.map +1 -1
- package/dist/bigconsole/assistant/previewController.js +19 -50
- package/dist/bigconsole/assistant/previewController.js.map +1 -1
- package/dist/bigconsole/assistant/useAssistantRunWorkspace.js +14 -0
- package/dist/bigconsole/assistant/useAssistantRunWorkspace.js.map +1 -0
- package/dist/bigconsole/components/preview/AiPreviewMiniPanel.js +32 -30
- package/dist/bigconsole/components/preview/AiPreviewMiniPanel.js.map +1 -1
- package/dist/bigconsole/components/preview/AiPreviewPanel.js +38 -36
- package/dist/bigconsole/components/preview/AiPreviewPanel.js.map +1 -1
- package/dist/bigconsole/components/preview/previewShared.js +1 -1
- package/dist/bigconsole/components/preview/previewShared.js.map +1 -1
- package/package.json +3 -3
- package/dist/bigconsole/assistant/useAssistantPanelShowsPreview.js +0 -25
- package/dist/bigconsole/assistant/useAssistantPanelShowsPreview.js.map +0 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
//#region src/bigconsole/assistant/assistantRunPersistence.ts
|
|
2
|
+
var e = "bigconsole.assistant.previewRun.v1:", t = [
|
|
3
|
+
"datasink",
|
|
4
|
+
"dashboard",
|
|
5
|
+
"parser",
|
|
6
|
+
"widget"
|
|
7
|
+
], n = [
|
|
8
|
+
"datasink",
|
|
9
|
+
"dashboard",
|
|
10
|
+
"parser"
|
|
11
|
+
], r = /^[A-Za-z0-9_-]{1,128}$/;
|
|
12
|
+
function i(t) {
|
|
13
|
+
return `${e}${t}`;
|
|
14
|
+
}
|
|
15
|
+
function a() {
|
|
16
|
+
try {
|
|
17
|
+
return globalThis.sessionStorage === void 0 ? null : globalThis.sessionStorage;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function o(e) {
|
|
23
|
+
return typeof e == "object" && !!e && !Array.isArray(e);
|
|
24
|
+
}
|
|
25
|
+
function s(e) {
|
|
26
|
+
return e === "pending" || e === "running" || e === "done" || e === "skipped";
|
|
27
|
+
}
|
|
28
|
+
function c(e) {
|
|
29
|
+
let t;
|
|
30
|
+
try {
|
|
31
|
+
t = JSON.parse(e);
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (!o(t) || t.v !== 1 || t.hasRun !== !0) return null;
|
|
36
|
+
let { previewOpen: i, actorId: a, updatedAt: c, steps: l, entityIds: u } = t;
|
|
37
|
+
if (typeof i != "boolean" || a !== null && typeof a != "string" || c !== null && (typeof c != "number" || !Number.isFinite(c)) || !o(l)) return null;
|
|
38
|
+
let { datasink: d, dashboard: f, parser: p, widget: m } = l;
|
|
39
|
+
if (!s(d) || !s(f) || !s(p) || !s(m) || !o(u)) return null;
|
|
40
|
+
let h = {};
|
|
41
|
+
for (let e of n) {
|
|
42
|
+
let t = u[e];
|
|
43
|
+
if (t !== void 0) {
|
|
44
|
+
if (typeof t != "string" || !r.test(t)) return null;
|
|
45
|
+
h[e] = t;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
v: 1,
|
|
50
|
+
actorId: a,
|
|
51
|
+
hasRun: !0,
|
|
52
|
+
previewOpen: i,
|
|
53
|
+
steps: {
|
|
54
|
+
datasink: d,
|
|
55
|
+
dashboard: f,
|
|
56
|
+
parser: p,
|
|
57
|
+
widget: m
|
|
58
|
+
},
|
|
59
|
+
entityIds: h,
|
|
60
|
+
updatedAt: c
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function l(e, n) {
|
|
64
|
+
let r = a();
|
|
65
|
+
if (!r) return null;
|
|
66
|
+
let o;
|
|
67
|
+
try {
|
|
68
|
+
o = r.getItem(i(e));
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (o === null) return null;
|
|
73
|
+
let s = c(o);
|
|
74
|
+
if (!s) return d(e), null;
|
|
75
|
+
if (s.actorId !== n) return null;
|
|
76
|
+
let l = { ...s.steps };
|
|
77
|
+
for (let e of t) l[e] === "running" && (l[e] = "pending");
|
|
78
|
+
return {
|
|
79
|
+
hasRun: !0,
|
|
80
|
+
previewOpen: s.previewOpen,
|
|
81
|
+
steps: l,
|
|
82
|
+
entityIds: s.entityIds,
|
|
83
|
+
updatedAt: s.updatedAt
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function u(e, t, n) {
|
|
87
|
+
let r = a();
|
|
88
|
+
if (!r) return;
|
|
89
|
+
let o = {
|
|
90
|
+
v: 1,
|
|
91
|
+
actorId: t,
|
|
92
|
+
hasRun: !0,
|
|
93
|
+
previewOpen: n.previewOpen,
|
|
94
|
+
steps: n.steps,
|
|
95
|
+
entityIds: n.entityIds,
|
|
96
|
+
updatedAt: n.updatedAt
|
|
97
|
+
};
|
|
98
|
+
try {
|
|
99
|
+
r.setItem(i(e), JSON.stringify(o));
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
102
|
+
function d(e) {
|
|
103
|
+
let t = a();
|
|
104
|
+
if (t) try {
|
|
105
|
+
t.removeItem(i(e));
|
|
106
|
+
} catch {}
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
export { l as readPersistedRun, d as removePersistedRun, u as writePersistedRun };
|
|
110
|
+
|
|
111
|
+
//# sourceMappingURL=assistantRunPersistence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistantRunPersistence.js","names":[],"sources":["../../../src/bigconsole/assistant/assistantRunPersistence.ts"],"sourcesContent":["/**\n * The build preview's run state, kept for the life of the TAB (BOFF-7799, A3).\n *\n * ★★★ WHY. BigConsole restores the conversation from the server\n * (`SERVER_HISTORY`), but the preview's run store lived only in memory. A\n * reload kept the chat and lost the page preview under the breadcrumb, so the\n * build a user had just watched was gone while the conversation that made it\n * was still on screen. This keeps the part the page preview draws, and nothing\n * else, in `sessionStorage`.\n *\n * ★★ WHAT IS KEPT, AND WHAT IS NOT. Kept: `hasRun`, whether the preview is\n * open, the four step statuses, the entity ids and the last-update time. None\n * of that is sensitive: the ids name workspace resources the server still\n * authorises on every fetch. NOT kept: the prompt (the user's own words), the\n * caption (the agent's narration) and the error text. `isRunning` is not kept\n * either: nothing can follow a turn across a reload, so a restored `running`\n * step drops back to `pending`, exactly as `finishRun` does.\n *\n * ★★ ONE ENTRY PER WORKSPACE, and it names its actor. The key carries the\n * workspace, so a reload in another workspace never reads this one. The record\n * carries the actor it was written for, because fe-libs found that the\n * workspace alone is not an identity: two accounts can share a workspace and a\n * tab (profile switching), and the second must not be shown the first one's\n * build (see fe-libs `AssistantPreviewRun.actorId`).\n *\n * ★ EVERY STORAGE ACCESS IS CONTAINED. `sessionStorage` can be missing (tests,\n * SSR), can throw on access (blocked site data, sandboxed frames) and can throw\n * on write (quota). Any of those means \"nothing kept\", never an exception in\n * the page surface or in the store action that triggered the write.\n */\nimport type { PreviewStepKey, PreviewStepStatus } from './assistantRunStore';\n\n/** `sessionStorage` key prefix; the workspace id follows it. */\nexport const RUN_STORAGE_PREFIX = 'bigconsole.assistant.previewRun.v1:';\n\n/** The slice of the run store that survives a reload. */\nexport interface PersistedRun {\n hasRun: true;\n previewOpen: boolean;\n steps: Record<PreviewStepKey, PreviewStepStatus>;\n entityIds: { datasink?: string; dashboard?: string; parser?: string };\n updatedAt: number | null;\n}\n\n/** What is actually written: the slice, its format version and its owner. */\ninterface StoredRun extends PersistedRun {\n v: 1;\n actorId: string | null;\n}\n\nconst STEP_KEYS: readonly PreviewStepKey[] = ['datasink', 'dashboard', 'parser', 'widget'];\nconst ENTITY_KEYS = ['datasink', 'dashboard', 'parser'] as const;\n\n/**\n * ★★ The id charset the store itself harvests (`ENTITY_LINK`, `JSON_ID_STEPS`).\n * Restored ids are interpolated into in-app paths (`/bigconsole/datasinks/<id>`),\n * so a value edited in devtools must not be able to smuggle `../` or a query\n * string into a navigation. Anything else is treated as a corrupt record.\n */\nconst ENTITY_ID = /^[A-Za-z0-9_-]{1,128}$/;\n\nexport function runStorageKey(workspaceId: string): string {\n return `${RUN_STORAGE_PREFIX}${workspaceId}`;\n}\n\n/** The tab's `sessionStorage`, or null wherever it cannot be used. */\nfunction sessionStore(): Storage | null {\n try {\n // ★ Reading the property is itself the access that throws when site data\n // is blocked, so the `typeof` guard sits inside the try, not before it.\n return typeof globalThis.sessionStorage === 'undefined' ? null : globalThis.sessionStorage;\n } catch {\n return null;\n }\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isStepStatus(value: unknown): value is PreviewStepStatus {\n return value === 'pending' || value === 'running' || value === 'done' || value === 'skipped';\n}\n\n/** Parses and validates one stored record. Any deviation reads as absent. */\nfunction parseStoredRun(raw: string): StoredRun | null {\n let value: unknown;\n try {\n value = JSON.parse(raw);\n } catch {\n return null;\n }\n if (!isRecord(value) || value.v !== 1 || value.hasRun !== true) return null;\n\n const { previewOpen, actorId, updatedAt, steps, entityIds } = value;\n if (typeof previewOpen !== 'boolean') return null;\n if (actorId !== null && typeof actorId !== 'string') return null;\n if (updatedAt !== null && (typeof updatedAt !== 'number' || !Number.isFinite(updatedAt))) return null;\n\n if (!isRecord(steps)) return null;\n const { datasink, dashboard, parser, widget } = steps;\n if (!isStepStatus(datasink) || !isStepStatus(dashboard) || !isStepStatus(parser) || !isStepStatus(widget)) {\n return null;\n }\n\n if (!isRecord(entityIds)) return null;\n const ids: PersistedRun['entityIds'] = {};\n for (const key of ENTITY_KEYS) {\n const id = entityIds[key];\n if (id === undefined) continue;\n if (typeof id !== 'string' || !ENTITY_ID.test(id)) return null;\n ids[key] = id;\n }\n\n return {\n v: 1,\n actorId,\n hasRun: true,\n previewOpen,\n steps: { datasink, dashboard, parser, widget },\n entityIds: ids,\n updatedAt,\n };\n}\n\n/**\n * The run kept for this workspace and actor, or null.\n *\n * ★ A record written for another actor is left in place, not deleted: it is\n * that actor's, and they may switch back. A corrupt record is removed.\n */\nexport function readPersistedRun(workspaceId: string, actorId: string | null): PersistedRun | null {\n const storage = sessionStore();\n if (!storage) return null;\n let raw: string | null;\n try {\n raw = storage.getItem(runStorageKey(workspaceId));\n } catch {\n return null;\n }\n if (raw === null) return null;\n\n const stored = parseStoredRun(raw);\n if (!stored) {\n removePersistedRun(workspaceId);\n return null;\n }\n if (stored.actorId !== actorId) return null;\n\n // Nothing follows a turn across a reload: a step that was in flight did not\n // land, so it drops back to `pending` for a later prompt (as `finishRun` does).\n const steps = { ...stored.steps };\n for (const key of STEP_KEYS) {\n if (steps[key] === 'running') steps[key] = 'pending';\n }\n return {\n hasRun: true,\n previewOpen: stored.previewOpen,\n steps,\n entityIds: stored.entityIds,\n updatedAt: stored.updatedAt,\n };\n}\n\n/** Keeps `run` for this workspace, owned by `actorId`. Best effort. */\nexport function writePersistedRun(workspaceId: string, actorId: string | null, run: PersistedRun): void {\n const storage = sessionStore();\n if (!storage) return;\n const stored: StoredRun = {\n v: 1,\n actorId,\n hasRun: true,\n previewOpen: run.previewOpen,\n steps: run.steps,\n entityIds: run.entityIds,\n updatedAt: run.updatedAt,\n };\n try {\n storage.setItem(runStorageKey(workspaceId), JSON.stringify(stored));\n } catch {\n // Quota or blocked storage: the preview simply does not survive a reload.\n }\n}\n\n/** Forgets the run kept for this workspace. Best effort. */\nexport function removePersistedRun(workspaceId: string): void {\n const storage = sessionStore();\n if (!storage) return;\n try {\n storage.removeItem(runStorageKey(workspaceId));\n } catch {\n // Blocked storage: there is nothing we could have read back either.\n }\n}\n"],"mappings":";AAiCA,IAAa,IAAqB,uCAiB5B,IAAuC;CAAC;CAAY;CAAa;CAAU;CAAS,EACpF,IAAc;CAAC;CAAY;CAAa;CAAS,EAQjD,IAAY;AAElB,SAAgB,EAAc,GAA6B;AACzD,QAAO,GAAG,IAAqB;;AAIjC,SAAS,IAA+B;AACtC,KAAI;AAGF,SAAc,WAAW,mBAAmB,SAAc,OAAO,WAAW;SACtE;AACN,SAAO;;;AAIX,SAAS,EAAS,GAAkD;AAClE,QAAO,OAAO,KAAU,cAAY,KAAkB,CAAC,MAAM,QAAQ,EAAM;;AAG7E,SAAS,EAAa,GAA4C;AAChE,QAAO,MAAU,aAAa,MAAU,aAAa,MAAU,UAAU,MAAU;;AAIrF,SAAS,EAAe,GAA+B;CACrD,IAAI;AACJ,KAAI;AACF,MAAQ,KAAK,MAAM,EAAI;SACjB;AACN,SAAO;;AAET,KAAI,CAAC,EAAS,EAAM,IAAI,EAAM,MAAM,KAAK,EAAM,WAAW,GAAM,QAAO;CAEvE,IAAM,EAAE,gBAAa,YAAS,cAAW,UAAO,iBAAc;AAK9D,KAJI,OAAO,KAAgB,aACvB,MAAY,QAAQ,OAAO,KAAY,YACvC,MAAc,SAAS,OAAO,KAAc,YAAY,CAAC,OAAO,SAAS,EAAU,KAEnF,CAAC,EAAS,EAAM,CAAE,QAAO;CAC7B,IAAM,EAAE,aAAU,cAAW,WAAQ,cAAW;AAKhD,KAJI,CAAC,EAAa,EAAS,IAAI,CAAC,EAAa,EAAU,IAAI,CAAC,EAAa,EAAO,IAAI,CAAC,EAAa,EAAO,IAIrG,CAAC,EAAS,EAAU,CAAE,QAAO;CACjC,IAAM,IAAiC,EAAE;AACzC,MAAK,IAAM,KAAO,GAAa;EAC7B,IAAM,IAAK,EAAU;AACjB,YAAO,KAAA,GACX;OAAI,OAAO,KAAO,YAAY,CAAC,EAAU,KAAK,EAAG,CAAE,QAAO;AAC1D,KAAI,KAAO;;;AAGb,QAAO;EACL,GAAG;EACH;EACA,QAAQ;EACR;EACA,OAAO;GAAE;GAAU;GAAW;GAAQ;GAAQ;EAC9C,WAAW;EACX;EACD;;AASH,SAAgB,EAAiB,GAAqB,GAA6C;CACjG,IAAM,IAAU,GAAc;AAC9B,KAAI,CAAC,EAAS,QAAO;CACrB,IAAI;AACJ,KAAI;AACF,MAAM,EAAQ,QAAQ,EAAc,EAAY,CAAC;SAC3C;AACN,SAAO;;AAET,KAAI,MAAQ,KAAM,QAAO;CAEzB,IAAM,IAAS,EAAe,EAAI;AAClC,KAAI,CAAC,EAEH,QADA,EAAmB,EAAY,EACxB;AAET,KAAI,EAAO,YAAY,EAAS,QAAO;CAIvC,IAAM,IAAQ,EAAE,GAAG,EAAO,OAAO;AACjC,MAAK,IAAM,KAAO,EAChB,CAAI,EAAM,OAAS,cAAW,EAAM,KAAO;AAE7C,QAAO;EACL,QAAQ;EACR,aAAa,EAAO;EACpB;EACA,WAAW,EAAO;EAClB,WAAW,EAAO;EACnB;;AAIH,SAAgB,EAAkB,GAAqB,GAAwB,GAAyB;CACtG,IAAM,IAAU,GAAc;AAC9B,KAAI,CAAC,EAAS;CACd,IAAM,IAAoB;EACxB,GAAG;EACH;EACA,QAAQ;EACR,aAAa,EAAI;EACjB,OAAO,EAAI;EACX,WAAW,EAAI;EACf,WAAW,EAAI;EAChB;AACD,KAAI;AACF,IAAQ,QAAQ,EAAc,EAAY,EAAE,KAAK,UAAU,EAAO,CAAC;SAC7D;;AAMV,SAAgB,EAAmB,GAA2B;CAC5D,IAAM,IAAU,GAAc;AACzB,OACL,KAAI;AACF,IAAQ,WAAW,EAAc,EAAY,CAAC;SACxC"}
|
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readPersistedRun as e, removePersistedRun as t, writePersistedRun as n } from "./assistantRunPersistence.js";
|
|
2
|
+
import { create as r } from "zustand";
|
|
3
|
+
import { getActorId as i } from "@burdenoff/fe-libs/shared/assistant/context";
|
|
2
4
|
//#region src/bigconsole/assistant/assistantRunStore.ts
|
|
3
|
-
var
|
|
5
|
+
var a = [
|
|
4
6
|
"datasink",
|
|
5
7
|
"dashboard",
|
|
6
8
|
"parser",
|
|
7
9
|
"widget"
|
|
8
|
-
],
|
|
10
|
+
], o = {
|
|
9
11
|
datasink: "pending",
|
|
10
12
|
dashboard: "pending",
|
|
11
13
|
parser: "pending",
|
|
12
14
|
widget: "pending"
|
|
13
|
-
},
|
|
15
|
+
}, s = /\/bigconsole\/(datasinks|dashboards|parsers)\/([A-Za-z0-9_-]+)/g, c = /widget\s+(added|created)/i, l = /^⏳\s*(.+?)…?$/, u = {
|
|
14
16
|
datasinks: "datasink",
|
|
15
17
|
dashboards: "dashboard",
|
|
16
18
|
parsers: "parser"
|
|
17
|
-
},
|
|
19
|
+
}, d = [
|
|
18
20
|
{
|
|
19
21
|
step: "datasink",
|
|
20
22
|
re: /"createDataSink"\s*:\s*\{[^{}]*?"id"\s*:\s*"([A-Za-z0-9_-]+)"/
|
|
@@ -27,8 +29,8 @@ var t = [
|
|
|
27
29
|
step: "parser",
|
|
28
30
|
re: /"createParser"\s*:\s*\{[^{}]*?"id"\s*:\s*"([A-Za-z0-9_-]+)"/
|
|
29
31
|
}
|
|
30
|
-
],
|
|
31
|
-
function
|
|
32
|
+
], f = /"createBigConsoleWidget"\s*:\s*\{/;
|
|
33
|
+
function p(e, t) {
|
|
32
34
|
let n = {
|
|
33
35
|
pending: 0,
|
|
34
36
|
running: 1,
|
|
@@ -37,69 +39,106 @@ function l(e, t) {
|
|
|
37
39
|
};
|
|
38
40
|
return n[t] > n[e] ? t : e;
|
|
39
41
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
function m() {
|
|
43
|
+
return {
|
|
44
|
+
isRunning: !1,
|
|
45
|
+
previewOpen: !1,
|
|
46
|
+
hasRun: !1,
|
|
47
|
+
prompt: "",
|
|
48
|
+
steps: { ...o },
|
|
49
|
+
entityIds: {},
|
|
50
|
+
caption: "",
|
|
51
|
+
error: null,
|
|
52
|
+
updatedAt: null
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function h(e, t) {
|
|
56
|
+
return e === null || t === null || e === t;
|
|
57
|
+
}
|
|
58
|
+
var g = r()((n, r) => ({
|
|
59
|
+
...m(),
|
|
44
60
|
richMounted: !1,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
let r = { ...e.steps };
|
|
53
|
-
for (let e of t) r[e] === "running" && (r[e] = "pending");
|
|
54
|
-
let i = t.find((e) => r[e] === "pending");
|
|
55
|
-
return i && (r[i] = "running"), {
|
|
61
|
+
workspaceId: null,
|
|
62
|
+
actorId: null,
|
|
63
|
+
startRun: (e) => n((t) => {
|
|
64
|
+
let n = { ...t.steps };
|
|
65
|
+
for (let e of a) n[e] === "running" && (n[e] = "pending");
|
|
66
|
+
let r = a.find((e) => n[e] === "pending");
|
|
67
|
+
return r && (n[r] = "running"), {
|
|
56
68
|
isRunning: !0,
|
|
57
69
|
previewOpen: !0,
|
|
58
70
|
hasRun: !0,
|
|
59
|
-
prompt:
|
|
60
|
-
steps:
|
|
71
|
+
prompt: e,
|
|
72
|
+
steps: n,
|
|
61
73
|
caption: "",
|
|
62
74
|
error: null,
|
|
63
75
|
updatedAt: Date.now()
|
|
64
76
|
};
|
|
65
77
|
}),
|
|
66
|
-
applyProgress: (
|
|
67
|
-
let
|
|
68
|
-
|
|
78
|
+
applyProgress: (e) => n((t) => {
|
|
79
|
+
let n = { ...t.steps }, r = { ...t.entityIds }, i = (e, t) => {
|
|
80
|
+
n[e] = p(n[e], "done"), t && e !== "widget" && (r[e] = t);
|
|
69
81
|
};
|
|
70
|
-
for (let
|
|
71
|
-
let
|
|
72
|
-
|
|
82
|
+
for (let t of e.matchAll(s)) {
|
|
83
|
+
let e = u[t[1] ?? ""];
|
|
84
|
+
e && t[2] && i(e, t[2]);
|
|
73
85
|
}
|
|
74
|
-
for (let { step:
|
|
75
|
-
let r =
|
|
76
|
-
r?.[1] &&
|
|
86
|
+
for (let { step: t, re: n } of d) {
|
|
87
|
+
let r = n.exec(e);
|
|
88
|
+
r?.[1] && i(t, r[1]);
|
|
77
89
|
}
|
|
78
|
-
(
|
|
79
|
-
let
|
|
80
|
-
return
|
|
81
|
-
steps:
|
|
82
|
-
entityIds:
|
|
83
|
-
caption:
|
|
90
|
+
(f.test(e) || c.test(e)) && (n.widget = p(n.widget, "done"), n.parser !== "done" && (n.parser = p(n.parser, "skipped")));
|
|
91
|
+
let o = a.find((e) => n[e] === "pending" || n[e] === "running");
|
|
92
|
+
return o && (n[o] = p(n[o], "running")), {
|
|
93
|
+
steps: n,
|
|
94
|
+
entityIds: r,
|
|
95
|
+
caption: e.split("\n").map((e) => e.trim()).reverse().map((e) => l.exec(e)?.[1]).find((e) => !!e) ?? t.caption,
|
|
84
96
|
updatedAt: Date.now()
|
|
85
97
|
};
|
|
86
98
|
}),
|
|
87
|
-
finishRun: (
|
|
88
|
-
let
|
|
89
|
-
for (let e of
|
|
99
|
+
finishRun: (e) => n((t) => {
|
|
100
|
+
let n = { ...t.steps };
|
|
101
|
+
for (let e of a) n[e] === "running" && (n[e] = "pending");
|
|
90
102
|
return {
|
|
91
103
|
isRunning: !1,
|
|
92
|
-
error:
|
|
93
|
-
steps:
|
|
104
|
+
error: e,
|
|
105
|
+
steps: n,
|
|
94
106
|
caption: "",
|
|
95
107
|
updatedAt: Date.now()
|
|
96
108
|
};
|
|
97
109
|
}),
|
|
98
|
-
openPreview: () =>
|
|
99
|
-
closePreview: () =>
|
|
100
|
-
setRichMounted: (
|
|
110
|
+
openPreview: () => n({ previewOpen: !0 }),
|
|
111
|
+
closePreview: () => n({ previewOpen: !1 }),
|
|
112
|
+
setRichMounted: (e) => n({ richMounted: e }),
|
|
113
|
+
bindWorkspace: (a) => {
|
|
114
|
+
let o = r().workspaceId;
|
|
115
|
+
if (o === a) return;
|
|
116
|
+
o !== null && t(o);
|
|
117
|
+
let s = a === null ? null : i(), c = a === null ? null : e(a, s);
|
|
118
|
+
n({
|
|
119
|
+
...m(),
|
|
120
|
+
...c,
|
|
121
|
+
workspaceId: a,
|
|
122
|
+
actorId: s
|
|
123
|
+
});
|
|
124
|
+
}
|
|
101
125
|
}));
|
|
126
|
+
g.subscribe((e, r) => {
|
|
127
|
+
if (e.workspaceId === null || e.workspaceId === r.workspaceId && e.hasRun === r.hasRun && e.previewOpen === r.previewOpen && e.steps === r.steps && e.entityIds === r.entityIds && e.updatedAt === r.updatedAt) return;
|
|
128
|
+
if (!e.hasRun) {
|
|
129
|
+
t(e.workspaceId);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
let i = {
|
|
133
|
+
hasRun: !0,
|
|
134
|
+
previewOpen: e.previewOpen,
|
|
135
|
+
steps: e.steps,
|
|
136
|
+
entityIds: e.entityIds,
|
|
137
|
+
updatedAt: e.updatedAt
|
|
138
|
+
};
|
|
139
|
+
n(e.workspaceId, e.actorId, i);
|
|
140
|
+
});
|
|
102
141
|
//#endregion
|
|
103
|
-
export {
|
|
142
|
+
export { a as PREVIEW_STEPS, h as runBelongsToWorkspace, g as useAssistantRunStore };
|
|
104
143
|
|
|
105
144
|
//# sourceMappingURL=assistantRunStore.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistantRunStore.js","names":[],"sources":["../../../src/bigconsole/assistant/assistantRunStore.ts"],"sourcesContent":["/**\n * What the assistant is building, right now.\n *\n * The agent's narration is the only live signal the frontend gets — there is no\n * event stream — so this store turns it into something a UI can render: a\n * monotone DataSink → Dashboard → Parser → Widget rail, plus the id of each\n * entity as it appears.\n *\n * Three rules make the parsing survive a model that phrases things differently:\n *\n * 1. Read the id from BOTH the link path (`/bigconsole/datasinks/<id>`) AND the\n * raw mutation JSON (`{ \"createDataSink\": { \"id\": \"<uuid>\", … } }`). The agent\n * freely prints one, the other, or both — if we only watched for the link a\n * turn that printed just JSON would leave the tab stuck on \"Waiting…\".\n * 2. Never regress. `progress.content` is REPLACED on every poll (not appended),\n * so a shorter string arriving later must not rewind the rail.\n * 3. Accumulate across an incremental conversation. Each prompt builds one piece;\n * a new prompt must NOT wipe the pieces already built, or the earlier tabs go\n * blank. `startRun` preserves prior steps/ids and only lights up the next one.\n */\nimport { create } from 'zustand';\n\nexport type PreviewStepKey = 'datasink' | 'dashboard' | 'parser' | 'widget';\nexport type PreviewStepStatus = 'pending' | 'running' | 'done' | 'skipped';\n\nexport const PREVIEW_STEPS: readonly PreviewStepKey[] = ['datasink', 'dashboard', 'parser', 'widget'];\n\ninterface AssistantRunState {\n /** A turn is in flight. */\n isRunning: boolean;\n /** The panel is on screen. Auto-opens when a turn starts; the user can close it. */\n previewOpen: boolean;\n /** True once any turn has run — gates the \"Preview\" button. */\n hasRun: boolean;\n /**\n * The full, context-rich panel (mounted inside BigConsoleRoot, with live entity\n * tabs) is on screen right now. The lightweight app-shell panel reads this and\n * steps aside so the two never render at once. It flips false the instant you\n * leave a /bigconsole page — that is what lets the mini panel take over and keep\n * the build visible on whatever screen you moved to.\n */\n richMounted: boolean;\n prompt: string;\n steps: Record<PreviewStepKey, PreviewStepStatus>;\n /** Ids harvested from the agent's links. A widget has no link of its own. */\n entityIds: { datasink?: string; dashboard?: string; parser?: string };\n /** Newest \"⏳ …\" line — what the agent says it is doing this second. */\n caption: string;\n /** Set when the turn fails, so the panel can stop pretending to be live. */\n error: string | null;\n /** Epoch ms of the last progress tick — drives the \"updated 3s ago\" strip. */\n updatedAt: number | null;\n\n startRun: (prompt: string) => void;\n applyProgress: (content: string) => void;\n finishRun: (error: string | null) => void;\n openPreview: () => void;\n closePreview: () => void;\n setRichMounted: (mounted: boolean) => void;\n}\n\nconst IDLE_STEPS: Record<PreviewStepKey, PreviewStepStatus> = {\n datasink: 'pending',\n dashboard: 'pending',\n parser: 'pending',\n widget: 'pending',\n};\n\n/** `/bigconsole/datasinks/<id>` — the trustworthy part of the narration. */\nconst ENTITY_LINK = /\\/bigconsole\\/(datasinks|dashboards|parsers)\\/([A-Za-z0-9_-]+)/g;\n/** The widget links to its dashboard, so it can only be detected from the prose. */\nconst WIDGET_DONE = /widget\\s+(added|created)/i;\nconst RUNNING_LINE = /^⏳\\s*(.+?)…?$/;\n\nconst LINK_TO_STEP: Record<string, PreviewStepKey> = {\n datasinks: 'datasink',\n dashboards: 'dashboard',\n parsers: 'parser',\n};\n\n/**\n * The id straight out of a raw mutation response the agent printed instead of\n * (or as well as) a link, e.g. `{ \"createDataSink\": { \"id\": \"<uuid>\", … } }`.\n * `[^{}]` keeps each match inside its own object so we grab that mutation's id,\n * not a neighbour's. This is what stops \"Waiting…\" when no link is narrated.\n */\nconst JSON_ID_STEPS: ReadonlyArray<{ step: Exclude<PreviewStepKey, 'widget'>; re: RegExp }> = [\n { step: 'datasink', re: /\"createDataSink\"\\s*:\\s*\\{[^{}]*?\"id\"\\s*:\\s*\"([A-Za-z0-9_-]+)\"/ },\n { step: 'dashboard', re: /\"createBigConsoleDashboard\"\\s*:\\s*\\{[^{}]*?\"id\"\\s*:\\s*\"([A-Za-z0-9_-]+)\"/ },\n { step: 'parser', re: /\"createParser\"\\s*:\\s*\\{[^{}]*?\"id\"\\s*:\\s*\"([A-Za-z0-9_-]+)\"/ },\n];\n/** The widget has no page of its own; its create response is enough to mark it done. */\nconst WIDGET_CREATE_JSON = /\"createBigConsoleWidget\"\\s*:\\s*\\{/;\n\n/** Only ever moves a step forward. */\nfunction advance(current: PreviewStepStatus, next: PreviewStepStatus): PreviewStepStatus {\n const rank: Record<PreviewStepStatus, number> = { pending: 0, running: 1, skipped: 2, done: 3 };\n return rank[next] > rank[current] ? next : current;\n}\n\nexport const useAssistantRunStore = create<AssistantRunState>()((set) => ({\n isRunning: false,\n previewOpen: false,\n hasRun: false,\n richMounted: false,\n prompt: '',\n steps: { ...IDLE_STEPS },\n entityIds: {},\n caption: '',\n error: null,\n updatedAt: null,\n\n startRun: (prompt) =>\n set((state) => {\n // A new prompt CONTINUES the same use case (incremental flow) — keep every\n // step and id already built. Clear any stale spinner from the prior turn,\n // then light up the earliest step that has not landed yet so the \"working\n // on it\" indicator sits on the right tab.\n const steps = { ...state.steps };\n for (const key of PREVIEW_STEPS) {\n if (steps[key] === 'running') steps[key] = 'pending';\n }\n const next = PREVIEW_STEPS.find((key) => steps[key] === 'pending');\n if (next) steps[next] = 'running';\n return {\n isRunning: true,\n // Auto-open: the whole point is that you SEE the build happen without\n // having to go looking for it.\n previewOpen: true,\n hasRun: true,\n prompt,\n steps,\n // entityIds intentionally preserved (merged) so earlier tabs keep content.\n caption: '',\n error: null,\n updatedAt: Date.now(),\n };\n }),\n\n applyProgress: (content) =>\n set((state) => {\n const steps = { ...state.steps };\n const entityIds = { ...state.entityIds };\n\n const markDone = (step: PreviewStepKey, id?: string) => {\n steps[step] = advance(steps[step], 'done');\n // Always record the freshest id seen this turn (never \"first wins\"): an\n // incremental prompt that rebuilds a piece must point the tab at the new\n // one. `step !== 'widget'` narrows to the keys entityIds actually holds.\n if (id && step !== 'widget') entityIds[step] = id;\n };\n\n // 1) Markdown link paths.\n for (const match of content.matchAll(ENTITY_LINK)) {\n const step = LINK_TO_STEP[match[1] ?? ''];\n if (step && match[2]) markDone(step, match[2]);\n }\n // 2) Raw mutation JSON (the agent often prints this instead of a link).\n for (const { step, re } of JSON_ID_STEPS) {\n const match = re.exec(content);\n if (match?.[1]) markDone(step, match[1]);\n }\n\n if (WIDGET_CREATE_JSON.test(content) || WIDGET_DONE.test(content)) {\n steps.widget = advance(steps.widget, 'done');\n // The parser is optional — the agent skips it when the rows are already\n // shaped. If the widget landed and the parser was never built, it was\n // skipped, not stuck.\n if (steps.parser !== 'done') steps.parser = advance(steps.parser, 'skipped');\n }\n\n // The first step that has not finished is the one being worked on.\n const active = PREVIEW_STEPS.find((key) => steps[key] === 'pending' || steps[key] === 'running');\n if (active) steps[active] = advance(steps[active], 'running');\n\n const caption =\n content\n .split('\\n')\n .map((line) => line.trim())\n .reverse()\n .map((line) => RUNNING_LINE.exec(line)?.[1])\n .find((label): label is string => Boolean(label)) ?? state.caption;\n\n return { steps, entityIds, caption, updatedAt: Date.now() };\n }),\n\n finishRun: (error) =>\n set((state) => {\n // The turn is over — stop the live spinner. A step still 'running' didn't\n // land this turn, so drop it back to 'pending' (a later prompt can pick it\n // up). NEVER blanket-skip pending steps: in the incremental flow the later\n // steps simply have not been asked for yet, and 'done' steps must stay done.\n const steps = { ...state.steps };\n for (const key of PREVIEW_STEPS) {\n if (steps[key] === 'running') steps[key] = 'pending';\n }\n return { isRunning: false, error, steps, caption: '', updatedAt: Date.now() };\n }),\n\n openPreview: () => set({ previewOpen: true }),\n closePreview: () => set({ previewOpen: false }),\n setRichMounted: (mounted) => set({ richMounted: mounted }),\n}));\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"assistantRunStore.js","names":[],"sources":["../../../src/bigconsole/assistant/assistantRunStore.ts"],"sourcesContent":["/**\n * What the assistant is building, right now.\n *\n * The agent's narration is the only live signal the frontend gets — there is no\n * event stream — so this store turns it into something a UI can render: a\n * monotone DataSink → Dashboard → Parser → Widget rail, plus the id of each\n * entity as it appears.\n *\n * Three rules make the parsing survive a model that phrases things differently:\n *\n * 1. Read the id from BOTH the link path (`/bigconsole/datasinks/<id>`) AND the\n * raw mutation JSON (`{ \"createDataSink\": { \"id\": \"<uuid>\", … } }`). The agent\n * freely prints one, the other, or both — if we only watched for the link a\n * turn that printed just JSON would leave the tab stuck on \"Waiting…\".\n * 2. Never regress. `progress.content` is REPLACED on every poll (not appended),\n * so a shorter string arriving later must not rewind the rail.\n * 3. Accumulate across an incremental conversation. Each prompt builds one piece;\n * a new prompt must NOT wipe the pieces already built, or the earlier tabs go\n * blank. `startRun` preserves prior steps/ids and only lights up the next one.\n *\n * ★★ It belongs to ONE workspace (BOFF-7799). The page surfaces bind the active\n * workspace (`bindWorkspace`); switching clears the run, and the slice the page\n * preview draws is kept in `sessionStorage` per workspace, so a reload in the\n * same tab brings the preview back beside the restored conversation. See\n * `assistantRunPersistence.ts` for what is kept and why.\n */\nimport { getActorId } from '@burdenoff/fe-libs/shared/assistant/context';\nimport { create } from 'zustand';\n\nimport { readPersistedRun, removePersistedRun, writePersistedRun, type PersistedRun } from './assistantRunPersistence';\n\nexport type PreviewStepKey = 'datasink' | 'dashboard' | 'parser' | 'widget';\nexport type PreviewStepStatus = 'pending' | 'running' | 'done' | 'skipped';\n\nexport const PREVIEW_STEPS: readonly PreviewStepKey[] = ['datasink', 'dashboard', 'parser', 'widget'];\n\ninterface AssistantRunState {\n /** A turn is in flight. */\n isRunning: boolean;\n /** The panel is on screen. Auto-opens when a turn starts; the user can close it. */\n previewOpen: boolean;\n /** True once any turn has run — gates the \"Preview\" button. */\n hasRun: boolean;\n /**\n * The full, context-rich panel (mounted inside BigConsoleRoot, with live entity\n * tabs) is on screen right now. The lightweight app-shell panel reads this and\n * steps aside so the two never render at once. It flips false the instant you\n * leave a /bigconsole page — that is what lets the mini panel take over and keep\n * the build visible on whatever screen you moved to.\n */\n richMounted: boolean;\n prompt: string;\n steps: Record<PreviewStepKey, PreviewStepStatus>;\n /** Ids harvested from the agent's links. A widget has no link of its own. */\n entityIds: { datasink?: string; dashboard?: string; parser?: string };\n /** Newest \"⏳ …\" line — what the agent says it is doing this second. */\n caption: string;\n /** Set when the turn fails, so the panel can stop pretending to be live. */\n error: string | null;\n /** Epoch ms of the last progress tick — drives the \"updated 3s ago\" strip. */\n updatedAt: number | null;\n /**\n * The workspace this run belongs to, and the one its kept copy is filed\n * under. Null until a page surface binds the active workspace.\n */\n workspaceId: string | null;\n /** The actor the kept copy is written for, read once when the workspace is bound. */\n actorId: string | null;\n\n startRun: (prompt: string) => void;\n applyProgress: (content: string) => void;\n finishRun: (error: string | null) => void;\n openPreview: () => void;\n closePreview: () => void;\n setRichMounted: (mounted: boolean) => void;\n /**\n * Point the store at the active workspace. A change CLEARS the run and forgets\n * the previous workspace's kept copy, then restores this workspace's copy, if\n * this tab kept one for this actor. Binding the same workspace is a no-op, so\n * every surface may call it.\n */\n bindWorkspace: (workspaceId: string | null) => void;\n}\n\nconst IDLE_STEPS: Record<PreviewStepKey, PreviewStepStatus> = {\n datasink: 'pending',\n dashboard: 'pending',\n parser: 'pending',\n widget: 'pending',\n};\n\n/** `/bigconsole/datasinks/<id>` — the trustworthy part of the narration. */\nconst ENTITY_LINK = /\\/bigconsole\\/(datasinks|dashboards|parsers)\\/([A-Za-z0-9_-]+)/g;\n/** The widget links to its dashboard, so it can only be detected from the prose. */\nconst WIDGET_DONE = /widget\\s+(added|created)/i;\nconst RUNNING_LINE = /^⏳\\s*(.+?)…?$/;\n\nconst LINK_TO_STEP: Record<string, PreviewStepKey> = {\n datasinks: 'datasink',\n dashboards: 'dashboard',\n parsers: 'parser',\n};\n\n/**\n * The id straight out of a raw mutation response the agent printed instead of\n * (or as well as) a link, e.g. `{ \"createDataSink\": { \"id\": \"<uuid>\", … } }`.\n * `[^{}]` keeps each match inside its own object so we grab that mutation's id,\n * not a neighbour's. This is what stops \"Waiting…\" when no link is narrated.\n */\nconst JSON_ID_STEPS: ReadonlyArray<{ step: Exclude<PreviewStepKey, 'widget'>; re: RegExp }> = [\n { step: 'datasink', re: /\"createDataSink\"\\s*:\\s*\\{[^{}]*?\"id\"\\s*:\\s*\"([A-Za-z0-9_-]+)\"/ },\n { step: 'dashboard', re: /\"createBigConsoleDashboard\"\\s*:\\s*\\{[^{}]*?\"id\"\\s*:\\s*\"([A-Za-z0-9_-]+)\"/ },\n { step: 'parser', re: /\"createParser\"\\s*:\\s*\\{[^{}]*?\"id\"\\s*:\\s*\"([A-Za-z0-9_-]+)\"/ },\n];\n/** The widget has no page of its own; its create response is enough to mark it done. */\nconst WIDGET_CREATE_JSON = /\"createBigConsoleWidget\"\\s*:\\s*\\{/;\n\n/** Only ever moves a step forward. */\nfunction advance(current: PreviewStepStatus, next: PreviewStepStatus): PreviewStepStatus {\n const rank: Record<PreviewStepStatus, number> = { pending: 0, running: 1, skipped: 2, done: 3 };\n return rank[next] > rank[current] ? next : current;\n}\n\ntype RunFields = Pick<\n AssistantRunState,\n 'isRunning' | 'previewOpen' | 'hasRun' | 'prompt' | 'steps' | 'entityIds' | 'caption' | 'error' | 'updatedAt'\n>;\n\n/** A run that has not started. `richMounted` is a mount flag, not run state, so it is not here. */\nfunction idleRun(): RunFields {\n return {\n isRunning: false,\n previewOpen: false,\n hasRun: false,\n prompt: '',\n steps: { ...IDLE_STEPS },\n entityIds: {},\n caption: '',\n error: null,\n updatedAt: null,\n };\n}\n\n/**\n * May a run that started in `runWorkspaceId` drive a store bound to\n * `boundWorkspaceId`?\n *\n * ★★ WHY IT EXISTS. A workspace switch clears the store, but a turn started in\n * the previous workspace keeps streaming. Without this its next snapshot would\n * refill the cleared store, and the refill would be kept under the NEW\n * workspace's key, which is the leak the switch exists to prevent.\n *\n * ★ Lenient on null, deliberately: fe-libs reports `null` when it cannot read\n * the workspace, and an unbound store has nothing to protect. Refusing either\n * would silently stop every preview rather than guard one.\n */\nexport function runBelongsToWorkspace(runWorkspaceId: string | null, boundWorkspaceId: string | null): boolean {\n return runWorkspaceId === null || boundWorkspaceId === null || runWorkspaceId === boundWorkspaceId;\n}\n\nexport const useAssistantRunStore = create<AssistantRunState>()((set, get) => ({\n ...idleRun(),\n richMounted: false,\n workspaceId: null,\n actorId: null,\n\n startRun: (prompt) =>\n set((state) => {\n // A new prompt CONTINUES the same use case (incremental flow) — keep every\n // step and id already built. Clear any stale spinner from the prior turn,\n // then light up the earliest step that has not landed yet so the \"working\n // on it\" indicator sits on the right tab.\n const steps = { ...state.steps };\n for (const key of PREVIEW_STEPS) {\n if (steps[key] === 'running') steps[key] = 'pending';\n }\n const next = PREVIEW_STEPS.find((key) => steps[key] === 'pending');\n if (next) steps[next] = 'running';\n return {\n isRunning: true,\n // Auto-open: the whole point is that you SEE the build happen without\n // having to go looking for it.\n previewOpen: true,\n hasRun: true,\n prompt,\n steps,\n // entityIds intentionally preserved (merged) so earlier tabs keep content.\n caption: '',\n error: null,\n updatedAt: Date.now(),\n };\n }),\n\n applyProgress: (content) =>\n set((state) => {\n const steps = { ...state.steps };\n const entityIds = { ...state.entityIds };\n\n const markDone = (step: PreviewStepKey, id?: string) => {\n steps[step] = advance(steps[step], 'done');\n // Always record the freshest id seen this turn (never \"first wins\"): an\n // incremental prompt that rebuilds a piece must point the tab at the new\n // one. `step !== 'widget'` narrows to the keys entityIds actually holds.\n if (id && step !== 'widget') entityIds[step] = id;\n };\n\n // 1) Markdown link paths.\n for (const match of content.matchAll(ENTITY_LINK)) {\n const step = LINK_TO_STEP[match[1] ?? ''];\n if (step && match[2]) markDone(step, match[2]);\n }\n // 2) Raw mutation JSON (the agent often prints this instead of a link).\n for (const { step, re } of JSON_ID_STEPS) {\n const match = re.exec(content);\n if (match?.[1]) markDone(step, match[1]);\n }\n\n if (WIDGET_CREATE_JSON.test(content) || WIDGET_DONE.test(content)) {\n steps.widget = advance(steps.widget, 'done');\n // The parser is optional — the agent skips it when the rows are already\n // shaped. If the widget landed and the parser was never built, it was\n // skipped, not stuck.\n if (steps.parser !== 'done') steps.parser = advance(steps.parser, 'skipped');\n }\n\n // The first step that has not finished is the one being worked on.\n const active = PREVIEW_STEPS.find((key) => steps[key] === 'pending' || steps[key] === 'running');\n if (active) steps[active] = advance(steps[active], 'running');\n\n const caption =\n content\n .split('\\n')\n .map((line) => line.trim())\n .reverse()\n .map((line) => RUNNING_LINE.exec(line)?.[1])\n .find((label): label is string => Boolean(label)) ?? state.caption;\n\n return { steps, entityIds, caption, updatedAt: Date.now() };\n }),\n\n finishRun: (error) =>\n set((state) => {\n // The turn is over — stop the live spinner. A step still 'running' didn't\n // land this turn, so drop it back to 'pending' (a later prompt can pick it\n // up). NEVER blanket-skip pending steps: in the incremental flow the later\n // steps simply have not been asked for yet, and 'done' steps must stay done.\n const steps = { ...state.steps };\n for (const key of PREVIEW_STEPS) {\n if (steps[key] === 'running') steps[key] = 'pending';\n }\n return { isRunning: false, error, steps, caption: '', updatedAt: Date.now() };\n }),\n\n openPreview: () => set({ previewOpen: true }),\n closePreview: () => set({ previewOpen: false }),\n setRichMounted: (mounted) => set({ richMounted: mounted }),\n\n bindWorkspace: (workspaceId) => {\n const previous = get().workspaceId;\n if (previous === workspaceId) return;\n if (previous !== null) removePersistedRun(previous);\n\n const actorId = workspaceId === null ? null : getActorId();\n const restored = workspaceId === null ? null : readPersistedRun(workspaceId, actorId);\n set({ ...idleRun(), ...restored, workspaceId, actorId });\n },\n}));\n\n/**\n * Keep the page preview's slice for the bound workspace on every change to it.\n *\n * ★ A subscription rather than a write in each action: five actions change the\n * slice, and a write missed in any one of them would restore a stale preview.\n * `bindWorkspace` also lands here, which is harmless: it rewrites what it read.\n */\nuseAssistantRunStore.subscribe((state, previous) => {\n if (state.workspaceId === null) return;\n if (\n state.workspaceId === previous.workspaceId &&\n state.hasRun === previous.hasRun &&\n state.previewOpen === previous.previewOpen &&\n state.steps === previous.steps &&\n state.entityIds === previous.entityIds &&\n state.updatedAt === previous.updatedAt\n ) {\n return;\n }\n if (!state.hasRun) {\n removePersistedRun(state.workspaceId);\n return;\n }\n const run: PersistedRun = {\n hasRun: true,\n previewOpen: state.previewOpen,\n steps: state.steps,\n entityIds: state.entityIds,\n updatedAt: state.updatedAt,\n };\n writePersistedRun(state.workspaceId, state.actorId, run);\n});\n"],"mappings":"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"}
|
|
@@ -1,60 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { jsx as c } from "react/jsx-runtime";
|
|
6
|
-
import { useI18n as l } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
7
|
-
import { AssistantEntityPreview as u, useAssistantStore as d } from "@burdenoff/fe-libs/shared/assistant";
|
|
1
|
+
import { runBelongsToWorkspace as e, useAssistantRunStore as t } from "./assistantRunStore.js";
|
|
2
|
+
import { previewRunAction as n } from "./previewRunAction.js";
|
|
3
|
+
import { useMemo as r } from "react";
|
|
4
|
+
import { useAssistantStore as i } from "@burdenoff/fe-libs/shared/assistant";
|
|
8
5
|
//#region src/bigconsole/assistant/previewController.tsx
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"data-variant": "assistant",
|
|
18
|
-
className: "min-w-0 p-3",
|
|
19
|
-
children: /* @__PURE__ */ c(u, {
|
|
20
|
-
title: o("bigconsole.widget.aiPreviewMini.title", "AI Preview"),
|
|
21
|
-
steps: h,
|
|
22
|
-
caption: s ? f || o("bigconsole.widget.aiPreviewMini.working", "Working…") : o("bigconsole.widget.aiPreviewMini.done", "Done"),
|
|
23
|
-
error: p,
|
|
24
|
-
meta: /* @__PURE__ */ c(n, {
|
|
25
|
-
state: s ? "running" : p ? "stopped" : "idle",
|
|
26
|
-
stateLabel: s ? o("bigconsole.widget.aiPreviewMini.live", "Live") : p ? o("bigconsole.widget.aiPreviewMini.stopped", "Stopped") : null,
|
|
27
|
-
updatedLabel: o("bigconsole.widget.aiPreviewMini.updated", "updated"),
|
|
28
|
-
updatedAt: m,
|
|
29
|
-
ago: (e) => i(e, o)
|
|
30
|
-
}),
|
|
31
|
-
labels: a(o)
|
|
32
|
-
})
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
var p = /* @__PURE__ */ new Set();
|
|
36
|
-
function m(e) {
|
|
37
|
-
let n = d.getState(), r = n.conversations.find((t) => t.id === e.run.conversationId)?.mode ?? n.mode;
|
|
38
|
-
if (o(e.kind, e.run.runId, r, p) === "ignore") return;
|
|
39
|
-
let i = t.getState();
|
|
40
|
-
if (e.kind === "start") {
|
|
41
|
-
p.add(e.run.runId), i.startRun("");
|
|
6
|
+
var a = /* @__PURE__ */ new Set();
|
|
7
|
+
function o(r) {
|
|
8
|
+
let o = i.getState(), s = o.conversations.find((e) => e.id === r.run.conversationId)?.mode ?? o.mode;
|
|
9
|
+
if (n(r.kind, r.run.runId, s, a) === "ignore") return;
|
|
10
|
+
let c = t.getState(), l = e(r.run.workspaceId, c.workspaceId);
|
|
11
|
+
if (r.kind === "start") {
|
|
12
|
+
if (!l) return;
|
|
13
|
+
a.add(r.run.runId), c.startRun("");
|
|
42
14
|
return;
|
|
43
15
|
}
|
|
44
|
-
if (
|
|
45
|
-
|
|
16
|
+
if (r.kind === "snapshot") {
|
|
17
|
+
if (!l) return;
|
|
18
|
+
c.applyProgress(r.content);
|
|
46
19
|
return;
|
|
47
20
|
}
|
|
48
|
-
|
|
21
|
+
a.delete(r.run.runId), l && c.finishRun(r.outcome === "error" ? r.error ?? "The build stopped." : null);
|
|
49
22
|
}
|
|
50
|
-
function
|
|
51
|
-
return
|
|
52
|
-
label: "Build preview",
|
|
53
|
-
render: () => /* @__PURE__ */ c(f, {}),
|
|
54
|
-
onEvent: m
|
|
55
|
-
}), []);
|
|
23
|
+
function s() {
|
|
24
|
+
return r(() => ({ onEvent: o }), []);
|
|
56
25
|
}
|
|
57
26
|
//#endregion
|
|
58
|
-
export {
|
|
27
|
+
export { s as useBigConsoleAssistantPreview };
|
|
59
28
|
|
|
60
29
|
//# sourceMappingURL=previewController.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"previewController.js","names":[],"sources":["../../../src/bigconsole/assistant/previewController.tsx"],"sourcesContent":["/**\n * BigConsole's preview capability for the shared assistant panel (BC3).\n *\n * ★★★ THIS IS WHAT REPLACES THE TRANSPORT AS THE RUN STORE'S DRIVER.\n *\n * Today `createSandboxAssistantTransport` calls `startRun` / `applyProgress` /\n * `finishRun` itself, because it owns the turn. The shared panel consumes NO\n * transport at all — `AssistantChatArea` calls fe-libs' own `assistant/api`\n * directly — so once BC4 mounts the panel there is nothing left to drive the\n * store, and the live build preview would simply stop moving.\n *\n * fe-libs' preview capability is that missing wire: the bridge announces\n * `start` / `snapshot` / `end` for the run in flight, and this controller\n * forwards them to the same three store actions the transport used to call. The\n * store, both preview panels, and everything downstream are untouched.\n *\n * ★★ ABSENCE IS THE OFF-SWITCH. A product that supplies no controller gets no\n * toggle and no overlay; there is deliberately no second `enablePreview` flag to\n * disagree with the controller's presence. So this hook always returns a\n * controller — gating belongs at the mount site (BC4), not here.\n */\nimport { useMemo } from 'react';\nimport {\n AssistantEntityPreview,\n useAssistantStore,\n type AssistantPreviewController,\n type AssistantPreviewEvent,\n type AssistantPreviewStep,\n} from '@burdenoff/fe-libs/shared/assistant';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\n\nimport { PreviewMeta, STEP_LABEL, formatAgo, previewLabels } from '../components/preview/previewShared';\nimport { PREVIEW_STEPS, useAssistantRunStore } from './assistantRunStore';\nimport { previewRunAction, type PreviewRunAction } from './previewRunAction';\n\n/**\n * The surface fe-libs renders inside its overlay.\n *\n * ★★ A COMPONENT, returned as JSX from `render()` — never hooks called inside\n * `render()` itself. fe-libs invokes `render()` from a single `PreviewSurface`\n * component, so hooks called there would belong to THAT fiber and any change in\n * their layout becomes \"rendered more hooks than during the previous render\".\n * Returning an element gives this surface its own fiber, which is both safer and\n * what lets it subscribe to the store independently.\n *\n * ★ Store-only by construction: no Apollo, no BigConsole context, no data\n * fetching — the same discipline that lets `AiPreviewMiniPanel` mount above the\n * router. The rich per-entity tabs stay in `AiPreviewPanel`, which lives inside\n * `BigConsoleRoot` where the data layer actually exists.\n *\n * ★ It renders the shared `AssistantEntityPreview` with the SAME mapping the\n * mini panel uses, and reuses `STEP_LABEL` / `previewLabels` / `PreviewMeta`\n * rather than restating them. Those helpers were extracted precisely because the\n * two existing panels kept drifting apart; a third copy here would reintroduce\n * the bug they were created to kill.\n */\nfunction BigConsoleAssistantPreviewSurface() {\n const { t } = useI18n();\n const isRunning = useAssistantRunStore((state) => state.isRunning);\n const steps = useAssistantRunStore((state) => state.steps);\n const caption = useAssistantRunStore((state) => state.caption);\n const error = useAssistantRunStore((state) => state.error);\n const updatedAt = useAssistantRunStore((state) => state.updatedAt);\n\n const railSteps: AssistantPreviewStep[] = PREVIEW_STEPS.map((key) => ({\n id: key,\n label: t(STEP_LABEL[key].key, STEP_LABEL[key].fallback),\n status: steps[key],\n }));\n\n return (\n <div data-testid=\"ai-preview-panel\" data-variant=\"assistant\" className=\"min-w-0 p-3\">\n <AssistantEntityPreview\n title={t('bigconsole.widget.aiPreviewMini.title', 'AI Preview')}\n steps={railSteps}\n // ★★ TWO branches, not three. The shared surface renders\n // `{error ? '' : caption}` — it BLANKS the caption whenever `error` is\n // set — so a failure branch here would compute a string that is then\n // discarded. The failure is named in `meta` instead, which\n // `PreviewHeader` renders without consulting `error`.\n caption={\n isRunning\n ? caption || t('bigconsole.widget.aiPreviewMini.working', 'Working…')\n : t('bigconsole.widget.aiPreviewMini.done', 'Done')\n }\n error={error}\n meta={\n <PreviewMeta\n state={isRunning ? 'running' : error ? 'stopped' : 'idle'}\n stateLabel={\n isRunning\n ? t('bigconsole.widget.aiPreviewMini.live', 'Live')\n : error\n ? t('bigconsole.widget.aiPreviewMini.stopped', 'Stopped')\n : null\n }\n updatedLabel={t('bigconsole.widget.aiPreviewMini.updated', 'updated')}\n updatedAt={updatedAt}\n ago={(seconds) => formatAgo(seconds, t)}\n />\n }\n labels={previewLabels(t)}\n />\n </div>\n );\n}\n\n/**\n * ★ Re-exported so consumers keep ONE import path, while the RULE itself lives\n * in a module that loads without a DOM. Importing THIS file pulls fe-libs'\n * assistant barrel, which touches `document` at load time — so the decision had\n * to move out before it could be tested. See `previewRunAction.ts`.\n *\n * ★ Local bindings, not `export … from`: a bare re-export creates no local name,\n * so `applyPreviewEvent` below would be calling something this module does not\n * actually have.\n */\nexport { previewRunAction };\nexport type { PreviewRunAction };\n\n/**\n * The runs this controller has adopted — Build runs only.\n *\n * ★ Module-level, like the handler itself: it must outlive the controller memo\n * and every re-render, and nothing renders it.\n */\nconst adoptedRuns = new Set<string>();\n\n/**\n * Forward one bridge event to the run store.\n *\n * ★ Module-level and imperative (`getState()`), NOT a hook: the bridge calls\n * this from an effect, and reading the store through a selector here would make\n * the controller depend on state it never renders.\n */\nfunction applyPreviewEvent(event: AssistantPreviewEvent): void {\n /**\n * ★ The mode of THIS RUN's conversation, never the panel's. The panel's `mode`\n * is whatever the user last selected, which need not be the mode the run in\n * flight was started in. Falls back to the panel mode only when the\n * conversation is gone.\n */\n const assistant = useAssistantStore.getState();\n const modeOfRun = assistant.conversations.find((c) => c.id === event.run.conversationId)?.mode ?? assistant.mode;\n\n if (previewRunAction(event.kind, event.run.runId, modeOfRun, adoptedRuns) === 'ignore') {\n return;\n }\n\n const store = useAssistantRunStore.getState();\n\n if (event.kind === 'start') {\n adoptedRuns.add(event.run.runId);\n /**\n * ★★ THE PROMPT IS NOT AVAILABLE, and that is a real gap rather than an\n * oversight. `AssistantPreviewRun` carries `runId`, `conversationId`,\n * `messageId`, `workspaceId` and `actorId` — no turn text — so the store's\n * `prompt` (which only feeds an informational line) is left empty. The\n * transport could pass it because it owned the turn; the bridge cannot,\n * because it observes one. Everything the preview actually draws comes from\n * `applyProgress`, so nothing visible depends on this.\n */\n store.startRun('');\n return;\n }\n\n if (event.kind === 'snapshot') {\n /**\n * ★ `content` is the PROSE with the tool-activity lines already removed by\n * fe-libs. That is what this store wants: it harvests entity links, raw\n * mutation ids and the newest `⏳ …` caption out of the agent's narration,\n * all of which live in the prose. The structured `event.steps` describe TOOL\n * calls, which are a different rail from BigConsole's four build steps.\n */\n store.applyProgress(event.content);\n return;\n }\n\n /**\n * ★★ `abandoned` is neither success nor failure — it is a run the bridge can\n * no longer follow (cancelled, superseded, or navigated away from mid-turn),\n * and the turn may well still be executing. Passing its text as an error would\n * paint a red \"Build stopped\" banner over a build that is very likely still\n * running; passing nothing at all would leave the spinner going forever. So it\n * ends the run WITHOUT an error, which stops the spinner and leaves every step\n * already marked `done` exactly as it was.\n */\n /**\n * ★ Released here, and only here. Run ids are `conversationId:messageId`, so\n * they are never reused and this is a leak rather than a correctness bug — but\n * an unbounded set that grows for the life of the tab is still a leak, and\n * `end` is the one event guaranteed to arrive for every adopted run\n * (`abandoned` included — that outcome exists precisely so a run the bridge\n * can no longer follow still terminates).\n */\n adoptedRuns.delete(event.run.runId);\n store.finishRun(event.outcome === 'error' ? (event.error ?? 'The build stopped.') : null);\n}\n\n/**\n * BigConsole's preview controller, stable across renders.\n *\n * ★★★ THE MEMO IS LOAD-BEARING, not tidiness. fe-libs' bridge deliberately does\n * NOT reset its run state when the controller identity changes — precisely so\n * that the natural `preview={{ render, onEvent }}` object literal does not\n * re-announce `start` on every poll tick. The cost of that choice is that\n * REPLACING a controller mid-run leaves the newcomer without a `start` for the\n * run already in flight. An empty dependency list is what guarantees one\n * controller for the life of the mount.\n *\n * ★ `label` is the overlay's toggle text. Plain English here, like FluidGrids':\n * the seam's user-facing strings are translated in BC5 along with the rest of\n * the bundle, and a half-translated toggle beside untranslated mode labels is\n * worse than a consistently English one.\n */\nexport function useBigConsoleAssistantPreview(): AssistantPreviewController {\n return useMemo<AssistantPreviewController>(\n () => ({\n label: 'Build preview',\n render: () => <BigConsoleAssistantPreviewSurface />,\n onEvent: applyPreviewEvent,\n }),\n []\n );\n}\n"],"mappings":";;;;;;;;AAwDA,SAAS,IAAoC;CAC3C,IAAM,EAAE,SAAM,GAAS,EACjB,IAAY,GAAsB,MAAU,EAAM,UAAU,EAC5D,IAAQ,GAAsB,MAAU,EAAM,MAAM,EACpD,IAAU,GAAsB,MAAU,EAAM,QAAQ,EACxD,IAAQ,GAAsB,MAAU,EAAM,MAAM,EACpD,IAAY,GAAsB,MAAU,EAAM,UAAU,EAE5D,IAAoC,EAAc,KAAK,OAAS;EACpE,IAAI;EACJ,OAAO,EAAE,EAAW,GAAK,KAAK,EAAW,GAAK,SAAS;EACvD,QAAQ,EAAM;EACf,EAAE;AAEH,QACE,kBAAC,OAAD;EAAK,eAAY;EAAmB,gBAAa;EAAY,WAAU;YACrE,kBAAC,GAAD;GACE,OAAO,EAAE,yCAAyC,aAAa;GAC/D,OAAO;GAMP,SACE,IACI,KAAW,EAAE,2CAA2C,WAAW,GACnE,EAAE,wCAAwC,OAAO;GAEhD;GACP,MACE,kBAAC,GAAD;IACE,OAAO,IAAY,YAAY,IAAQ,YAAY;IACnD,YACE,IACI,EAAE,wCAAwC,OAAO,GACjD,IACE,EAAE,2CAA2C,UAAU,GACvD;IAER,cAAc,EAAE,2CAA2C,UAAU;IAC1D;IACX,MAAM,MAAY,EAAU,GAAS,EAAE;IACvC,CAAA;GAEJ,QAAQ,EAAc,EAAE;GACxB,CAAA;EACE,CAAA;;AAuBV,IAAM,oBAAc,IAAI,KAAa;AASrC,SAAS,EAAkB,GAAoC;CAO7D,IAAM,IAAY,EAAkB,UAAU,EACxC,IAAY,EAAU,cAAc,MAAM,MAAM,EAAE,OAAO,EAAM,IAAI,eAAe,EAAE,QAAQ,EAAU;AAE5G,KAAI,EAAiB,EAAM,MAAM,EAAM,IAAI,OAAO,GAAW,EAAY,KAAK,SAC5E;CAGF,IAAM,IAAQ,EAAqB,UAAU;AAE7C,KAAI,EAAM,SAAS,SAAS;AAW1B,EAVA,EAAY,IAAI,EAAM,IAAI,MAAM,EAUhC,EAAM,SAAS,GAAG;AAClB;;AAGF,KAAI,EAAM,SAAS,YAAY;AAQ7B,IAAM,cAAc,EAAM,QAAQ;AAClC;;AAqBF,CADA,EAAY,OAAO,EAAM,IAAI,MAAM,EACnC,EAAM,UAAU,EAAM,YAAY,UAAW,EAAM,SAAS,uBAAwB,KAAK;;AAmB3F,SAAgB,IAA4D;AAC1E,QAAO,SACE;EACL,OAAO;EACP,cAAc,kBAAC,GAAD,EAAqC,CAAA;EACnD,SAAS;EACV,GACD,EAAE,CACH"}
|
|
1
|
+
{"version":3,"file":"previewController.js","names":[],"sources":["../../../src/bigconsole/assistant/previewController.tsx"],"sourcesContent":["/**\n * BigConsole's preview capability for the shared assistant panel (BC3).\n *\n * ★★★ THIS IS WHAT REPLACES THE TRANSPORT AS THE RUN STORE'S DRIVER.\n *\n * Today `createSandboxAssistantTransport` calls `startRun` / `applyProgress` /\n * `finishRun` itself, because it owns the turn. The shared panel consumes NO\n * transport at all — `AssistantChatArea` calls fe-libs' own `assistant/api`\n * directly — so once BC4 mounts the panel there is nothing left to drive the\n * store, and the live build preview would simply stop moving.\n *\n * fe-libs' preview capability is that missing wire: the bridge announces\n * `start` / `snapshot` / `end` for the run in flight, and this controller\n * forwards them to the same three store actions the transport used to call. The\n * store, both preview panels, and everything downstream are untouched.\n *\n * ★★★ HEADLESS (BOFF-7799). The controller carries `onEvent` and nothing else.\n * It used to also `render` a copy of the preview INSIDE the panel, behind a\n * \"Build preview\" toggle. That copy was offered even with no run in the tab\n * (an empty rail captioned \"Done\"), and opening it made the page preview under\n * the breadcrumb stand down, so users saw the build only as an overlay in the\n * chat. fe-libs 2026.930.3 made `render` optional: with no `render` the panel\n * shows no toggle, never opens the overlay and never publishes\n * `data-assistant-overlay=\"preview\"`, while the bridge still delivers `start`,\n * `snapshot` and `end`. The page surfaces (`AiPreviewPanel`,\n * `AiPreviewMiniPanel`) are now the only surfaces, and they stay beside the\n * chat by reading `DOCK_CLEARANCE`.\n *\n * ★★ ABSENCE IS STILL THE OFF-SWITCH for the events. A product that supplies no\n * controller gets no events; there is deliberately no second `enablePreview`\n * flag. So this hook always returns a controller — gating belongs at the mount\n * site (BC4), not here.\n */\nimport { useMemo } from 'react';\nimport {\n useAssistantStore,\n type AssistantPreviewController,\n type AssistantPreviewEvent,\n} from '@burdenoff/fe-libs/shared/assistant';\n\nimport { runBelongsToWorkspace, useAssistantRunStore } from './assistantRunStore';\nimport { previewRunAction, type PreviewRunAction } from './previewRunAction';\n\n/**\n * ★ Re-exported so consumers keep ONE import path, while the RULE itself lives\n * in a module that loads without a DOM. Importing THIS file pulls fe-libs'\n * assistant barrel, which touches `document` at load time — so the decision had\n * to move out before it could be tested. See `previewRunAction.ts`.\n *\n * ★ Local bindings, not `export … from`: a bare re-export creates no local name,\n * so `applyPreviewEvent` below would be calling something this module does not\n * actually have.\n */\nexport { previewRunAction };\nexport type { PreviewRunAction };\n\n/**\n * The runs this controller has adopted — Build runs only.\n *\n * ★ Module-level, like the handler itself: it must outlive the controller memo\n * and every re-render, and nothing renders it.\n */\nconst adoptedRuns = new Set<string>();\n\n/**\n * Forward one bridge event to the run store.\n *\n * ★ Module-level and imperative (`getState()`), NOT a hook: the bridge calls\n * this from an effect, and reading the store through a selector here would make\n * the controller depend on state it never renders.\n */\nfunction applyPreviewEvent(event: AssistantPreviewEvent): void {\n /**\n * ★ The mode of THIS RUN's conversation, never the panel's. The panel's `mode`\n * is whatever the user last selected, which need not be the mode the run in\n * flight was started in. Falls back to the panel mode only when the\n * conversation is gone.\n */\n const assistant = useAssistantStore.getState();\n const modeOfRun = assistant.conversations.find((c) => c.id === event.run.conversationId)?.mode ?? assistant.mode;\n\n if (previewRunAction(event.kind, event.run.runId, modeOfRun, adoptedRuns) === 'ignore') {\n return;\n }\n\n const store = useAssistantRunStore.getState();\n /**\n * ★★ A run drives only the workspace it started in. A workspace switch clears\n * the store (`bindWorkspace`), but the previous workspace's turn keeps\n * streaming; its snapshots must not refill the cleared store, or the refill\n * is kept under the new workspace's key.\n */\n const belongs = runBelongsToWorkspace(event.run.workspaceId, store.workspaceId);\n\n if (event.kind === 'start') {\n if (!belongs) return;\n adoptedRuns.add(event.run.runId);\n /**\n * ★★ THE PROMPT IS NOT AVAILABLE, and that is a real gap rather than an\n * oversight. `AssistantPreviewRun` carries `runId`, `conversationId`,\n * `messageId`, `workspaceId` and `actorId` — no turn text — so the store's\n * `prompt` (which only feeds an informational line) is left empty. The\n * transport could pass it because it owned the turn; the bridge cannot,\n * because it observes one. Everything the preview actually draws comes from\n * `applyProgress`, so nothing visible depends on this.\n */\n store.startRun('');\n return;\n }\n\n if (event.kind === 'snapshot') {\n if (!belongs) return;\n /**\n * ★ `content` is the PROSE with the tool-activity lines already removed by\n * fe-libs. That is what this store wants: it harvests entity links, raw\n * mutation ids and the newest `⏳ …` caption out of the agent's narration,\n * all of which live in the prose. The structured `event.steps` describe TOOL\n * calls, which are a different rail from BigConsole's four build steps.\n */\n store.applyProgress(event.content);\n return;\n }\n\n /**\n * ★★ `abandoned` is neither success nor failure — it is a run the bridge can\n * no longer follow (cancelled, superseded, or navigated away from mid-turn),\n * and the turn may well still be executing. Passing its text as an error would\n * paint a red \"Build stopped\" banner over a build that is very likely still\n * running; passing nothing at all would leave the spinner going forever. So it\n * ends the run WITHOUT an error, which stops the spinner and leaves every step\n * already marked `done` exactly as it was.\n */\n /**\n * ★ Released here, and only here. Run ids are `conversationId:messageId`, so\n * they are never reused and this is a leak rather than a correctness bug — but\n * an unbounded set that grows for the life of the tab is still a leak, and\n * `end` is the one event guaranteed to arrive for every adopted run\n * (`abandoned` included — that outcome exists precisely so a run the bridge\n * can no longer follow still terminates).\n */\n adoptedRuns.delete(event.run.runId);\n if (!belongs) return;\n store.finishRun(event.outcome === 'error' ? (event.error ?? 'The build stopped.') : null);\n}\n\n/**\n * BigConsole's preview controller, stable across renders.\n *\n * ★★★ THE MEMO IS LOAD-BEARING, not tidiness. fe-libs' bridge deliberately does\n * NOT reset its run state when the controller identity changes — precisely so\n * that the natural `preview={{ render, onEvent }}` object literal does not\n * re-announce `start` on every poll tick. The cost of that choice is that\n * REPLACING a controller mid-run leaves the newcomer without a `start` for the\n * run already in flight. An empty dependency list is what guarantees one\n * controller for the life of the mount.\n *\n * ★★ NO `render` AND NO `label`, and never add them back conditionally.\n * fe-libs asks a product to decide once whether its controller is headless:\n * adding `render` later swaps the panel's hook layout mid-mount. A `label`\n * would be dead weight, because a headless controller has no toggle to name.\n */\nexport function useBigConsoleAssistantPreview(): AssistantPreviewController {\n return useMemo<AssistantPreviewController>(() => ({ onEvent: applyPreviewEvent }), []);\n}\n"],"mappings":";;;;;AA8DA,IAAM,oBAAc,IAAI,KAAa;AASrC,SAAS,EAAkB,GAAoC;CAO7D,IAAM,IAAY,EAAkB,UAAU,EACxC,IAAY,EAAU,cAAc,MAAM,MAAM,EAAE,OAAO,EAAM,IAAI,eAAe,EAAE,QAAQ,EAAU;AAE5G,KAAI,EAAiB,EAAM,MAAM,EAAM,IAAI,OAAO,GAAW,EAAY,KAAK,SAC5E;CAGF,IAAM,IAAQ,EAAqB,UAAU,EAOvC,IAAU,EAAsB,EAAM,IAAI,aAAa,EAAM,YAAY;AAE/E,KAAI,EAAM,SAAS,SAAS;AAC1B,MAAI,CAAC,EAAS;AAWd,EAVA,EAAY,IAAI,EAAM,IAAI,MAAM,EAUhC,EAAM,SAAS,GAAG;AAClB;;AAGF,KAAI,EAAM,SAAS,YAAY;AAC7B,MAAI,CAAC,EAAS;AAQd,IAAM,cAAc,EAAM,QAAQ;AAClC;;AAoBF,GAAY,OAAO,EAAM,IAAI,MAAM,EAC9B,KACL,EAAM,UAAU,EAAM,YAAY,UAAW,EAAM,SAAS,uBAAwB,KAAK;;AAmB3F,SAAgB,IAA4D;AAC1E,QAAO,SAA2C,EAAE,SAAS,GAAmB,GAAG,EAAE,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useAssistantRunStore as e } from "./assistantRunStore.js";
|
|
2
|
+
import { useEffect as t } from "react";
|
|
3
|
+
import { useActiveContext as n } from "@burdenoff/fe-libs/shared/providers/shell/ActiveContextProvider";
|
|
4
|
+
//#region src/bigconsole/assistant/useAssistantRunWorkspace.ts
|
|
5
|
+
function r() {
|
|
6
|
+
let { workspaceId: r } = n(), i = e((e) => e.bindWorkspace);
|
|
7
|
+
t(() => {
|
|
8
|
+
i(r || null);
|
|
9
|
+
}, [i, r]);
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { r as useAssistantRunWorkspace };
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=useAssistantRunWorkspace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAssistantRunWorkspace.js","names":[],"sources":["../../../src/bigconsole/assistant/useAssistantRunWorkspace.ts"],"sourcesContent":["import { useEffect } from 'react';\nimport { useActiveContext } from '@burdenoff/fe-libs/shared/providers/shell/ActiveContextProvider';\n\nimport { useAssistantRunStore } from './assistantRunStore';\n\n/**\n * Binds the build-preview run store to the ACTIVE workspace (BOFF-7799).\n *\n * ★★ Called by both page surfaces, which is what makes a reload in the same tab\n * restore the preview under the breadcrumb and a workspace switch clear it.\n * Binding is idempotent, so the rich panel and the app-shell mini panel may\n * both call it on the same page.\n *\n * ★ From the REACTIVE context, as `BigConsoleAssistant` reads it, never from a\n * helper that reads storage once: the shell does not remount on a workspace\n * switch, so a one-shot read would keep the previous workspace's build on\n * screen.\n *\n * ★ `|| null`: an empty string is \"no workspace\", not a workspace called \"\".\n */\nexport function useAssistantRunWorkspace(): void {\n const { workspaceId } = useActiveContext();\n const bindWorkspace = useAssistantRunStore((state) => state.bindWorkspace);\n\n useEffect(() => {\n bindWorkspace(workspaceId || null);\n }, [bindWorkspace, workspaceId]);\n}\n"],"mappings":";;;;AAoBA,SAAgB,IAAiC;CAC/C,IAAM,EAAE,mBAAgB,GAAkB,EACpC,IAAgB,GAAsB,MAAU,EAAM,cAAc;AAE1E,SAAgB;AACd,IAAc,KAAe,KAAK;IACjC,CAAC,GAAe,EAAY,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { PREVIEW_STEPS as e, useAssistantRunStore as t } from "../../assistant/assistantRunStore.js";
|
|
2
|
+
import { useAssistantRunWorkspace as n } from "../../assistant/useAssistantRunWorkspace.js";
|
|
3
3
|
import { Section as r } from "../common/Section.js";
|
|
4
4
|
import "../common/index.js";
|
|
5
5
|
import { DOCK_CLEARANCE as i, PreviewMeta as a, STEP_LABEL as o, formatAgo as s, previewLabels as c } from "./previewShared.js";
|
|
@@ -10,58 +10,60 @@ import { useI18n as y } from "@burdenoff/fe-libs/shared/providers/shell/I18nProv
|
|
|
10
10
|
import { AssistantEntityPreview as b } from "@burdenoff/fe-libs/shared/assistant";
|
|
11
11
|
//#region src/bigconsole/components/preview/AiPreviewMiniPanel.tsx
|
|
12
12
|
function x({ onNavigate: x }) {
|
|
13
|
-
let { t: S } = y()
|
|
13
|
+
let { t: S } = y();
|
|
14
|
+
n();
|
|
15
|
+
let C = t((e) => e.isRunning), w = t((e) => e.previewOpen), T = t((e) => e.hasRun), E = t((e) => e.richMounted), D = t((e) => e.steps), O = t((e) => e.entityIds), k = t((e) => e.caption), A = t((e) => e.error), j = t((e) => e.updatedAt), M = t((e) => e.openPreview), N = t((e) => e.closePreview), P = (e) => {
|
|
14
16
|
x ? x(e) : typeof window < "u" && window.location.assign(e);
|
|
15
17
|
};
|
|
16
|
-
if (!
|
|
17
|
-
if (!
|
|
18
|
+
if (!T || E) return null;
|
|
19
|
+
if (!w) return /* @__PURE__ */ v("div", {
|
|
18
20
|
className: "flex flex-wrap items-center justify-end gap-control-gap px-4 pt-3 md:px-6",
|
|
19
21
|
style: { marginRight: i },
|
|
20
22
|
children: [/* @__PURE__ */ _("span", {
|
|
21
23
|
className: "text-xs text-text-muted",
|
|
22
|
-
children:
|
|
24
|
+
children: C ? S("bigconsole.widget.aiPreview.buildRunning", "AI build running") : S("bigconsole.widget.aiPreview.buildFinished", "AI build finished")
|
|
23
25
|
}), /* @__PURE__ */ v(g, {
|
|
24
26
|
size: "sm",
|
|
25
27
|
variant: "outline",
|
|
26
|
-
onClick:
|
|
28
|
+
onClick: M,
|
|
27
29
|
"data-testid": "ai-preview-open",
|
|
28
30
|
children: [
|
|
29
31
|
/* @__PURE__ */ _(m, { className: "mr-1.5 size-4" }),
|
|
30
32
|
S("bigconsole.widget.aiPreviewMini.previewButton", "Preview"),
|
|
31
|
-
|
|
33
|
+
C ? /* @__PURE__ */ _(p, { className: "ml-1.5 size-3.5 animate-spin" }) : null
|
|
32
34
|
]
|
|
33
35
|
})]
|
|
34
36
|
});
|
|
35
|
-
let
|
|
36
|
-
|
|
37
|
+
let F = [];
|
|
38
|
+
O.datasink && F.push({
|
|
37
39
|
key: "datasink",
|
|
38
40
|
label: S("bigconsole.widget.aiPreviewMini.link.dataSink", "Data Sink"),
|
|
39
|
-
path: `/bigconsole/datasinks/${
|
|
41
|
+
path: `/bigconsole/datasinks/${O.datasink}`,
|
|
40
42
|
icon: /* @__PURE__ */ _(d, {
|
|
41
43
|
className: "size-4 shrink-0 text-text-muted",
|
|
42
44
|
"aria-hidden": "true"
|
|
43
45
|
})
|
|
44
|
-
}),
|
|
46
|
+
}), O.dashboard && F.push({
|
|
45
47
|
key: "dashboard",
|
|
46
48
|
label: S("bigconsole.widget.aiPreviewMini.link.dashboard", "Dashboard"),
|
|
47
|
-
path: `/bigconsole/dashboards/${
|
|
49
|
+
path: `/bigconsole/dashboards/${O.dashboard}`,
|
|
48
50
|
icon: /* @__PURE__ */ _(f, {
|
|
49
51
|
className: "size-4 shrink-0 text-text-muted",
|
|
50
52
|
"aria-hidden": "true"
|
|
51
53
|
})
|
|
52
|
-
}),
|
|
54
|
+
}), O.parser && F.push({
|
|
53
55
|
key: "parser",
|
|
54
56
|
label: S("bigconsole.widget.aiPreviewMini.link.parser", "Parser"),
|
|
55
|
-
path: `/bigconsole/parsers/${
|
|
57
|
+
path: `/bigconsole/parsers/${O.parser}`,
|
|
56
58
|
icon: /* @__PURE__ */ _(u, {
|
|
57
59
|
className: "size-4 shrink-0 text-text-muted",
|
|
58
60
|
"aria-hidden": "true"
|
|
59
61
|
})
|
|
60
62
|
});
|
|
61
|
-
let
|
|
63
|
+
let I = e.map((e) => ({
|
|
62
64
|
id: e,
|
|
63
65
|
label: S(o[e].key, o[e].fallback),
|
|
64
|
-
status:
|
|
66
|
+
status: D[e]
|
|
65
67
|
}));
|
|
66
68
|
return /* @__PURE__ */ v("div", {
|
|
67
69
|
"data-testid": "ai-preview-panel",
|
|
@@ -73,20 +75,20 @@ function x({ onNavigate: x }) {
|
|
|
73
75
|
children: S("bigconsole.widget.aiPreviewMini.title", "AI Preview")
|
|
74
76
|
}), /* @__PURE__ */ _(b, {
|
|
75
77
|
title: S("bigconsole.widget.aiPreviewMini.title", "AI Preview"),
|
|
76
|
-
steps:
|
|
77
|
-
caption:
|
|
78
|
-
error:
|
|
78
|
+
steps: I,
|
|
79
|
+
caption: C ? k || S("bigconsole.widget.aiPreviewMini.working", "Working…") : S("bigconsole.widget.aiPreviewMini.done", "Done"),
|
|
80
|
+
error: A,
|
|
79
81
|
meta: /* @__PURE__ */ _(a, {
|
|
80
|
-
state:
|
|
81
|
-
stateLabel:
|
|
82
|
+
state: C ? "running" : A ? "stopped" : "idle",
|
|
83
|
+
stateLabel: C ? S("bigconsole.widget.aiPreviewMini.live", "Live") : A ? S("bigconsole.widget.aiPreviewMini.stopped", "Stopped") : null,
|
|
82
84
|
updatedLabel: S("bigconsole.widget.aiPreviewMini.updated", "updated"),
|
|
83
|
-
updatedAt:
|
|
85
|
+
updatedAt: j,
|
|
84
86
|
ago: (e) => s(e, S)
|
|
85
87
|
}),
|
|
86
88
|
headerActions: /* @__PURE__ */ _(g, {
|
|
87
89
|
size: "sm",
|
|
88
90
|
variant: "ghost",
|
|
89
|
-
onClick:
|
|
91
|
+
onClick: N,
|
|
90
92
|
"aria-label": S("bigconsole.widget.aiPreviewMini.closeAriaLabel", "Close preview"),
|
|
91
93
|
"data-testid": "ai-preview-close",
|
|
92
94
|
children: /* @__PURE__ */ _(h, { className: "size-4" })
|
|
@@ -101,10 +103,10 @@ function x({ onNavigate: x }) {
|
|
|
101
103
|
title: S("bigconsole.widget.aiPreviewMini.builtSoFar", "Built so far"),
|
|
102
104
|
description: S("bigconsole.widget.aiPreviewMini.builtSoFarHint", "Open any piece to see the real thing."),
|
|
103
105
|
bodyClassName: "flex flex-col gap-list-gap",
|
|
104
|
-
children: [
|
|
106
|
+
children: [F.length === 0 ? /* @__PURE__ */ _("p", {
|
|
105
107
|
className: "text-sm text-text-muted",
|
|
106
|
-
children:
|
|
107
|
-
}) :
|
|
108
|
+
children: C ? S("bigconsole.widget.aiPreviewMini.building", "Building… links appear here as each piece is created.") : S("bigconsole.widget.aiPreviewMini.nothingBuilt", "Nothing built yet.")
|
|
109
|
+
}) : F.map((e) => /* @__PURE__ */ v("div", {
|
|
108
110
|
"data-testid": "ai-preview-row",
|
|
109
111
|
className: "flex min-w-0 items-center justify-between gap-control-gap rounded-card border border-[var(--color-border-subtle)] bg-bg-canvas px-3 py-2",
|
|
110
112
|
children: [/* @__PURE__ */ v("p", {
|
|
@@ -116,15 +118,15 @@ function x({ onNavigate: x }) {
|
|
|
116
118
|
}), /* @__PURE__ */ v(g, {
|
|
117
119
|
size: "sm",
|
|
118
120
|
variant: "ghost",
|
|
119
|
-
onClick: () =>
|
|
121
|
+
onClick: () => P(e.path),
|
|
120
122
|
children: [S("bigconsole.widget.aiPreviewMini.open", "Open"), /* @__PURE__ */ _(l, { className: "ml-1 size-3.5" })]
|
|
121
123
|
})]
|
|
122
|
-
}, e.key)),
|
|
124
|
+
}, e.key)), O.dashboard ? /* @__PURE__ */ v(g, {
|
|
123
125
|
size: "sm",
|
|
124
126
|
variant: "outline",
|
|
125
127
|
className: "h-auto w-full whitespace-normal py-1.5 text-center",
|
|
126
128
|
"data-testid": "ai-preview-open-in-bigconsole",
|
|
127
|
-
onClick: () =>
|
|
129
|
+
onClick: () => P(`/bigconsole/dashboards/${O.dashboard ?? ""}`),
|
|
128
130
|
children: [S("bigconsole.widget.aiPreviewMini.openInBigConsole", "Open in BigConsole for the live view"), /* @__PURE__ */ _(l, { className: "ml-1 size-3.5" })]
|
|
129
131
|
}) : null]
|
|
130
132
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AiPreviewMiniPanel.js","names":[],"sources":["../../../../src/bigconsole/components/preview/AiPreviewMiniPanel.tsx"],"sourcesContent":["/**\n * AI Preview — the app-shell copy.\n *\n * The full <AiPreviewPanel> lives inside BigConsoleRoot and only exists on\n * /bigconsole pages, but the assistant widget is global: you can start a build\n * from anywhere. This lightweight panel is mounted by the app shell so the build\n * is visible on WHATEVER page you are on — workflows, store, workspaces, search.\n *\n * It is deliberately store-only: no Apollo, no BigConsole context, no data\n * fetching. That is what lets it mount above the router. It shows the same live\n * step rail and caption as the full panel, plus click-through links to each\n * entity as it lands. The rich per-entity tabs stay on the /bigconsole pages,\n * where the data layer actually exists.\n *\n * It renders nothing while the full panel is up (`richMounted`), so the two\n * never stack. Leave a /bigconsole page mid-run and the full panel unmounts,\n * `richMounted` flips false, and this one takes over without a flicker.\n *\n * Section emphasis (BOFF-5639): the chrome (header, progress) is recessed and\n * the \"Built so far\" list is the one raised surface — the same ladder as the\n * full panel, so the two copies read as one component. Spacing is identical to\n * the full panel too; they used to differ (`pt-1` vs `pt-3`), which shifted the\n * page under the user as they navigated.\n */\nimport type { ReactNode } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { AssistantEntityPreview, type AssistantPreviewStep } from '@burdenoff/fe-libs/shared/assistant';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useAssistantPanelShowsPreview } from '../../assistant/useAssistantPanelShowsPreview';\nimport { ArrowUpRight, Braces, Database, LayoutDashboard, Loader2, Sparkles, X } from 'lucide-react';\n\nimport { PREVIEW_STEPS, useAssistantRunStore } from '../../assistant/assistantRunStore';\nimport { Section } from '../common';\nimport { DOCK_CLEARANCE, PreviewMeta, STEP_LABEL, formatAgo, previewLabels } from './previewShared';\n\ninterface AiPreviewMiniPanelProps {\n /**\n * Route to an in-app path. The links are absolute (`/bigconsole/...`) because\n * this panel renders outside the BigConsole router. Falls back to a full\n * navigation when the host does not supply a handler.\n */\n onNavigate?: (path: string) => void;\n}\n\ninterface QuickLink {\n key: string;\n label: string;\n path: string;\n icon: ReactNode;\n}\n\nexport function AiPreviewMiniPanel({ onNavigate }: AiPreviewMiniPanelProps) {\n const { t } = useI18n();\n // ★★★ ONE SURFACE PER RUN. fe-libs renders this same controller inside its own\n // in-chat overlay; without this the page surface stayed up beside it and one\n // run was drawn twice, side by side (measured in prod, BOFF-7799).\n const panelShowsPreview = useAssistantPanelShowsPreview();\n const isRunning = useAssistantRunStore((state) => state.isRunning);\n const previewOpen = useAssistantRunStore((state) => state.previewOpen);\n const hasRun = useAssistantRunStore((state) => state.hasRun);\n const richMounted = useAssistantRunStore((state) => state.richMounted);\n const steps = useAssistantRunStore((state) => state.steps);\n const entityIds = useAssistantRunStore((state) => state.entityIds);\n const caption = useAssistantRunStore((state) => state.caption);\n const error = useAssistantRunStore((state) => state.error);\n const updatedAt = useAssistantRunStore((state) => state.updatedAt);\n const openPreview = useAssistantRunStore((state) => state.openPreview);\n const closePreview = useAssistantRunStore((state) => state.closePreview);\n\n const go = (path: string) => {\n if (onNavigate) onNavigate(path);\n else if (typeof window !== 'undefined') window.location.assign(path);\n };\n\n // Nothing has run, or the full in-context panel is already on screen.\n if (!hasRun || richMounted) return null;\n // The shared panel's overlay is already showing this run — stand down rather\n // than draw it a second time beside itself.\n if (panelShowsPreview) return null;\n\n if (!previewOpen) {\n return (\n <div\n className=\"flex flex-wrap items-center justify-end gap-control-gap px-4 pt-3 md:px-6\"\n style={{ marginRight: DOCK_CLEARANCE }}\n >\n <span className=\"text-xs text-text-muted\">\n {isRunning\n ? t('bigconsole.widget.aiPreview.buildRunning', 'AI build running')\n : t('bigconsole.widget.aiPreview.buildFinished', 'AI build finished')}\n </span>\n <Button size=\"sm\" variant=\"outline\" onClick={openPreview} data-testid=\"ai-preview-open\">\n <Sparkles className=\"mr-1.5 size-4\" />\n {t('bigconsole.widget.aiPreviewMini.previewButton', 'Preview')}\n {isRunning ? <Loader2 className=\"ml-1.5 size-3.5 animate-spin\" /> : null}\n </Button>\n </div>\n );\n }\n\n const links: QuickLink[] = [];\n if (entityIds.datasink)\n links.push({\n key: 'datasink',\n label: t('bigconsole.widget.aiPreviewMini.link.dataSink', 'Data Sink'),\n path: `/bigconsole/datasinks/${entityIds.datasink}`,\n icon: <Database className=\"size-4 shrink-0 text-text-muted\" aria-hidden=\"true\" />,\n });\n if (entityIds.dashboard)\n links.push({\n key: 'dashboard',\n label: t('bigconsole.widget.aiPreviewMini.link.dashboard', 'Dashboard'),\n path: `/bigconsole/dashboards/${entityIds.dashboard}`,\n icon: <LayoutDashboard className=\"size-4 shrink-0 text-text-muted\" aria-hidden=\"true\" />,\n });\n if (entityIds.parser)\n links.push({\n key: 'parser',\n label: t('bigconsole.widget.aiPreviewMini.link.parser', 'Parser'),\n path: `/bigconsole/parsers/${entityIds.parser}`,\n icon: <Braces className=\"size-4 shrink-0 text-text-muted\" aria-hidden=\"true\" />,\n });\n\n const railSteps: AssistantPreviewStep[] = PREVIEW_STEPS.map((key) => ({\n id: key,\n label: t(STEP_LABEL[key].key, STEP_LABEL[key].fallback),\n status: steps[key],\n }));\n\n return (\n // ★ The wrapper owns POSITIONING only — clearance for the assistant dock,\n // page gutters, and the testids this panel already had. Everything visual\n // belongs to the shared surface, which is what keeps this copy and the full\n // panel from drifting apart now that `previewShared` no longer holds a shell\n // recipe for them both.\n //\n // ★★ PADDING, not margin: `mx-4 md:mx-6` is the page gutter and it is\n // RESPONSIVE. See DOCK_CLEARANCE's docblock.\n <div\n data-testid=\"ai-preview-panel\"\n data-variant=\"mini\"\n style={{ paddingRight: DOCK_CLEARANCE }}\n className=\"mx-4 mb-3 mt-3 min-w-0 md:mx-6\"\n >\n {/* ★★ The panel's own heading, restored. The shared surface renders its\n title as a styled <span> inside a <section aria-label>, which labels\n the region but leaves NO heading — while it still renders an <h3> for\n the step rail. main had <h2>AI Preview</h2> above that <h3>, so the\n migration both removed the panel from screen-reader heading navigation\n and orphaned the h3 under whatever <h2> the page beneath happens to\n end with. Visually hidden because the surface already shows the title;\n this only restores the structure. */}\n <h2 className=\"sr-only\">{t('bigconsole.widget.aiPreviewMini.title', 'AI Preview')}</h2>\n <AssistantEntityPreview\n title={t('bigconsole.widget.aiPreviewMini.title', 'AI Preview')}\n steps={railSteps}\n // ★★ TWO branches, not three, and deliberately so: the shared surface\n // renders `{error ? '' : caption}` — it BLANKS the caption whenever\n // `error` is set, so a failure branch here would compute a string that\n // is then discarded. The failure is named in `meta` instead, which\n // `PreviewHeader` renders without consulting `error`.\n caption={\n isRunning\n ? caption || t('bigconsole.widget.aiPreviewMini.working', 'Working…')\n : t('bigconsole.widget.aiPreviewMini.done', 'Done')\n }\n error={error}\n meta={\n <PreviewMeta\n state={isRunning ? 'running' : error ? 'stopped' : 'idle'}\n stateLabel={\n isRunning\n ? t('bigconsole.widget.aiPreviewMini.live', 'Live')\n : error\n ? t('bigconsole.widget.aiPreviewMini.stopped', 'Stopped')\n : null\n }\n updatedLabel={t('bigconsole.widget.aiPreviewMini.updated', 'updated')}\n updatedAt={updatedAt}\n ago={(seconds) => formatAgo(seconds, t)}\n />\n }\n headerActions={\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={closePreview}\n aria-label={t('bigconsole.widget.aiPreviewMini.closeAriaLabel', 'Close preview')}\n data-testid=\"ai-preview-close\"\n >\n <X className=\"size-4\" />\n </Button>\n }\n labels={previewLabels(t)}\n >\n {/* ★ No tabs here, by design: the rich per-entity views need Apollo and\n the BigConsole providers, which do not exist above the router. Links\n are what this copy can honestly offer. */}\n <div className=\"p-3\">\n <Section\n emphasis=\"standard\"\n padding=\"compact\"\n headingLevel={3}\n title={t('bigconsole.widget.aiPreviewMini.builtSoFar', 'Built so far')}\n description={t('bigconsole.widget.aiPreviewMini.builtSoFarHint', 'Open any piece to see the real thing.')}\n bodyClassName=\"flex flex-col gap-list-gap\"\n >\n {links.length === 0 ? (\n <p className=\"text-sm text-text-muted\">\n {isRunning\n ? t(\n 'bigconsole.widget.aiPreviewMini.building',\n 'Building… links appear here as each piece is created.'\n )\n : t('bigconsole.widget.aiPreviewMini.nothingBuilt', 'Nothing built yet.')}\n </p>\n ) : (\n links.map((link) => (\n <div\n key={link.key}\n data-testid=\"ai-preview-row\"\n className=\"flex min-w-0 items-center justify-between gap-control-gap rounded-card border border-[var(--color-border-subtle)] bg-bg-canvas px-3 py-2\"\n >\n <p className=\"flex min-w-0 items-center gap-icon-gap text-sm font-medium text-text-primary\">\n {link.icon}\n <span className=\"truncate\">\n {t('bigconsole.widget.aiPreviewMini.linkReady', '{{label}} ready', { label: link.label })}\n </span>\n </p>\n <Button size=\"sm\" variant=\"ghost\" onClick={() => go(link.path)}>\n {t('bigconsole.widget.aiPreviewMini.open', 'Open')}\n <ArrowUpRight className=\"ml-1 size-3.5\" />\n </Button>\n </div>\n ))\n )}\n {entityIds.dashboard ? (\n // fe-libs `Button` is `whitespace-nowrap` with a fixed `h-8` at\n // `size=\"sm\"`, so this long label overflowed the panel (which is\n // `overflow-hidden`) at 390px — and the hi/ta strings are longer\n // still. `h-auto whitespace-normal` lets it wrap to two lines\n // instead of being clipped.\n <Button\n size=\"sm\"\n variant=\"outline\"\n className=\"h-auto w-full whitespace-normal py-1.5 text-center\"\n data-testid=\"ai-preview-open-in-bigconsole\"\n onClick={() => go(`/bigconsole/dashboards/${entityIds.dashboard ?? ''}`)}\n >\n {t('bigconsole.widget.aiPreviewMini.openInBigConsole', 'Open in BigConsole for the live view')}\n <ArrowUpRight className=\"ml-1 size-3.5\" />\n </Button>\n ) : null}\n </Section>\n </div>\n </AssistantEntityPreview>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AiPreviewMiniPanel.js","names":[],"sources":["../../../../src/bigconsole/components/preview/AiPreviewMiniPanel.tsx"],"sourcesContent":["/**\n * AI Preview — the app-shell copy.\n *\n * The full <AiPreviewPanel> lives inside BigConsoleRoot and only exists on\n * /bigconsole pages, but the assistant widget is global: you can start a build\n * from anywhere. This lightweight panel is mounted by the app shell so the build\n * is visible on WHATEVER page you are on — workflows, store, workspaces, search.\n *\n * It is deliberately store-only: no Apollo, no BigConsole context, no data\n * fetching. That is what lets it mount above the router. It shows the same live\n * step rail and caption as the full panel, plus click-through links to each\n * entity as it lands. The rich per-entity tabs stay on the /bigconsole pages,\n * where the data layer actually exists.\n *\n * It renders nothing while the full panel is up (`richMounted`), so the two\n * never stack. Leave a /bigconsole page mid-run and the full panel unmounts,\n * `richMounted` flips false, and this one takes over without a flicker.\n *\n * Section emphasis (BOFF-5639): the chrome (header, progress) is recessed and\n * the \"Built so far\" list is the one raised surface — the same ladder as the\n * full panel, so the two copies read as one component. Spacing is identical to\n * the full panel too; they used to differ (`pt-1` vs `pt-3`), which shifted the\n * page under the user as they navigated.\n */\nimport type { ReactNode } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { AssistantEntityPreview, type AssistantPreviewStep } from '@burdenoff/fe-libs/shared/assistant';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ArrowUpRight, Braces, Database, LayoutDashboard, Loader2, Sparkles, X } from 'lucide-react';\n\nimport { PREVIEW_STEPS, useAssistantRunStore } from '../../assistant/assistantRunStore';\nimport { useAssistantRunWorkspace } from '../../assistant/useAssistantRunWorkspace';\nimport { Section } from '../common';\nimport { DOCK_CLEARANCE, PreviewMeta, STEP_LABEL, formatAgo, previewLabels } from './previewShared';\n\ninterface AiPreviewMiniPanelProps {\n /**\n * Route to an in-app path. The links are absolute (`/bigconsole/...`) because\n * this panel renders outside the BigConsole router. Falls back to a full\n * navigation when the host does not supply a handler.\n */\n onNavigate?: (path: string) => void;\n}\n\ninterface QuickLink {\n key: string;\n label: string;\n path: string;\n icon: ReactNode;\n}\n\nexport function AiPreviewMiniPanel({ onNavigate }: AiPreviewMiniPanelProps) {\n const { t } = useI18n();\n // ★★★ THE PAGE SURFACES ARE THE ONLY SURFACES (BOFF-7799). The assistant's\n // controller is headless, so nothing inside the panel competes with this one\n // and there is nothing to stand down for. Binding the workspace restores the\n // run after a reload in this tab and clears it on a workspace switch.\n useAssistantRunWorkspace();\n const isRunning = useAssistantRunStore((state) => state.isRunning);\n const previewOpen = useAssistantRunStore((state) => state.previewOpen);\n const hasRun = useAssistantRunStore((state) => state.hasRun);\n const richMounted = useAssistantRunStore((state) => state.richMounted);\n const steps = useAssistantRunStore((state) => state.steps);\n const entityIds = useAssistantRunStore((state) => state.entityIds);\n const caption = useAssistantRunStore((state) => state.caption);\n const error = useAssistantRunStore((state) => state.error);\n const updatedAt = useAssistantRunStore((state) => state.updatedAt);\n const openPreview = useAssistantRunStore((state) => state.openPreview);\n const closePreview = useAssistantRunStore((state) => state.closePreview);\n\n const go = (path: string) => {\n if (onNavigate) onNavigate(path);\n else if (typeof window !== 'undefined') window.location.assign(path);\n };\n\n // Nothing has run, or the full in-context panel is already on screen.\n if (!hasRun || richMounted) return null;\n\n if (!previewOpen) {\n return (\n <div\n className=\"flex flex-wrap items-center justify-end gap-control-gap px-4 pt-3 md:px-6\"\n style={{ marginRight: DOCK_CLEARANCE }}\n >\n <span className=\"text-xs text-text-muted\">\n {isRunning\n ? t('bigconsole.widget.aiPreview.buildRunning', 'AI build running')\n : t('bigconsole.widget.aiPreview.buildFinished', 'AI build finished')}\n </span>\n <Button size=\"sm\" variant=\"outline\" onClick={openPreview} data-testid=\"ai-preview-open\">\n <Sparkles className=\"mr-1.5 size-4\" />\n {t('bigconsole.widget.aiPreviewMini.previewButton', 'Preview')}\n {isRunning ? <Loader2 className=\"ml-1.5 size-3.5 animate-spin\" /> : null}\n </Button>\n </div>\n );\n }\n\n const links: QuickLink[] = [];\n if (entityIds.datasink)\n links.push({\n key: 'datasink',\n label: t('bigconsole.widget.aiPreviewMini.link.dataSink', 'Data Sink'),\n path: `/bigconsole/datasinks/${entityIds.datasink}`,\n icon: <Database className=\"size-4 shrink-0 text-text-muted\" aria-hidden=\"true\" />,\n });\n if (entityIds.dashboard)\n links.push({\n key: 'dashboard',\n label: t('bigconsole.widget.aiPreviewMini.link.dashboard', 'Dashboard'),\n path: `/bigconsole/dashboards/${entityIds.dashboard}`,\n icon: <LayoutDashboard className=\"size-4 shrink-0 text-text-muted\" aria-hidden=\"true\" />,\n });\n if (entityIds.parser)\n links.push({\n key: 'parser',\n label: t('bigconsole.widget.aiPreviewMini.link.parser', 'Parser'),\n path: `/bigconsole/parsers/${entityIds.parser}`,\n icon: <Braces className=\"size-4 shrink-0 text-text-muted\" aria-hidden=\"true\" />,\n });\n\n const railSteps: AssistantPreviewStep[] = PREVIEW_STEPS.map((key) => ({\n id: key,\n label: t(STEP_LABEL[key].key, STEP_LABEL[key].fallback),\n status: steps[key],\n }));\n\n return (\n // ★ The wrapper owns POSITIONING only — clearance for the assistant dock,\n // page gutters, and the testids this panel already had. Everything visual\n // belongs to the shared surface, which is what keeps this copy and the full\n // panel from drifting apart now that `previewShared` no longer holds a shell\n // recipe for them both.\n //\n // ★★ PADDING, not margin: `mx-4 md:mx-6` is the page gutter and it is\n // RESPONSIVE. See DOCK_CLEARANCE's docblock.\n <div\n data-testid=\"ai-preview-panel\"\n data-variant=\"mini\"\n style={{ paddingRight: DOCK_CLEARANCE }}\n className=\"mx-4 mb-3 mt-3 min-w-0 md:mx-6\"\n >\n {/* ★★ The panel's own heading, restored. The shared surface renders its\n title as a styled <span> inside a <section aria-label>, which labels\n the region but leaves NO heading — while it still renders an <h3> for\n the step rail. main had <h2>AI Preview</h2> above that <h3>, so the\n migration both removed the panel from screen-reader heading navigation\n and orphaned the h3 under whatever <h2> the page beneath happens to\n end with. Visually hidden because the surface already shows the title;\n this only restores the structure. */}\n <h2 className=\"sr-only\">{t('bigconsole.widget.aiPreviewMini.title', 'AI Preview')}</h2>\n <AssistantEntityPreview\n title={t('bigconsole.widget.aiPreviewMini.title', 'AI Preview')}\n steps={railSteps}\n // ★★ TWO branches, not three, and deliberately so: the shared surface\n // renders `{error ? '' : caption}` — it BLANKS the caption whenever\n // `error` is set, so a failure branch here would compute a string that\n // is then discarded. The failure is named in `meta` instead, which\n // `PreviewHeader` renders without consulting `error`.\n caption={\n isRunning\n ? caption || t('bigconsole.widget.aiPreviewMini.working', 'Working…')\n : t('bigconsole.widget.aiPreviewMini.done', 'Done')\n }\n error={error}\n meta={\n <PreviewMeta\n state={isRunning ? 'running' : error ? 'stopped' : 'idle'}\n stateLabel={\n isRunning\n ? t('bigconsole.widget.aiPreviewMini.live', 'Live')\n : error\n ? t('bigconsole.widget.aiPreviewMini.stopped', 'Stopped')\n : null\n }\n updatedLabel={t('bigconsole.widget.aiPreviewMini.updated', 'updated')}\n updatedAt={updatedAt}\n ago={(seconds) => formatAgo(seconds, t)}\n />\n }\n headerActions={\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={closePreview}\n aria-label={t('bigconsole.widget.aiPreviewMini.closeAriaLabel', 'Close preview')}\n data-testid=\"ai-preview-close\"\n >\n <X className=\"size-4\" />\n </Button>\n }\n labels={previewLabels(t)}\n >\n {/* ★ No tabs here, by design: the rich per-entity views need Apollo and\n the BigConsole providers, which do not exist above the router. Links\n are what this copy can honestly offer. */}\n <div className=\"p-3\">\n <Section\n emphasis=\"standard\"\n padding=\"compact\"\n headingLevel={3}\n title={t('bigconsole.widget.aiPreviewMini.builtSoFar', 'Built so far')}\n description={t('bigconsole.widget.aiPreviewMini.builtSoFarHint', 'Open any piece to see the real thing.')}\n bodyClassName=\"flex flex-col gap-list-gap\"\n >\n {links.length === 0 ? (\n <p className=\"text-sm text-text-muted\">\n {isRunning\n ? t(\n 'bigconsole.widget.aiPreviewMini.building',\n 'Building… links appear here as each piece is created.'\n )\n : t('bigconsole.widget.aiPreviewMini.nothingBuilt', 'Nothing built yet.')}\n </p>\n ) : (\n links.map((link) => (\n <div\n key={link.key}\n data-testid=\"ai-preview-row\"\n className=\"flex min-w-0 items-center justify-between gap-control-gap rounded-card border border-[var(--color-border-subtle)] bg-bg-canvas px-3 py-2\"\n >\n <p className=\"flex min-w-0 items-center gap-icon-gap text-sm font-medium text-text-primary\">\n {link.icon}\n <span className=\"truncate\">\n {t('bigconsole.widget.aiPreviewMini.linkReady', '{{label}} ready', { label: link.label })}\n </span>\n </p>\n <Button size=\"sm\" variant=\"ghost\" onClick={() => go(link.path)}>\n {t('bigconsole.widget.aiPreviewMini.open', 'Open')}\n <ArrowUpRight className=\"ml-1 size-3.5\" />\n </Button>\n </div>\n ))\n )}\n {entityIds.dashboard ? (\n // fe-libs `Button` is `whitespace-nowrap` with a fixed `h-8` at\n // `size=\"sm\"`, so this long label overflowed the panel (which is\n // `overflow-hidden`) at 390px — and the hi/ta strings are longer\n // still. `h-auto whitespace-normal` lets it wrap to two lines\n // instead of being clipped.\n <Button\n size=\"sm\"\n variant=\"outline\"\n className=\"h-auto w-full whitespace-normal py-1.5 text-center\"\n data-testid=\"ai-preview-open-in-bigconsole\"\n onClick={() => go(`/bigconsole/dashboards/${entityIds.dashboard ?? ''}`)}\n >\n {t('bigconsole.widget.aiPreviewMini.openInBigConsole', 'Open in BigConsole for the live view')}\n <ArrowUpRight className=\"ml-1 size-3.5\" />\n </Button>\n ) : null}\n </Section>\n </div>\n </AssistantEntityPreview>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useBigConsoleNavigate as e } from "../../context/BigConsoleContext.js";
|
|
2
2
|
import "../../context/index.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { PREVIEW_STEPS as t, useAssistantRunStore as n } from "../../assistant/assistantRunStore.js";
|
|
4
|
+
import { useAssistantRunWorkspace as r } from "../../assistant/useAssistantRunWorkspace.js";
|
|
5
5
|
import { Section as i } from "../common/Section.js";
|
|
6
6
|
import "../common/index.js";
|
|
7
7
|
import { DOCK_CLEARANCE as a, PreviewMeta as o, STEP_LABEL as s, formatAgo as c, previewLabels as l } from "./previewShared.js";
|
|
@@ -31,37 +31,39 @@ function D({ onOpen: e }) {
|
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
function O() {
|
|
34
|
-
let { t: _ } = w()
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
let { t: _ } = w();
|
|
35
|
+
r();
|
|
36
|
+
let O = e(), k = n((e) => e.isRunning), A = n((e) => e.previewOpen), j = n((e) => e.hasRun), M = n((e) => e.steps), N = n((e) => e.entityIds), P = n((e) => e.caption), F = n((e) => e.error), I = n((e) => e.updatedAt), L = n((e) => e.openPreview), R = n((e) => e.closePreview), z = n((e) => e.setRichMounted);
|
|
37
|
+
m(() => (z(!0), () => {
|
|
38
|
+
z(!1);
|
|
39
|
+
}), [z]);
|
|
40
|
+
let [B, V] = g(0), H = h(!1);
|
|
39
41
|
if (m(() => {
|
|
40
|
-
|
|
41
|
-
}, [
|
|
42
|
-
if (!
|
|
42
|
+
k && !H.current && V((e) => e + 1), H.current = k;
|
|
43
|
+
}, [k]), !j) return null;
|
|
44
|
+
if (!A) return /* @__PURE__ */ C("div", {
|
|
43
45
|
className: "flex flex-wrap items-center justify-end gap-control-gap px-4 pt-3 md:px-6",
|
|
44
46
|
style: { marginRight: a },
|
|
45
47
|
children: [/* @__PURE__ */ S("span", {
|
|
46
48
|
className: "text-xs text-text-muted",
|
|
47
|
-
children:
|
|
49
|
+
children: k ? _("bigconsole.widget.aiPreview.buildRunning", "AI build running") : _("bigconsole.widget.aiPreview.buildFinished", "AI build finished")
|
|
48
50
|
}), /* @__PURE__ */ C(x, {
|
|
49
51
|
size: "sm",
|
|
50
52
|
variant: "outline",
|
|
51
|
-
onClick:
|
|
53
|
+
onClick: L,
|
|
52
54
|
"data-testid": "ai-preview-open",
|
|
53
55
|
children: [
|
|
54
56
|
/* @__PURE__ */ S(y, { className: "mr-1.5 size-4" }),
|
|
55
57
|
_("bigconsole.widget.aiPreview.previewButton", "Preview"),
|
|
56
|
-
|
|
58
|
+
k ? /* @__PURE__ */ S(v, { className: "ml-1.5 size-3.5 animate-spin" }) : null
|
|
57
59
|
]
|
|
58
60
|
})]
|
|
59
61
|
});
|
|
60
|
-
let
|
|
62
|
+
let U = N.dashboard, W = t.map((e) => ({
|
|
61
63
|
id: e,
|
|
62
64
|
label: _(s[e].key, s[e].fallback),
|
|
63
|
-
status:
|
|
64
|
-
})),
|
|
65
|
+
status: M[e]
|
|
66
|
+
})), G = [
|
|
65
67
|
{
|
|
66
68
|
id: "datasink",
|
|
67
69
|
label: _("bigconsole.widget.aiPreview.tab.dataSink", "Data Sink"),
|
|
@@ -71,10 +73,10 @@ function O() {
|
|
|
71
73
|
headingLevel: 3,
|
|
72
74
|
title: _("bigconsole.widget.aiPreview.dataSinkRows", "Data sink rows"),
|
|
73
75
|
description: _("bigconsole.widget.aiPreview.dataSinkRowsHint", "The rows the assistant just landed, straight from the sink."),
|
|
74
|
-
actions:
|
|
76
|
+
actions: N.datasink ? /* @__PURE__ */ S(D, { onOpen: () => O(`/datasinks/${N.datasink}`) }) : void 0,
|
|
75
77
|
children: /* @__PURE__ */ S(d, {
|
|
76
|
-
id:
|
|
77
|
-
live:
|
|
78
|
+
id: N.datasink,
|
|
79
|
+
live: k
|
|
78
80
|
})
|
|
79
81
|
}) })
|
|
80
82
|
},
|
|
@@ -87,8 +89,8 @@ function O() {
|
|
|
87
89
|
headingLevel: 3,
|
|
88
90
|
title: _("bigconsole.widget.aiPreview.tab.dashboard", "Dashboard"),
|
|
89
91
|
description: _("bigconsole.widget.aiPreview.dashboardHint", "The live board, rendered exactly as the real page renders it."),
|
|
90
|
-
actions:
|
|
91
|
-
children: /* @__PURE__ */ S(u, { dashboardId:
|
|
92
|
+
actions: U ? /* @__PURE__ */ S(D, { onOpen: () => O(`/dashboards/${U}`) }) : void 0,
|
|
93
|
+
children: /* @__PURE__ */ S(u, { dashboardId: U })
|
|
92
94
|
}) })
|
|
93
95
|
},
|
|
94
96
|
{
|
|
@@ -100,10 +102,10 @@ function O() {
|
|
|
100
102
|
headingLevel: 3,
|
|
101
103
|
title: _("bigconsole.widget.aiPreview.parserOutput", "Parser output"),
|
|
102
104
|
description: _("bigconsole.widget.aiPreview.parserOutputHint", "The rows after the parser's transform, so you can check the shape."),
|
|
103
|
-
actions:
|
|
105
|
+
actions: N.parser ? /* @__PURE__ */ S(D, { onOpen: () => O(`/parsers/${N.parser}`) }) : void 0,
|
|
104
106
|
children: /* @__PURE__ */ S(f, {
|
|
105
|
-
parserId:
|
|
106
|
-
live:
|
|
107
|
+
parserId: N.parser,
|
|
108
|
+
live: k
|
|
107
109
|
})
|
|
108
110
|
}) })
|
|
109
111
|
},
|
|
@@ -116,10 +118,10 @@ function O() {
|
|
|
116
118
|
headingLevel: 3,
|
|
117
119
|
title: _("bigconsole.widget.aiPreview.widgets", "Widgets"),
|
|
118
120
|
description: _("bigconsole.widget.aiPreview.widgetsHint", "Every widget on the new dashboard, rendered live."),
|
|
119
|
-
actions:
|
|
121
|
+
actions: U ? /* @__PURE__ */ S(D, { onOpen: () => O(`/dashboards/${U}`) }) : void 0,
|
|
120
122
|
children: /* @__PURE__ */ S(p, {
|
|
121
|
-
dashboardId:
|
|
122
|
-
live:
|
|
123
|
+
dashboardId: U,
|
|
124
|
+
live: k
|
|
123
125
|
})
|
|
124
126
|
}) })
|
|
125
127
|
}
|
|
@@ -133,23 +135,23 @@ function O() {
|
|
|
133
135
|
children: _("bigconsole.widget.aiPreview.title", "AI Preview")
|
|
134
136
|
}), /* @__PURE__ */ S(T, {
|
|
135
137
|
title: _("bigconsole.widget.aiPreview.title", "AI Preview"),
|
|
136
|
-
runKey: String(
|
|
137
|
-
steps:
|
|
138
|
-
tabs:
|
|
138
|
+
runKey: String(B),
|
|
139
|
+
steps: W,
|
|
140
|
+
tabs: G,
|
|
139
141
|
follow: "produced",
|
|
140
|
-
caption:
|
|
141
|
-
error:
|
|
142
|
+
caption: k ? P || _("bigconsole.widget.aiPreview.working", "Working…") : _("bigconsole.widget.aiPreview.done", "Done"),
|
|
143
|
+
error: F,
|
|
142
144
|
meta: /* @__PURE__ */ S(o, {
|
|
143
|
-
state:
|
|
144
|
-
stateLabel:
|
|
145
|
+
state: k ? "running" : F ? "stopped" : "idle",
|
|
146
|
+
stateLabel: k ? _("bigconsole.widget.aiPreview.live", "Live") : F ? _("bigconsole.widget.aiPreview.stopped", "Stopped") : null,
|
|
145
147
|
updatedLabel: _("bigconsole.widget.aiPreview.updated", "updated"),
|
|
146
|
-
updatedAt:
|
|
148
|
+
updatedAt: I,
|
|
147
149
|
ago: (e) => c(e, _)
|
|
148
150
|
}),
|
|
149
151
|
headerActions: /* @__PURE__ */ S(x, {
|
|
150
152
|
size: "sm",
|
|
151
153
|
variant: "ghost",
|
|
152
|
-
onClick:
|
|
154
|
+
onClick: R,
|
|
153
155
|
"aria-label": _("bigconsole.widget.aiPreview.closeAriaLabel", "Close preview"),
|
|
154
156
|
"data-testid": "ai-preview-close",
|
|
155
157
|
children: /* @__PURE__ */ S(b, { className: "size-4" })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AiPreviewPanel.js","names":[],"sources":["../../../../src/bigconsole/components/preview/AiPreviewPanel.tsx"],"sourcesContent":["/**\n * AI Preview — watch the assistant build, with the real content inline.\n *\n * Opens by itself the moment a prompt starts. A step rail (driven by the agent's\n * own narration) shows DataSink → Dashboard → Parser → Widget filling in, and\n * the four tabs show the ACTUAL content of each entity as it lands — the data\n * sink's rows, the live dashboard, the parser's transformed output, the rendered\n * widget charts — not a list of links. Each tab has an \"Open\" button that jumps\n * to the real page.\n *\n * The content renderers need Apollo + the BigConsole providers, which is why\n * this full panel lives inside BigConsoleRoot; the store-only app-shell mini\n * panel (AiPreviewMiniPanel) keeps the lighter link view for other pages.\n *\n * ★★★ THE SHELL, RAIL AND TABS ARE fe-libs' NOW (`AssistantEntityPreview`).\n * What stays here is what is actually BigConsole's: which entities exist, how to\n * render each one, and where the panel sits relative to the assistant sidebar.\n * The shared surface brought three things this file could not have on its own:\n *\n * - a viewer's tab choice is HONOURED. The local `useEffect` re-selected a tab\n * on every store tick, so clicking one held for about 1.5 seconds before the\n * next progress update took it back — you could not read a tab while the\n * build continued.\n * - the rail can show a FAILED step instead of drawing a failure as \"not\n * started\" (the store still cannot produce one — see `assistantRunStore` —\n * but the surface no longer loses it if it ever does).\n * - keyboard navigation across the tabs, which the Radix tablist here never\n * had overflow handling for below ~430px.\n */\nimport { useEffect, useRef, useState, type ReactNode } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport {\n AssistantEntityPreview,\n type AssistantPreviewStep,\n type AssistantPreviewTab,\n} from '@burdenoff/fe-libs/shared/assistant';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useAssistantPanelShowsPreview } from '../../assistant/useAssistantPanelShowsPreview';\nimport { ArrowUpRight, Loader2, Sparkles, X } from 'lucide-react';\n\nimport { PREVIEW_STEPS, useAssistantRunStore } from '../../assistant/assistantRunStore';\nimport { useBigConsoleNavigate } from '../../context';\nimport { Section } from '../common';\nimport { DOCK_CLEARANCE, PreviewMeta, STEP_LABEL, formatAgo, previewLabels } from './previewShared';\nimport { DashboardContent, DataSinkContent, ParserContent, WidgetsContent } from './AiPreviewContent';\n\n/**\n * The scroll box each tab's content sits in.\n *\n * ★★ The cap is NOT cosmetic and does not belong in fe-libs: the shared surface\n * is laid out by whatever contains it, and this panel is injected ABOVE a\n * routed page — so without a cap a long data-sink preview pushes the whole page\n * down. `min(440px, 60vh)` is the original value; the hard 440px alone ate most\n * of the fold on a short laptop screen.\n */\nfunction TabBody({ children }: { children: ReactNode }) {\n return (\n <div className=\"min-w-0 overflow-y-auto p-3\" style={{ maxHeight: 'min(440px, 60vh)' }}>\n {children}\n </div>\n );\n}\n\n/** Jump-to-the-real-page button, shared by all four tabs. */\nfunction OpenEntityButton({ onOpen }: { onOpen: () => void }) {\n const { t } = useI18n();\n return (\n <Button size=\"sm\" variant=\"outline\" onClick={onOpen} data-testid=\"ai-preview-open-entity\">\n {t('bigconsole.widget.aiPreview.open', 'Open')}\n <ArrowUpRight className=\"ml-1 size-3.5\" />\n </Button>\n );\n}\n\nexport function AiPreviewPanel() {\n const { t } = useI18n();\n // ★★★ ONE SURFACE PER RUN. fe-libs renders this same controller inside its own\n // in-chat overlay; without this the page surface stayed up beside it and one\n // run was drawn twice, side by side (measured in prod, BOFF-7799).\n const panelShowsPreview = useAssistantPanelShowsPreview();\n const navigate = useBigConsoleNavigate();\n\n const isRunning = useAssistantRunStore((state) => state.isRunning);\n const previewOpen = useAssistantRunStore((state) => state.previewOpen);\n const hasRun = useAssistantRunStore((state) => state.hasRun);\n const steps = useAssistantRunStore((state) => state.steps);\n const entityIds = useAssistantRunStore((state) => state.entityIds);\n const caption = useAssistantRunStore((state) => state.caption);\n const error = useAssistantRunStore((state) => state.error);\n const updatedAt = useAssistantRunStore((state) => state.updatedAt);\n const openPreview = useAssistantRunStore((state) => state.openPreview);\n const closePreview = useAssistantRunStore((state) => state.closePreview);\n const setRichMounted = useAssistantRunStore((state) => state.setRichMounted);\n\n useEffect(() => {\n setRichMounted(true);\n return () => {\n setRichMounted(false);\n };\n }, [setRichMounted]);\n\n /**\n * ★★★ WHAT COUNTS AS A \"RUN\" HERE, for the purpose of releasing a pinned tab.\n *\n * The store has no run id and deliberately never resets — a new prompt\n * CONTINUES the same build, preserving every step and entity id already made\n * (`assistantRunStore.startRun`). So the key is minted here, on the rising\n * edge of `isRunning`: each new turn is a new run, which releases a tab the\n * viewer pinned during the previous one. That is the right side to err on —\n * asking for something new and being shown the old tab is worse than losing a\n * pin you made a turn ago.\n */\n const [runKey, setRunKey] = useState(0);\n const wasRunningRef = useRef(false);\n useEffect(() => {\n if (isRunning && !wasRunningRef.current) setRunKey((n) => n + 1);\n wasRunningRef.current = isRunning;\n }, [isRunning]);\n\n if (!hasRun) return null;\n // The shared panel's overlay is already showing this run — stand down rather\n // than draw it a second time beside itself.\n if (panelShowsPreview) return null;\n\n if (!previewOpen) {\n return (\n <div\n className=\"flex flex-wrap items-center justify-end gap-control-gap px-4 pt-3 md:px-6\"\n style={{ marginRight: DOCK_CLEARANCE }}\n >\n {/* The lone button gave no clue what it belonged to. */}\n <span className=\"text-xs text-text-muted\">\n {isRunning\n ? t('bigconsole.widget.aiPreview.buildRunning', 'AI build running')\n : t('bigconsole.widget.aiPreview.buildFinished', 'AI build finished')}\n </span>\n <Button size=\"sm\" variant=\"outline\" onClick={openPreview} data-testid=\"ai-preview-open\">\n <Sparkles className=\"mr-1.5 size-4\" />\n {t('bigconsole.widget.aiPreview.previewButton', 'Preview')}\n {isRunning ? <Loader2 className=\"ml-1.5 size-3.5 animate-spin\" /> : null}\n </Button>\n </div>\n );\n }\n\n const dashboardId = entityIds.dashboard;\n\n const railSteps: AssistantPreviewStep[] = PREVIEW_STEPS.map((key) => ({\n id: key,\n label: t(STEP_LABEL[key].key, STEP_LABEL[key].fallback),\n status: steps[key],\n }));\n\n const tabs: AssistantPreviewTab[] = [\n {\n id: 'datasink',\n label: t('bigconsole.widget.aiPreview.tab.dataSink', 'Data Sink'),\n render: () => (\n <TabBody>\n <Section\n emphasis=\"standard\"\n padding=\"compact\"\n headingLevel={3}\n title={t('bigconsole.widget.aiPreview.dataSinkRows', 'Data sink rows')}\n description={t(\n 'bigconsole.widget.aiPreview.dataSinkRowsHint',\n 'The rows the assistant just landed, straight from the sink.'\n )}\n actions={\n entityIds.datasink ? (\n <OpenEntityButton onOpen={() => navigate(`/datasinks/${entityIds.datasink}`)} />\n ) : undefined\n }\n >\n <DataSinkContent id={entityIds.datasink} live={isRunning} />\n </Section>\n </TabBody>\n ),\n },\n {\n id: 'dashboard',\n label: t('bigconsole.widget.aiPreview.tab.dashboard', 'Dashboard'),\n render: () => (\n <TabBody>\n <Section\n emphasis=\"standard\"\n padding=\"compact\"\n headingLevel={3}\n title={t('bigconsole.widget.aiPreview.tab.dashboard', 'Dashboard')}\n description={t(\n 'bigconsole.widget.aiPreview.dashboardHint',\n 'The live board, rendered exactly as the real page renders it.'\n )}\n actions={\n dashboardId ? <OpenEntityButton onOpen={() => navigate(`/dashboards/${dashboardId}`)} /> : undefined\n }\n >\n <DashboardContent dashboardId={dashboardId} />\n </Section>\n </TabBody>\n ),\n },\n {\n id: 'parser',\n label: t('bigconsole.widget.aiPreview.tab.parser', 'Parser'),\n render: () => (\n <TabBody>\n <Section\n emphasis=\"standard\"\n padding=\"compact\"\n headingLevel={3}\n title={t('bigconsole.widget.aiPreview.parserOutput', 'Parser output')}\n description={t(\n 'bigconsole.widget.aiPreview.parserOutputHint',\n \"The rows after the parser's transform, so you can check the shape.\"\n )}\n actions={\n entityIds.parser ? (\n <OpenEntityButton onOpen={() => navigate(`/parsers/${entityIds.parser}`)} />\n ) : undefined\n }\n >\n <ParserContent parserId={entityIds.parser} live={isRunning} />\n </Section>\n </TabBody>\n ),\n },\n {\n id: 'widget',\n label: t('bigconsole.widget.aiPreview.tab.widget', 'Widget'),\n render: () => (\n <TabBody>\n <Section\n emphasis=\"standard\"\n padding=\"compact\"\n headingLevel={3}\n title={t('bigconsole.widget.aiPreview.widgets', 'Widgets')}\n description={t(\n 'bigconsole.widget.aiPreview.widgetsHint',\n 'Every widget on the new dashboard, rendered live.'\n )}\n actions={\n dashboardId ? <OpenEntityButton onOpen={() => navigate(`/dashboards/${dashboardId}`)} /> : undefined\n }\n >\n <WidgetsContent dashboardId={dashboardId} live={isRunning} />\n </Section>\n </TabBody>\n ),\n },\n ];\n\n return (\n // ★ The wrapper owns POSITIONING only — clearance for the assistant dock,\n // page gutters, and the testid BigConsole already had. Everything visual\n // belongs to the shared surface.\n //\n // ★★ PADDING, not margin: `mx-4 md:mx-6` is the page gutter and it is\n // RESPONSIVE. See DOCK_CLEARANCE's docblock.\n <div data-testid=\"ai-preview-panel\" style={{ paddingRight: DOCK_CLEARANCE }} className=\"mx-4 mt-3 min-w-0 md:mx-6\">\n {/* ★★ The panel's own heading, restored. The shared surface renders its\n title as a styled <span> inside a <section aria-label>, which labels\n the region but leaves NO heading — while it still renders an <h3> for\n the step rail. main had <h2>AI Preview</h2> above that <h3>, so the\n migration both removed the panel from screen-reader heading navigation\n and orphaned the h3 under whatever <h2> the page beneath happens to\n end with. Visually hidden because the surface already shows the title;\n this only restores the structure. */}\n <h2 className=\"sr-only\">{t('bigconsole.widget.aiPreview.title', 'AI Preview')}</h2>\n <AssistantEntityPreview\n title={t('bigconsole.widget.aiPreview.title', 'AI Preview')}\n runKey={String(runKey)}\n steps={railSteps}\n tabs={tabs}\n // ★ Explicit, though it is the default: this panel's own rule was\n // \"show the LATEST thing that actually landed — so the moment the data\n // sink is created you see its rows, not an empty 'waiting for the\n // dashboard' tab\". Stating it keeps that reasoning attached to the\n // consumer that discovered it, not only to the library that adopted it.\n follow=\"produced\"\n // ★★ TWO branches, not three, and deliberately so: the shared surface\n // renders `{error ? '' : caption}` — it BLANKS the caption whenever\n // `error` is set, so a failure branch here would compute a string that\n // is then discarded. The failure is named in `meta` instead, which\n // `PreviewHeader` renders without consulting `error`.\n caption={\n isRunning\n ? caption || t('bigconsole.widget.aiPreview.working', 'Working…')\n : t('bigconsole.widget.aiPreview.done', 'Done')\n }\n error={error}\n meta={\n <PreviewMeta\n state={isRunning ? 'running' : error ? 'stopped' : 'idle'}\n stateLabel={\n isRunning\n ? t('bigconsole.widget.aiPreview.live', 'Live')\n : error\n ? t('bigconsole.widget.aiPreview.stopped', 'Stopped')\n : null\n }\n updatedLabel={t('bigconsole.widget.aiPreview.updated', 'updated')}\n updatedAt={updatedAt}\n ago={(seconds) => formatAgo(seconds, t)}\n />\n }\n headerActions={\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={closePreview}\n aria-label={t('bigconsole.widget.aiPreview.closeAriaLabel', 'Close preview')}\n data-testid=\"ai-preview-close\"\n >\n <X className=\"size-4\" />\n </Button>\n }\n labels={previewLabels(t)}\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AiPreviewPanel.js","names":[],"sources":["../../../../src/bigconsole/components/preview/AiPreviewPanel.tsx"],"sourcesContent":["/**\n * AI Preview — watch the assistant build, with the real content inline.\n *\n * Opens by itself the moment a prompt starts. A step rail (driven by the agent's\n * own narration) shows DataSink → Dashboard → Parser → Widget filling in, and\n * the four tabs show the ACTUAL content of each entity as it lands — the data\n * sink's rows, the live dashboard, the parser's transformed output, the rendered\n * widget charts — not a list of links. Each tab has an \"Open\" button that jumps\n * to the real page.\n *\n * The content renderers need Apollo + the BigConsole providers, which is why\n * this full panel lives inside BigConsoleRoot; the store-only app-shell mini\n * panel (AiPreviewMiniPanel) keeps the lighter link view for other pages.\n *\n * ★★★ THE SHELL, RAIL AND TABS ARE fe-libs' NOW (`AssistantEntityPreview`).\n * What stays here is what is actually BigConsole's: which entities exist, how to\n * render each one, and where the panel sits relative to the assistant sidebar.\n * The shared surface brought three things this file could not have on its own:\n *\n * - a viewer's tab choice is HONOURED. The local `useEffect` re-selected a tab\n * on every store tick, so clicking one held for about 1.5 seconds before the\n * next progress update took it back — you could not read a tab while the\n * build continued.\n * - the rail can show a FAILED step instead of drawing a failure as \"not\n * started\" (the store still cannot produce one — see `assistantRunStore` —\n * but the surface no longer loses it if it ever does).\n * - keyboard navigation across the tabs, which the Radix tablist here never\n * had overflow handling for below ~430px.\n */\nimport { useEffect, useRef, useState, type ReactNode } from 'react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport {\n AssistantEntityPreview,\n type AssistantPreviewStep,\n type AssistantPreviewTab,\n} from '@burdenoff/fe-libs/shared/assistant';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { ArrowUpRight, Loader2, Sparkles, X } from 'lucide-react';\n\nimport { PREVIEW_STEPS, useAssistantRunStore } from '../../assistant/assistantRunStore';\nimport { useAssistantRunWorkspace } from '../../assistant/useAssistantRunWorkspace';\nimport { useBigConsoleNavigate } from '../../context';\nimport { Section } from '../common';\nimport { DOCK_CLEARANCE, PreviewMeta, STEP_LABEL, formatAgo, previewLabels } from './previewShared';\nimport { DashboardContent, DataSinkContent, ParserContent, WidgetsContent } from './AiPreviewContent';\n\n/**\n * The scroll box each tab's content sits in.\n *\n * ★★ The cap is NOT cosmetic and does not belong in fe-libs: the shared surface\n * is laid out by whatever contains it, and this panel is injected ABOVE a\n * routed page — so without a cap a long data-sink preview pushes the whole page\n * down. `min(440px, 60vh)` is the original value; the hard 440px alone ate most\n * of the fold on a short laptop screen.\n */\nfunction TabBody({ children }: { children: ReactNode }) {\n return (\n <div className=\"min-w-0 overflow-y-auto p-3\" style={{ maxHeight: 'min(440px, 60vh)' }}>\n {children}\n </div>\n );\n}\n\n/** Jump-to-the-real-page button, shared by all four tabs. */\nfunction OpenEntityButton({ onOpen }: { onOpen: () => void }) {\n const { t } = useI18n();\n return (\n <Button size=\"sm\" variant=\"outline\" onClick={onOpen} data-testid=\"ai-preview-open-entity\">\n {t('bigconsole.widget.aiPreview.open', 'Open')}\n <ArrowUpRight className=\"ml-1 size-3.5\" />\n </Button>\n );\n}\n\nexport function AiPreviewPanel() {\n const { t } = useI18n();\n // ★★★ THE PAGE SURFACES ARE THE ONLY SURFACES (BOFF-7799). The assistant's\n // controller is headless, so nothing inside the panel competes with this one\n // and there is nothing to stand down for. Binding the workspace restores the\n // run after a reload in this tab and clears it on a workspace switch.\n useAssistantRunWorkspace();\n const navigate = useBigConsoleNavigate();\n\n const isRunning = useAssistantRunStore((state) => state.isRunning);\n const previewOpen = useAssistantRunStore((state) => state.previewOpen);\n const hasRun = useAssistantRunStore((state) => state.hasRun);\n const steps = useAssistantRunStore((state) => state.steps);\n const entityIds = useAssistantRunStore((state) => state.entityIds);\n const caption = useAssistantRunStore((state) => state.caption);\n const error = useAssistantRunStore((state) => state.error);\n const updatedAt = useAssistantRunStore((state) => state.updatedAt);\n const openPreview = useAssistantRunStore((state) => state.openPreview);\n const closePreview = useAssistantRunStore((state) => state.closePreview);\n const setRichMounted = useAssistantRunStore((state) => state.setRichMounted);\n\n useEffect(() => {\n setRichMounted(true);\n return () => {\n setRichMounted(false);\n };\n }, [setRichMounted]);\n\n /**\n * ★★★ WHAT COUNTS AS A \"RUN\" HERE, for the purpose of releasing a pinned tab.\n *\n * The store has no run id and deliberately never resets — a new prompt\n * CONTINUES the same build, preserving every step and entity id already made\n * (`assistantRunStore.startRun`). So the key is minted here, on the rising\n * edge of `isRunning`: each new turn is a new run, which releases a tab the\n * viewer pinned during the previous one. That is the right side to err on —\n * asking for something new and being shown the old tab is worse than losing a\n * pin you made a turn ago.\n */\n const [runKey, setRunKey] = useState(0);\n const wasRunningRef = useRef(false);\n useEffect(() => {\n if (isRunning && !wasRunningRef.current) setRunKey((n) => n + 1);\n wasRunningRef.current = isRunning;\n }, [isRunning]);\n\n if (!hasRun) return null;\n\n if (!previewOpen) {\n return (\n <div\n className=\"flex flex-wrap items-center justify-end gap-control-gap px-4 pt-3 md:px-6\"\n style={{ marginRight: DOCK_CLEARANCE }}\n >\n {/* The lone button gave no clue what it belonged to. */}\n <span className=\"text-xs text-text-muted\">\n {isRunning\n ? t('bigconsole.widget.aiPreview.buildRunning', 'AI build running')\n : t('bigconsole.widget.aiPreview.buildFinished', 'AI build finished')}\n </span>\n <Button size=\"sm\" variant=\"outline\" onClick={openPreview} data-testid=\"ai-preview-open\">\n <Sparkles className=\"mr-1.5 size-4\" />\n {t('bigconsole.widget.aiPreview.previewButton', 'Preview')}\n {isRunning ? <Loader2 className=\"ml-1.5 size-3.5 animate-spin\" /> : null}\n </Button>\n </div>\n );\n }\n\n const dashboardId = entityIds.dashboard;\n\n const railSteps: AssistantPreviewStep[] = PREVIEW_STEPS.map((key) => ({\n id: key,\n label: t(STEP_LABEL[key].key, STEP_LABEL[key].fallback),\n status: steps[key],\n }));\n\n const tabs: AssistantPreviewTab[] = [\n {\n id: 'datasink',\n label: t('bigconsole.widget.aiPreview.tab.dataSink', 'Data Sink'),\n render: () => (\n <TabBody>\n <Section\n emphasis=\"standard\"\n padding=\"compact\"\n headingLevel={3}\n title={t('bigconsole.widget.aiPreview.dataSinkRows', 'Data sink rows')}\n description={t(\n 'bigconsole.widget.aiPreview.dataSinkRowsHint',\n 'The rows the assistant just landed, straight from the sink.'\n )}\n actions={\n entityIds.datasink ? (\n <OpenEntityButton onOpen={() => navigate(`/datasinks/${entityIds.datasink}`)} />\n ) : undefined\n }\n >\n <DataSinkContent id={entityIds.datasink} live={isRunning} />\n </Section>\n </TabBody>\n ),\n },\n {\n id: 'dashboard',\n label: t('bigconsole.widget.aiPreview.tab.dashboard', 'Dashboard'),\n render: () => (\n <TabBody>\n <Section\n emphasis=\"standard\"\n padding=\"compact\"\n headingLevel={3}\n title={t('bigconsole.widget.aiPreview.tab.dashboard', 'Dashboard')}\n description={t(\n 'bigconsole.widget.aiPreview.dashboardHint',\n 'The live board, rendered exactly as the real page renders it.'\n )}\n actions={\n dashboardId ? <OpenEntityButton onOpen={() => navigate(`/dashboards/${dashboardId}`)} /> : undefined\n }\n >\n <DashboardContent dashboardId={dashboardId} />\n </Section>\n </TabBody>\n ),\n },\n {\n id: 'parser',\n label: t('bigconsole.widget.aiPreview.tab.parser', 'Parser'),\n render: () => (\n <TabBody>\n <Section\n emphasis=\"standard\"\n padding=\"compact\"\n headingLevel={3}\n title={t('bigconsole.widget.aiPreview.parserOutput', 'Parser output')}\n description={t(\n 'bigconsole.widget.aiPreview.parserOutputHint',\n \"The rows after the parser's transform, so you can check the shape.\"\n )}\n actions={\n entityIds.parser ? (\n <OpenEntityButton onOpen={() => navigate(`/parsers/${entityIds.parser}`)} />\n ) : undefined\n }\n >\n <ParserContent parserId={entityIds.parser} live={isRunning} />\n </Section>\n </TabBody>\n ),\n },\n {\n id: 'widget',\n label: t('bigconsole.widget.aiPreview.tab.widget', 'Widget'),\n render: () => (\n <TabBody>\n <Section\n emphasis=\"standard\"\n padding=\"compact\"\n headingLevel={3}\n title={t('bigconsole.widget.aiPreview.widgets', 'Widgets')}\n description={t(\n 'bigconsole.widget.aiPreview.widgetsHint',\n 'Every widget on the new dashboard, rendered live.'\n )}\n actions={\n dashboardId ? <OpenEntityButton onOpen={() => navigate(`/dashboards/${dashboardId}`)} /> : undefined\n }\n >\n <WidgetsContent dashboardId={dashboardId} live={isRunning} />\n </Section>\n </TabBody>\n ),\n },\n ];\n\n return (\n // ★ The wrapper owns POSITIONING only — clearance for the assistant dock,\n // page gutters, and the testid BigConsole already had. Everything visual\n // belongs to the shared surface.\n //\n // ★★ PADDING, not margin: `mx-4 md:mx-6` is the page gutter and it is\n // RESPONSIVE. See DOCK_CLEARANCE's docblock.\n <div data-testid=\"ai-preview-panel\" style={{ paddingRight: DOCK_CLEARANCE }} className=\"mx-4 mt-3 min-w-0 md:mx-6\">\n {/* ★★ The panel's own heading, restored. The shared surface renders its\n title as a styled <span> inside a <section aria-label>, which labels\n the region but leaves NO heading — while it still renders an <h3> for\n the step rail. main had <h2>AI Preview</h2> above that <h3>, so the\n migration both removed the panel from screen-reader heading navigation\n and orphaned the h3 under whatever <h2> the page beneath happens to\n end with. Visually hidden because the surface already shows the title;\n this only restores the structure. */}\n <h2 className=\"sr-only\">{t('bigconsole.widget.aiPreview.title', 'AI Preview')}</h2>\n <AssistantEntityPreview\n title={t('bigconsole.widget.aiPreview.title', 'AI Preview')}\n runKey={String(runKey)}\n steps={railSteps}\n tabs={tabs}\n // ★ Explicit, though it is the default: this panel's own rule was\n // \"show the LATEST thing that actually landed — so the moment the data\n // sink is created you see its rows, not an empty 'waiting for the\n // dashboard' tab\". Stating it keeps that reasoning attached to the\n // consumer that discovered it, not only to the library that adopted it.\n follow=\"produced\"\n // ★★ TWO branches, not three, and deliberately so: the shared surface\n // renders `{error ? '' : caption}` — it BLANKS the caption whenever\n // `error` is set, so a failure branch here would compute a string that\n // is then discarded. The failure is named in `meta` instead, which\n // `PreviewHeader` renders without consulting `error`.\n caption={\n isRunning\n ? caption || t('bigconsole.widget.aiPreview.working', 'Working…')\n : t('bigconsole.widget.aiPreview.done', 'Done')\n }\n error={error}\n meta={\n <PreviewMeta\n state={isRunning ? 'running' : error ? 'stopped' : 'idle'}\n stateLabel={\n isRunning\n ? t('bigconsole.widget.aiPreview.live', 'Live')\n : error\n ? t('bigconsole.widget.aiPreview.stopped', 'Stopped')\n : null\n }\n updatedLabel={t('bigconsole.widget.aiPreview.updated', 'updated')}\n updatedAt={updatedAt}\n ago={(seconds) => formatAgo(seconds, t)}\n />\n }\n headerActions={\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={closePreview}\n aria-label={t('bigconsole.widget.aiPreview.closeAriaLabel', 'Close preview')}\n data-testid=\"ai-preview-close\"\n >\n <X className=\"size-4\" />\n </Button>\n }\n labels={previewLabels(t)}\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@ import { jsx as n, jsxs as r } from "react/jsx-runtime";
|
|
|
4
4
|
function i(e, t) {
|
|
5
5
|
return e < 5 ? t("bigconsole.widget.aiPreview.agoJustNow", "just now") : e < 60 ? t("bigconsole.widget.aiPreview.agoSeconds", "{{n}}s ago", { n: e }) : e < 3600 ? t("bigconsole.widget.aiPreview.agoMinutes", "{{n}}m ago", { n: Math.floor(e / 60) }) : t("bigconsole.widget.aiPreview.agoHours", "{{n}}h ago", { n: Math.floor(e / 3600) });
|
|
6
6
|
}
|
|
7
|
-
var a = "var(--assistant-dock-width, 0px)", o = {
|
|
7
|
+
var a = "var(--assistant-panel-inset, var(--assistant-dock-width, 0px))", o = {
|
|
8
8
|
datasink: {
|
|
9
9
|
key: "bigconsole.widget.aiPreview.tab.dataSink",
|
|
10
10
|
fallback: "Data Sink"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"previewShared.js","names":[],"sources":["../../../../src/bigconsole/components/preview/previewShared.tsx"],"sourcesContent":["/**\n * What is still BigConsole's about the AI preview surfaces.\n *\n * ★★★ THE SHELL, DIVIDERS AND STEP RAIL HAVE MOVED TO fe-libs\n * (`AssistantEntityPreview` / `AssistantStepRail`). They existed here so the\n * full panel and the mini panel could not drift apart; the shared component now\n * guarantees that for both, and for every other product that adopts it.\n *\n * What remains is genuinely local: BigConsole's relationship to the assistant\n * DOCK, and how it words a relative timestamp.\n *\n * Nothing here touches Apollo, React Query, BigConsole context — or, since BC4,\n * the DOM: the dock clearance is a CSS custom property fe-libs publishes, so\n * what is left is one constant plus formatting, which is why the mini panel can\n * live in the app shell without dragging the data layer along.\n */\nimport { useEffect, useState } from 'react';\n\nimport type { AssistantEntityPreviewProps } from '@burdenoff/fe-libs/shared/assistant';\n\nimport type { PreviewStepKey } from '../../assistant/assistantRunStore';\n\n/** The `t` both panels already hold — accepted rather than imported, so this\n * file keeps no provider requirement of its own. */\ntype Translate = (key: string, fallback: string, params?: Record<string, string | number>) => string;\n\n/**\n * \"just now\" / \"40s ago\" / \"12m ago\" / \"3h ago\".\n *\n * ★★ TRANSLATED, and it now has an hour bucket. It used to return English in\n * all three locales (en, hi, ta), and a panel left open past an hour read \"240m ago\" rather\n * than \"4h ago\" — both of which were only visible on the surface that outlives\n * a build, which is exactly the one people leave sitting there.\n */\nexport function formatAgo(seconds: number, t: Translate): string {\n if (seconds < 5) return t('bigconsole.widget.aiPreview.agoJustNow', 'just now');\n if (seconds < 60) {\n return t('bigconsole.widget.aiPreview.agoSeconds', '{{n}}s ago', { n: seconds });\n }\n if (seconds < 3600) {\n return t('bigconsole.widget.aiPreview.agoMinutes', '{{n}}m ago', { n: Math.floor(seconds / 60) });\n }\n // ★ `{{n}}`, not `{n}` — fe-libs' single-brace interpolation was broken until\n // 2026.910.7 and shipped raw braces to users across the fleet. Double braces\n // are what this repo already uses (see `…aiPreviewMini.linkReady`).\n return t('bigconsole.widget.aiPreview.agoHours', '{{n}}h ago', { n: Math.floor(seconds / 3600) });\n}\n\n/**\n * Clearance for the assistant dock, as a CSS length.\n *\n * ★★★ THIS REPLACED A 500ms `querySelector` POLL (BC4, BOFF-7337). The old hook\n * measured `[data-testid=\"assistant-widget-window\"][data-layout=\"sidebar\"]` on an\n * interval because the legacy widget lived in fe-libs, was mounted by the app\n * shell, and emitted nothing when it opened or closed — reading the DOM really\n * was the only signal available.\n *\n * The shared overlay publishes its width instead. fe-libs sets\n * `--assistant-dock-width` on `<html>` from\n * `publishedAssistantDockWidth({ isOpen, docked, isDesktop })` — 440px only when\n * the panel is open AND docked AND the viewport is at least 640px — and REMOVES\n * the property on unmount. So the `0px` fallback covers closed, floating,\n * phone-width and absent alike, which is exactly the set of cases the poll\n * hand-rolled, including its `rect.width < innerWidth * 0.75` phone guard.\n *\n * ★★★ THE FALLBACK FAILS OPEN, SO THE VERSIONS ARE COUPLED. A host shell that\n * pins this package but still mounts the LEGACY `AssistantWidget` publishes no\n * variable at all, this resolves to `0px`, and the preview renders underneath a\n * fixed 440px dock where tabs and Open buttons silently stop taking clicks.\n * Nothing throws and no test fails. bigconsole-app therefore bumps this\n * dependency and mounts `AssistantOverlay` in the SAME commit.\n *\n * ★★ A STRING, NOT A HOOK. It needs no React context and no provider ancestry —\n * which matters because the mini panel is mounted by the app shell, outside the\n * assistant's own tree — and the browser recomputes the margin on open/close\n * with no re-render at all.\n *\n * ★ APPLIED TWO WAYS, DELIBERATELY. The collapsed bars carry only `px-*`, so\n * they take it as `marginRight` and a closed dock reproduces the old `0`\n * exactly. The open panels carry `mx-4 md:mx-6`, so they take it as\n * `paddingRight` and leave those margins alone: the old\n * `assistantWidth ? assistantWidth + 16 : undefined` fell through to the\n * RESPONSIVE 24px gutter at md when closed, and folding a flat `+ 16px` into a\n * calc would have quietly shrunk it on every desktop width. Padding keeps the\n * closed state pixel-identical and only adds clearance while docked.\n */\nexport const DOCK_CLEARANCE = 'var(--assistant-dock-width, 0px)';\n\n/**\n * Step labels, reusing the TAB keys.\n *\n * ★★ They used to be English constants in the store (`PREVIEW_STEP_LABEL`), so\n * the rail rendered \"Data Sink / Dashboard / Parser / Widget\" in every locale —\n * directly beside the SAME four names rendered translated as tab labels. The\n * keys already exist and are already seeded; nothing new was needed to fix it.\n *\n * ★ Lives here because BOTH panels render the rail. It was defined twice, which\n * is the exact duplication this migration exists to remove.\n */\nexport const STEP_LABEL: Record<PreviewStepKey, { key: string; fallback: string }> = {\n datasink: { key: 'bigconsole.widget.aiPreview.tab.dataSink', fallback: 'Data Sink' },\n dashboard: { key: 'bigconsole.widget.aiPreview.tab.dashboard', fallback: 'Dashboard' },\n parser: { key: 'bigconsole.widget.aiPreview.tab.parser', fallback: 'Parser' },\n widget: { key: 'bigconsole.widget.aiPreview.tab.widget', fallback: 'Widget' },\n};\n\n/**\n * The shared surface's chrome strings, already translated.\n *\n * ★★ ONE copy, deliberately. BOTH defects this migration produced were the same\n * bug written twice — the failure caption and the reopen clock each had to be\n * fixed in the full panel AND the mini panel — so anything the two pass\n * identically belongs here rather than inlined in each. That is the same reason\n * the shell lived in this file before it moved to fe-libs.\n *\n * ★ Both surfaces use the `aiPreview` namespace, not `aiPreviewMini`: the rail\n * says the same thing wherever it is rendered, and splitting the keys would mean\n * translating \"running\" and \"failed\" twice per locale for no difference.\n *\n * `AssistantEntityPreviewProps['labels']` rather than a hand-written shape, so a\n * new label added upstream is a type error here instead of a silently missing\n * string.\n */\nexport function previewLabels(t: Translate): AssistantEntityPreviewProps['labels'] {\n const buildProgress = t('bigconsole.widget.aiPreview.buildProgress', 'Build progress');\n const skipped = t('bigconsole.widget.aiPreview.stepSkipped', 'skipped');\n return {\n // ★ main named the failure \"Build stopped\" in a hardcoded English <h3>. The\n // shared surface cannot know that word — it takes it as a label — so this\n // both restores the name AND makes it translatable for the first time.\n errorHeading: t('bigconsole.widget.aiPreview.errorHeading', 'Build stopped'),\n steps: buildProgress,\n stepsHeading: buildProgress,\n skipped,\n stepStatus: {\n pending: t('bigconsole.widget.aiPreview.stepPending', 'not started'),\n running: t('bigconsole.widget.aiPreview.stepRunning', 'running'),\n done: t('bigconsole.widget.aiPreview.stepDone', 'completed'),\n failed: t('bigconsole.widget.aiPreview.stepFailed', 'failed'),\n skipped,\n },\n };\n}\n\n/**\n * The status strip both panels hand to the shared surface as `meta`.\n *\n * ★★★ ONE copy, and this one is not a tidiness argument — the SAME DEFECT was\n * written into both panels twice over. The migration changed this chip from\n * `bg-status-success-bg` to `bg-status-success-bgSubtle` in both files, and that\n * utility DOES NOT EXIST: the token is `--color-status-success-bgSubtle` but the\n * Tailwind preset exposes it as `status-success-bg-subtle`. The app shell emits\n * `.bg-status-success-bg-subtle` and never `.bg-status-success-bgSubtle`, so the\n * \"Live\" pill lost its background entirely and became transparent text.\n * Nothing failed: an unknown utility is simply absent from the stylesheet.\n *\n * ★ Fixed with the ARBITRARY-VALUE form, `bg-[var(--color-status-…-bgSubtle)]`,\n * which is what the other ~30 sites in this repo use. It names the token\n * directly, so it cannot be broken by a preset key being spelled differently\n * from the variable — which is the entire mechanism of the bug above.\n *\n * ★★ It also carries the FAILED state, which the caption cannot. The shared\n * surface blanks `caption` whenever `error` is set — deliberately, so a rotating\n * \"Creating…\" cannot sit under a dead run — so a product that wants the failure\n * NAMED (as this one did on main, with \"Stopped\") has to say it somewhere the\n * error does not suppress. `meta` is that place: `PreviewHeader` renders it\n * without consulting `error`.\n *\n * Labels are passed in already translated, so the `t('literal.key', …)` calls\n * stay greppable at each call site and each panel keeps its own namespace.\n */\nexport function PreviewMeta({\n state,\n stateLabel,\n updatedLabel,\n updatedAt,\n ago,\n}: {\n state: 'running' | 'stopped' | 'idle';\n stateLabel: string | null;\n updatedLabel: string;\n /** When the run last changed, or null when there is nothing to age. */\n updatedAt: number | null;\n /** Renders the elapsed seconds — `formatAgo` bound to the caller's `t`. */\n ago: (seconds: number) => string;\n}) {\n /**\n * ★★★ THE CLOCK LIVES HERE, in the one element that displays it.\n *\n * It used to live in each panel, and that made a 1 Hz `setState` on the PANEL\n * — which re-rendered everything below it. `AssistantEntityPreview` calls\n * `activeTab.render()` on every render with no memo, so the Widgets tab's live\n * dashboard and every Recharts widget inside it re-rendered once a second, for\n * as long as the preview stayed open. main did not: its ticker was gated on\n * `isRunning`, so it stopped when the build did. Ungating it to fix the frozen\n * timestamp is right, but it has to cost one <span>, not the whole subtree.\n *\n * ★★ Mounting is also the gate now. The mini panel is mounted on EVERY route\n * and returns null when the full panel is up — but a hook cannot sit below an\n * early return, so its interval ran on every page regardless of whether\n * anything was shown. Owning the timer here means \"not rendered\" is \"not\n * ticking\", by construction rather than by remembering to pass a flag.\n */\n const [now, setNow] = useState(() => Date.now());\n useEffect(() => {\n if (updatedAt === null) return;\n // ★ Re-read on mount so a reopened panel cannot show the previous open's\n // reading for up to a second. See the note on lint in the git history:\n // reading the clock in render is an ESLint error (Date.now is impure).\n setNow(Date.now());\n const id = window.setInterval(() => setNow(Date.now()), 1000);\n return () => window.clearInterval(id);\n }, [updatedAt]);\n\n const agoSeconds = updatedAt === null ? 0 : Math.max(0, Math.floor((now - updatedAt) / 1000));\n\n const chip =\n 'inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide';\n return (\n <span className=\"inline-flex items-center gap-2\" data-testid=\"ai-preview-ago\">\n {state !== 'idle' && stateLabel ? (\n <span\n className={\n state === 'running'\n ? `${chip} bg-[var(--color-status-success-bgSubtle)] text-status-success-text`\n : `${chip} bg-[var(--color-status-error-bgSubtle)] text-status-error-text`\n }\n data-testid={state === 'running' ? 'ai-preview-live' : 'ai-preview-stopped'}\n >\n {state === 'running' ? (\n <span className=\"relative flex size-2\">\n <span className=\"absolute inline-flex size-full rounded-full bg-status-success-text opacity-75 motion-safe:animate-ping\" />\n <span className=\"relative inline-flex size-2 rounded-full bg-status-success-text\" />\n </span>\n ) : (\n /* ★ Still, not pulsing: the ping says \"this is happening now\", which\n is the opposite of what a stopped run means. */\n <span className=\"relative inline-flex size-2 rounded-full bg-status-error-text\" />\n )}\n {stateLabel}\n </span>\n ) : null}\n <span>\n {updatedLabel} {ago(agoSeconds)}\n </span>\n </span>\n );\n}\n"],"mappings":";;;AAkCA,SAAgB,EAAU,GAAiB,GAAsB;AAW/D,QAVI,IAAU,IAAU,EAAE,0CAA0C,WAAW,GAC3E,IAAU,KACL,EAAE,0CAA0C,cAAc,EAAE,GAAG,GAAS,CAAC,GAE9E,IAAU,OACL,EAAE,0CAA0C,cAAc,EAAE,GAAG,KAAK,MAAM,IAAU,GAAG,EAAE,CAAC,GAK5F,EAAE,wCAAwC,cAAc,EAAE,GAAG,KAAK,MAAM,IAAU,KAAK,EAAE,CAAC;;AAyCnG,IAAa,IAAiB,oCAajB,IAAwE;CACnF,UAAU;EAAE,KAAK;EAA4C,UAAU;EAAa;CACpF,WAAW;EAAE,KAAK;EAA6C,UAAU;EAAa;CACtF,QAAQ;EAAE,KAAK;EAA0C,UAAU;EAAU;CAC7E,QAAQ;EAAE,KAAK;EAA0C,UAAU;EAAU;CAC9E;AAmBD,SAAgB,EAAc,GAAqD;CACjF,IAAM,IAAgB,EAAE,6CAA6C,iBAAiB,EAChF,IAAU,EAAE,2CAA2C,UAAU;AACvE,QAAO;EAIL,cAAc,EAAE,4CAA4C,gBAAgB;EAC5E,OAAO;EACP,cAAc;EACd;EACA,YAAY;GACV,SAAS,EAAE,2CAA2C,cAAc;GACpE,SAAS,EAAE,2CAA2C,UAAU;GAChE,MAAM,EAAE,wCAAwC,YAAY;GAC5D,QAAQ,EAAE,0CAA0C,SAAS;GAC7D;GACD;EACF;;AA8BH,SAAgB,EAAY,EAC1B,UACA,eACA,iBACA,cACA,UASC;CAkBD,IAAM,CAAC,GAAK,KAAU,QAAe,KAAK,KAAK,CAAC;AAChD,SAAgB;AACd,MAAI,MAAc,KAAM;AAIxB,IAAO,KAAK,KAAK,CAAC;EAClB,IAAM,IAAK,OAAO,kBAAkB,EAAO,KAAK,KAAK,CAAC,EAAE,IAAK;AAC7D,eAAa,OAAO,cAAc,EAAG;IACpC,CAAC,EAAU,CAAC;CAEf,IAAM,IAAa,MAAc,OAAO,IAAI,KAAK,IAAI,GAAG,KAAK,OAAO,IAAM,KAAa,IAAK,CAAC,EAEvF,IACJ;AACF,QACE,kBAAC,QAAD;EAAM,WAAU;EAAiC,eAAY;YAA7D,CACG,MAAU,UAAU,IACnB,kBAAC,QAAD;GACE,WACE,MAAU,YACN,GAAG,EAAK,uEACR,GAAG,EAAK;GAEd,eAAa,MAAU,YAAY,oBAAoB;aANzD,CAQG,MAAU,YACT,kBAAC,QAAD;IAAM,WAAU;cAAhB,CACE,kBAAC,QAAD,EAAM,WAAU,0GAA2G,CAAA,EAC3H,kBAAC,QAAD,EAAM,WAAU,mEAAoE,CAAA,CAC/E;QAIP,kBAAC,QAAD,EAAM,WAAU,iEAAkE,CAAA,EAEnF,EACI;OACL,MACJ,kBAAC,QAAD,EAAA,UAAA;GACG;GAAa;GAAE,EAAI,EAAW;GAC1B,EAAA,CAAA,CACF"}
|
|
1
|
+
{"version":3,"file":"previewShared.js","names":[],"sources":["../../../../src/bigconsole/components/preview/previewShared.tsx"],"sourcesContent":["/**\n * What is still BigConsole's about the AI preview surfaces.\n *\n * ★★★ THE SHELL, DIVIDERS AND STEP RAIL HAVE MOVED TO fe-libs\n * (`AssistantEntityPreview` / `AssistantStepRail`). They existed here so the\n * full panel and the mini panel could not drift apart; the shared component now\n * guarantees that for both, and for every other product that adopts it.\n *\n * What remains is genuinely local: BigConsole's relationship to the assistant\n * DOCK, and how it words a relative timestamp.\n *\n * Nothing here touches Apollo, React Query, BigConsole context — or, since BC4,\n * the DOM: the dock clearance is a CSS custom property fe-libs publishes, so\n * what is left is one constant plus formatting, which is why the mini panel can\n * live in the app shell without dragging the data layer along.\n */\nimport { useEffect, useState } from 'react';\n\nimport type { AssistantEntityPreviewProps } from '@burdenoff/fe-libs/shared/assistant';\n\nimport type { PreviewStepKey } from '../../assistant/assistantRunStore';\n\n/** The `t` both panels already hold — accepted rather than imported, so this\n * file keeps no provider requirement of its own. */\ntype Translate = (key: string, fallback: string, params?: Record<string, string | number>) => string;\n\n/**\n * \"just now\" / \"40s ago\" / \"12m ago\" / \"3h ago\".\n *\n * ★★ TRANSLATED, and it now has an hour bucket. It used to return English in\n * all three locales (en, hi, ta), and a panel left open past an hour read \"240m ago\" rather\n * than \"4h ago\" — both of which were only visible on the surface that outlives\n * a build, which is exactly the one people leave sitting there.\n */\nexport function formatAgo(seconds: number, t: Translate): string {\n if (seconds < 5) return t('bigconsole.widget.aiPreview.agoJustNow', 'just now');\n if (seconds < 60) {\n return t('bigconsole.widget.aiPreview.agoSeconds', '{{n}}s ago', { n: seconds });\n }\n if (seconds < 3600) {\n return t('bigconsole.widget.aiPreview.agoMinutes', '{{n}}m ago', { n: Math.floor(seconds / 60) });\n }\n // ★ `{{n}}`, not `{n}` — fe-libs' single-brace interpolation was broken until\n // 2026.910.7 and shipped raw braces to users across the fleet. Double braces\n // are what this repo already uses (see `…aiPreviewMini.linkReady`).\n return t('bigconsole.widget.aiPreview.agoHours', '{{n}}h ago', { n: Math.floor(seconds / 3600) });\n}\n\n/**\n * Clearance for the assistant dock, as a CSS length.\n *\n * ★★★ THIS REPLACED A 500ms `querySelector` POLL (BC4, BOFF-7337). The old hook\n * measured `[data-testid=\"assistant-widget-window\"][data-layout=\"sidebar\"]` on an\n * interval because the legacy widget lived in fe-libs, was mounted by the app\n * shell, and emitted nothing when it opened or closed — reading the DOM really\n * was the only signal available.\n *\n * The shared overlay publishes its width instead. fe-libs sets\n * `--assistant-dock-width` on `<html>` from\n * `publishedAssistantDockWidth({ isOpen, docked, isDesktop })` — 440px only when\n * the panel is open AND docked AND the viewport is at least 640px — and REMOVES\n * the property on unmount.\n *\n * ★★★ THE FLOATING PANEL NEEDS CLEARANCE TOO (BOFF-7799, fe-libs 2026.930.3).\n * The dock width is `0px` while the panel FLOATS, which is the default, so\n * reading it alone left the right 45 % of the preview (its close button, its\n * \"Live · updated\" status and the tabs' Open buttons) under the open chat.\n * fe-libs now also publishes `--assistant-panel-inset` from\n * `publishedAssistantPanelInset`: 440px docked, 544px floating (the 520px\n * window plus its 24px right gap), and `0px` closed or below 640px, where the\n * panel is a bottom sheet. It is read FIRST, with the dock width as the fallback\n * for an fe-libs that does not publish it, and `0px` after that for a host with\n * no overlay mounted at all.\n *\n * ★★★ THE FALLBACK FAILS OPEN, SO THE VERSIONS ARE COUPLED. A host shell that\n * pins this package but still mounts the LEGACY `AssistantWidget` publishes no\n * variable at all, this resolves to `0px`, and the preview renders underneath a\n * fixed 440px dock where tabs and Open buttons silently stop taking clicks.\n * Nothing throws and no test fails. bigconsole-app therefore bumps this\n * dependency and mounts `AssistantOverlay` in the SAME commit.\n *\n * ★★ A STRING, NOT A HOOK. It needs no React context and no provider ancestry —\n * which matters because the mini panel is mounted by the app shell, outside the\n * assistant's own tree — and the browser recomputes the margin on open/close\n * with no re-render at all.\n *\n * ★ APPLIED TWO WAYS, DELIBERATELY. The collapsed bars carry only `px-*`, so\n * they take it as `marginRight` and a closed dock reproduces the old `0`\n * exactly. The open panels carry `mx-4 md:mx-6`, so they take it as\n * `paddingRight` and leave those margins alone: the old\n * `assistantWidth ? assistantWidth + 16 : undefined` fell through to the\n * RESPONSIVE 24px gutter at md when closed, and folding a flat `+ 16px` into a\n * calc would have quietly shrunk it on every desktop width. Padding keeps the\n * closed state pixel-identical and only adds clearance while the panel is open.\n */\nexport const DOCK_CLEARANCE = 'var(--assistant-panel-inset, var(--assistant-dock-width, 0px))';\n\n/**\n * Step labels, reusing the TAB keys.\n *\n * ★★ They used to be English constants in the store (`PREVIEW_STEP_LABEL`), so\n * the rail rendered \"Data Sink / Dashboard / Parser / Widget\" in every locale —\n * directly beside the SAME four names rendered translated as tab labels. The\n * keys already exist and are already seeded; nothing new was needed to fix it.\n *\n * ★ Lives here because BOTH panels render the rail. It was defined twice, which\n * is the exact duplication this migration exists to remove.\n */\nexport const STEP_LABEL: Record<PreviewStepKey, { key: string; fallback: string }> = {\n datasink: { key: 'bigconsole.widget.aiPreview.tab.dataSink', fallback: 'Data Sink' },\n dashboard: { key: 'bigconsole.widget.aiPreview.tab.dashboard', fallback: 'Dashboard' },\n parser: { key: 'bigconsole.widget.aiPreview.tab.parser', fallback: 'Parser' },\n widget: { key: 'bigconsole.widget.aiPreview.tab.widget', fallback: 'Widget' },\n};\n\n/**\n * The shared surface's chrome strings, already translated.\n *\n * ★★ ONE copy, deliberately. BOTH defects this migration produced were the same\n * bug written twice — the failure caption and the reopen clock each had to be\n * fixed in the full panel AND the mini panel — so anything the two pass\n * identically belongs here rather than inlined in each. That is the same reason\n * the shell lived in this file before it moved to fe-libs.\n *\n * ★ Both surfaces use the `aiPreview` namespace, not `aiPreviewMini`: the rail\n * says the same thing wherever it is rendered, and splitting the keys would mean\n * translating \"running\" and \"failed\" twice per locale for no difference.\n *\n * `AssistantEntityPreviewProps['labels']` rather than a hand-written shape, so a\n * new label added upstream is a type error here instead of a silently missing\n * string.\n */\nexport function previewLabels(t: Translate): AssistantEntityPreviewProps['labels'] {\n const buildProgress = t('bigconsole.widget.aiPreview.buildProgress', 'Build progress');\n const skipped = t('bigconsole.widget.aiPreview.stepSkipped', 'skipped');\n return {\n // ★ main named the failure \"Build stopped\" in a hardcoded English <h3>. The\n // shared surface cannot know that word — it takes it as a label — so this\n // both restores the name AND makes it translatable for the first time.\n errorHeading: t('bigconsole.widget.aiPreview.errorHeading', 'Build stopped'),\n steps: buildProgress,\n stepsHeading: buildProgress,\n skipped,\n stepStatus: {\n pending: t('bigconsole.widget.aiPreview.stepPending', 'not started'),\n running: t('bigconsole.widget.aiPreview.stepRunning', 'running'),\n done: t('bigconsole.widget.aiPreview.stepDone', 'completed'),\n failed: t('bigconsole.widget.aiPreview.stepFailed', 'failed'),\n skipped,\n },\n };\n}\n\n/**\n * The status strip both panels hand to the shared surface as `meta`.\n *\n * ★★★ ONE copy, and this one is not a tidiness argument — the SAME DEFECT was\n * written into both panels twice over. The migration changed this chip from\n * `bg-status-success-bg` to `bg-status-success-bgSubtle` in both files, and that\n * utility DOES NOT EXIST: the token is `--color-status-success-bgSubtle` but the\n * Tailwind preset exposes it as `status-success-bg-subtle`. The app shell emits\n * `.bg-status-success-bg-subtle` and never `.bg-status-success-bgSubtle`, so the\n * \"Live\" pill lost its background entirely and became transparent text.\n * Nothing failed: an unknown utility is simply absent from the stylesheet.\n *\n * ★ Fixed with the ARBITRARY-VALUE form, `bg-[var(--color-status-…-bgSubtle)]`,\n * which is what the other ~30 sites in this repo use. It names the token\n * directly, so it cannot be broken by a preset key being spelled differently\n * from the variable — which is the entire mechanism of the bug above.\n *\n * ★★ It also carries the FAILED state, which the caption cannot. The shared\n * surface blanks `caption` whenever `error` is set — deliberately, so a rotating\n * \"Creating…\" cannot sit under a dead run — so a product that wants the failure\n * NAMED (as this one did on main, with \"Stopped\") has to say it somewhere the\n * error does not suppress. `meta` is that place: `PreviewHeader` renders it\n * without consulting `error`.\n *\n * Labels are passed in already translated, so the `t('literal.key', …)` calls\n * stay greppable at each call site and each panel keeps its own namespace.\n */\nexport function PreviewMeta({\n state,\n stateLabel,\n updatedLabel,\n updatedAt,\n ago,\n}: {\n state: 'running' | 'stopped' | 'idle';\n stateLabel: string | null;\n updatedLabel: string;\n /** When the run last changed, or null when there is nothing to age. */\n updatedAt: number | null;\n /** Renders the elapsed seconds — `formatAgo` bound to the caller's `t`. */\n ago: (seconds: number) => string;\n}) {\n /**\n * ★★★ THE CLOCK LIVES HERE, in the one element that displays it.\n *\n * It used to live in each panel, and that made a 1 Hz `setState` on the PANEL\n * — which re-rendered everything below it. `AssistantEntityPreview` calls\n * `activeTab.render()` on every render with no memo, so the Widgets tab's live\n * dashboard and every Recharts widget inside it re-rendered once a second, for\n * as long as the preview stayed open. main did not: its ticker was gated on\n * `isRunning`, so it stopped when the build did. Ungating it to fix the frozen\n * timestamp is right, but it has to cost one <span>, not the whole subtree.\n *\n * ★★ Mounting is also the gate now. The mini panel is mounted on EVERY route\n * and returns null when the full panel is up — but a hook cannot sit below an\n * early return, so its interval ran on every page regardless of whether\n * anything was shown. Owning the timer here means \"not rendered\" is \"not\n * ticking\", by construction rather than by remembering to pass a flag.\n */\n const [now, setNow] = useState(() => Date.now());\n useEffect(() => {\n if (updatedAt === null) return;\n // ★ Re-read on mount so a reopened panel cannot show the previous open's\n // reading for up to a second. See the note on lint in the git history:\n // reading the clock in render is an ESLint error (Date.now is impure).\n setNow(Date.now());\n const id = window.setInterval(() => setNow(Date.now()), 1000);\n return () => window.clearInterval(id);\n }, [updatedAt]);\n\n const agoSeconds = updatedAt === null ? 0 : Math.max(0, Math.floor((now - updatedAt) / 1000));\n\n const chip =\n 'inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide';\n return (\n <span className=\"inline-flex items-center gap-2\" data-testid=\"ai-preview-ago\">\n {state !== 'idle' && stateLabel ? (\n <span\n className={\n state === 'running'\n ? `${chip} bg-[var(--color-status-success-bgSubtle)] text-status-success-text`\n : `${chip} bg-[var(--color-status-error-bgSubtle)] text-status-error-text`\n }\n data-testid={state === 'running' ? 'ai-preview-live' : 'ai-preview-stopped'}\n >\n {state === 'running' ? (\n <span className=\"relative flex size-2\">\n <span className=\"absolute inline-flex size-full rounded-full bg-status-success-text opacity-75 motion-safe:animate-ping\" />\n <span className=\"relative inline-flex size-2 rounded-full bg-status-success-text\" />\n </span>\n ) : (\n /* ★ Still, not pulsing: the ping says \"this is happening now\", which\n is the opposite of what a stopped run means. */\n <span className=\"relative inline-flex size-2 rounded-full bg-status-error-text\" />\n )}\n {stateLabel}\n </span>\n ) : null}\n <span>\n {updatedLabel} {ago(agoSeconds)}\n </span>\n </span>\n );\n}\n"],"mappings":";;;AAkCA,SAAgB,EAAU,GAAiB,GAAsB;AAW/D,QAVI,IAAU,IAAU,EAAE,0CAA0C,WAAW,GAC3E,IAAU,KACL,EAAE,0CAA0C,cAAc,EAAE,GAAG,GAAS,CAAC,GAE9E,IAAU,OACL,EAAE,0CAA0C,cAAc,EAAE,GAAG,KAAK,MAAM,IAAU,GAAG,EAAE,CAAC,GAK5F,EAAE,wCAAwC,cAAc,EAAE,GAAG,KAAK,MAAM,IAAU,KAAK,EAAE,CAAC;;AAkDnG,IAAa,IAAiB,kEAajB,IAAwE;CACnF,UAAU;EAAE,KAAK;EAA4C,UAAU;EAAa;CACpF,WAAW;EAAE,KAAK;EAA6C,UAAU;EAAa;CACtF,QAAQ;EAAE,KAAK;EAA0C,UAAU;EAAU;CAC7E,QAAQ;EAAE,KAAK;EAA0C,UAAU;EAAU;CAC9E;AAmBD,SAAgB,EAAc,GAAqD;CACjF,IAAM,IAAgB,EAAE,6CAA6C,iBAAiB,EAChF,IAAU,EAAE,2CAA2C,UAAU;AACvE,QAAO;EAIL,cAAc,EAAE,4CAA4C,gBAAgB;EAC5E,OAAO;EACP,cAAc;EACd;EACA,YAAY;GACV,SAAS,EAAE,2CAA2C,cAAc;GACpE,SAAS,EAAE,2CAA2C,UAAU;GAChE,MAAM,EAAE,wCAAwC,YAAY;GAC5D,QAAQ,EAAE,0CAA0C,SAAS;GAC7D;GACD;EACF;;AA8BH,SAAgB,EAAY,EAC1B,UACA,eACA,iBACA,cACA,UASC;CAkBD,IAAM,CAAC,GAAK,KAAU,QAAe,KAAK,KAAK,CAAC;AAChD,SAAgB;AACd,MAAI,MAAc,KAAM;AAIxB,IAAO,KAAK,KAAK,CAAC;EAClB,IAAM,IAAK,OAAO,kBAAkB,EAAO,KAAK,KAAK,CAAC,EAAE,IAAK;AAC7D,eAAa,OAAO,cAAc,EAAG;IACpC,CAAC,EAAU,CAAC;CAEf,IAAM,IAAa,MAAc,OAAO,IAAI,KAAK,IAAI,GAAG,KAAK,OAAO,IAAM,KAAa,IAAK,CAAC,EAEvF,IACJ;AACF,QACE,kBAAC,QAAD;EAAM,WAAU;EAAiC,eAAY;YAA7D,CACG,MAAU,UAAU,IACnB,kBAAC,QAAD;GACE,WACE,MAAU,YACN,GAAG,EAAK,uEACR,GAAG,EAAK;GAEd,eAAa,MAAU,YAAY,oBAAoB;aANzD,CAQG,MAAU,YACT,kBAAC,QAAD;IAAM,WAAU;cAAhB,CACE,kBAAC,QAAD,EAAM,WAAU,0GAA2G,CAAA,EAC3H,kBAAC,QAAD,EAAM,WAAU,mEAAoE,CAAA,CAC/E;QAIP,kBAAC,QAAD,EAAM,WAAU,iEAAkE,CAAA,EAEnF,EACI;OACL,MACJ,kBAAC,QAAD,EAAA,UAAA;GACG;GAAa;GAAE,EAAI,EAAW;GAC1B,EAAA,CAAA,CACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burdenoff/microfe-bigconsole",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.930.1",
|
|
4
4
|
"description": "BigConsole - AI-powered analytics and dashboard platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"@apollo/client": "^4.0.11",
|
|
52
|
-
"@burdenoff/fe-libs": "2026.
|
|
52
|
+
"@burdenoff/fe-libs": "2026.930.3",
|
|
53
53
|
"@burdenoff/fluidgrids-fe-sdk": "2026.731.4",
|
|
54
54
|
"@iconify/react": "^6.0.2",
|
|
55
55
|
"@monaco-editor/react": "^4.7.0",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"zustand": "^5.0.3"
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
|
-
"@burdenoff/fe-libs": "2026.
|
|
87
|
+
"@burdenoff/fe-libs": "2026.930.3",
|
|
88
88
|
"@burdenoff/fluidgrids-fe-sdk": "2026.731.4",
|
|
89
89
|
"@eslint/js": "^10.0.1",
|
|
90
90
|
"@graphql-codegen/add": "^5.0.0",
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { useEffect as e, useState as t } from "react";
|
|
2
|
-
//#region src/bigconsole/assistant/useAssistantPanelShowsPreview.ts
|
|
3
|
-
var n = "[data-assistant-panel]", r = "data-assistant-overlay", i = "preview";
|
|
4
|
-
function a(e = document) {
|
|
5
|
-
return e.querySelector(n)?.getAttribute(r) === i;
|
|
6
|
-
}
|
|
7
|
-
function o() {
|
|
8
|
-
let [n, i] = t(() => typeof document > "u" ? !1 : a());
|
|
9
|
-
return e(() => {
|
|
10
|
-
if (typeof document > "u") return;
|
|
11
|
-
let e = () => i(a());
|
|
12
|
-
e();
|
|
13
|
-
let t = new MutationObserver(e);
|
|
14
|
-
return t.observe(document.body, {
|
|
15
|
-
subtree: !0,
|
|
16
|
-
childList: !0,
|
|
17
|
-
attributes: !0,
|
|
18
|
-
attributeFilter: [r]
|
|
19
|
-
}), () => t.disconnect();
|
|
20
|
-
}, []), n;
|
|
21
|
-
}
|
|
22
|
-
//#endregion
|
|
23
|
-
export { o as useAssistantPanelShowsPreview };
|
|
24
|
-
|
|
25
|
-
//# sourceMappingURL=useAssistantPanelShowsPreview.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useAssistantPanelShowsPreview.js","names":[],"sources":["../../../src/bigconsole/assistant/useAssistantPanelShowsPreview.ts"],"sourcesContent":["import { useEffect, useState } from 'react';\n\n/**\n * Is the shared assistant panel currently showing the preview ITSELF?\n *\n * ★★★ WHY THIS EXISTS. The preview has three surfaces — the rich page panel,\n * the mini page panel, and fe-libs' in-chat overlay — and only one may show per\n * run. BigConsole already arbitrates its own two (`richMounted`), but nothing\n * arbitrated against the third: fe-libs renders the SAME controller via\n * `controller.render()` inside its overlay, driven by its own local state, so\n * opening the overlay left the page surface up as well.\n *\n * Measured on app.bigconsole.com 2026-09-24, one Build run, one click of the\n * panel's \"Build preview\" toggle:\n * before -> page surface [283,145,1109,270], no overlay\n * after -> page surface STILL [283,145,1109,270]\n * plus the same preview again at [897,166,518,160] inside the\n * overlay, inside the assistant panel\n * Two live surfaces for one run, side by side, showing the same build.\n *\n * ★★ IT READS THE ATTRIBUTE fe-libs ALREADY PUBLISHES, rather than adding a\n * contract. `AIAssistantPanel` writes `data-assistant-overlay=\"preview\"` on its\n * root for exactly this kind of cross-package coordination — the same mechanism\n * `assistantPanelOwnsEscape` reads to decide who owns the Escape key. Verified\n * live before building on it: the attribute is absent before the toggle and\n * `\"preview\"` after.\n *\n * ★ A MutationObserver, not a poll or a render-time read. The attribute changes\n * from inside fe-libs' own state, on a node this package does not own and does\n * not re-render with, so there is no React signal to hang it on.\n */\nconst PANEL_SELECTOR = '[data-assistant-panel]';\nconst OVERLAY_ATTR = 'data-assistant-overlay';\nconst PREVIEW = 'preview';\n\n/** Reads the current answer from the DOM. Safe to call before any panel mounts. */\nexport function panelShowsPreviewNow(doc: Pick<Document, 'querySelector'> = document): boolean {\n return doc.querySelector(PANEL_SELECTOR)?.getAttribute(OVERLAY_ATTR) === PREVIEW;\n}\n\nexport function useAssistantPanelShowsPreview(): boolean {\n const [showing, setShowing] = useState(() => (typeof document === 'undefined' ? false : panelShowsPreviewNow()));\n\n useEffect(() => {\n if (typeof document === 'undefined') return;\n const read = () => setShowing(panelShowsPreviewNow());\n read();\n // ★ `subtree` is required: the panel mounts and unmounts below <body>, so an\n // observer bound to attributes alone would never see a panel that appears\n // after this hook runs — which is the common case, since the page surface\n // exists before the user opens the assistant.\n const observer = new MutationObserver(read);\n observer.observe(document.body, {\n subtree: true,\n childList: true,\n attributes: true,\n attributeFilter: [OVERLAY_ATTR],\n });\n return () => observer.disconnect();\n }, []);\n\n return showing;\n}\n"],"mappings":";;AA+BA,IAAM,IAAiB,0BACjB,IAAe,0BACf,IAAU;AAGhB,SAAgB,EAAqB,IAAuC,UAAmB;AAC7F,QAAO,EAAI,cAAc,EAAe,EAAE,aAAa,EAAa,KAAK;;AAG3E,SAAgB,IAAyC;CACvD,IAAM,CAAC,GAAS,KAAc,QAAgB,OAAO,WAAa,MAAc,KAAQ,GAAsB,CAAE;AAoBhH,QAlBA,QAAgB;AACd,MAAI,OAAO,WAAa,IAAa;EACrC,IAAM,UAAa,EAAW,GAAsB,CAAC;AACrD,KAAM;EAKN,IAAM,IAAW,IAAI,iBAAiB,EAAK;AAO3C,SANA,EAAS,QAAQ,SAAS,MAAM;GAC9B,SAAS;GACT,WAAW;GACX,YAAY;GACZ,iBAAiB,CAAC,EAAa;GAChC,CAAC,QACW,EAAS,YAAY;IACjC,EAAE,CAAC,EAEC"}
|