@miragon/mcp-toolkit-ui 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/app/builder/BuilderBanners.d.ts +19 -0
- package/dist/app/builder/BuilderBanners.d.ts.map +1 -0
- package/dist/app/builder/BuilderBanners.js +22 -0
- package/dist/app/builder/BuilderBanners.js.map +1 -0
- package/dist/app/builder/CanvasRow.d.ts +26 -0
- package/dist/app/builder/CanvasRow.d.ts.map +1 -0
- package/dist/app/builder/CanvasRow.js +98 -0
- package/dist/app/builder/CanvasRow.js.map +1 -0
- package/dist/app/builder/CellPropsSheet.d.ts +19 -0
- package/dist/app/builder/CellPropsSheet.d.ts.map +1 -0
- package/dist/app/builder/CellPropsSheet.js +28 -0
- package/dist/app/builder/CellPropsSheet.js.map +1 -0
- package/dist/app/builder/EmptyHint.d.ts +6 -0
- package/dist/app/builder/EmptyHint.d.ts.map +1 -0
- package/dist/app/builder/EmptyHint.js +10 -0
- package/dist/app/builder/EmptyHint.js.map +1 -0
- package/dist/app/builder/LayoutSurface.d.ts +27 -0
- package/dist/app/builder/LayoutSurface.d.ts.map +1 -0
- package/dist/app/builder/LayoutSurface.js +45 -0
- package/dist/app/builder/LayoutSurface.js.map +1 -0
- package/dist/app/builder/Workspace.d.ts +9 -3
- package/dist/app/builder/Workspace.d.ts.map +1 -1
- package/dist/app/builder/Workspace.js +3 -97
- package/dist/app/builder/Workspace.js.map +1 -1
- package/dist/app/builder/builder-reducer.d.ts.map +1 -1
- package/dist/app/builder/builder-reducer.js +176 -164
- package/dist/app/builder/builder-reducer.js.map +1 -1
- package/dist/app/builder/use-builder-catalogue.d.ts +33 -0
- package/dist/app/builder/use-builder-catalogue.d.ts.map +1 -0
- package/dist/app/builder/use-builder-catalogue.js +155 -0
- package/dist/app/builder/use-builder-catalogue.js.map +1 -0
- package/dist/app/builder/use-key-step-entries.d.ts +22 -0
- package/dist/app/builder/use-key-step-entries.d.ts.map +1 -0
- package/dist/app/builder/use-key-step-entries.js +46 -0
- package/dist/app/builder/use-key-step-entries.js.map +1 -0
- package/dist/app/builder/use-save-dashboard.d.ts +33 -0
- package/dist/app/builder/use-save-dashboard.d.ts.map +1 -0
- package/dist/app/builder/use-save-dashboard.js +78 -0
- package/dist/app/builder/use-save-dashboard.js.map +1 -0
- package/dist/app/host-bridge.d.ts +4 -4
- package/dist/app/host-bridge.js +2 -2
- package/dist/app/layout-builder.d.ts.map +1 -1
- package/dist/app/layout-builder.js +46 -277
- package/dist/app/layout-builder.js.map +1 -1
- package/dist/app/mcp-app-view-chrome.d.ts +77 -0
- package/dist/app/mcp-app-view-chrome.d.ts.map +1 -0
- package/dist/app/mcp-app-view-chrome.js +53 -0
- package/dist/app/mcp-app-view-chrome.js.map +1 -0
- package/dist/app/mcp-app-view.d.ts +3 -9
- package/dist/app/mcp-app-view.d.ts.map +1 -1
- package/dist/app/mcp-app-view.js +4 -31
- package/dist/app/mcp-app-view.js.map +1 -1
- package/dist/render-cases-primitives.d.ts +17 -0
- package/dist/render-cases-primitives.d.ts.map +1 -0
- package/dist/render-cases-primitives.js +88 -0
- package/dist/render-cases-primitives.js.map +1 -0
- package/dist/render-cases.d.ts +4 -0
- package/dist/render-cases.d.ts.map +1 -0
- package/dist/render-cases.js +140 -0
- package/dist/render-cases.js.map +1 -0
- package/package.json +12 -7
- package/src/app/builder/BuilderBanners.tsx +61 -0
- package/src/app/builder/CanvasRow.tsx +316 -0
- package/src/app/builder/CellPropsSheet.tsx +47 -0
- package/src/app/builder/EmptyHint.tsx +29 -0
- package/src/app/builder/LayoutSurface.tsx +140 -0
- package/src/app/builder/Workspace.tsx +33 -342
- package/src/app/builder/builder-reducer.ts +204 -170
- package/src/app/builder/use-builder-catalogue.ts +187 -0
- package/src/app/builder/use-key-step-entries.ts +60 -0
- package/src/app/builder/use-save-dashboard.ts +97 -0
- package/src/app/host-bridge.tsx +4 -4
- package/src/app/layout-builder.tsx +92 -497
- package/src/app/mcp-app-view-chrome.tsx +204 -0
- package/src/app/mcp-app-view.tsx +32 -140
- package/src/lib/parse-tool-result.property.test.ts +90 -0
- package/src/render-cases-primitives.tsx +255 -0
- package/src/render-cases.tsx +239 -0
- package/src/render-smoke.test.tsx +100 -0
- package/src/ui-catalog.test.ts +52 -0
- package/ui-catalog.json +1 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import { defaultStepIdFor, keysToEntries } from "./builder-model.js";
|
|
3
|
+
// -------------------------------------------------------------------------- //
|
|
4
|
+
// Keys + steps editor state — the pipeline tab's editable entry lists
|
|
5
|
+
// -------------------------------------------------------------------------- //
|
|
6
|
+
/**
|
|
7
|
+
* Owns the key/step editor entries and their mutations, extracted from
|
|
8
|
+
* `layout-builder.tsx` unchanged. `addKey` de-dupes a prefilled name (the
|
|
9
|
+
* catalogue's "Add as key" action) and `addStep` derives a unique context
|
|
10
|
+
* id via `defaultStepIdFor`.
|
|
11
|
+
*/
|
|
12
|
+
export function useKeyStepEntries({ initialKeys, initialSteps, }) {
|
|
13
|
+
const [keyEntries, setKeyEntries] = useState(() => keysToEntries(initialKeys));
|
|
14
|
+
const [stepEntries, setStepEntries] = useState(() => initialSteps ?? []);
|
|
15
|
+
const addKey = useCallback((prefilledName) => {
|
|
16
|
+
setKeyEntries((prev) => {
|
|
17
|
+
if (prefilledName) {
|
|
18
|
+
if (prev.some((e) => e.name === prefilledName))
|
|
19
|
+
return prev;
|
|
20
|
+
return [...prev, { name: prefilledName, rawValue: "" }];
|
|
21
|
+
}
|
|
22
|
+
return [...prev, { name: "", rawValue: "" }];
|
|
23
|
+
});
|
|
24
|
+
}, []);
|
|
25
|
+
const updateKey = useCallback((idx, patch) => {
|
|
26
|
+
setKeyEntries((prev) => prev.map((e, i) => (i === idx ? { ...e, ...patch } : e)));
|
|
27
|
+
}, []);
|
|
28
|
+
const removeKey = useCallback((idx) => {
|
|
29
|
+
setKeyEntries((prev) => prev.filter((_, i) => i !== idx));
|
|
30
|
+
}, []);
|
|
31
|
+
const addStep = useCallback((stepId) => {
|
|
32
|
+
setStepEntries((prev) => {
|
|
33
|
+
const taken = new Set(prev.map((s) => s.id));
|
|
34
|
+
const newId = stepId ? defaultStepIdFor(stepId, taken) : `step-${prev.length + 1}`;
|
|
35
|
+
return [...prev, { id: newId, step: stepId ?? "", optional: false }];
|
|
36
|
+
});
|
|
37
|
+
}, []);
|
|
38
|
+
const updateStep = useCallback((idx, patch) => {
|
|
39
|
+
setStepEntries((prev) => prev.map((s, i) => (i === idx ? { ...s, ...patch } : s)));
|
|
40
|
+
}, []);
|
|
41
|
+
const removeStep = useCallback((idx) => {
|
|
42
|
+
setStepEntries((prev) => prev.filter((_, i) => i !== idx));
|
|
43
|
+
}, []);
|
|
44
|
+
return { keyEntries, stepEntries, addKey, updateKey, removeKey, addStep, updateStep, removeStep };
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=use-key-step-entries.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-key-step-entries.js","sourceRoot":"","sources":["../../../src/app/builder/use-key-step-entries.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { PipelineStepRef } from "@miragon/mcp-toolkit-core";
|
|
2
|
+
import { type DraftLayout, type KeyEntry } from "./builder-model.js";
|
|
3
|
+
/**
|
|
4
|
+
* Owns the save dialog's state (open flag, name/description inputs, busy
|
|
5
|
+
* flag, inline error) and the `save-dashboard` call. Extracted from
|
|
6
|
+
* `layout-builder.tsx` unchanged: a rejected save surfaces its reason
|
|
7
|
+
* inline in the SaveDialog instead of failing silently (Finding [3]).
|
|
8
|
+
*/
|
|
9
|
+
export declare function useSaveDashboard({ callTool, saveToolName, dashboardId, title, keyEntries, stepEntries, draft, onSaved, }: {
|
|
10
|
+
callTool: (name: string, args: object) => Promise<unknown>;
|
|
11
|
+
saveToolName: string;
|
|
12
|
+
dashboardId?: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
keyEntries: KeyEntry[];
|
|
15
|
+
stepEntries: PipelineStepRef[];
|
|
16
|
+
draft: DraftLayout;
|
|
17
|
+
onSaved?: (result: {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
}) => void;
|
|
21
|
+
}): {
|
|
22
|
+
isBusy: boolean;
|
|
23
|
+
saveOpen: boolean;
|
|
24
|
+
setSaveOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
25
|
+
saveName: string;
|
|
26
|
+
setSaveName: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
27
|
+
saveDescription: string;
|
|
28
|
+
setSaveDescription: import("react").Dispatch<import("react").SetStateAction<string>>;
|
|
29
|
+
saveError: string | null;
|
|
30
|
+
setSaveError: import("react").Dispatch<import("react").SetStateAction<string | null>>;
|
|
31
|
+
saveDraft: () => Promise<void>;
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=use-save-dashboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-save-dashboard.d.ts","sourceRoot":"","sources":["../../../src/app/builder/use-save-dashboard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAEhE,OAAO,EAAgC,KAAK,WAAW,EAAE,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAMlG;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,KAAK,EACL,UAAU,EACV,WAAW,EACX,KAAK,EACL,OAAO,GACR,EAAE;IACD,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC1D,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,QAAQ,EAAE,CAAA;IACtB,WAAW,EAAE,eAAe,EAAE,CAAA;IAC9B,KAAK,EAAE,WAAW,CAAA;IAClB,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CACzD;;;;;;;;;;;EA+DA"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import { parseToolResult } from "../../lib/parse-tool-result.js";
|
|
3
|
+
import { draftToLayout, entriesToKeys } from "./builder-model.js";
|
|
4
|
+
// -------------------------------------------------------------------------- //
|
|
5
|
+
// Save-dashboard hook — dialog state + the `save-dashboard` call
|
|
6
|
+
// -------------------------------------------------------------------------- //
|
|
7
|
+
/**
|
|
8
|
+
* Owns the save dialog's state (open flag, name/description inputs, busy
|
|
9
|
+
* flag, inline error) and the `save-dashboard` call. Extracted from
|
|
10
|
+
* `layout-builder.tsx` unchanged: a rejected save surfaces its reason
|
|
11
|
+
* inline in the SaveDialog instead of failing silently (Finding [3]).
|
|
12
|
+
*/
|
|
13
|
+
export function useSaveDashboard({ callTool, saveToolName, dashboardId, title, keyEntries, stepEntries, draft, onSaved, }) {
|
|
14
|
+
const [isBusy, setBusy] = useState(false);
|
|
15
|
+
const [saveOpen, setSaveOpen] = useState(false);
|
|
16
|
+
const [saveName, setSaveName] = useState(title ?? "");
|
|
17
|
+
const [saveDescription, setSaveDescription] = useState("");
|
|
18
|
+
// Last save failure (Finding [3]). Shown inline in the SaveDialog so a
|
|
19
|
+
// rejected `save-dashboard` call no longer fails silently.
|
|
20
|
+
const [saveError, setSaveError] = useState(null);
|
|
21
|
+
const saveDraft = useCallback(async () => {
|
|
22
|
+
if (!saveName.trim())
|
|
23
|
+
return;
|
|
24
|
+
setBusy(true);
|
|
25
|
+
setSaveError(null);
|
|
26
|
+
try {
|
|
27
|
+
const result = await callTool(saveToolName, {
|
|
28
|
+
id: dashboardId,
|
|
29
|
+
name: saveName.trim(),
|
|
30
|
+
description: saveDescription.trim() || undefined,
|
|
31
|
+
keys: entriesToKeys(keyEntries),
|
|
32
|
+
steps: stepEntries.filter((s) => s.step.trim().length > 0),
|
|
33
|
+
layout: draftToLayout(draft),
|
|
34
|
+
title,
|
|
35
|
+
});
|
|
36
|
+
const saved = parseToolResult(result);
|
|
37
|
+
setSaveOpen(false);
|
|
38
|
+
if (saved?.id && onSaved) {
|
|
39
|
+
onSaved({
|
|
40
|
+
id: saved.id,
|
|
41
|
+
name: saved.name ?? saveName.trim(),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
// Previously uncaught: a rejected `save-dashboard` left the dialog open
|
|
47
|
+
// with no feedback. Surface the reason inline (Finding [3]).
|
|
48
|
+
setSaveError(err instanceof Error ? err.message : String(err));
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
setBusy(false);
|
|
52
|
+
}
|
|
53
|
+
}, [
|
|
54
|
+
callTool,
|
|
55
|
+
dashboardId,
|
|
56
|
+
draft,
|
|
57
|
+
keyEntries,
|
|
58
|
+
onSaved,
|
|
59
|
+
saveDescription,
|
|
60
|
+
saveName,
|
|
61
|
+
saveToolName,
|
|
62
|
+
stepEntries,
|
|
63
|
+
title,
|
|
64
|
+
]);
|
|
65
|
+
return {
|
|
66
|
+
isBusy,
|
|
67
|
+
saveOpen,
|
|
68
|
+
setSaveOpen,
|
|
69
|
+
saveName,
|
|
70
|
+
setSaveName,
|
|
71
|
+
saveDescription,
|
|
72
|
+
setSaveDescription,
|
|
73
|
+
saveError,
|
|
74
|
+
setSaveError,
|
|
75
|
+
saveDraft,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=use-save-dashboard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-save-dashboard.js","sourceRoot":"","sources":["../../../src/app/builder/use-save-dashboard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAE7C,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAA;AAChE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAmC,MAAM,oBAAoB,CAAA;AAElG,gFAAgF;AAChF,iEAAiE;AACjE,gFAAgF;AAEhF;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,EAC/B,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,KAAK,EACL,UAAU,EACV,WAAW,EACX,KAAK,EACL,OAAO,GAUR;IACC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACzC,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAC/C,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,KAAK,IAAI,EAAE,CAAC,CAAA;IACrD,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IAC1D,uEAAuE;IACvE,2DAA2D;IAC3D,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAA;IAE/D,MAAM,SAAS,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACvC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE;YAAE,OAAM;QAC5B,OAAO,CAAC,IAAI,CAAC,CAAA;QACb,YAAY,CAAC,IAAI,CAAC,CAAA;QAClB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,YAAY,EAAE;gBAC1C,EAAE,EAAE,WAAW;gBACf,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE;gBACrB,WAAW,EAAE,eAAe,CAAC,IAAI,EAAE,IAAI,SAAS;gBAChD,IAAI,EAAE,aAAa,CAAC,UAAU,CAAC;gBAC/B,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;gBAC1D,MAAM,EAAE,aAAa,CAAC,KAAK,CAAC;gBAC5B,KAAK;aACN,CAAC,CAAA;YACF,MAAM,KAAK,GAAG,eAAe,CAAwC,MAAM,CAAC,CAAA;YAC5E,WAAW,CAAC,KAAK,CAAC,CAAA;YAClB,IAAI,KAAK,EAAE,EAAE,IAAI,OAAO,EAAE,CAAC;gBACzB,OAAO,CAAC;oBACN,EAAE,EAAE,KAAK,CAAC,EAAE;oBACZ,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,QAAQ,CAAC,IAAI,EAAE;iBACpC,CAAC,CAAA;YACJ,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,wEAAwE;YACxE,6DAA6D;YAC7D,YAAY,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;QAChE,CAAC;gBAAS,CAAC;YACT,OAAO,CAAC,KAAK,CAAC,CAAA;QAChB,CAAC;IACH,CAAC,EAAE;QACD,QAAQ;QACR,WAAW;QACX,KAAK;QACL,UAAU;QACV,OAAO;QACP,eAAe;QACf,QAAQ;QACR,YAAY;QACZ,WAAW;QACX,KAAK;KACN,CAAC,CAAA;IAEF,OAAO;QACL,MAAM;QACN,QAAQ;QACR,WAAW;QACX,QAAQ;QACR,WAAW;QACX,eAAe;QACf,kBAAkB;QAClB,SAAS;QACT,YAAY;QACZ,SAAS;KACV,CAAA;AACH,CAAC"}
|
|
@@ -4,7 +4,7 @@ import { type ReactNode } from "react";
|
|
|
4
4
|
* host runtime. A `HostBridge` is the single seam a portable widget talks to:
|
|
5
5
|
* write the widget against {@link useHostBridge} and it runs unchanged in our
|
|
6
6
|
* own `mcp-use` host, inside ChatGPT (OpenAI Apps SDK), or as a standalone web
|
|
7
|
-
* app driven directly by a `@modelcontextprotocol/
|
|
7
|
+
* app driven directly by a `@modelcontextprotocol/client` client — only the
|
|
8
8
|
* surrounding adapter changes.
|
|
9
9
|
*
|
|
10
10
|
* This is deliberately the *minimum* a deterministic UI needs, not a mirror of
|
|
@@ -164,7 +164,7 @@ export declare function createChatGptHostBridge(sdk?: OpenAiAppsSdk | null): Hos
|
|
|
164
164
|
export interface StandaloneHostBridgeOptions {
|
|
165
165
|
/**
|
|
166
166
|
* The only required capability: how to call a tool on the server. In a
|
|
167
|
-
* standalone web app this typically wraps a `@modelcontextprotocol/
|
|
167
|
+
* standalone web app this typically wraps a `@modelcontextprotocol/client`
|
|
168
168
|
* `Client.callTool({ name, arguments })`, but any function with this shape
|
|
169
169
|
* works — that injection is the whole point: hand-built widgets render
|
|
170
170
|
* against *any* existing MCP server without the toolkit's host runtime.
|
|
@@ -197,7 +197,7 @@ export interface StandaloneHostBridgeOptions {
|
|
|
197
197
|
/**
|
|
198
198
|
* Build a {@link HostBridge} for a standalone web app driven against an existing
|
|
199
199
|
* MCP server — no toolkit host, no `createFrameworkApp`. You inject `callTool`
|
|
200
|
-
* (usually a thin wrapper over a `@modelcontextprotocol/
|
|
200
|
+
* (usually a thin wrapper over a `@modelcontextprotocol/client` client); the
|
|
201
201
|
* conversation-shaped verbs (`sendFollowup`, `setModelContext`) are no-ops
|
|
202
202
|
* unless you wire handlers, and `openExternal` defaults to `window.open`.
|
|
203
203
|
*
|
|
@@ -207,7 +207,7 @@ export interface StandaloneHostBridgeOptions {
|
|
|
207
207
|
* harness also builds on this factory so host simulation has a single source.
|
|
208
208
|
*
|
|
209
209
|
* @example
|
|
210
|
-
* const client = new Client(…) // @modelcontextprotocol/
|
|
210
|
+
* const client = new Client(…) // @modelcontextprotocol/client
|
|
211
211
|
* const bridge = createStandaloneHostBridge({
|
|
212
212
|
* callTool: (name, args) => client.callTool({ name, arguments: args }),
|
|
213
213
|
* getData: () => currentRecord,
|
package/dist/app/host-bridge.js
CHANGED
|
@@ -173,7 +173,7 @@ export function createChatGptHostBridge(sdk) {
|
|
|
173
173
|
/**
|
|
174
174
|
* Build a {@link HostBridge} for a standalone web app driven against an existing
|
|
175
175
|
* MCP server — no toolkit host, no `createFrameworkApp`. You inject `callTool`
|
|
176
|
-
* (usually a thin wrapper over a `@modelcontextprotocol/
|
|
176
|
+
* (usually a thin wrapper over a `@modelcontextprotocol/client` client); the
|
|
177
177
|
* conversation-shaped verbs (`sendFollowup`, `setModelContext`) are no-ops
|
|
178
178
|
* unless you wire handlers, and `openExternal` defaults to `window.open`.
|
|
179
179
|
*
|
|
@@ -183,7 +183,7 @@ export function createChatGptHostBridge(sdk) {
|
|
|
183
183
|
* harness also builds on this factory so host simulation has a single source.
|
|
184
184
|
*
|
|
185
185
|
* @example
|
|
186
|
-
* const client = new Client(…) // @modelcontextprotocol/
|
|
186
|
+
* const client = new Client(…) // @modelcontextprotocol/client
|
|
187
187
|
* const bridge = createStandaloneHostBridge({
|
|
188
188
|
* callTool: (name, args) => client.callTool({ name, arguments: args }),
|
|
189
189
|
* getData: () => currentRecord,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"layout-builder.d.ts","sourceRoot":"","sources":["../../src/app/layout-builder.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"layout-builder.d.ts","sourceRoot":"","sources":["../../src/app/layout-builder.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAuB,MAAM,2BAA2B,CAAA;AAOnG,OAAO,EAAkB,KAAK,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAc9E,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAO3D,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAM9D,MAAM,WAAW,kBAAkB;IACjC,aAAa,CAAC,EAAE,YAAY,CAAA;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,YAAY,CAAC,EAAE,eAAe,EAAE,CAAA;IAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;IACxC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC1D,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,mBAAmB,CAAA;IAC5B,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;IACxD;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE;QACjB,MAAM,EAAE,YAAY,CAAA;QACpB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QAC9B,KAAK,CAAC,EAAE,eAAe,EAAE,CAAA;QACzB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,OAAO,CAAC,EAAE;YACR,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;YAC7B,OAAO,EAAE,MAAM,EAAE,CAAA;YACjB,QAAQ,EAAE,MAAM,CACd,MAAM,EACN;gBAAE,IAAI,EAAE,OAAO,CAAC;gBAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBAAC,IAAI,EAAE,MAAM,CAAC;gBAAC,SAAS,EAAE,MAAM,CAAA;aAAE,CAClF,CAAA;YACD,MAAM,EAAE;gBAAE,MAAM,EAAE,MAAM,CAAC;gBAAC,MAAM,EAAE,MAAM,CAAA;aAAE,EAAE,CAAA;SAC7C,CAAA;KACF,KAAK,IAAI,CAAA;CACX;AAMD,wBAAgB,aAAa,CAAC,EAC5B,aAAa,EACb,KAAK,EACL,WAAW,EACX,YAAY,EACZ,OAAO,EACP,QAAQ,EACR,iBAA2C,EAC3C,YAA+B,EAC/B,WAAW,EACX,MAAM,EACN,OAAO,EACP,MAAM,GACP,EAAE,kBAAkB,+BA0XpB"}
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback,
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { defaultStepIdFor, draftToLayout, entriesToKeys, initDraft, keysToEntries, sizeToSpan, toPipelineContext, } from "./builder/builder-model.js";
|
|
2
|
+
import { useCallback, useMemo, useReducer, useState } from "react";
|
|
3
|
+
import { Eye, Layers, SlidersHorizontal } from "lucide-react";
|
|
4
|
+
import { draftToLayout, entriesToKeys, initDraft, sizeToSpan } from "./builder/builder-model.js";
|
|
6
5
|
import { draftReducer, initBuilderState, } from "./builder/builder-reducer.js";
|
|
7
6
|
import { DEFAULT_LABELS } from "./builder/labels.js";
|
|
8
7
|
import { Toolbar } from "./builder/Toolbar.js";
|
|
9
8
|
import { PipelineStrip } from "./builder/PipelineStrip.js";
|
|
10
|
-
import { Workspace } from "./builder/Workspace.js";
|
|
11
9
|
import { CatalogueSheet } from "./builder/CatalogueSheet.js";
|
|
12
10
|
import { SaveDialog } from "./builder/SaveDialog.js";
|
|
13
11
|
import { PreviewPane } from "./builder/PreviewPane.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
12
|
+
import { LayoutSurface } from "./builder/LayoutSurface.js";
|
|
13
|
+
import { BuilderBanners } from "./builder/BuilderBanners.js";
|
|
14
|
+
import { EmptyHint } from "./builder/EmptyHint.js";
|
|
15
|
+
import { CellPropsSheet } from "./builder/CellPropsSheet.js";
|
|
16
|
+
import { useBuilderCatalogue } from "./builder/use-builder-catalogue.js";
|
|
17
|
+
import { useKeyStepEntries } from "./builder/use-key-step-entries.js";
|
|
18
|
+
import { useSaveDashboard } from "./builder/use-save-dashboard.js";
|
|
16
19
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../primitives/tabs.js";
|
|
17
|
-
import { cn } from "../lib/utils.js";
|
|
18
20
|
// -------------------------------------------------------------------------- //
|
|
19
21
|
// Main component
|
|
20
22
|
// -------------------------------------------------------------------------- //
|
|
21
|
-
const EMPTY_CONTEXT = { steps: {}, keys: {}, errors: [] };
|
|
22
23
|
export function LayoutBuilder({ initialLayout, title, initialKeys, initialSteps, widgets, callTool, catalogueToolName = "get-builder-catalogue", saveToolName = "save-dashboard", dashboardId, labels, onSaved, onExit, }) {
|
|
23
24
|
const L = useMemo(() => ({ ...DEFAULT_LABELS, ...labels }), [labels]);
|
|
24
25
|
// ── Draft layout (pure reducer) ─────────────────────────────────────────
|
|
@@ -54,33 +55,22 @@ export function LayoutBuilder({ initialLayout, title, initialKeys, initialSteps,
|
|
|
54
55
|
setRenameRejected(false);
|
|
55
56
|
}
|
|
56
57
|
// ── Keys + steps editor state ───────────────────────────────────────────
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
const
|
|
62
|
-
const [liveUnreachable, setLiveUnreachable] = useState([]);
|
|
63
|
-
const [liveCatalog, setLiveCatalog] = useState([]);
|
|
64
|
-
const [liveSteps, setLiveSteps] = useState([]);
|
|
65
|
-
const [liveContext, setLiveContext] = useState(EMPTY_CONTEXT);
|
|
66
|
-
// Pipeline validation issues from the catalogue (Finding [10]/[6]). The
|
|
67
|
-
// catalogue is fail-soft, but `render-view` is fail-hard, so we surface
|
|
68
|
-
// these so the user sees — before committing — that the pipeline as
|
|
69
|
-
// configured wouldn't render.
|
|
70
|
-
const [validationIssues, setValidationIssues] = useState([]);
|
|
71
|
-
const [status, setStatus] = useState("idle");
|
|
72
|
-
// Last catalogue-fetch failure (Finding [3]). Surfaced as a toolbar banner
|
|
73
|
-
// instead of only `console.warn`-ing, so the user knows the palette/context
|
|
74
|
-
// may be stale. Cleared on the next successful refresh.
|
|
75
|
-
const [refreshError, setRefreshError] = useState(null);
|
|
58
|
+
const { keyEntries, stepEntries, addKey, updateKey, removeKey, addStep, updateStep, removeStep } = useKeyStepEntries({ initialKeys, initialSteps });
|
|
59
|
+
// ── Catalogue fetch (mount + on key/step edits) ─────────────────────────
|
|
60
|
+
// The builder fetches its own catalogue from the app-only
|
|
61
|
+
// `get-builder-catalogue` tool; the LLM never sees this data.
|
|
62
|
+
const { liveReachable, liveUnreachable, liveCatalog, liveSteps, liveContext, validationIssues, status, refreshError, widgetById, propsSchemaByWidgetId, paletteByApp, liveKeyCount, stepsByApp, } = useBuilderCatalogue({ callTool, catalogueToolName, keyEntries, stepEntries });
|
|
76
63
|
// ── Save dialog ─────────────────────────────────────────────────────────
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
64
|
+
const { isBusy, saveOpen, setSaveOpen, saveName, setSaveName, saveDescription, setSaveDescription, saveError, setSaveError, saveDraft, } = useSaveDashboard({
|
|
65
|
+
callTool,
|
|
66
|
+
saveToolName,
|
|
67
|
+
dashboardId,
|
|
68
|
+
title,
|
|
69
|
+
keyEntries,
|
|
70
|
+
stepEntries,
|
|
71
|
+
draft,
|
|
72
|
+
onSaved,
|
|
73
|
+
});
|
|
84
74
|
// ── DnD ──────────────────────────────────────────────────────────────────
|
|
85
75
|
const [dragOverRow, setDragOverRow] = useState(null);
|
|
86
76
|
// Which cell (if any) is having its props configured. The
|
|
@@ -89,48 +79,6 @@ export function LayoutBuilder({ initialLayout, title, initialKeys, initialSteps,
|
|
|
89
79
|
// — including across tabs. Stored as `{ rowIdx, cellIdx }` against the
|
|
90
80
|
// currently active draft surface (rows or active tab's rows).
|
|
91
81
|
const [configuringCell, setConfiguringCell] = useState(null);
|
|
92
|
-
// ── Derived ─────────────────────────────────────────────────────────────
|
|
93
|
-
const widgetById = useMemo(() => {
|
|
94
|
-
const map = new Map();
|
|
95
|
-
for (const w of liveReachable)
|
|
96
|
-
map.set(w.id, w);
|
|
97
|
-
return map;
|
|
98
|
-
}, [liveReachable]);
|
|
99
|
-
// Per-widget propsSchema lookup. Merged from both reachable and
|
|
100
|
-
// unreachable lists because a saved layout can reference a widget whose
|
|
101
|
-
// `requires` aren't currently satisfied — we still want the user to be
|
|
102
|
-
// able to edit its props.
|
|
103
|
-
const propsSchemaByWidgetId = useMemo(() => {
|
|
104
|
-
const map = new Map();
|
|
105
|
-
for (const w of liveReachable) {
|
|
106
|
-
if (w.propsSchema)
|
|
107
|
-
map.set(w.id, w.propsSchema);
|
|
108
|
-
}
|
|
109
|
-
for (const w of liveUnreachable) {
|
|
110
|
-
if (w.propsSchema)
|
|
111
|
-
map.set(w.id, w.propsSchema);
|
|
112
|
-
}
|
|
113
|
-
return map;
|
|
114
|
-
}, [liveReachable, liveUnreachable]);
|
|
115
|
-
const paletteByApp = useMemo(() => {
|
|
116
|
-
const groups = new Map();
|
|
117
|
-
for (const w of liveReachable) {
|
|
118
|
-
if (!groups.has(w.app))
|
|
119
|
-
groups.set(w.app, []);
|
|
120
|
-
groups.get(w.app).push(w);
|
|
121
|
-
}
|
|
122
|
-
return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
123
|
-
}, [liveReachable]);
|
|
124
|
-
const liveKeyCount = useMemo(() => liveCatalog.filter((c) => c.inContext).length, [liveCatalog]);
|
|
125
|
-
const stepsByApp = useMemo(() => {
|
|
126
|
-
const groups = new Map();
|
|
127
|
-
for (const s of liveSteps) {
|
|
128
|
-
if (!groups.has(s.app))
|
|
129
|
-
groups.set(s.app, []);
|
|
130
|
-
groups.get(s.app).push(s);
|
|
131
|
-
}
|
|
132
|
-
return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
133
|
-
}, [liveSteps]);
|
|
134
82
|
const activeRows = draft.kind === "tabs" ? (draft.tabs[activeTabIndex]?.rows ?? []) : draft.rows;
|
|
135
83
|
// ── Layout mutations (dispatch pure actions) ────────────────────────────
|
|
136
84
|
const addWidgetToRow = useCallback((widgetId, targetRowIdx) => {
|
|
@@ -171,149 +119,6 @@ export function LayoutBuilder({ initialLayout, title, initialKeys, initialSteps,
|
|
|
171
119
|
setEditingTabIdx(null);
|
|
172
120
|
}, [draft, setEditingTabIdx]);
|
|
173
121
|
const removeTab = useCallback((tabIdx) => dispatch({ type: "removeTab", tabIdx }), []);
|
|
174
|
-
// ── Keys/steps mutations ────────────────────────────────────────────────
|
|
175
|
-
const addKey = useCallback((prefilledName) => {
|
|
176
|
-
setKeyEntries((prev) => {
|
|
177
|
-
if (prefilledName) {
|
|
178
|
-
if (prev.some((e) => e.name === prefilledName))
|
|
179
|
-
return prev;
|
|
180
|
-
return [...prev, { name: prefilledName, rawValue: "" }];
|
|
181
|
-
}
|
|
182
|
-
return [...prev, { name: "", rawValue: "" }];
|
|
183
|
-
});
|
|
184
|
-
}, []);
|
|
185
|
-
const updateKey = useCallback((idx, patch) => {
|
|
186
|
-
setKeyEntries((prev) => prev.map((e, i) => (i === idx ? { ...e, ...patch } : e)));
|
|
187
|
-
}, []);
|
|
188
|
-
const removeKey = useCallback((idx) => {
|
|
189
|
-
setKeyEntries((prev) => prev.filter((_, i) => i !== idx));
|
|
190
|
-
}, []);
|
|
191
|
-
const addStep = useCallback((stepId) => {
|
|
192
|
-
setStepEntries((prev) => {
|
|
193
|
-
const taken = new Set(prev.map((s) => s.id));
|
|
194
|
-
const newId = stepId ? defaultStepIdFor(stepId, taken) : `step-${prev.length + 1}`;
|
|
195
|
-
return [...prev, { id: newId, step: stepId ?? "", optional: false }];
|
|
196
|
-
});
|
|
197
|
-
}, []);
|
|
198
|
-
const updateStep = useCallback((idx, patch) => {
|
|
199
|
-
setStepEntries((prev) => prev.map((s, i) => (i === idx ? { ...s, ...patch } : s)));
|
|
200
|
-
}, []);
|
|
201
|
-
const removeStep = useCallback((idx) => {
|
|
202
|
-
setStepEntries((prev) => prev.filter((_, i) => i !== idx));
|
|
203
|
-
}, []);
|
|
204
|
-
// ── Catalogue fetch (mount + on key/step edits) ─────────────────────────
|
|
205
|
-
// The builder fetches its own catalogue from the app-only
|
|
206
|
-
// `get-builder-catalogue` tool; the LLM never sees this data.
|
|
207
|
-
const refreshBuilder = useCallback(async () => {
|
|
208
|
-
setStatus("refreshing");
|
|
209
|
-
try {
|
|
210
|
-
const result = await callTool(catalogueToolName, {
|
|
211
|
-
keys: entriesToKeys(keyEntries),
|
|
212
|
-
steps: stepEntries.filter((s) => s.step.trim().length > 0),
|
|
213
|
-
});
|
|
214
|
-
const sc = parseToolResult(result);
|
|
215
|
-
if (sc?.reachableWidgets)
|
|
216
|
-
setLiveReachable(sc.reachableWidgets);
|
|
217
|
-
if (sc?.unreachableWidgets)
|
|
218
|
-
setLiveUnreachable(sc.unreachableWidgets);
|
|
219
|
-
if (sc?.availableSteps)
|
|
220
|
-
setLiveSteps(sc.availableSteps);
|
|
221
|
-
if (sc?.keyCatalog)
|
|
222
|
-
setLiveCatalog(sc.keyCatalog);
|
|
223
|
-
if (sc?.context)
|
|
224
|
-
setLiveContext(toPipelineContext(sc.context));
|
|
225
|
-
// Pipeline issues are fail-soft in the catalogue but fail-hard in
|
|
226
|
-
// render-view; surface them so the user sees the render path wouldn't
|
|
227
|
-
// run as configured (Finding [10]/[6]). Coerce undefined → [] so a
|
|
228
|
-
// fixed pipeline clears a previously-shown warning.
|
|
229
|
-
setValidationIssues(sc?.validationIssues ?? []);
|
|
230
|
-
// A successful fetch clears any stale failure banner.
|
|
231
|
-
setRefreshError(null);
|
|
232
|
-
}
|
|
233
|
-
catch (err) {
|
|
234
|
-
// Fail soft: when the host's server hasn't registered the app-only
|
|
235
|
-
// `get-builder-catalogue` tool (the builder platform is opt-in via
|
|
236
|
-
// `app.builder`), the call rejects. Degrade to the static catalogue
|
|
237
|
-
// already derived from the layout rather than crashing the iframe —
|
|
238
|
-
// but surface the failure so the user knows the palette may be stale
|
|
239
|
-
// (Finding [3]).
|
|
240
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
241
|
-
console.warn(`[mcp-toolkit] builder catalogue fetch failed (${catalogueToolName}): ${reason}. ` +
|
|
242
|
-
"The builder platform may be disabled on this server (app.builder).");
|
|
243
|
-
setRefreshError(reason);
|
|
244
|
-
}
|
|
245
|
-
finally {
|
|
246
|
-
setStatus("idle");
|
|
247
|
-
}
|
|
248
|
-
}, [catalogueToolName, callTool, keyEntries, stepEntries]);
|
|
249
|
-
// Fetch the catalogue on mount (no debounce) so the builder is ready
|
|
250
|
-
// immediately. Then on every keys/steps edit, debounce 600ms before
|
|
251
|
-
// re-fetching. The ref pattern keeps the effect dependent on only
|
|
252
|
-
// keyEntries / stepEntries.
|
|
253
|
-
const refreshRef = useRef(refreshBuilder);
|
|
254
|
-
useEffect(() => {
|
|
255
|
-
refreshRef.current = refreshBuilder;
|
|
256
|
-
}, [refreshBuilder]);
|
|
257
|
-
const isFirstRun = useRef(true);
|
|
258
|
-
useEffect(() => {
|
|
259
|
-
if (isFirstRun.current) {
|
|
260
|
-
isFirstRun.current = false;
|
|
261
|
-
void refreshRef.current();
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
setStatus("pending");
|
|
265
|
-
const handle = window.setTimeout(() => {
|
|
266
|
-
void refreshRef.current();
|
|
267
|
-
}, 600);
|
|
268
|
-
return () => {
|
|
269
|
-
window.clearTimeout(handle);
|
|
270
|
-
};
|
|
271
|
-
}, [keyEntries, stepEntries]);
|
|
272
|
-
// ── Save ────────────────────────────────────────────────────────────────
|
|
273
|
-
const saveDraft = useCallback(async () => {
|
|
274
|
-
if (!saveName.trim())
|
|
275
|
-
return;
|
|
276
|
-
setBusy(true);
|
|
277
|
-
setSaveError(null);
|
|
278
|
-
try {
|
|
279
|
-
const result = await callTool(saveToolName, {
|
|
280
|
-
id: dashboardId,
|
|
281
|
-
name: saveName.trim(),
|
|
282
|
-
description: saveDescription.trim() || undefined,
|
|
283
|
-
keys: entriesToKeys(keyEntries),
|
|
284
|
-
steps: stepEntries.filter((s) => s.step.trim().length > 0),
|
|
285
|
-
layout: draftToLayout(draft),
|
|
286
|
-
title,
|
|
287
|
-
});
|
|
288
|
-
const saved = parseToolResult(result);
|
|
289
|
-
setSaveOpen(false);
|
|
290
|
-
if (saved?.id && onSaved) {
|
|
291
|
-
onSaved({
|
|
292
|
-
id: saved.id,
|
|
293
|
-
name: saved.name ?? saveName.trim(),
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
catch (err) {
|
|
298
|
-
// Previously uncaught: a rejected `save-dashboard` left the dialog open
|
|
299
|
-
// with no feedback. Surface the reason inline (Finding [3]).
|
|
300
|
-
setSaveError(err instanceof Error ? err.message : String(err));
|
|
301
|
-
}
|
|
302
|
-
finally {
|
|
303
|
-
setBusy(false);
|
|
304
|
-
}
|
|
305
|
-
}, [
|
|
306
|
-
callTool,
|
|
307
|
-
dashboardId,
|
|
308
|
-
draft,
|
|
309
|
-
keyEntries,
|
|
310
|
-
onSaved,
|
|
311
|
-
saveDescription,
|
|
312
|
-
saveName,
|
|
313
|
-
saveToolName,
|
|
314
|
-
stepEntries,
|
|
315
|
-
title,
|
|
316
|
-
]);
|
|
317
122
|
// ── Smart action: add a producing step for a missing key ────────────────
|
|
318
123
|
const addProducingStepFor = useCallback((key) => {
|
|
319
124
|
const producer = liveCatalog.find((c) => c.key === key)?.producedBySteps[0];
|
|
@@ -324,25 +129,6 @@ export function LayoutBuilder({ initialLayout, title, initialKeys, initialSteps,
|
|
|
324
129
|
const widgetProps = useMemo(() => ({ keys: liveContext.keys, context: liveContext }), [liveContext]);
|
|
325
130
|
const hasAnyCell = activeRows.some((r) => r.row.length > 0);
|
|
326
131
|
const liveKeysMap = liveContext.keys;
|
|
327
|
-
const layoutContent = draft.kind === "tabs" ? (_jsxs(Tabs, { value: draft.tabs[activeTabIndex]?.label ?? "", onValueChange: (label) => {
|
|
328
|
-
const idx = draft.tabs.findIndex((t) => t.label === label);
|
|
329
|
-
if (idx >= 0)
|
|
330
|
-
setActiveTabIndex(idx);
|
|
331
|
-
}, children: [_jsxs("div", { className: "mb-3 flex items-center gap-1", children: [_jsx(TabsList, { children: draft.tabs.map((tab, idx) => (_jsxs("div", { className: "group relative inline-flex", children: [_jsx(TabsTrigger, { value: tab.label, className: "pr-7", onDoubleClick: () => setEditingTabIdx(idx), title: "Double-click to rename", children: editingTabIdx === idx ? (_jsx("input", { autoFocus: true, defaultValue: tab.label, "aria-invalid": renameRejected, onClick: (e) => e.stopPropagation(), onPointerDown: (e) => e.stopPropagation(), onKeyDown: (e) => {
|
|
332
|
-
if (e.key === "Enter") {
|
|
333
|
-
e.preventDefault();
|
|
334
|
-
commitRename(idx, e.currentTarget.value);
|
|
335
|
-
}
|
|
336
|
-
else if (e.key === "Escape") {
|
|
337
|
-
e.preventDefault();
|
|
338
|
-
setEditingTabIdx(null);
|
|
339
|
-
}
|
|
340
|
-
}, onBlur: (e) => {
|
|
341
|
-
commitRename(idx, e.currentTarget.value);
|
|
342
|
-
}, className: cn("w-24 border-none bg-transparent p-0 text-inherit outline-none focus:ring-0", renameRejected && "text-destructive"), size: Math.max(tab.label.length + 1, 6) })) : (_jsx("span", { children: tab.label })) }), _jsx("button", { type: "button", "aria-label": L.removeTab, title: L.removeTab, onPointerDown: (e) => e.stopPropagation(), onClick: (e) => {
|
|
343
|
-
e.stopPropagation();
|
|
344
|
-
removeTab(idx);
|
|
345
|
-
}, className: "text-muted-foreground/60 hover:text-foreground absolute top-1/2 right-1 -translate-y-1/2 rounded p-0.5 opacity-0 transition-opacity group-hover:opacity-100 data-[state=active]:opacity-100", children: _jsx(X, { className: "size-3" }) })] }, tab.label))) }), _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: addTab, "aria-label": L.addTab, title: L.addTab, children: _jsx(Plus, {}) }), _jsx("span", { className: "text-muted-foreground/70 ml-2 hidden text-[10px] sm:inline", children: "double-click a tab to rename" })] }), renameRejected && editingTabIdx !== null && (_jsx("p", { className: "text-destructive -mt-1 mb-2 text-[11px]", role: "alert", children: L.renameDuplicate })), draft.tabs.map((tab, idx) => (_jsx(TabsContent, { value: tab.label, className: "mt-0", children: idx === activeTabIndex ? (_jsx(Workspace, { L: L, paletteByApp: paletteByApp, paletteCount: liveReachable.length, activeRows: activeRows, focusedRowIndex: focusedRowIndex, setFocusedRowIndex: setFocusedRowIndex, dragOverRow: dragOverRow, setDragOverRow: setDragOverRow, onAddRow: addRow, onRemoveRow: removeRow, onMoveRow: moveRow, onSetCellSpan: setCellSpan, onRemoveCell: removeCell, onAddWidget: addWidgetToRow, onConfigureCell: (rowIdx, cellIdx) => setConfiguringCell({ rowIdx, cellIdx }), propsSchemaByWidgetId: propsSchemaByWidgetId, widgets: widgets, widgetProps: widgetProps })) : null }, tab.label)))] })) : (_jsx(Workspace, { L: L, paletteByApp: paletteByApp, paletteCount: liveReachable.length, activeRows: activeRows, focusedRowIndex: focusedRowIndex, setFocusedRowIndex: setFocusedRowIndex, dragOverRow: dragOverRow, setDragOverRow: setDragOverRow, onAddRow: addRow, onRemoveRow: removeRow, onMoveRow: moveRow, onSetCellSpan: setCellSpan, onRemoveCell: removeCell, onAddWidget: addWidgetToRow, onConfigureCell: (rowIdx, cellIdx) => setConfiguringCell({ rowIdx, cellIdx }), propsSchemaByWidgetId: propsSchemaByWidgetId, widgets: widgets, widgetProps: widgetProps, showAddTabButton: true, onAddTab: addTab }));
|
|
346
132
|
return (_jsxs("div", { className: "flex min-h-[60vh] flex-col gap-4 p-1", children: [_jsx(Toolbar, { L: L, previewMode: view === "preview", isBusy: isBusy, status: status, liveKeys: liveKeyCount, reachableCount: liveReachable.length, unreachableCount: liveUnreachable.length, onOpenCatalogue: () => setCatalogueOpen(true), onOpenSave: () => setSaveOpen(true), onDone: onExit
|
|
347
133
|
? () => onExit({
|
|
348
134
|
layout: draftToLayout(draft),
|
|
@@ -364,7 +150,26 @@ export function LayoutBuilder({ initialLayout, title, initialKeys, initialSteps,
|
|
|
364
150
|
errors: liveContext.errors,
|
|
365
151
|
},
|
|
366
152
|
})
|
|
367
|
-
: undefined, hasAnyCell: hasAnyCell }), _jsx(BuilderBanners, { L: L, catalogueToolName: catalogueToolName, refreshError: refreshError, validationIssues: validationIssues }), _jsxs(Tabs, { value: view, onValueChange: (v) => setView(v), children: [_jsxs(TabsList, { children: [_jsxs(TabsTrigger, { value: "layout", children: [_jsx(Layers, { className: "size-3.5" }), L.viewLayoutTab] }), _jsxs(TabsTrigger, { value: "pipeline", children: [_jsx(SlidersHorizontal, { className: "size-3.5" }), L.viewPipelineTab, (keyEntries.length > 0 || stepEntries.length > 0) && (_jsxs("span", { className: "bg-muted text-muted-foreground ml-1 rounded px-1 font-mono text-[10px]", children: [keyEntries.length, "/", stepEntries.length] }))] }), _jsxs(TabsTrigger, { value: "preview", disabled: !hasAnyCell, children: [_jsx(Eye, { className: "size-3.5" }), L.viewPreviewTab] })] }), _jsx(TabsContent, { value: "layout", className: "mt-4", children: liveReachable.length === 0 && !hasAnyCell ? (_jsx(EmptyHint, { L: L, onGotoPipeline: () => setView("pipeline") })) : (
|
|
153
|
+
: undefined, hasAnyCell: hasAnyCell }), _jsx(BuilderBanners, { L: L, catalogueToolName: catalogueToolName, refreshError: refreshError, validationIssues: validationIssues }), _jsxs(Tabs, { value: view, onValueChange: (v) => setView(v), children: [_jsxs(TabsList, { children: [_jsxs(TabsTrigger, { value: "layout", children: [_jsx(Layers, { className: "size-3.5" }), L.viewLayoutTab] }), _jsxs(TabsTrigger, { value: "pipeline", children: [_jsx(SlidersHorizontal, { className: "size-3.5" }), L.viewPipelineTab, (keyEntries.length > 0 || stepEntries.length > 0) && (_jsxs("span", { className: "bg-muted text-muted-foreground ml-1 rounded px-1 font-mono text-[10px]", children: [keyEntries.length, "/", stepEntries.length] }))] }), _jsxs(TabsTrigger, { value: "preview", disabled: !hasAnyCell, children: [_jsx(Eye, { className: "size-3.5" }), L.viewPreviewTab] })] }), _jsx(TabsContent, { value: "layout", className: "mt-4", children: liveReachable.length === 0 && !hasAnyCell ? (_jsx(EmptyHint, { L: L, onGotoPipeline: () => setView("pipeline") })) : (_jsx(LayoutSurface, { L: L, draft: draft, activeTabIndex: activeTabIndex, setActiveTabIndex: setActiveTabIndex, editingTabIdx: editingTabIdx, setEditingTabIdx: setEditingTabIdx, renameRejected: renameRejected, commitRename: commitRename, onAddTab: addTab, onRemoveTab: removeTab, workspace: {
|
|
154
|
+
L,
|
|
155
|
+
paletteByApp,
|
|
156
|
+
paletteCount: liveReachable.length,
|
|
157
|
+
activeRows,
|
|
158
|
+
focusedRowIndex,
|
|
159
|
+
setFocusedRowIndex,
|
|
160
|
+
dragOverRow,
|
|
161
|
+
setDragOverRow,
|
|
162
|
+
onAddRow: addRow,
|
|
163
|
+
onRemoveRow: removeRow,
|
|
164
|
+
onMoveRow: moveRow,
|
|
165
|
+
onSetCellSpan: setCellSpan,
|
|
166
|
+
onRemoveCell: removeCell,
|
|
167
|
+
onAddWidget: addWidgetToRow,
|
|
168
|
+
onConfigureCell: (rowIdx, cellIdx) => setConfiguringCell({ rowIdx, cellIdx }),
|
|
169
|
+
propsSchemaByWidgetId,
|
|
170
|
+
widgets,
|
|
171
|
+
widgetProps,
|
|
172
|
+
} })) }), _jsx(TabsContent, { value: "pipeline", className: "mt-4", children: _jsx(PipelineStrip, { L: L, keyEntries: keyEntries, stepEntries: stepEntries, keyCatalog: liveCatalog, availableSteps: liveSteps, stepsByApp: stepsByApp, liveKeys: liveKeysMap, onAddKey: addKey, onUpdateKey: updateKey, onRemoveKey: removeKey, onAddStep: addStep, onUpdateStep: updateStep, onRemoveStep: removeStep, status: status }) }), _jsx(TabsContent, { value: "preview", className: "mt-4", children: _jsx(PreviewPane, { draft: draft, activeTabIndex: activeTabIndex, setActiveTabIndex: setActiveTabIndex, widgets: widgets, widgetProps: widgetProps }) })] }), _jsx(CatalogueSheet, { L: L, open: catalogueOpen, onOpenChange: setCatalogueOpen, keyCatalog: liveCatalog, availableSteps: liveSteps, unreachableWidgets: liveUnreachable, onSeedKey: (name) => {
|
|
368
173
|
addKey(name);
|
|
369
174
|
setCatalogueOpen(false);
|
|
370
175
|
setView("pipeline");
|
|
@@ -377,42 +182,6 @@ export function LayoutBuilder({ initialLayout, title, initialKeys, initialSteps,
|
|
|
377
182
|
// dismissed so a fresh attempt starts clean.
|
|
378
183
|
setSaveError(null);
|
|
379
184
|
setSaveOpen(open);
|
|
380
|
-
}, name: saveName, setName: setSaveName, description: saveDescription, setDescription: setSaveDescription, onSubmit: saveDraft, busy: isBusy, error: saveError }), (() =>
|
|
381
|
-
if (!configuringCell)
|
|
382
|
-
return null;
|
|
383
|
-
const cell = activeRows[configuringCell.rowIdx]?.row[configuringCell.cellIdx];
|
|
384
|
-
if (!cell)
|
|
385
|
-
return null;
|
|
386
|
-
const schema = propsSchemaByWidgetId.get(cell.widget);
|
|
387
|
-
return (_jsx(WidgetPropsSheet, { open: true, onOpenChange: (open) => {
|
|
388
|
-
if (!open)
|
|
389
|
-
setConfiguringCell(null);
|
|
390
|
-
}, widgetId: cell.widget, schema: schema, value: cell.props, onApply: (next) => {
|
|
391
|
-
setCellProps(configuringCell.rowIdx, configuringCell.cellIdx, next);
|
|
392
|
-
setConfiguringCell(null);
|
|
393
|
-
} }));
|
|
394
|
-
})()] }));
|
|
395
|
-
}
|
|
396
|
-
function EmptyHint({ L, onGotoPipeline, }) {
|
|
397
|
-
return (_jsxs("div", { className: "border-muted-foreground/30 text-muted-foreground flex flex-col items-center gap-2 rounded-md border border-dashed p-10 text-sm", children: [_jsx(Layers, { className: "size-5" }), _jsx("p", { children: "No widgets reachable yet." }), _jsxs("p", { className: "text-xs", children: ["Open the ", _jsx("span", { className: "font-medium", children: L.viewPipelineTab }), " tab to seed keys or add a pipeline step \u2014 the palette unlocks as soon as a widget's contract is satisfied."] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: onGotoPipeline, className: "mt-1", children: [_jsx(SlidersHorizontal, {}), " Open ", L.viewPipelineTab] })] }));
|
|
398
|
-
}
|
|
399
|
-
// -------------------------------------------------------------------------- //
|
|
400
|
-
// Builder banners — catalogue-fetch failure + pipeline validation warning
|
|
401
|
-
// -------------------------------------------------------------------------- //
|
|
402
|
-
/**
|
|
403
|
-
* Inline feedback strip under the toolbar. Surfaces two things the builder
|
|
404
|
-
* previously swallowed:
|
|
405
|
-
* - a `get-builder-catalogue` fetch failure (Finding [3]) — only console-
|
|
406
|
-
* logged before, so the user couldn't tell the palette was stale, and
|
|
407
|
-
* - pipeline validation issues (Finding [10]/[6]) the catalogue reports
|
|
408
|
-
* fail-soft but `render-view` would reject fail-hard.
|
|
409
|
-
*
|
|
410
|
-
* Renders nothing when there's no error and no issues, so the happy path is
|
|
411
|
-
* visually unchanged.
|
|
412
|
-
*/
|
|
413
|
-
function BuilderBanners({ L, catalogueToolName, refreshError, validationIssues, }) {
|
|
414
|
-
if (!refreshError && validationIssues.length === 0)
|
|
415
|
-
return null;
|
|
416
|
-
return (_jsxs("div", { className: "flex flex-col gap-2", role: "status", "aria-live": "polite", children: [refreshError && (_jsxs("div", { className: "border-destructive/30 bg-destructive/10 text-destructive flex items-start gap-2 rounded-md border px-3 py-2 text-xs", children: [_jsx(AlertTriangle, { className: "mt-0.5 size-3.5 shrink-0", "aria-hidden": "true" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "font-medium", children: L.catalogueError }), _jsxs("p", { className: "text-destructive/80 mt-0.5 break-words", children: [_jsx("span", { className: "font-mono", children: catalogueToolName }), ": ", refreshError] })] })] })), validationIssues.length > 0 && (_jsxs("div", { className: "flex items-start gap-2 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-700 dark:text-amber-400", children: [_jsx(AlertTriangle, { className: "mt-0.5 size-3.5 shrink-0", "aria-hidden": "true" }), _jsxs("div", { className: "min-w-0", children: [_jsx("p", { className: "font-medium", children: L.validationWarning }), _jsx("ul", { className: "mt-0.5 list-inside list-disc space-y-0.5", children: validationIssues.map((issue, idx) => (_jsx("li", { className: "break-words", children: issue }, idx))) })] })] }))] }));
|
|
185
|
+
}, name: saveName, setName: setSaveName, description: saveDescription, setDescription: setSaveDescription, onSubmit: saveDraft, busy: isBusy, error: saveError }), _jsx(CellPropsSheet, { configuringCell: configuringCell, activeRows: activeRows, propsSchemaByWidgetId: propsSchemaByWidgetId, onApply: setCellProps, onClose: () => setConfiguringCell(null) })] }));
|
|
417
186
|
}
|
|
418
187
|
//# sourceMappingURL=layout-builder.js.map
|