@burdenoff/microfe-bigconsole 2026.912.1 → 2026.912.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,136 +2,129 @@ import { Section as e } from "../common/Section.js";
2
2
  import "../common/index.js";
3
3
  import { PREVIEW_STEPS as t, useAssistantRunStore as n } from "../../assistant/assistantRunStore.js";
4
4
  import { PreviewMeta as r, STEP_LABEL as i, formatAgo as a, previewLabels as o, useAssistantSidebarWidth as s } from "./previewShared.js";
5
- import { useEffect as c, useState as l } from "react";
6
- import { ArrowUpRight as u, Braces as d, Database as f, LayoutDashboard as p, Loader2 as m, Sparkles as h, X as g } from "lucide-react";
7
- import { useI18n as _ } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
8
- import { Button as v } from "@burdenoff/fe-libs/ui";
9
- import { jsx as y, jsxs as b } from "react/jsx-runtime";
10
- import { AssistantEntityPreview as x } from "@burdenoff/fe-libs/shared/assistant";
5
+ import { ArrowUpRight as c, Braces as l, Database as u, LayoutDashboard as d, Loader2 as f, Sparkles as p, X as m } from "lucide-react";
6
+ import { useI18n as h } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
7
+ import { Button as g } from "@burdenoff/fe-libs/ui";
8
+ import { jsx as _, jsxs as v } from "react/jsx-runtime";
9
+ import { AssistantEntityPreview as y } from "@burdenoff/fe-libs/shared/assistant";
11
10
  //#region src/bigconsole/components/preview/AiPreviewMiniPanel.tsx
12
- function S({ onNavigate: S }) {
13
- let { t: C } = _(), w = n((e) => e.isRunning), T = n((e) => e.previewOpen), E = n((e) => e.hasRun), D = n((e) => e.richMounted), O = n((e) => e.steps), k = n((e) => e.entityIds), A = n((e) => e.caption), j = n((e) => e.error), M = n((e) => e.updatedAt), N = n((e) => e.openPreview), P = n((e) => e.closePreview), [F, I] = l(() => Date.now());
14
- c(() => {
15
- if (!T) return;
16
- I(Date.now());
17
- let e = window.setInterval(() => I(Date.now()), 1e3);
18
- return () => window.clearInterval(e);
19
- }, [T]);
20
- let L = s(E && !D), R = (e) => {
21
- S ? S(e) : typeof window < "u" && window.location.assign(e);
11
+ function b({ onNavigate: b }) {
12
+ let { t: x } = h(), S = n((e) => e.isRunning), C = n((e) => e.previewOpen), w = n((e) => e.hasRun), T = n((e) => e.richMounted), E = n((e) => e.steps), D = n((e) => e.entityIds), O = n((e) => e.caption), k = n((e) => e.error), A = n((e) => e.updatedAt), j = n((e) => e.openPreview), M = n((e) => e.closePreview), N = s(w && !T), P = (e) => {
13
+ b ? b(e) : typeof window < "u" && window.location.assign(e);
22
14
  };
23
- if (!E || D) return null;
24
- if (!T) return /* @__PURE__ */ b("div", {
15
+ if (!w || T) return null;
16
+ if (!C) return /* @__PURE__ */ v("div", {
25
17
  className: "flex flex-wrap items-center justify-end gap-control-gap px-4 pt-3 md:px-6",
26
- style: { marginRight: L },
27
- children: [/* @__PURE__ */ y("span", {
18
+ style: { marginRight: N },
19
+ children: [/* @__PURE__ */ _("span", {
28
20
  className: "text-xs text-text-muted",
29
- children: w ? C("bigconsole.widget.aiPreview.buildRunning", "AI build running") : C("bigconsole.widget.aiPreview.buildFinished", "AI build finished")
30
- }), /* @__PURE__ */ b(v, {
21
+ children: S ? x("bigconsole.widget.aiPreview.buildRunning", "AI build running") : x("bigconsole.widget.aiPreview.buildFinished", "AI build finished")
22
+ }), /* @__PURE__ */ v(g, {
31
23
  size: "sm",
32
24
  variant: "outline",
33
- onClick: N,
25
+ onClick: j,
34
26
  "data-testid": "ai-preview-open",
35
27
  children: [
36
- /* @__PURE__ */ y(h, { className: "mr-1.5 size-4" }),
37
- C("bigconsole.widget.aiPreviewMini.previewButton", "Preview"),
38
- w ? /* @__PURE__ */ y(m, { className: "ml-1.5 size-3.5 animate-spin" }) : null
28
+ /* @__PURE__ */ _(p, { className: "mr-1.5 size-4" }),
29
+ x("bigconsole.widget.aiPreviewMini.previewButton", "Preview"),
30
+ S ? /* @__PURE__ */ _(f, { className: "ml-1.5 size-3.5 animate-spin" }) : null
39
31
  ]
40
32
  })]
41
33
  });
42
- let z = M ? Math.max(0, Math.floor((F - M) / 1e3)) : 0, B = [];
43
- k.datasink && B.push({
34
+ let F = [];
35
+ D.datasink && F.push({
44
36
  key: "datasink",
45
- label: C("bigconsole.widget.aiPreviewMini.link.dataSink", "Data Sink"),
46
- path: `/bigconsole/datasinks/${k.datasink}`,
47
- icon: /* @__PURE__ */ y(f, {
37
+ label: x("bigconsole.widget.aiPreviewMini.link.dataSink", "Data Sink"),
38
+ path: `/bigconsole/datasinks/${D.datasink}`,
39
+ icon: /* @__PURE__ */ _(u, {
48
40
  className: "size-4 shrink-0 text-text-muted",
49
41
  "aria-hidden": "true"
50
42
  })
51
- }), k.dashboard && B.push({
43
+ }), D.dashboard && F.push({
52
44
  key: "dashboard",
53
- label: C("bigconsole.widget.aiPreviewMini.link.dashboard", "Dashboard"),
54
- path: `/bigconsole/dashboards/${k.dashboard}`,
55
- icon: /* @__PURE__ */ y(p, {
45
+ label: x("bigconsole.widget.aiPreviewMini.link.dashboard", "Dashboard"),
46
+ path: `/bigconsole/dashboards/${D.dashboard}`,
47
+ icon: /* @__PURE__ */ _(d, {
56
48
  className: "size-4 shrink-0 text-text-muted",
57
49
  "aria-hidden": "true"
58
50
  })
59
- }), k.parser && B.push({
51
+ }), D.parser && F.push({
60
52
  key: "parser",
61
- label: C("bigconsole.widget.aiPreviewMini.link.parser", "Parser"),
62
- path: `/bigconsole/parsers/${k.parser}`,
63
- icon: /* @__PURE__ */ y(d, {
53
+ label: x("bigconsole.widget.aiPreviewMini.link.parser", "Parser"),
54
+ path: `/bigconsole/parsers/${D.parser}`,
55
+ icon: /* @__PURE__ */ _(l, {
64
56
  className: "size-4 shrink-0 text-text-muted",
65
57
  "aria-hidden": "true"
66
58
  })
67
59
  });
68
- let V = t.map((e) => ({
60
+ let I = t.map((e) => ({
69
61
  id: e,
70
- label: C(i[e].key, i[e].fallback),
71
- status: O[e]
62
+ label: x(i[e].key, i[e].fallback),
63
+ status: E[e]
72
64
  }));
73
- return /* @__PURE__ */ b("div", {
65
+ return /* @__PURE__ */ v("div", {
74
66
  "data-testid": "ai-preview-panel",
75
67
  "data-variant": "mini",
76
- style: { marginRight: L ? L + 16 : void 0 },
68
+ style: { marginRight: N ? N + 16 : void 0 },
77
69
  className: "mx-4 mb-3 mt-3 min-w-0 md:mx-6",
78
- children: [/* @__PURE__ */ y("h2", {
70
+ children: [/* @__PURE__ */ _("h2", {
79
71
  className: "sr-only",
80
- children: C("bigconsole.widget.aiPreviewMini.title", "AI Preview")
81
- }), /* @__PURE__ */ y(x, {
82
- title: C("bigconsole.widget.aiPreviewMini.title", "AI Preview"),
83
- steps: V,
84
- caption: w ? A || C("bigconsole.widget.aiPreviewMini.working", "Working…") : C("bigconsole.widget.aiPreviewMini.done", "Done"),
85
- error: j,
86
- meta: /* @__PURE__ */ y(r, {
87
- state: w ? "running" : j ? "stopped" : "idle",
88
- stateLabel: w ? C("bigconsole.widget.aiPreviewMini.live", "Live") : j ? C("bigconsole.widget.aiPreviewMini.stopped", "Stopped") : null,
89
- updatedLabel: C("bigconsole.widget.aiPreviewMini.updated", "updated"),
90
- ago: a(z, C)
72
+ children: x("bigconsole.widget.aiPreviewMini.title", "AI Preview")
73
+ }), /* @__PURE__ */ _(y, {
74
+ title: x("bigconsole.widget.aiPreviewMini.title", "AI Preview"),
75
+ steps: I,
76
+ caption: S ? O || x("bigconsole.widget.aiPreviewMini.working", "Working…") : x("bigconsole.widget.aiPreviewMini.done", "Done"),
77
+ error: k,
78
+ meta: /* @__PURE__ */ _(r, {
79
+ state: S ? "running" : k ? "stopped" : "idle",
80
+ stateLabel: S ? x("bigconsole.widget.aiPreviewMini.live", "Live") : k ? x("bigconsole.widget.aiPreviewMini.stopped", "Stopped") : null,
81
+ updatedLabel: x("bigconsole.widget.aiPreviewMini.updated", "updated"),
82
+ updatedAt: A,
83
+ ago: (e) => a(e, x)
91
84
  }),
92
- headerActions: /* @__PURE__ */ y(v, {
85
+ headerActions: /* @__PURE__ */ _(g, {
93
86
  size: "sm",
94
87
  variant: "ghost",
95
- onClick: P,
96
- "aria-label": C("bigconsole.widget.aiPreviewMini.closeAriaLabel", "Close preview"),
88
+ onClick: M,
89
+ "aria-label": x("bigconsole.widget.aiPreviewMini.closeAriaLabel", "Close preview"),
97
90
  "data-testid": "ai-preview-close",
98
- children: /* @__PURE__ */ y(g, { className: "size-4" })
91
+ children: /* @__PURE__ */ _(m, { className: "size-4" })
99
92
  }),
100
- labels: o(C),
101
- children: /* @__PURE__ */ y("div", {
93
+ labels: o(x),
94
+ children: /* @__PURE__ */ _("div", {
102
95
  className: "p-3",
103
- children: /* @__PURE__ */ b(e, {
96
+ children: /* @__PURE__ */ v(e, {
104
97
  emphasis: "standard",
105
98
  padding: "compact",
106
99
  headingLevel: 3,
107
- title: C("bigconsole.widget.aiPreviewMini.builtSoFar", "Built so far"),
108
- description: C("bigconsole.widget.aiPreviewMini.builtSoFarHint", "Open any piece to see the real thing."),
100
+ title: x("bigconsole.widget.aiPreviewMini.builtSoFar", "Built so far"),
101
+ description: x("bigconsole.widget.aiPreviewMini.builtSoFarHint", "Open any piece to see the real thing."),
109
102
  bodyClassName: "flex flex-col gap-list-gap",
110
- children: [B.length === 0 ? /* @__PURE__ */ y("p", {
103
+ children: [F.length === 0 ? /* @__PURE__ */ _("p", {
111
104
  className: "text-sm text-text-muted",
112
- children: w ? C("bigconsole.widget.aiPreviewMini.building", "Building… links appear here as each piece is created.") : C("bigconsole.widget.aiPreviewMini.nothingBuilt", "Nothing built yet.")
113
- }) : B.map((e) => /* @__PURE__ */ b("div", {
105
+ children: S ? x("bigconsole.widget.aiPreviewMini.building", "Building… links appear here as each piece is created.") : x("bigconsole.widget.aiPreviewMini.nothingBuilt", "Nothing built yet.")
106
+ }) : F.map((e) => /* @__PURE__ */ v("div", {
114
107
  "data-testid": "ai-preview-row",
115
108
  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",
116
- children: [/* @__PURE__ */ b("p", {
109
+ children: [/* @__PURE__ */ v("p", {
117
110
  className: "flex min-w-0 items-center gap-icon-gap text-sm font-medium text-text-primary",
118
- children: [e.icon, /* @__PURE__ */ y("span", {
111
+ children: [e.icon, /* @__PURE__ */ _("span", {
119
112
  className: "truncate",
120
- children: C("bigconsole.widget.aiPreviewMini.linkReady", "{{label}} ready", { label: e.label })
113
+ children: x("bigconsole.widget.aiPreviewMini.linkReady", "{{label}} ready", { label: e.label })
121
114
  })]
122
- }), /* @__PURE__ */ b(v, {
115
+ }), /* @__PURE__ */ v(g, {
123
116
  size: "sm",
124
117
  variant: "ghost",
125
- onClick: () => R(e.path),
126
- children: [C("bigconsole.widget.aiPreviewMini.open", "Open"), /* @__PURE__ */ y(u, { className: "ml-1 size-3.5" })]
118
+ onClick: () => P(e.path),
119
+ children: [x("bigconsole.widget.aiPreviewMini.open", "Open"), /* @__PURE__ */ _(c, { className: "ml-1 size-3.5" })]
127
120
  })]
128
- }, e.key)), k.dashboard ? /* @__PURE__ */ b(v, {
121
+ }, e.key)), D.dashboard ? /* @__PURE__ */ v(g, {
129
122
  size: "sm",
130
123
  variant: "outline",
131
124
  className: "h-auto w-full whitespace-normal py-1.5 text-center",
132
125
  "data-testid": "ai-preview-open-in-bigconsole",
133
- onClick: () => R(`/bigconsole/dashboards/${k.dashboard ?? ""}`),
134
- children: [C("bigconsole.widget.aiPreviewMini.openInBigConsole", "Open in BigConsole for the live view"), /* @__PURE__ */ y(u, { className: "ml-1 size-3.5" })]
126
+ onClick: () => P(`/bigconsole/dashboards/${D.dashboard ?? ""}`),
127
+ children: [x("bigconsole.widget.aiPreviewMini.openInBigConsole", "Open in BigConsole for the live view"), /* @__PURE__ */ _(c, { className: "ml-1 size-3.5" })]
135
128
  }) : null]
136
129
  })
137
130
  })
@@ -139,6 +132,6 @@ function S({ onNavigate: S }) {
139
132
  });
140
133
  }
141
134
  //#endregion
142
- export { S as AiPreviewMiniPanel };
135
+ export { b as AiPreviewMiniPanel };
143
136
 
144
137
  //# sourceMappingURL=AiPreviewMiniPanel.js.map
@@ -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 { useEffect, useState, 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 { Section } from '../common';\nimport { PreviewMeta, STEP_LABEL, formatAgo, previewLabels, useAssistantSidebarWidth } 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 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 [now, setNow] = useState(() => Date.now());\n /**\n * ★★ Ticks while the panel is OPEN, not only while a turn is running. Gating\n * on `isRunning` froze the \"updated …\" strip the moment a build finished, so\n * a panel left open for an hour still claimed it was updated \"just now\".\n */\n useEffect(() => {\n if (!previewOpen) return;\n // ★ See AiPreviewPanel: re-read on OPEN so a reopen cannot show the previous\n // open's stale reading, and why the lint warning here is the right trade.\n setNow(Date.now());\n const id = window.setInterval(() => setNow(Date.now()), 1000);\n return () => window.clearInterval(id);\n }, [previewOpen]);\n\n // ★ Gated: this panel is mounted on EVERY page, and a hook cannot live below\n // an early return — so without this the 500ms poll ran for the lifetime of\n // the tab whether or not the assistant had ever been opened.\n const assistantWidth = useAssistantSidebarWidth(hasRun && !richMounted);\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: assistantWidth }}\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 agoSeconds = updatedAt ? Math.max(0, Math.floor((now - updatedAt) / 1000)) : 0;\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 — margin against the assistant\n // sidebar, page gutters, and the testids this panel already had. Everything\n // visual belongs to the shared surface, which is what keeps this copy and\n // the full panel from drifting apart now that `previewShared` no longer\n // holds a shell recipe for them both.\n <div\n data-testid=\"ai-preview-panel\"\n data-variant=\"mini\"\n style={{ marginRight: assistantWidth ? assistantWidth + 16 : undefined }}\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 ago={formatAgo(agoSeconds, 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 { Section } from '../common';\nimport { PreviewMeta, STEP_LABEL, formatAgo, previewLabels, useAssistantSidebarWidth } 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 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 // ★ Gated: this panel is mounted on EVERY page, and a hook cannot live below\n // an early return — so without this the 500ms poll ran for the lifetime of\n // the tab whether or not the assistant had ever been opened.\n const assistantWidth = useAssistantSidebarWidth(hasRun && !richMounted);\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: assistantWidth }}\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 — margin against the assistant\n // sidebar, page gutters, and the testids this panel already had. Everything\n // visual belongs to the shared surface, which is what keeps this copy and\n // the full panel from drifting apart now that `previewShared` no longer\n // holds a shell recipe for them both.\n <div\n data-testid=\"ai-preview-panel\"\n data-variant=\"mini\"\n style={{ marginRight: assistantWidth ? assistantWidth + 16 : undefined }}\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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"}
@@ -34,22 +34,15 @@ function D() {
34
34
  p(() => (R(!0), () => {
35
35
  R(!1);
36
36
  }), [R]);
37
- let [z, B] = h(() => Date.now());
37
+ let [z, B] = h(0), V = m(!1);
38
38
  p(() => {
39
- if (!k) return;
40
- B(Date.now());
41
- let e = window.setInterval(() => B(Date.now()), 1e3);
42
- return () => window.clearInterval(e);
43
- }, [k]);
44
- let [V, H] = h(0), U = m(!1);
45
- p(() => {
46
- O && !U.current && H((e) => e + 1), U.current = O;
39
+ O && !V.current && B((e) => e + 1), V.current = O;
47
40
  }, [O]);
48
- let W = c(A);
41
+ let H = c(A);
49
42
  if (!A) return null;
50
43
  if (!k) return /* @__PURE__ */ C("div", {
51
44
  className: "flex flex-wrap items-center justify-end gap-control-gap px-4 pt-3 md:px-6",
52
- style: { marginRight: W },
45
+ style: { marginRight: H },
53
46
  children: [/* @__PURE__ */ S("span", {
54
47
  className: "text-xs text-text-muted",
55
48
  children: O ? g("bigconsole.widget.aiPreview.buildRunning", "AI build running") : g("bigconsole.widget.aiPreview.buildFinished", "AI build finished")
@@ -65,11 +58,11 @@ function D() {
65
58
  ]
66
59
  })]
67
60
  });
68
- let G = F ? Math.max(0, Math.floor((z - F) / 1e3)) : 0, K = M.dashboard, q = n.map((e) => ({
61
+ let U = M.dashboard, W = n.map((e) => ({
69
62
  id: e,
70
63
  label: g(a[e].key, a[e].fallback),
71
64
  status: j[e]
72
- })), J = [
65
+ })), G = [
73
66
  {
74
67
  id: "datasink",
75
68
  label: g("bigconsole.widget.aiPreview.tab.dataSink", "Data Sink"),
@@ -95,8 +88,8 @@ function D() {
95
88
  headingLevel: 3,
96
89
  title: g("bigconsole.widget.aiPreview.tab.dashboard", "Dashboard"),
97
90
  description: g("bigconsole.widget.aiPreview.dashboardHint", "The live board, rendered exactly as the real page renders it."),
98
- actions: K ? /* @__PURE__ */ S(E, { onOpen: () => D(`/dashboards/${K}`) }) : void 0,
99
- children: /* @__PURE__ */ S(l, { dashboardId: K })
91
+ actions: U ? /* @__PURE__ */ S(E, { onOpen: () => D(`/dashboards/${U}`) }) : void 0,
92
+ children: /* @__PURE__ */ S(l, { dashboardId: U })
100
93
  }) })
101
94
  },
102
95
  {
@@ -124,9 +117,9 @@ function D() {
124
117
  headingLevel: 3,
125
118
  title: g("bigconsole.widget.aiPreview.widgets", "Widgets"),
126
119
  description: g("bigconsole.widget.aiPreview.widgetsHint", "Every widget on the new dashboard, rendered live."),
127
- actions: K ? /* @__PURE__ */ S(E, { onOpen: () => D(`/dashboards/${K}`) }) : void 0,
120
+ actions: U ? /* @__PURE__ */ S(E, { onOpen: () => D(`/dashboards/${U}`) }) : void 0,
128
121
  children: /* @__PURE__ */ S(f, {
129
- dashboardId: K,
122
+ dashboardId: U,
130
123
  live: O
131
124
  })
132
125
  }) })
@@ -134,16 +127,16 @@ function D() {
134
127
  ];
135
128
  return /* @__PURE__ */ C("div", {
136
129
  "data-testid": "ai-preview-panel",
137
- style: { marginRight: W ? W + 16 : void 0 },
130
+ style: { marginRight: H ? H + 16 : void 0 },
138
131
  className: "mx-4 mt-3 min-w-0 md:mx-6",
139
132
  children: [/* @__PURE__ */ S("h2", {
140
133
  className: "sr-only",
141
134
  children: g("bigconsole.widget.aiPreview.title", "AI Preview")
142
135
  }), /* @__PURE__ */ S(w, {
143
136
  title: g("bigconsole.widget.aiPreview.title", "AI Preview"),
144
- runKey: String(V),
145
- steps: q,
146
- tabs: J,
137
+ runKey: String(z),
138
+ steps: W,
139
+ tabs: G,
147
140
  follow: "produced",
148
141
  caption: O ? N || g("bigconsole.widget.aiPreview.working", "Working…") : g("bigconsole.widget.aiPreview.done", "Done"),
149
142
  error: P,
@@ -151,7 +144,8 @@ function D() {
151
144
  state: O ? "running" : P ? "stopped" : "idle",
152
145
  stateLabel: O ? g("bigconsole.widget.aiPreview.live", "Live") : P ? g("bigconsole.widget.aiPreview.stopped", "Stopped") : null,
153
146
  updatedLabel: g("bigconsole.widget.aiPreview.updated", "updated"),
154
- ago: o(G, g)
147
+ updatedAt: F,
148
+ ago: (e) => o(e, g)
155
149
  }),
156
150
  headerActions: /* @__PURE__ */ S(x, {
157
151
  size: "sm",
@@ -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 { ArrowUpRight, Loader2, Sparkles, X } from 'lucide-react';\n\nimport { PREVIEW_STEPS, useAssistantRunStore } from '../../assistant/assistantRunStore';\nimport { useBigConsoleNavigate } from '../../context';\nimport { Section } from '../common';\nimport { PreviewMeta, STEP_LABEL, formatAgo, previewLabels, useAssistantSidebarWidth } 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 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 const [now, setNow] = useState(() => Date.now());\n /**\n * ★★ Ticks while the panel is OPEN, not only while a turn is running.\n *\n * Gating on `isRunning` froze the \"updated …\" strip the instant a build\n * finished: a panel left open for an hour still claimed it was updated\n * \"just now\", which is the one moment the reading is least trustworthy.\n */\n useEffect(() => {\n if (!previewOpen) return;\n // ★ Re-read on OPEN, not only on the first tick. `now` still holds the\n // PREVIOUS open's value, so without this a panel reopened after an hour\n // reads \"just now\" for a second before snapping to \"60m ago\" — the same\n // frozen-timestamp bug this effect exists to fix, narrowed to the reopen.\n // This trips the cascading-render lint warning; reading the clock in render\n // instead is an ESLint *error* (Date.now is impure), so the warning is the\n // cheaper of the two. One extra render on open is exactly what is wanted.\n setNow(Date.now());\n const intervalId = window.setInterval(() => setNow(Date.now()), 1000);\n return () => window.clearInterval(intervalId);\n }, [previewOpen]);\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 // Keep clear of the assistant.\n //\n // The assistant sidebar is `position: fixed` — it OVERLAYS the page instead of\n // reflowing it. A panel that spans the full content width therefore runs\n // underneath it, and everything in that overlapped strip (tabs, Open buttons)\n // silently stops taking clicks. Measure the real element rather than hardcoding\n // 560px: the widget is responsive, and it can be closed mid-run.\n // ★ Gated: no build, no poll. See the hook's docblock.\n const assistantWidth = useAssistantSidebarWidth(hasRun);\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: assistantWidth }}\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 agoSeconds = updatedAt ? Math.max(0, Math.floor((now - updatedAt) / 1000)) : 0;\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 — margin against the assistant\n // sidebar, page gutters, and the testid BigConsole already had. Everything\n // visual belongs to the shared surface.\n <div\n data-testid=\"ai-preview-panel\"\n style={{ marginRight: assistantWidth ? assistantWidth + 16 : undefined }}\n className=\"mx-4 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.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 ago={formatAgo(agoSeconds, 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 { useBigConsoleNavigate } from '../../context';\nimport { Section } from '../common';\nimport { PreviewMeta, STEP_LABEL, formatAgo, previewLabels, useAssistantSidebarWidth } 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 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 // Keep clear of the assistant.\n //\n // The assistant sidebar is `position: fixed` — it OVERLAYS the page instead of\n // reflowing it. A panel that spans the full content width therefore runs\n // underneath it, and everything in that overlapped strip (tabs, Open buttons)\n // silently stops taking clicks. Measure the real element rather than hardcoding\n // 560px: the widget is responsive, and it can be closed mid-run.\n // ★ Gated: no build, no poll. See the hook's docblock.\n const assistantWidth = useAssistantSidebarWidth(hasRun);\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: assistantWidth }}\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 — margin against the assistant\n // sidebar, page gutters, and the testid BigConsole already had. Everything\n // visual belongs to the shared surface.\n <div\n data-testid=\"ai-preview-panel\"\n style={{ marginRight: assistantWidth ? assistantWidth + 16 : undefined }}\n className=\"mx-4 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.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":";;;;;;;;;;;;;;AAsDA,SAAS,EAAQ,EAAE,eAAqC;AACtD,QACE,kBAAC,OAAD;EAAK,WAAU;EAA8B,OAAO,EAAE,WAAW,oBAAoB;EAClF;EACG,CAAA;;AAKV,SAAS,EAAiB,EAAE,aAAkC;CAC5D,IAAM,EAAE,MAAM,GAAS;AACvB,QACE,kBAAC,GAAD;EAAQ,MAAK;EAAK,SAAQ;EAAU,SAAS;EAAQ,eAAY;YAAjE,CACG,EAAE,oCAAoC,OAAO,EAC9C,kBAAC,GAAD,EAAc,WAAU,iBAAkB,CAAA,CACnC;;;AAIb,SAAgB,IAAiB;CAC/B,IAAM,EAAE,SAAM,GAAS,EACjB,IAAW,GAAuB,EAElC,IAAY,GAAsB,MAAU,EAAM,UAAU,EAC5D,IAAc,GAAsB,MAAU,EAAM,YAAY,EAChE,IAAS,GAAsB,MAAU,EAAM,OAAO,EACtD,IAAQ,GAAsB,MAAU,EAAM,MAAM,EACpD,IAAY,GAAsB,MAAU,EAAM,UAAU,EAC5D,IAAU,GAAsB,MAAU,EAAM,QAAQ,EACxD,IAAQ,GAAsB,MAAU,EAAM,MAAM,EACpD,IAAY,GAAsB,MAAU,EAAM,UAAU,EAC5D,IAAc,GAAsB,MAAU,EAAM,YAAY,EAChE,IAAe,GAAsB,MAAU,EAAM,aAAa,EAClE,IAAiB,GAAsB,MAAU,EAAM,eAAe;AAE5E,UACE,EAAe,GAAK,QACP;AACX,IAAe,GAAM;KAEtB,CAAC,EAAe,CAAC;CAapB,IAAM,CAAC,GAAQ,KAAa,EAAS,EAAE,EACjC,IAAgB,EAAO,GAAM;AACnC,SAAgB;AAEd,EADI,KAAa,CAAC,EAAc,WAAS,GAAW,MAAM,IAAI,EAAE,EAChE,EAAc,UAAU;IACvB,CAAC,EAAU,CAAC;CAUf,IAAM,IAAiB,EAAyB,EAAO;AAEvD,KAAI,CAAC,EAAQ,QAAO;AAEpB,KAAI,CAAC,EACH,QACE,kBAAC,OAAD;EACE,WAAU;EACV,OAAO,EAAE,aAAa,GAAgB;YAFxC,CAKE,kBAAC,QAAD;GAAM,WAAU;aACb,IACG,EAAE,4CAA4C,mBAAmB,GACjE,EAAE,6CAA6C,oBAAoB;GAClE,CAAA,EACP,kBAAC,GAAD;GAAQ,MAAK;GAAK,SAAQ;GAAU,SAAS;GAAa,eAAY;aAAtE;IACE,kBAAC,GAAD,EAAU,WAAU,iBAAkB,CAAA;IACrC,EAAE,6CAA6C,UAAU;IACzD,IAAY,kBAAC,GAAD,EAAS,WAAU,gCAAiC,CAAA,GAAG;IAC7D;KACL;;CAIV,IAAM,IAAc,EAAU,WAExB,IAAoC,EAAc,KAAK,OAAS;EACpE,IAAI;EACJ,OAAO,EAAE,EAAW,GAAK,KAAK,EAAW,GAAK,SAAS;EACvD,QAAQ,EAAM;EACf,EAAE,EAEG,IAA8B;EAClC;GACE,IAAI;GACJ,OAAO,EAAE,4CAA4C,YAAY;GACjE,cACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;IACE,UAAS;IACT,SAAQ;IACR,cAAc;IACd,OAAO,EAAE,4CAA4C,iBAAiB;IACtE,aAAa,EACX,gDACA,8DACD;IACD,SACE,EAAU,WACR,kBAAC,GAAD,EAAkB,cAAc,EAAS,cAAc,EAAU,WAAW,EAAI,CAAA,GAC9E,KAAA;cAGN,kBAAC,GAAD;KAAiB,IAAI,EAAU;KAAU,MAAM;KAAa,CAAA;IACpD,CAAA,EACF,CAAA;GAEb;EACD;GACE,IAAI;GACJ,OAAO,EAAE,6CAA6C,YAAY;GAClE,cACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;IACE,UAAS;IACT,SAAQ;IACR,cAAc;IACd,OAAO,EAAE,6CAA6C,YAAY;IAClE,aAAa,EACX,6CACA,gEACD;IACD,SACE,IAAc,kBAAC,GAAD,EAAkB,cAAc,EAAS,eAAe,IAAc,EAAI,CAAA,GAAG,KAAA;cAG7F,kBAAC,GAAD,EAA+B,gBAAe,CAAA;IACtC,CAAA,EACF,CAAA;GAEb;EACD;GACE,IAAI;GACJ,OAAO,EAAE,0CAA0C,SAAS;GAC5D,cACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;IACE,UAAS;IACT,SAAQ;IACR,cAAc;IACd,OAAO,EAAE,4CAA4C,gBAAgB;IACrE,aAAa,EACX,gDACA,qEACD;IACD,SACE,EAAU,SACR,kBAAC,GAAD,EAAkB,cAAc,EAAS,YAAY,EAAU,SAAS,EAAI,CAAA,GAC1E,KAAA;cAGN,kBAAC,GAAD;KAAe,UAAU,EAAU;KAAQ,MAAM;KAAa,CAAA;IACtD,CAAA,EACF,CAAA;GAEb;EACD;GACE,IAAI;GACJ,OAAO,EAAE,0CAA0C,SAAS;GAC5D,cACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;IACE,UAAS;IACT,SAAQ;IACR,cAAc;IACd,OAAO,EAAE,uCAAuC,UAAU;IAC1D,aAAa,EACX,2CACA,oDACD;IACD,SACE,IAAc,kBAAC,GAAD,EAAkB,cAAc,EAAS,eAAe,IAAc,EAAI,CAAA,GAAG,KAAA;cAG7F,kBAAC,GAAD;KAA6B;KAAa,MAAM;KAAa,CAAA;IACrD,CAAA,EACF,CAAA;GAEb;EACF;AAED,QAIE,kBAAC,OAAD;EACE,eAAY;EACZ,OAAO,EAAE,aAAa,IAAiB,IAAiB,KAAK,KAAA,GAAW;EACxE,WAAU;YAHZ,CAaE,kBAAC,MAAD;GAAI,WAAU;aAAW,EAAE,qCAAqC,aAAa;GAAM,CAAA,EACnF,kBAAC,GAAD;GACE,OAAO,EAAE,qCAAqC,aAAa;GAC3D,QAAQ,OAAO,EAAO;GACtB,OAAO;GACD;GAMN,QAAO;GAMP,SACE,IACI,KAAW,EAAE,uCAAuC,WAAW,GAC/D,EAAE,oCAAoC,OAAO;GAE5C;GACP,MACE,kBAAC,GAAD;IACE,OAAO,IAAY,YAAY,IAAQ,YAAY;IACnD,YACE,IACI,EAAE,oCAAoC,OAAO,GAC7C,IACE,EAAE,uCAAuC,UAAU,GACnD;IAER,cAAc,EAAE,uCAAuC,UAAU;IACtD;IACX,MAAM,MAAY,EAAU,GAAS,EAAE;IACvC,CAAA;GAEJ,eACE,kBAAC,GAAD;IACE,MAAK;IACL,SAAQ;IACR,SAAS;IACT,cAAY,EAAE,8CAA8C,gBAAgB;IAC5E,eAAY;cAEZ,kBAAC,GAAD,EAAG,WAAU,UAAW,CAAA;IACjB,CAAA;GAEX,QAAQ,EAAc,EAAE;GACxB,CAAA,CACE"}
@@ -43,6 +43,7 @@ var o = {
43
43
  function s(e) {
44
44
  let t = e("bigconsole.widget.aiPreview.buildProgress", "Build progress"), n = e("bigconsole.widget.aiPreview.stepSkipped", "skipped");
45
45
  return {
46
+ errorHeading: e("bigconsole.widget.aiPreview.errorHeading", "Build stopped"),
46
47
  steps: t,
47
48
  stepsHeading: t,
48
49
  skipped: n,
@@ -55,22 +56,29 @@ function s(e) {
55
56
  }
56
57
  };
57
58
  }
58
- function c({ state: e, stateLabel: t, updatedLabel: i, ago: a }) {
59
- let o = "inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide";
59
+ function c({ state: i, stateLabel: a, updatedLabel: o, updatedAt: s, ago: c }) {
60
+ let [l, u] = t(() => Date.now());
61
+ e(() => {
62
+ if (s === null) return;
63
+ u(Date.now());
64
+ let e = window.setInterval(() => u(Date.now()), 1e3);
65
+ return () => window.clearInterval(e);
66
+ }, [s]);
67
+ let d = s === null ? 0 : Math.max(0, Math.floor((l - s) / 1e3)), f = "inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide";
60
68
  return /* @__PURE__ */ r("span", {
61
69
  className: "inline-flex items-center gap-2",
62
70
  "data-testid": "ai-preview-ago",
63
- children: [e !== "idle" && t ? /* @__PURE__ */ r("span", {
64
- className: e === "running" ? `${o} bg-[var(--color-status-success-bgSubtle)] text-status-success-text` : `${o} bg-[var(--color-status-error-bgSubtle)] text-status-error-text`,
65
- "data-testid": e === "running" ? "ai-preview-live" : "ai-preview-stopped",
66
- children: [e === "running" ? /* @__PURE__ */ r("span", {
71
+ children: [i !== "idle" && a ? /* @__PURE__ */ r("span", {
72
+ className: i === "running" ? `${f} bg-[var(--color-status-success-bgSubtle)] text-status-success-text` : `${f} bg-[var(--color-status-error-bgSubtle)] text-status-error-text`,
73
+ "data-testid": i === "running" ? "ai-preview-live" : "ai-preview-stopped",
74
+ children: [i === "running" ? /* @__PURE__ */ r("span", {
67
75
  className: "relative flex size-2",
68
76
  children: [/* @__PURE__ */ n("span", { className: "absolute inline-flex size-full rounded-full bg-status-success-text opacity-75 motion-safe:animate-ping" }), /* @__PURE__ */ n("span", { className: "relative inline-flex size-2 rounded-full bg-status-success-text" })]
69
- }) : /* @__PURE__ */ n("span", { className: "relative inline-flex size-2 rounded-full bg-status-error-text" }), t]
77
+ }) : /* @__PURE__ */ n("span", { className: "relative inline-flex size-2 rounded-full bg-status-error-text" }), a]
70
78
  }) : null, /* @__PURE__ */ r("span", { children: [
71
- i,
79
+ o,
72
80
  " ",
73
- a
81
+ c(d)
74
82
  ] })]
75
83
  });
76
84
  }
@@ -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 * sidebar, and how it words a relative timestamp.\n *\n * Nothing here touches Apollo, React Query, or BigConsole context — it is pure\n * DOM + formatting, which is why the mini panel can live in the app shell\n * 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 * Width of the assistant sidebar, or 0 when it is closed.\n *\n * Polled rather than subscribed: the widget lives in fe-libs, is mounted by the\n * app shell, and emits nothing when it opens or closes. Reading the DOM is the\n * only honest signal available, and it is cheap.\n *\n * ★★★ `enabled` IS NOT AN OPTIMISATION, IT IS THE FIX. The mini panel is\n * mounted by the app shell on every page, and a hook cannot be called below an\n * early return — so this 500ms poll ran for the lifetime of the tab on every\n * route, for every user, whether or not the assistant had ever been opened.\n * Moving the call site was impossible; making the hook do nothing was not.\n */\nexport function useAssistantSidebarWidth(enabled: boolean): number {\n const [width, setWidth] = useState(0);\n\n useEffect(() => {\n if (!enabled) {\n setWidth(0);\n return;\n }\n const measure = () => {\n const el = document.querySelector('[data-testid=\"assistant-widget-window\"][data-layout=\"sidebar\"]');\n const rect = el?.getBoundingClientRect();\n // Below `sm` the sidebar is full-width; reserving that would leave no room\n // for the preview at all, so on small screens let it stack underneath.\n const next = rect && rect.width < window.innerWidth * 0.75 ? Math.round(rect.width) : 0;\n setWidth((current) => (current === next ? current : next));\n };\n measure();\n const intervalId = window.setInterval(measure, 500);\n window.addEventListener('resize', measure);\n return () => {\n window.clearInterval(intervalId);\n window.removeEventListener('resize', measure);\n };\n }, [enabled]);\n\n return width;\n}\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 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 ago,\n}: {\n state: 'running' | 'stopped' | 'idle';\n stateLabel: string | null;\n updatedLabel: string;\n ago: string;\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}\n </span>\n </span>\n );\n}\n"],"mappings":";;;AAiCA,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;;AAgBnG,SAAgB,EAAyB,GAA0B;CACjE,IAAM,CAAC,GAAO,KAAY,EAAS,EAAE;AAwBrC,QAtBA,QAAgB;AACd,MAAI,CAAC,GAAS;AACZ,KAAS,EAAE;AACX;;EAEF,IAAM,UAAgB;GAEpB,IAAM,IADK,SAAS,cAAc,qEAAiE,EAClF,uBAAuB,EAGlC,IAAO,KAAQ,EAAK,QAAQ,OAAO,aAAa,MAAO,KAAK,MAAM,EAAK,MAAM,GAAG;AACtF,MAAU,MAAa,MAAY,IAAO,IAAU,EAAM;;AAE5D,KAAS;EACT,IAAM,IAAa,OAAO,YAAY,GAAS,IAAI;AAEnD,SADA,OAAO,iBAAiB,UAAU,EAAQ,QAC7B;AAEX,GADA,OAAO,cAAc,EAAW,EAChC,OAAO,oBAAoB,UAAU,EAAQ;;IAE9C,CAAC,EAAQ,CAAC,EAEN;;AAcT,IAAa,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;EACL,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,UAMC;CACD,IAAM,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;GACX,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 * sidebar, and how it words a relative timestamp.\n *\n * Nothing here touches Apollo, React Query, or BigConsole context — it is pure\n * DOM + formatting, which is why the mini panel can live in the app shell\n * 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 * Width of the assistant sidebar, or 0 when it is closed.\n *\n * Polled rather than subscribed: the widget lives in fe-libs, is mounted by the\n * app shell, and emits nothing when it opens or closes. Reading the DOM is the\n * only honest signal available, and it is cheap.\n *\n * ★★★ `enabled` IS NOT AN OPTIMISATION, IT IS THE FIX. The mini panel is\n * mounted by the app shell on every page, and a hook cannot be called below an\n * early return — so this 500ms poll ran for the lifetime of the tab on every\n * route, for every user, whether or not the assistant had ever been opened.\n * Moving the call site was impossible; making the hook do nothing was not.\n */\nexport function useAssistantSidebarWidth(enabled: boolean): number {\n const [width, setWidth] = useState(0);\n\n useEffect(() => {\n if (!enabled) {\n setWidth(0);\n return;\n }\n const measure = () => {\n const el = document.querySelector('[data-testid=\"assistant-widget-window\"][data-layout=\"sidebar\"]');\n const rect = el?.getBoundingClientRect();\n // Below `sm` the sidebar is full-width; reserving that would leave no room\n // for the preview at all, so on small screens let it stack underneath.\n const next = rect && rect.width < window.innerWidth * 0.75 ? Math.round(rect.width) : 0;\n setWidth((current) => (current === next ? current : next));\n };\n measure();\n const intervalId = window.setInterval(measure, 500);\n window.addEventListener('resize', measure);\n return () => {\n window.clearInterval(intervalId);\n window.removeEventListener('resize', measure);\n };\n }, [enabled]);\n\n return width;\n}\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":";;;AAiCA,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;;AAgBnG,SAAgB,EAAyB,GAA0B;CACjE,IAAM,CAAC,GAAO,KAAY,EAAS,EAAE;AAwBrC,QAtBA,QAAgB;AACd,MAAI,CAAC,GAAS;AACZ,KAAS,EAAE;AACX;;EAEF,IAAM,UAAgB;GAEpB,IAAM,IADK,SAAS,cAAc,qEAAiE,EAClF,uBAAuB,EAGlC,IAAO,KAAQ,EAAK,QAAQ,OAAO,aAAa,MAAO,KAAK,MAAM,EAAK,MAAM,GAAG;AACtF,MAAU,MAAa,MAAY,IAAO,IAAU,EAAM;;AAE5D,KAAS;EACT,IAAM,IAAa,OAAO,YAAY,GAAS,IAAI;AAEnD,SADA,OAAO,iBAAiB,UAAU,EAAQ,QAC7B;AAEX,GADA,OAAO,cAAc,EAAW,EAChC,OAAO,oBAAoB,UAAU,EAAQ;;IAE9C,CAAC,EAAQ,CAAC,EAEN;;AAcT,IAAa,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.912.1",
3
+ "version": "2026.912.3",
4
4
  "description": "BigConsole - AI-powered analytics and dashboard platform",
5
5
  "type": "module",
6
6
  "files": [
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@apollo/client": "^4.0.11",
51
- "@burdenoff/fe-libs": "2026.912.2",
51
+ "@burdenoff/fe-libs": "2026.912.3",
52
52
  "@burdenoff/fluidgrids-fe-sdk": "2026.731.4",
53
53
  "@iconify/react": "^6.0.2",
54
54
  "@monaco-editor/react": "^4.7.0",
@@ -83,7 +83,7 @@
83
83
  "zustand": "^5.0.3"
84
84
  },
85
85
  "devDependencies": {
86
- "@burdenoff/fe-libs": "2026.912.2",
86
+ "@burdenoff/fe-libs": "2026.912.3",
87
87
  "@burdenoff/fluidgrids-fe-sdk": "2026.731.4",
88
88
  "@eslint/js": "^10.0.1",
89
89
  "@graphql-codegen/add": "^5.0.0",