@miragon/mcp-toolkit-ui 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/dist/app/adapt-data-widget.d.ts +33 -0
- package/dist/app/adapt-data-widget.d.ts.map +1 -0
- package/dist/app/adapt-data-widget.js +35 -0
- package/dist/app/adapt-data-widget.js.map +1 -0
- package/dist/app/builder/CatalogueSheet.d.ts +13 -0
- package/dist/app/builder/CatalogueSheet.d.ts.map +1 -0
- package/dist/app/builder/CatalogueSheet.js +33 -0
- package/dist/app/builder/CatalogueSheet.js.map +1 -0
- package/dist/app/builder/PipelineStrip.d.ts +22 -0
- package/dist/app/builder/PipelineStrip.d.ts.map +1 -0
- package/dist/app/builder/PipelineStrip.js +31 -0
- package/dist/app/builder/PipelineStrip.js.map +1 -0
- package/dist/app/builder/PreviewPane.d.ts +13 -0
- package/dist/app/builder/PreviewPane.d.ts.map +1 -0
- package/dist/app/builder/PreviewPane.js +21 -0
- package/dist/app/builder/PreviewPane.js.map +1 -0
- package/dist/app/builder/SaveDialog.d.ts +14 -0
- package/dist/app/builder/SaveDialog.d.ts.map +1 -0
- package/dist/app/builder/SaveDialog.js +14 -0
- package/dist/app/builder/SaveDialog.js.map +1 -0
- package/dist/app/builder/Toolbar.d.ts +17 -0
- package/dist/app/builder/Toolbar.d.ts.map +1 -0
- package/dist/app/builder/Toolbar.js +15 -0
- package/dist/app/builder/Toolbar.js.map +1 -0
- package/dist/app/builder/Workspace.d.ts +28 -0
- package/dist/app/builder/Workspace.d.ts.map +1 -0
- package/dist/app/builder/Workspace.js +117 -0
- package/dist/app/builder/Workspace.js.map +1 -0
- package/dist/app/builder/builder-model.d.ts +86 -0
- package/dist/app/builder/builder-model.d.ts.map +1 -0
- package/dist/app/builder/builder-model.js +174 -0
- package/dist/app/builder/builder-model.js.map +1 -0
- package/dist/app/builder/builder-reducer.d.ts +71 -0
- package/dist/app/builder/builder-reducer.d.ts.map +1 -0
- package/dist/app/builder/builder-reducer.js +201 -0
- package/dist/app/builder/builder-reducer.js.map +1 -0
- package/dist/app/builder/labels.d.ts +78 -0
- package/dist/app/builder/labels.d.ts.map +1 -0
- package/dist/app/builder/labels.js +71 -0
- package/dist/app/builder/labels.js.map +1 -0
- package/dist/app/builder/status-chip.d.ts +10 -0
- package/dist/app/builder/status-chip.d.ts.map +1 -0
- package/dist/app/builder/status-chip.js +16 -0
- package/dist/app/builder/status-chip.js.map +1 -0
- package/dist/app/host-bridge.d.ts +211 -0
- package/dist/app/host-bridge.d.ts.map +1 -0
- package/dist/app/host-bridge.js +247 -0
- package/dist/app/host-bridge.js.map +1 -0
- package/dist/app/index.d.ts +16 -0
- package/dist/app/index.d.ts.map +1 -0
- package/dist/app/index.js +18 -0
- package/dist/app/index.js.map +1 -0
- package/dist/app/layout-builder.d.ts +48 -0
- package/dist/app/layout-builder.d.ts.map +1 -0
- package/dist/app/layout-builder.js +418 -0
- package/dist/app/layout-builder.js.map +1 -0
- package/dist/app/mcp-app-view.d.ts +43 -0
- package/dist/app/mcp-app-view.d.ts.map +1 -0
- package/dist/app/mcp-app-view.js +169 -0
- package/dist/app/mcp-app-view.js.map +1 -0
- package/dist/app/mcp-toolkit-app.d.ts +18 -0
- package/dist/app/mcp-toolkit-app.d.ts.map +1 -0
- package/dist/app/mcp-toolkit-app.js +22 -0
- package/dist/app/mcp-toolkit-app.js.map +1 -0
- package/dist/app/use-host-actions.d.ts +57 -0
- package/dist/app/use-host-actions.d.ts.map +1 -0
- package/dist/app/use-host-actions.js +42 -0
- package/dist/app/use-host-actions.js.map +1 -0
- package/dist/app/widget-fixture.d.ts +180 -0
- package/dist/app/widget-fixture.d.ts.map +1 -0
- package/dist/app/widget-fixture.js +273 -0
- package/dist/app/widget-fixture.js.map +1 -0
- package/dist/app/widget-props-sheet.d.ts +20 -0
- package/dist/app/widget-props-sheet.d.ts.map +1 -0
- package/dist/app/widget-props-sheet.js +87 -0
- package/dist/app/widget-props-sheet.js.map +1 -0
- package/dist/app/widget-renderer.d.ts +87 -0
- package/dist/app/widget-renderer.d.ts.map +1 -0
- package/dist/app/widget-renderer.js +105 -0
- package/dist/app/widget-renderer.js.map +1 -0
- package/dist/components/DrillButton.d.ts +22 -0
- package/dist/components/DrillButton.d.ts.map +1 -0
- package/dist/components/DrillButton.js +13 -0
- package/dist/components/DrillButton.js.map +1 -0
- package/dist/components/FilterBar.d.ts +28 -0
- package/dist/components/FilterBar.d.ts.map +1 -0
- package/dist/components/FilterBar.js +12 -0
- package/dist/components/FilterBar.js.map +1 -0
- package/dist/components/GridLayout.d.ts +26 -0
- package/dist/components/GridLayout.d.ts.map +1 -0
- package/dist/components/GridLayout.js +31 -0
- package/dist/components/GridLayout.js.map +1 -0
- package/dist/components/GroupCard.d.ts +21 -0
- package/dist/components/GroupCard.d.ts.map +1 -0
- package/dist/components/GroupCard.js +24 -0
- package/dist/components/GroupCard.js.map +1 -0
- package/dist/components/KpiGrid.d.ts +43 -0
- package/dist/components/KpiGrid.d.ts.map +1 -0
- package/dist/components/KpiGrid.js +35 -0
- package/dist/components/KpiGrid.js.map +1 -0
- package/dist/components/ListFooter.d.ts +22 -0
- package/dist/components/ListFooter.d.ts.map +1 -0
- package/dist/components/ListFooter.js +13 -0
- package/dist/components/ListFooter.js.map +1 -0
- package/dist/components/Pills.d.ts +25 -0
- package/dist/components/Pills.d.ts.map +1 -0
- package/dist/components/Pills.js +17 -0
- package/dist/components/Pills.js.map +1 -0
- package/dist/components/SectionHeading.d.ts +16 -0
- package/dist/components/SectionHeading.d.ts.map +1 -0
- package/dist/components/SectionHeading.js +10 -0
- package/dist/components/SectionHeading.js.map +1 -0
- package/dist/components/WidgetHeader.d.ts +21 -0
- package/dist/components/WidgetHeader.d.ts.map +1 -0
- package/dist/components/WidgetHeader.js +11 -0
- package/dist/components/WidgetHeader.js.map +1 -0
- package/dist/hooks/index.d.ts +11 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +7 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/use-debounced-value.d.ts +7 -0
- package/dist/hooks/use-debounced-value.d.ts.map +1 -0
- package/dist/hooks/use-debounced-value.js +15 -0
- package/dist/hooks/use-debounced-value.js.map +1 -0
- package/dist/hooks/use-mobile.d.ts +6 -0
- package/dist/hooks/use-mobile.d.ts.map +1 -0
- package/dist/hooks/use-mobile.js +19 -0
- package/dist/hooks/use-mobile.js.map +1 -0
- package/dist/hooks/use-tool-query.d.ts +21 -0
- package/dist/hooks/use-tool-query.d.ts.map +1 -0
- package/dist/hooks/use-tool-query.js +47 -0
- package/dist/hooks/use-tool-query.js.map +1 -0
- package/dist/hooks/use-tool-result-recovery.d.ts +54 -0
- package/dist/hooks/use-tool-result-recovery.d.ts.map +1 -0
- package/dist/hooks/use-tool-result-recovery.js +93 -0
- package/dist/hooks/use-tool-result-recovery.js.map +1 -0
- package/dist/hooks/use-view-data.d.ts +19 -0
- package/dist/hooks/use-view-data.d.ts.map +1 -0
- package/dist/hooks/use-view-data.js +18 -0
- package/dist/hooks/use-view-data.js.map +1 -0
- package/dist/hooks/use-view-tool-query.d.ts +18 -0
- package/dist/hooks/use-view-tool-query.d.ts.map +1 -0
- package/dist/hooks/use-view-tool-query.js +29 -0
- package/dist/hooks/use-view-tool-query.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/parse-tool-result.d.ts +39 -0
- package/dist/lib/parse-tool-result.d.ts.map +1 -0
- package/dist/lib/parse-tool-result.js +66 -0
- package/dist/lib/parse-tool-result.js.map +1 -0
- package/dist/lib/tone-utils.d.ts +18 -0
- package/dist/lib/tone-utils.d.ts.map +1 -0
- package/dist/lib/tone-utils.js +25 -0
- package/dist/lib/tone-utils.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/locale/index.d.ts +3 -0
- package/dist/locale/index.d.ts.map +1 -0
- package/dist/locale/index.js +7 -0
- package/dist/locale/index.js.map +1 -0
- package/dist/locale/locale-provider.d.ts +50 -0
- package/dist/locale/locale-provider.d.ts.map +1 -0
- package/dist/locale/locale-provider.js +47 -0
- package/dist/locale/locale-provider.js.map +1 -0
- package/dist/primitives/alert.d.ts +10 -0
- package/dist/primitives/alert.d.ts.map +1 -0
- package/dist/primitives/alert.js +25 -0
- package/dist/primitives/alert.js.map +1 -0
- package/dist/primitives/badge.d.ts +8 -0
- package/dist/primitives/badge.d.ts.map +1 -0
- package/dist/primitives/badge.js +34 -0
- package/dist/primitives/badge.js.map +1 -0
- package/dist/primitives/button.d.ts +11 -0
- package/dist/primitives/button.d.ts.map +1 -0
- package/dist/primitives/button.js +36 -0
- package/dist/primitives/button.js.map +1 -0
- package/dist/primitives/card.d.ts +12 -0
- package/dist/primitives/card.d.ts.map +1 -0
- package/dist/primitives/card.js +25 -0
- package/dist/primitives/card.js.map +1 -0
- package/dist/primitives/dialog.d.ts +18 -0
- package/dist/primitives/dialog.d.ts.map +1 -0
- package/dist/primitives/dialog.js +38 -0
- package/dist/primitives/dialog.js.map +1 -0
- package/dist/primitives/input.d.ts +4 -0
- package/dist/primitives/input.d.ts.map +1 -0
- package/dist/primitives/input.js +7 -0
- package/dist/primitives/input.js.map +1 -0
- package/dist/primitives/scroll-area.d.ts +6 -0
- package/dist/primitives/scroll-area.d.ts.map +1 -0
- package/dist/primitives/scroll-area.js +11 -0
- package/dist/primitives/scroll-area.js.map +1 -0
- package/dist/primitives/select.d.ts +16 -0
- package/dist/primitives/select.d.ts.map +1 -0
- package/dist/primitives/select.js +38 -0
- package/dist/primitives/select.js.map +1 -0
- package/dist/primitives/separator.d.ts +4 -0
- package/dist/primitives/separator.d.ts.map +1 -0
- package/dist/primitives/separator.js +9 -0
- package/dist/primitives/separator.js.map +1 -0
- package/dist/primitives/sheet.d.ts +15 -0
- package/dist/primitives/sheet.d.ts.map +1 -0
- package/dist/primitives/sheet.js +41 -0
- package/dist/primitives/sheet.js.map +1 -0
- package/dist/primitives/skeleton.d.ts +3 -0
- package/dist/primitives/skeleton.d.ts.map +1 -0
- package/dist/primitives/skeleton.js +7 -0
- package/dist/primitives/skeleton.js.map +1 -0
- package/dist/primitives/switch.d.ts +5 -0
- package/dist/primitives/switch.d.ts.map +1 -0
- package/dist/primitives/switch.js +8 -0
- package/dist/primitives/switch.js.map +1 -0
- package/dist/primitives/table.d.ts +11 -0
- package/dist/primitives/table.d.ts.map +1 -0
- package/dist/primitives/table.js +28 -0
- package/dist/primitives/table.js.map +1 -0
- package/dist/primitives/tabs.d.ts +12 -0
- package/dist/primitives/tabs.d.ts.map +1 -0
- package/dist/primitives/tabs.js +29 -0
- package/dist/primitives/tabs.js.map +1 -0
- package/dist/providers/query-provider.d.ts +11 -0
- package/dist/providers/query-provider.d.ts.map +1 -0
- package/dist/providers/query-provider.js +20 -0
- package/dist/providers/query-provider.js.map +1 -0
- package/dist/theme/create-theme.d.ts +130 -0
- package/dist/theme/create-theme.d.ts.map +1 -0
- package/dist/theme/create-theme.js +65 -0
- package/dist/theme/create-theme.js.map +1 -0
- package/dist/theme/index.d.ts +7 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/theme/index.js +7 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/presets.d.ts +25 -0
- package/dist/theme/presets.d.ts.map +1 -0
- package/dist/theme/presets.js +68 -0
- package/dist/theme/presets.js.map +1 -0
- package/dist/theme/theme-provider.d.ts +69 -0
- package/dist/theme/theme-provider.d.ts.map +1 -0
- package/dist/theme/theme-provider.js +79 -0
- package/dist/theme/theme-provider.js.map +1 -0
- package/package.json +71 -0
- package/src/app/adapt-data-widget.tsx +51 -0
- package/src/app/builder/CatalogueSheet.tsx +230 -0
- package/src/app/builder/PipelineStrip.tsx +260 -0
- package/src/app/builder/PreviewPane.tsx +72 -0
- package/src/app/builder/SaveDialog.tsx +95 -0
- package/src/app/builder/Toolbar.tsx +95 -0
- package/src/app/builder/Workspace.tsx +483 -0
- package/src/app/builder/builder-model.test.ts +351 -0
- package/src/app/builder/builder-model.ts +213 -0
- package/src/app/builder/builder-reducer.test.ts +441 -0
- package/src/app/builder/builder-reducer.ts +289 -0
- package/src/app/builder/labels.ts +139 -0
- package/src/app/builder/status-chip.tsx +37 -0
- package/src/app/host-bridge.test.ts +206 -0
- package/src/app/host-bridge.tsx +371 -0
- package/src/app/index.ts +51 -0
- package/src/app/layout-builder.tsx +872 -0
- package/src/app/mcp-app-view.tsx +367 -0
- package/src/app/mcp-toolkit-app.tsx +25 -0
- package/src/app/use-host-actions.test.ts +16 -0
- package/src/app/use-host-actions.tsx +88 -0
- package/src/app/widget-fixture.test.ts +108 -0
- package/src/app/widget-fixture.tsx +441 -0
- package/src/app/widget-props-sheet.tsx +285 -0
- package/src/app/widget-renderer.test.ts +42 -0
- package/src/app/widget-renderer.tsx +200 -0
- package/src/components/DrillButton.tsx +37 -0
- package/src/components/FilterBar.tsx +68 -0
- package/src/components/GridLayout.tsx +49 -0
- package/src/components/GroupCard.tsx +52 -0
- package/src/components/KpiGrid.tsx +134 -0
- package/src/components/ListFooter.tsx +48 -0
- package/src/components/Pills.tsx +45 -0
- package/src/components/SectionHeading.tsx +26 -0
- package/src/components/WidgetHeader.tsx +51 -0
- package/src/globals.css +168 -0
- package/src/hooks/index.ts +17 -0
- package/src/hooks/use-debounced-value.ts +15 -0
- package/src/hooks/use-mobile.ts +24 -0
- package/src/hooks/use-tool-query.ts +63 -0
- package/src/hooks/use-tool-result-recovery.test.ts +65 -0
- package/src/hooks/use-tool-result-recovery.ts +144 -0
- package/src/hooks/use-view-data.ts +31 -0
- package/src/hooks/use-view-tool-query.ts +38 -0
- package/src/index.ts +134 -0
- package/src/lib/parse-tool-result.test.ts +139 -0
- package/src/lib/parse-tool-result.ts +89 -0
- package/src/lib/tone-utils.test.ts +35 -0
- package/src/lib/tone-utils.ts +38 -0
- package/src/lib/utils.ts +6 -0
- package/src/locale/index.ts +7 -0
- package/src/locale/locale-provider.tsx +81 -0
- package/src/primitives/alert.tsx +60 -0
- package/src/primitives/badge.tsx +49 -0
- package/src/primitives/button.tsx +62 -0
- package/src/primitives/card.tsx +92 -0
- package/src/primitives/dialog.tsx +144 -0
- package/src/primitives/input.tsx +21 -0
- package/src/primitives/scroll-area.tsx +54 -0
- package/src/primitives/select.tsx +173 -0
- package/src/primitives/separator.tsx +21 -0
- package/src/primitives/sheet.tsx +134 -0
- package/src/primitives/skeleton.tsx +13 -0
- package/src/primitives/switch.tsx +24 -0
- package/src/primitives/table.tsx +87 -0
- package/src/primitives/tabs.tsx +79 -0
- package/src/providers/query-provider.tsx +38 -0
- package/src/theme/create-theme.test.ts +108 -0
- package/src/theme/create-theme.ts +158 -0
- package/src/theme/index.ts +9 -0
- package/src/theme/presets.ts +82 -0
- package/src/theme/theme-provider.tsx +152 -0
- package/src/ui-catalog.test.ts +112 -0
- package/ui-catalog.json +1280 -0
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react"
|
|
2
|
+
import { AlertTriangle, Eye, Layers, Plus, SlidersHorizontal, X } from "lucide-react"
|
|
3
|
+
import type {
|
|
4
|
+
AvailableStep,
|
|
5
|
+
KeyCatalogEntry,
|
|
6
|
+
LayoutConfig,
|
|
7
|
+
PipelineContext,
|
|
8
|
+
PipelineStepRef,
|
|
9
|
+
ReachableWidget,
|
|
10
|
+
RowDef,
|
|
11
|
+
UnreachableWidget,
|
|
12
|
+
WidgetProps,
|
|
13
|
+
} from "@miragon/mcp-toolkit-core"
|
|
14
|
+
import { parseToolResult } from "../lib/parse-tool-result.js"
|
|
15
|
+
import {
|
|
16
|
+
defaultStepIdFor,
|
|
17
|
+
draftToLayout,
|
|
18
|
+
entriesToKeys,
|
|
19
|
+
initDraft,
|
|
20
|
+
keysToEntries,
|
|
21
|
+
sizeToSpan,
|
|
22
|
+
toPipelineContext,
|
|
23
|
+
type KeyEntry,
|
|
24
|
+
} from "./builder/builder-model.js"
|
|
25
|
+
import {
|
|
26
|
+
draftReducer,
|
|
27
|
+
initBuilderState,
|
|
28
|
+
type BuilderDraftState,
|
|
29
|
+
} from "./builder/builder-reducer.js"
|
|
30
|
+
import { DEFAULT_LABELS, type LayoutBuilderLabels } from "./builder/labels.js"
|
|
31
|
+
import { Toolbar } from "./builder/Toolbar.js"
|
|
32
|
+
import { PipelineStrip } from "./builder/PipelineStrip.js"
|
|
33
|
+
import { Workspace } from "./builder/Workspace.js"
|
|
34
|
+
import { CatalogueSheet } from "./builder/CatalogueSheet.js"
|
|
35
|
+
import { SaveDialog } from "./builder/SaveDialog.js"
|
|
36
|
+
import { PreviewPane } from "./builder/PreviewPane.js"
|
|
37
|
+
import { WidgetPropsSheet } from "./widget-props-sheet.js"
|
|
38
|
+
import { Button } from "../primitives/button.js"
|
|
39
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../primitives/tabs.js"
|
|
40
|
+
import { cn } from "../lib/utils.js"
|
|
41
|
+
import { type WidgetComponent } from "./widget-renderer.js"
|
|
42
|
+
|
|
43
|
+
// -------------------------------------------------------------------------- //
|
|
44
|
+
// Public label contract — re-exported from ./builder/labels so callers keep
|
|
45
|
+
// importing `LayoutBuilderLabels` from this module unchanged.
|
|
46
|
+
// -------------------------------------------------------------------------- //
|
|
47
|
+
|
|
48
|
+
export type { LayoutBuilderLabels } from "./builder/labels.js"
|
|
49
|
+
|
|
50
|
+
// -------------------------------------------------------------------------- //
|
|
51
|
+
// Props
|
|
52
|
+
// -------------------------------------------------------------------------- //
|
|
53
|
+
|
|
54
|
+
export interface LayoutBuilderProps {
|
|
55
|
+
initialLayout?: LayoutConfig
|
|
56
|
+
title?: string
|
|
57
|
+
initialKeys?: Record<string, unknown>
|
|
58
|
+
initialSteps?: PipelineStepRef[]
|
|
59
|
+
widgets: Record<string, WidgetComponent>
|
|
60
|
+
callTool: (name: string, args: object) => Promise<unknown>
|
|
61
|
+
/** App-only catalogue tool name. Default: `get-builder-catalogue`. */
|
|
62
|
+
catalogueToolName?: string
|
|
63
|
+
saveToolName?: string
|
|
64
|
+
dashboardId?: string
|
|
65
|
+
labels?: LayoutBuilderLabels
|
|
66
|
+
onSaved?: (result: { id: string; name: string }) => void
|
|
67
|
+
/**
|
|
68
|
+
* Called when the user clicks "Done" / exits the builder. Receives
|
|
69
|
+
* the final draft so the parent can swap into the rendered view with
|
|
70
|
+
* the just-built layout. Omit `commit` to leave parent state untouched.
|
|
71
|
+
*/
|
|
72
|
+
onExit?: (commit?: {
|
|
73
|
+
layout: LayoutConfig
|
|
74
|
+
keys?: Record<string, unknown>
|
|
75
|
+
steps?: PipelineStepRef[]
|
|
76
|
+
title?: string
|
|
77
|
+
context?: {
|
|
78
|
+
keys: Record<string, unknown>
|
|
79
|
+
stepIds: string[]
|
|
80
|
+
stepData: Record<
|
|
81
|
+
string,
|
|
82
|
+
{ data: unknown; keys: Record<string, unknown>; _app: string; _dataType: string }
|
|
83
|
+
>
|
|
84
|
+
errors: { stepId: string; reason: string }[]
|
|
85
|
+
}
|
|
86
|
+
}) => void
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// -------------------------------------------------------------------------- //
|
|
90
|
+
// Main component
|
|
91
|
+
// -------------------------------------------------------------------------- //
|
|
92
|
+
|
|
93
|
+
const EMPTY_CONTEXT: PipelineContext = { steps: {}, keys: {}, errors: [] }
|
|
94
|
+
|
|
95
|
+
export function LayoutBuilder({
|
|
96
|
+
initialLayout,
|
|
97
|
+
title,
|
|
98
|
+
initialKeys,
|
|
99
|
+
initialSteps,
|
|
100
|
+
widgets,
|
|
101
|
+
callTool,
|
|
102
|
+
catalogueToolName = "get-builder-catalogue",
|
|
103
|
+
saveToolName = "save-dashboard",
|
|
104
|
+
dashboardId,
|
|
105
|
+
labels,
|
|
106
|
+
onSaved,
|
|
107
|
+
onExit,
|
|
108
|
+
}: LayoutBuilderProps) {
|
|
109
|
+
const L = useMemo(() => ({ ...DEFAULT_LABELS, ...labels }), [labels])
|
|
110
|
+
|
|
111
|
+
// ── Draft layout (pure reducer) ─────────────────────────────────────────
|
|
112
|
+
// The draft plus the UI indices coupled to draft mutations
|
|
113
|
+
// (activeTabIndex / focusedRowIndex / editingTabIdx) live in a single pure
|
|
114
|
+
// reducer (see ./builder/builder-reducer.ts) so every mutation is testable
|
|
115
|
+
// and the old setState-in-updater pattern is gone.
|
|
116
|
+
const [builderState, dispatch] = useReducer(
|
|
117
|
+
draftReducer,
|
|
118
|
+
initialLayout,
|
|
119
|
+
(layout): BuilderDraftState => initBuilderState(initDraft(layout)),
|
|
120
|
+
)
|
|
121
|
+
const { draft, activeTabIndex, focusedRowIndex, editingTabIdx, renameRejectedAt } = builderState
|
|
122
|
+
const setActiveTabIndex = useCallback(
|
|
123
|
+
(idx: number) => dispatch({ type: "selectTab", tabIdx: idx }),
|
|
124
|
+
[],
|
|
125
|
+
)
|
|
126
|
+
const setFocusedRowIndex = useCallback(
|
|
127
|
+
(idx: number) => dispatch({ type: "setFocusedRow", rowIdx: idx }),
|
|
128
|
+
[],
|
|
129
|
+
)
|
|
130
|
+
const setEditingTabIdx = useCallback((idx: number | null) => {
|
|
131
|
+
dispatch(idx === null ? { type: "stopEditingTab" } : { type: "startEditingTab", tabIdx: idx })
|
|
132
|
+
}, [])
|
|
133
|
+
const [view, setView] = useState<"layout" | "pipeline" | "preview">("layout")
|
|
134
|
+
const [catalogueOpen, setCatalogueOpen] = useState(false)
|
|
135
|
+
|
|
136
|
+
// ── Inline rename feedback ──────────────────────────────────────────────
|
|
137
|
+
// The reducer rejects duplicate tab labels and bumps `renameRejectedAt`
|
|
138
|
+
// (Phase 1 signal). Mirror each bump into a transient flag so the rename
|
|
139
|
+
// input can show "name already exists" inline (Finding [3]). Tracked via
|
|
140
|
+
// the render-phase setState pattern used elsewhere in this codebase; cleared
|
|
141
|
+
// when the inline editor closes (editingTabIdx → null).
|
|
142
|
+
const [renameRejected, setRenameRejected] = useState(false)
|
|
143
|
+
const [prevRenameRejectedAt, setPrevRenameRejectedAt] = useState(renameRejectedAt)
|
|
144
|
+
if (renameRejectedAt !== prevRenameRejectedAt) {
|
|
145
|
+
setPrevRenameRejectedAt(renameRejectedAt)
|
|
146
|
+
setRenameRejected(true)
|
|
147
|
+
}
|
|
148
|
+
const [prevEditingTabIdx, setPrevEditingTabIdx] = useState(editingTabIdx)
|
|
149
|
+
if (editingTabIdx !== prevEditingTabIdx) {
|
|
150
|
+
setPrevEditingTabIdx(editingTabIdx)
|
|
151
|
+
if (editingTabIdx === null) setRenameRejected(false)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Keys + steps editor state ───────────────────────────────────────────
|
|
155
|
+
const [keyEntries, setKeyEntries] = useState<KeyEntry[]>(() => keysToEntries(initialKeys))
|
|
156
|
+
const [stepEntries, setStepEntries] = useState<PipelineStepRef[]>(() => initialSteps ?? [])
|
|
157
|
+
|
|
158
|
+
// ── Live snapshots from get-builder-catalogue responses ─────────────────
|
|
159
|
+
// Start empty — the first effect below fetches the catalogue on mount.
|
|
160
|
+
const [liveReachable, setLiveReachable] = useState<ReachableWidget[]>([])
|
|
161
|
+
const [liveUnreachable, setLiveUnreachable] = useState<UnreachableWidget[]>([])
|
|
162
|
+
const [liveCatalog, setLiveCatalog] = useState<KeyCatalogEntry[]>([])
|
|
163
|
+
const [liveSteps, setLiveSteps] = useState<AvailableStep[]>([])
|
|
164
|
+
const [liveContext, setLiveContext] = useState<PipelineContext>(EMPTY_CONTEXT)
|
|
165
|
+
// Pipeline validation issues from the catalogue (Finding [10]/[6]). The
|
|
166
|
+
// catalogue is fail-soft, but `render-view` is fail-hard, so we surface
|
|
167
|
+
// these so the user sees — before committing — that the pipeline as
|
|
168
|
+
// configured wouldn't render.
|
|
169
|
+
const [validationIssues, setValidationIssues] = useState<string[]>([])
|
|
170
|
+
|
|
171
|
+
// ── Refresh status (for the auto-apply chip) ────────────────────────────
|
|
172
|
+
type RefreshStatus = "idle" | "pending" | "refreshing"
|
|
173
|
+
const [status, setStatus] = useState<RefreshStatus>("idle")
|
|
174
|
+
// Last catalogue-fetch failure (Finding [3]). Surfaced as a toolbar banner
|
|
175
|
+
// instead of only `console.warn`-ing, so the user knows the palette/context
|
|
176
|
+
// may be stale. Cleared on the next successful refresh.
|
|
177
|
+
const [refreshError, setRefreshError] = useState<string | null>(null)
|
|
178
|
+
|
|
179
|
+
// ── Save dialog ─────────────────────────────────────────────────────────
|
|
180
|
+
const [isBusy, setBusy] = useState(false)
|
|
181
|
+
const [saveOpen, setSaveOpen] = useState(false)
|
|
182
|
+
const [saveName, setSaveName] = useState(title ?? "")
|
|
183
|
+
const [saveDescription, setSaveDescription] = useState("")
|
|
184
|
+
// Last save failure (Finding [3]). Shown inline in the SaveDialog so a
|
|
185
|
+
// rejected `save-dashboard` call no longer fails silently.
|
|
186
|
+
const [saveError, setSaveError] = useState<string | null>(null)
|
|
187
|
+
|
|
188
|
+
// ── DnD ──────────────────────────────────────────────────────────────────
|
|
189
|
+
const [dragOverRow, setDragOverRow] = useState<number | null>(null)
|
|
190
|
+
|
|
191
|
+
// Which cell (if any) is having its props configured. The
|
|
192
|
+
// WidgetPropsSheet is mounted once at the LayoutBuilder root so that
|
|
193
|
+
// exactly one editor can be open at a time across the entire workspace
|
|
194
|
+
// — including across tabs. Stored as `{ rowIdx, cellIdx }` against the
|
|
195
|
+
// currently active draft surface (rows or active tab's rows).
|
|
196
|
+
const [configuringCell, setConfiguringCell] = useState<{
|
|
197
|
+
rowIdx: number
|
|
198
|
+
cellIdx: number
|
|
199
|
+
} | null>(null)
|
|
200
|
+
|
|
201
|
+
// ── Derived ─────────────────────────────────────────────────────────────
|
|
202
|
+
const widgetById = useMemo(() => {
|
|
203
|
+
const map = new Map<string, ReachableWidget>()
|
|
204
|
+
for (const w of liveReachable) map.set(w.id, w)
|
|
205
|
+
return map
|
|
206
|
+
}, [liveReachable])
|
|
207
|
+
|
|
208
|
+
// Per-widget propsSchema lookup. Merged from both reachable and
|
|
209
|
+
// unreachable lists because a saved layout can reference a widget whose
|
|
210
|
+
// `requires` aren't currently satisfied — we still want the user to be
|
|
211
|
+
// able to edit its props.
|
|
212
|
+
const propsSchemaByWidgetId = useMemo(() => {
|
|
213
|
+
const map = new Map<string, Record<string, unknown>>()
|
|
214
|
+
for (const w of liveReachable) {
|
|
215
|
+
if (w.propsSchema) map.set(w.id, w.propsSchema)
|
|
216
|
+
}
|
|
217
|
+
for (const w of liveUnreachable) {
|
|
218
|
+
if (w.propsSchema) map.set(w.id, w.propsSchema)
|
|
219
|
+
}
|
|
220
|
+
return map
|
|
221
|
+
}, [liveReachable, liveUnreachable])
|
|
222
|
+
|
|
223
|
+
const paletteByApp = useMemo(() => {
|
|
224
|
+
const groups = new Map<string, ReachableWidget[]>()
|
|
225
|
+
for (const w of liveReachable) {
|
|
226
|
+
if (!groups.has(w.app)) groups.set(w.app, [])
|
|
227
|
+
groups.get(w.app)!.push(w)
|
|
228
|
+
}
|
|
229
|
+
return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b))
|
|
230
|
+
}, [liveReachable])
|
|
231
|
+
|
|
232
|
+
const liveKeyCount = useMemo(() => liveCatalog.filter((c) => c.inContext).length, [liveCatalog])
|
|
233
|
+
|
|
234
|
+
const stepsByApp = useMemo(() => {
|
|
235
|
+
const groups = new Map<string, AvailableStep[]>()
|
|
236
|
+
for (const s of liveSteps) {
|
|
237
|
+
if (!groups.has(s.app)) groups.set(s.app, [])
|
|
238
|
+
groups.get(s.app)!.push(s)
|
|
239
|
+
}
|
|
240
|
+
return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b))
|
|
241
|
+
}, [liveSteps])
|
|
242
|
+
|
|
243
|
+
const activeRows: RowDef[] =
|
|
244
|
+
draft.kind === "tabs" ? (draft.tabs[activeTabIndex]?.rows ?? []) : draft.rows
|
|
245
|
+
|
|
246
|
+
// ── Layout mutations (dispatch pure actions) ────────────────────────────
|
|
247
|
+
const addWidgetToRow = useCallback(
|
|
248
|
+
(widgetId: string, targetRowIdx?: number) => {
|
|
249
|
+
const widget = widgetById.get(widgetId)
|
|
250
|
+
if (!widget) return
|
|
251
|
+
dispatch({
|
|
252
|
+
type: "addWidgetToRow",
|
|
253
|
+
widgetId,
|
|
254
|
+
defaultSpan: sizeToSpan(widget.size),
|
|
255
|
+
targetRowIdx,
|
|
256
|
+
})
|
|
257
|
+
},
|
|
258
|
+
[widgetById],
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
const addRow = useCallback(() => dispatch({ type: "addRow" }), [])
|
|
262
|
+
|
|
263
|
+
const removeRow = useCallback((rowIdx: number) => dispatch({ type: "removeRow", rowIdx }), [])
|
|
264
|
+
|
|
265
|
+
const moveRow = useCallback(
|
|
266
|
+
(rowIdx: number, direction: -1 | 1) => dispatch({ type: "moveRow", rowIdx, direction }),
|
|
267
|
+
[],
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
const setCellSpan = useCallback(
|
|
271
|
+
(rowIdx: number, cellIdx: number, span: number) =>
|
|
272
|
+
dispatch({ type: "setCellSpan", rowIdx, cellIdx, span }),
|
|
273
|
+
[],
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
const removeCell = useCallback(
|
|
277
|
+
(rowIdx: number, cellIdx: number) => dispatch({ type: "removeCell", rowIdx, cellIdx }),
|
|
278
|
+
[],
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
// Sets (or clears, when `next` is undefined) the per-instance `props` on
|
|
282
|
+
// a cell. Triggered by the WidgetPropsSheet's Apply button. Storing
|
|
283
|
+
// `undefined` rather than `{}` keeps the saved layout's wire format
|
|
284
|
+
// identical to one a hand-coded LLM call would produce — the field is
|
|
285
|
+
// simply absent.
|
|
286
|
+
const setCellProps = useCallback(
|
|
287
|
+
(rowIdx: number, cellIdx: number, next: Record<string, unknown> | undefined) =>
|
|
288
|
+
dispatch({ type: "setCellProps", rowIdx, cellIdx, next }),
|
|
289
|
+
[],
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
const addTab = useCallback(() => dispatch({ type: "addTab" }), [])
|
|
293
|
+
|
|
294
|
+
// Commit an inline tab rename and decide whether to close the editor.
|
|
295
|
+
// Dispatches the rename, then keeps the input open when the trimmed label
|
|
296
|
+
// collides with another tab so the reducer's rejection (`renameRejectedAt`)
|
|
297
|
+
// can drive the inline "name already exists" feedback instead of the editor
|
|
298
|
+
// silently snapping shut on the old label.
|
|
299
|
+
const commitRename = useCallback(
|
|
300
|
+
(tabIdx: number, label: string) => {
|
|
301
|
+
dispatch({ type: "renameTab", tabIdx, label })
|
|
302
|
+
const trimmed = label.trim()
|
|
303
|
+
const isDuplicate =
|
|
304
|
+
draft.kind === "tabs" &&
|
|
305
|
+
trimmed.length > 0 &&
|
|
306
|
+
draft.tabs.some((t, i) => i !== tabIdx && t.label === trimmed)
|
|
307
|
+
if (!isDuplicate) setEditingTabIdx(null)
|
|
308
|
+
},
|
|
309
|
+
[draft, setEditingTabIdx],
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
const removeTab = useCallback((tabIdx: number) => dispatch({ type: "removeTab", tabIdx }), [])
|
|
313
|
+
|
|
314
|
+
// ── Keys/steps mutations ────────────────────────────────────────────────
|
|
315
|
+
const addKey = useCallback((prefilledName?: string) => {
|
|
316
|
+
setKeyEntries((prev) => {
|
|
317
|
+
if (prefilledName) {
|
|
318
|
+
if (prev.some((e) => e.name === prefilledName)) return prev
|
|
319
|
+
return [...prev, { name: prefilledName, rawValue: "" }]
|
|
320
|
+
}
|
|
321
|
+
return [...prev, { name: "", rawValue: "" }]
|
|
322
|
+
})
|
|
323
|
+
}, [])
|
|
324
|
+
|
|
325
|
+
const updateKey = useCallback((idx: number, patch: Partial<KeyEntry>) => {
|
|
326
|
+
setKeyEntries((prev) => prev.map((e, i) => (i === idx ? { ...e, ...patch } : e)))
|
|
327
|
+
}, [])
|
|
328
|
+
|
|
329
|
+
const removeKey = useCallback((idx: number) => {
|
|
330
|
+
setKeyEntries((prev) => prev.filter((_, i) => i !== idx))
|
|
331
|
+
}, [])
|
|
332
|
+
|
|
333
|
+
const addStep = useCallback((stepId?: string) => {
|
|
334
|
+
setStepEntries((prev) => {
|
|
335
|
+
const taken = new Set(prev.map((s) => s.id))
|
|
336
|
+
const newId = stepId ? defaultStepIdFor(stepId, taken) : `step-${prev.length + 1}`
|
|
337
|
+
return [...prev, { id: newId, step: stepId ?? "", optional: false }]
|
|
338
|
+
})
|
|
339
|
+
}, [])
|
|
340
|
+
|
|
341
|
+
const updateStep = useCallback((idx: number, patch: Partial<PipelineStepRef>) => {
|
|
342
|
+
setStepEntries((prev) => prev.map((s, i) => (i === idx ? { ...s, ...patch } : s)))
|
|
343
|
+
}, [])
|
|
344
|
+
|
|
345
|
+
const removeStep = useCallback((idx: number) => {
|
|
346
|
+
setStepEntries((prev) => prev.filter((_, i) => i !== idx))
|
|
347
|
+
}, [])
|
|
348
|
+
|
|
349
|
+
// ── Catalogue fetch (mount + on key/step edits) ─────────────────────────
|
|
350
|
+
// The builder fetches its own catalogue from the app-only
|
|
351
|
+
// `get-builder-catalogue` tool; the LLM never sees this data.
|
|
352
|
+
const refreshBuilder = useCallback(async () => {
|
|
353
|
+
setStatus("refreshing")
|
|
354
|
+
try {
|
|
355
|
+
const result = await callTool(catalogueToolName, {
|
|
356
|
+
keys: entriesToKeys(keyEntries),
|
|
357
|
+
steps: stepEntries.filter((s) => s.step.trim().length > 0),
|
|
358
|
+
})
|
|
359
|
+
const sc = parseToolResult<{
|
|
360
|
+
reachableWidgets?: ReachableWidget[]
|
|
361
|
+
unreachableWidgets?: UnreachableWidget[]
|
|
362
|
+
availableSteps?: AvailableStep[]
|
|
363
|
+
keyCatalog?: KeyCatalogEntry[]
|
|
364
|
+
validationIssues?: string[]
|
|
365
|
+
context?: {
|
|
366
|
+
keys: Record<string, unknown>
|
|
367
|
+
stepIds: string[]
|
|
368
|
+
stepData: Record<
|
|
369
|
+
string,
|
|
370
|
+
{ data: unknown; keys: Record<string, unknown>; _app: string; _dataType: string }
|
|
371
|
+
>
|
|
372
|
+
errors: { stepId: string; reason: string }[]
|
|
373
|
+
}
|
|
374
|
+
} | null>(result)
|
|
375
|
+
if (sc?.reachableWidgets) setLiveReachable(sc.reachableWidgets)
|
|
376
|
+
if (sc?.unreachableWidgets) setLiveUnreachable(sc.unreachableWidgets)
|
|
377
|
+
if (sc?.availableSteps) setLiveSteps(sc.availableSteps)
|
|
378
|
+
if (sc?.keyCatalog) setLiveCatalog(sc.keyCatalog)
|
|
379
|
+
if (sc?.context) setLiveContext(toPipelineContext(sc.context))
|
|
380
|
+
// Pipeline issues are fail-soft in the catalogue but fail-hard in
|
|
381
|
+
// render-view; surface them so the user sees the render path wouldn't
|
|
382
|
+
// run as configured (Finding [10]/[6]). Coerce undefined → [] so a
|
|
383
|
+
// fixed pipeline clears a previously-shown warning.
|
|
384
|
+
setValidationIssues(sc?.validationIssues ?? [])
|
|
385
|
+
// A successful fetch clears any stale failure banner.
|
|
386
|
+
setRefreshError(null)
|
|
387
|
+
} catch (err) {
|
|
388
|
+
// Fail soft: when the host's server hasn't registered the app-only
|
|
389
|
+
// `get-builder-catalogue` tool (the builder platform is opt-in via
|
|
390
|
+
// `app.builder`), the call rejects. Degrade to the static catalogue
|
|
391
|
+
// already derived from the layout rather than crashing the iframe —
|
|
392
|
+
// but surface the failure so the user knows the palette may be stale
|
|
393
|
+
// (Finding [3]).
|
|
394
|
+
const reason = err instanceof Error ? err.message : String(err)
|
|
395
|
+
console.warn(
|
|
396
|
+
`[mcp-toolkit] builder catalogue fetch failed (${catalogueToolName}): ${reason}. ` +
|
|
397
|
+
"The builder platform may be disabled on this server (app.builder).",
|
|
398
|
+
)
|
|
399
|
+
setRefreshError(reason)
|
|
400
|
+
} finally {
|
|
401
|
+
setStatus("idle")
|
|
402
|
+
}
|
|
403
|
+
}, [catalogueToolName, callTool, keyEntries, stepEntries])
|
|
404
|
+
|
|
405
|
+
// Fetch the catalogue on mount (no debounce) so the builder is ready
|
|
406
|
+
// immediately. Then on every keys/steps edit, debounce 600ms before
|
|
407
|
+
// re-fetching. The ref pattern keeps the effect dependent on only
|
|
408
|
+
// keyEntries / stepEntries.
|
|
409
|
+
const refreshRef = useRef(refreshBuilder)
|
|
410
|
+
useEffect(() => {
|
|
411
|
+
refreshRef.current = refreshBuilder
|
|
412
|
+
}, [refreshBuilder])
|
|
413
|
+
const isFirstRun = useRef(true)
|
|
414
|
+
useEffect(() => {
|
|
415
|
+
if (isFirstRun.current) {
|
|
416
|
+
isFirstRun.current = false
|
|
417
|
+
void refreshRef.current()
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
setStatus("pending")
|
|
421
|
+
const handle = window.setTimeout(() => {
|
|
422
|
+
void refreshRef.current()
|
|
423
|
+
}, 600)
|
|
424
|
+
return () => {
|
|
425
|
+
window.clearTimeout(handle)
|
|
426
|
+
}
|
|
427
|
+
}, [keyEntries, stepEntries])
|
|
428
|
+
|
|
429
|
+
// ── Save ────────────────────────────────────────────────────────────────
|
|
430
|
+
const saveDraft = useCallback(async () => {
|
|
431
|
+
if (!saveName.trim()) return
|
|
432
|
+
setBusy(true)
|
|
433
|
+
setSaveError(null)
|
|
434
|
+
try {
|
|
435
|
+
const result = await callTool(saveToolName, {
|
|
436
|
+
id: dashboardId,
|
|
437
|
+
name: saveName.trim(),
|
|
438
|
+
description: saveDescription.trim() || undefined,
|
|
439
|
+
keys: entriesToKeys(keyEntries),
|
|
440
|
+
steps: stepEntries.filter((s) => s.step.trim().length > 0),
|
|
441
|
+
layout: draftToLayout(draft),
|
|
442
|
+
title,
|
|
443
|
+
})
|
|
444
|
+
const saved = parseToolResult<{ id?: string; name?: string } | null>(result)
|
|
445
|
+
setSaveOpen(false)
|
|
446
|
+
if (saved?.id && onSaved) {
|
|
447
|
+
onSaved({
|
|
448
|
+
id: saved.id,
|
|
449
|
+
name: saved.name ?? saveName.trim(),
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
} catch (err) {
|
|
453
|
+
// Previously uncaught: a rejected `save-dashboard` left the dialog open
|
|
454
|
+
// with no feedback. Surface the reason inline (Finding [3]).
|
|
455
|
+
setSaveError(err instanceof Error ? err.message : String(err))
|
|
456
|
+
} finally {
|
|
457
|
+
setBusy(false)
|
|
458
|
+
}
|
|
459
|
+
}, [
|
|
460
|
+
callTool,
|
|
461
|
+
dashboardId,
|
|
462
|
+
draft,
|
|
463
|
+
keyEntries,
|
|
464
|
+
onSaved,
|
|
465
|
+
saveDescription,
|
|
466
|
+
saveName,
|
|
467
|
+
saveToolName,
|
|
468
|
+
stepEntries,
|
|
469
|
+
title,
|
|
470
|
+
])
|
|
471
|
+
|
|
472
|
+
// ── Smart action: add a producing step for a missing key ────────────────
|
|
473
|
+
const addProducingStepFor = useCallback(
|
|
474
|
+
(key: string) => {
|
|
475
|
+
const producer = liveCatalog.find((c) => c.key === key)?.producedBySteps[0]
|
|
476
|
+
if (!producer) return
|
|
477
|
+
addStep(producer)
|
|
478
|
+
},
|
|
479
|
+
[addStep, liveCatalog],
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
const widgetProps: WidgetProps = useMemo(
|
|
483
|
+
() => ({ keys: liveContext.keys, context: liveContext }),
|
|
484
|
+
[liveContext],
|
|
485
|
+
)
|
|
486
|
+
const hasAnyCell = activeRows.some((r) => r.row.length > 0)
|
|
487
|
+
const liveKeysMap = liveContext.keys
|
|
488
|
+
|
|
489
|
+
const layoutContent =
|
|
490
|
+
draft.kind === "tabs" ? (
|
|
491
|
+
<Tabs
|
|
492
|
+
value={draft.tabs[activeTabIndex]?.label ?? ""}
|
|
493
|
+
onValueChange={(label) => {
|
|
494
|
+
const idx = draft.tabs.findIndex((t) => t.label === label)
|
|
495
|
+
if (idx >= 0) setActiveTabIndex(idx)
|
|
496
|
+
}}
|
|
497
|
+
>
|
|
498
|
+
<div className="mb-3 flex items-center gap-1">
|
|
499
|
+
<TabsList>
|
|
500
|
+
{draft.tabs.map((tab, idx) => (
|
|
501
|
+
<div key={tab.label} className="group relative inline-flex">
|
|
502
|
+
<TabsTrigger
|
|
503
|
+
value={tab.label}
|
|
504
|
+
className="pr-7"
|
|
505
|
+
onDoubleClick={() => setEditingTabIdx(idx)}
|
|
506
|
+
title="Double-click to rename"
|
|
507
|
+
>
|
|
508
|
+
{editingTabIdx === idx ? (
|
|
509
|
+
<input
|
|
510
|
+
autoFocus
|
|
511
|
+
defaultValue={tab.label}
|
|
512
|
+
aria-invalid={renameRejected}
|
|
513
|
+
onClick={(e) => e.stopPropagation()}
|
|
514
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
515
|
+
onKeyDown={(e) => {
|
|
516
|
+
if (e.key === "Enter") {
|
|
517
|
+
e.preventDefault()
|
|
518
|
+
commitRename(idx, e.currentTarget.value)
|
|
519
|
+
} else if (e.key === "Escape") {
|
|
520
|
+
e.preventDefault()
|
|
521
|
+
setEditingTabIdx(null)
|
|
522
|
+
}
|
|
523
|
+
}}
|
|
524
|
+
onBlur={(e) => {
|
|
525
|
+
commitRename(idx, e.currentTarget.value)
|
|
526
|
+
}}
|
|
527
|
+
className={cn(
|
|
528
|
+
"w-24 border-none bg-transparent p-0 text-inherit outline-none focus:ring-0",
|
|
529
|
+
renameRejected && "text-destructive",
|
|
530
|
+
)}
|
|
531
|
+
size={Math.max(tab.label.length + 1, 6)}
|
|
532
|
+
/>
|
|
533
|
+
) : (
|
|
534
|
+
<span>{tab.label}</span>
|
|
535
|
+
)}
|
|
536
|
+
</TabsTrigger>
|
|
537
|
+
<button
|
|
538
|
+
type="button"
|
|
539
|
+
aria-label={L.removeTab}
|
|
540
|
+
title={L.removeTab}
|
|
541
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
542
|
+
onClick={(e) => {
|
|
543
|
+
e.stopPropagation()
|
|
544
|
+
removeTab(idx)
|
|
545
|
+
}}
|
|
546
|
+
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"
|
|
547
|
+
>
|
|
548
|
+
<X className="size-3" />
|
|
549
|
+
</button>
|
|
550
|
+
</div>
|
|
551
|
+
))}
|
|
552
|
+
</TabsList>
|
|
553
|
+
<Button
|
|
554
|
+
variant="ghost"
|
|
555
|
+
size="icon-sm"
|
|
556
|
+
onClick={addTab}
|
|
557
|
+
aria-label={L.addTab}
|
|
558
|
+
title={L.addTab}
|
|
559
|
+
>
|
|
560
|
+
<Plus />
|
|
561
|
+
</Button>
|
|
562
|
+
<span className="text-muted-foreground/70 ml-2 hidden text-[10px] sm:inline">
|
|
563
|
+
double-click a tab to rename
|
|
564
|
+
</span>
|
|
565
|
+
</div>
|
|
566
|
+
{renameRejected && editingTabIdx !== null && (
|
|
567
|
+
<p className="text-destructive -mt-1 mb-2 text-[11px]" role="alert">
|
|
568
|
+
{L.renameDuplicate}
|
|
569
|
+
</p>
|
|
570
|
+
)}
|
|
571
|
+
{draft.tabs.map((tab, idx) => (
|
|
572
|
+
<TabsContent key={tab.label} value={tab.label} className="mt-0">
|
|
573
|
+
{idx === activeTabIndex ? (
|
|
574
|
+
<Workspace
|
|
575
|
+
L={L}
|
|
576
|
+
paletteByApp={paletteByApp}
|
|
577
|
+
paletteCount={liveReachable.length}
|
|
578
|
+
activeRows={activeRows}
|
|
579
|
+
focusedRowIndex={focusedRowIndex}
|
|
580
|
+
setFocusedRowIndex={setFocusedRowIndex}
|
|
581
|
+
dragOverRow={dragOverRow}
|
|
582
|
+
setDragOverRow={setDragOverRow}
|
|
583
|
+
onAddRow={addRow}
|
|
584
|
+
onRemoveRow={removeRow}
|
|
585
|
+
onMoveRow={moveRow}
|
|
586
|
+
onSetCellSpan={setCellSpan}
|
|
587
|
+
onRemoveCell={removeCell}
|
|
588
|
+
onAddWidget={addWidgetToRow}
|
|
589
|
+
onConfigureCell={(rowIdx, cellIdx) => setConfiguringCell({ rowIdx, cellIdx })}
|
|
590
|
+
propsSchemaByWidgetId={propsSchemaByWidgetId}
|
|
591
|
+
widgets={widgets}
|
|
592
|
+
widgetProps={widgetProps}
|
|
593
|
+
/>
|
|
594
|
+
) : null}
|
|
595
|
+
</TabsContent>
|
|
596
|
+
))}
|
|
597
|
+
</Tabs>
|
|
598
|
+
) : (
|
|
599
|
+
<Workspace
|
|
600
|
+
L={L}
|
|
601
|
+
paletteByApp={paletteByApp}
|
|
602
|
+
paletteCount={liveReachable.length}
|
|
603
|
+
activeRows={activeRows}
|
|
604
|
+
focusedRowIndex={focusedRowIndex}
|
|
605
|
+
setFocusedRowIndex={setFocusedRowIndex}
|
|
606
|
+
dragOverRow={dragOverRow}
|
|
607
|
+
setDragOverRow={setDragOverRow}
|
|
608
|
+
onAddRow={addRow}
|
|
609
|
+
onRemoveRow={removeRow}
|
|
610
|
+
onMoveRow={moveRow}
|
|
611
|
+
onSetCellSpan={setCellSpan}
|
|
612
|
+
onRemoveCell={removeCell}
|
|
613
|
+
onAddWidget={addWidgetToRow}
|
|
614
|
+
onConfigureCell={(rowIdx, cellIdx) => setConfiguringCell({ rowIdx, cellIdx })}
|
|
615
|
+
propsSchemaByWidgetId={propsSchemaByWidgetId}
|
|
616
|
+
widgets={widgets}
|
|
617
|
+
widgetProps={widgetProps}
|
|
618
|
+
showAddTabButton
|
|
619
|
+
onAddTab={addTab}
|
|
620
|
+
/>
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
return (
|
|
624
|
+
<div className="flex min-h-[60vh] flex-col gap-4 p-1">
|
|
625
|
+
<Toolbar
|
|
626
|
+
L={L}
|
|
627
|
+
previewMode={view === "preview"}
|
|
628
|
+
isBusy={isBusy}
|
|
629
|
+
status={status}
|
|
630
|
+
liveKeys={liveKeyCount}
|
|
631
|
+
reachableCount={liveReachable.length}
|
|
632
|
+
unreachableCount={liveUnreachable.length}
|
|
633
|
+
onOpenCatalogue={() => setCatalogueOpen(true)}
|
|
634
|
+
onOpenSave={() => setSaveOpen(true)}
|
|
635
|
+
onDone={
|
|
636
|
+
onExit
|
|
637
|
+
? () =>
|
|
638
|
+
onExit({
|
|
639
|
+
layout: draftToLayout(draft),
|
|
640
|
+
keys: entriesToKeys(keyEntries),
|
|
641
|
+
steps: stepEntries.filter((s) => s.step.trim().length > 0),
|
|
642
|
+
title,
|
|
643
|
+
context: {
|
|
644
|
+
keys: liveContext.keys,
|
|
645
|
+
stepIds: Object.keys(liveContext.steps),
|
|
646
|
+
stepData: Object.fromEntries(
|
|
647
|
+
Object.entries(liveContext.steps).map(([id, result]) => [
|
|
648
|
+
id,
|
|
649
|
+
{
|
|
650
|
+
data: result.data,
|
|
651
|
+
keys: result.keys,
|
|
652
|
+
_app: result._app,
|
|
653
|
+
_dataType: result._dataType,
|
|
654
|
+
},
|
|
655
|
+
]),
|
|
656
|
+
),
|
|
657
|
+
errors: liveContext.errors,
|
|
658
|
+
},
|
|
659
|
+
})
|
|
660
|
+
: undefined
|
|
661
|
+
}
|
|
662
|
+
hasAnyCell={hasAnyCell}
|
|
663
|
+
/>
|
|
664
|
+
|
|
665
|
+
<BuilderBanners
|
|
666
|
+
L={L}
|
|
667
|
+
catalogueToolName={catalogueToolName}
|
|
668
|
+
refreshError={refreshError}
|
|
669
|
+
validationIssues={validationIssues}
|
|
670
|
+
/>
|
|
671
|
+
|
|
672
|
+
<Tabs value={view} onValueChange={(v) => setView(v as typeof view)}>
|
|
673
|
+
<TabsList>
|
|
674
|
+
<TabsTrigger value="layout">
|
|
675
|
+
<Layers className="size-3.5" />
|
|
676
|
+
{L.viewLayoutTab}
|
|
677
|
+
</TabsTrigger>
|
|
678
|
+
<TabsTrigger value="pipeline">
|
|
679
|
+
<SlidersHorizontal className="size-3.5" />
|
|
680
|
+
{L.viewPipelineTab}
|
|
681
|
+
{(keyEntries.length > 0 || stepEntries.length > 0) && (
|
|
682
|
+
<span className="bg-muted text-muted-foreground ml-1 rounded px-1 font-mono text-[10px]">
|
|
683
|
+
{keyEntries.length}/{stepEntries.length}
|
|
684
|
+
</span>
|
|
685
|
+
)}
|
|
686
|
+
</TabsTrigger>
|
|
687
|
+
<TabsTrigger value="preview" disabled={!hasAnyCell}>
|
|
688
|
+
<Eye className="size-3.5" />
|
|
689
|
+
{L.viewPreviewTab}
|
|
690
|
+
</TabsTrigger>
|
|
691
|
+
</TabsList>
|
|
692
|
+
|
|
693
|
+
<TabsContent value="layout" className="mt-4">
|
|
694
|
+
{liveReachable.length === 0 && !hasAnyCell ? (
|
|
695
|
+
<EmptyHint L={L} onGotoPipeline={() => setView("pipeline")} />
|
|
696
|
+
) : (
|
|
697
|
+
layoutContent
|
|
698
|
+
)}
|
|
699
|
+
</TabsContent>
|
|
700
|
+
|
|
701
|
+
<TabsContent value="pipeline" className="mt-4">
|
|
702
|
+
<PipelineStrip
|
|
703
|
+
L={L}
|
|
704
|
+
keyEntries={keyEntries}
|
|
705
|
+
stepEntries={stepEntries}
|
|
706
|
+
keyCatalog={liveCatalog}
|
|
707
|
+
availableSteps={liveSteps}
|
|
708
|
+
stepsByApp={stepsByApp}
|
|
709
|
+
liveKeys={liveKeysMap}
|
|
710
|
+
onAddKey={addKey}
|
|
711
|
+
onUpdateKey={updateKey}
|
|
712
|
+
onRemoveKey={removeKey}
|
|
713
|
+
onAddStep={addStep}
|
|
714
|
+
onUpdateStep={updateStep}
|
|
715
|
+
onRemoveStep={removeStep}
|
|
716
|
+
status={status}
|
|
717
|
+
/>
|
|
718
|
+
</TabsContent>
|
|
719
|
+
|
|
720
|
+
<TabsContent value="preview" className="mt-4">
|
|
721
|
+
<PreviewPane
|
|
722
|
+
draft={draft}
|
|
723
|
+
activeTabIndex={activeTabIndex}
|
|
724
|
+
setActiveTabIndex={setActiveTabIndex}
|
|
725
|
+
widgets={widgets}
|
|
726
|
+
widgetProps={widgetProps}
|
|
727
|
+
/>
|
|
728
|
+
</TabsContent>
|
|
729
|
+
</Tabs>
|
|
730
|
+
|
|
731
|
+
<CatalogueSheet
|
|
732
|
+
L={L}
|
|
733
|
+
open={catalogueOpen}
|
|
734
|
+
onOpenChange={setCatalogueOpen}
|
|
735
|
+
keyCatalog={liveCatalog}
|
|
736
|
+
availableSteps={liveSteps}
|
|
737
|
+
unreachableWidgets={liveUnreachable}
|
|
738
|
+
onSeedKey={(name) => {
|
|
739
|
+
addKey(name)
|
|
740
|
+
setCatalogueOpen(false)
|
|
741
|
+
setView("pipeline")
|
|
742
|
+
}}
|
|
743
|
+
onAddProducingStep={(key) => {
|
|
744
|
+
addProducingStepFor(key)
|
|
745
|
+
setCatalogueOpen(false)
|
|
746
|
+
setView("pipeline")
|
|
747
|
+
}}
|
|
748
|
+
/>
|
|
749
|
+
|
|
750
|
+
<SaveDialog
|
|
751
|
+
L={L}
|
|
752
|
+
open={saveOpen}
|
|
753
|
+
onOpenChange={(open) => {
|
|
754
|
+
// Clear any stale save error whenever the dialog is opened or
|
|
755
|
+
// dismissed so a fresh attempt starts clean.
|
|
756
|
+
setSaveError(null)
|
|
757
|
+
setSaveOpen(open)
|
|
758
|
+
}}
|
|
759
|
+
name={saveName}
|
|
760
|
+
setName={setSaveName}
|
|
761
|
+
description={saveDescription}
|
|
762
|
+
setDescription={setSaveDescription}
|
|
763
|
+
onSubmit={saveDraft}
|
|
764
|
+
busy={isBusy}
|
|
765
|
+
error={saveError}
|
|
766
|
+
/>
|
|
767
|
+
|
|
768
|
+
{(() => {
|
|
769
|
+
if (!configuringCell) return null
|
|
770
|
+
const cell = activeRows[configuringCell.rowIdx]?.row[configuringCell.cellIdx]
|
|
771
|
+
if (!cell) return null
|
|
772
|
+
const schema = propsSchemaByWidgetId.get(cell.widget)
|
|
773
|
+
return (
|
|
774
|
+
<WidgetPropsSheet
|
|
775
|
+
open
|
|
776
|
+
onOpenChange={(open) => {
|
|
777
|
+
if (!open) setConfiguringCell(null)
|
|
778
|
+
}}
|
|
779
|
+
widgetId={cell.widget}
|
|
780
|
+
schema={schema}
|
|
781
|
+
value={cell.props}
|
|
782
|
+
onApply={(next) => {
|
|
783
|
+
setCellProps(configuringCell.rowIdx, configuringCell.cellIdx, next)
|
|
784
|
+
setConfiguringCell(null)
|
|
785
|
+
}}
|
|
786
|
+
/>
|
|
787
|
+
)
|
|
788
|
+
})()}
|
|
789
|
+
</div>
|
|
790
|
+
)
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function EmptyHint({
|
|
794
|
+
L,
|
|
795
|
+
onGotoPipeline,
|
|
796
|
+
}: {
|
|
797
|
+
L: Required<LayoutBuilderLabels>
|
|
798
|
+
onGotoPipeline: () => void
|
|
799
|
+
}) {
|
|
800
|
+
return (
|
|
801
|
+
<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">
|
|
802
|
+
<Layers className="size-5" />
|
|
803
|
+
<p>No widgets reachable yet.</p>
|
|
804
|
+
<p className="text-xs">
|
|
805
|
+
Open the <span className="font-medium">{L.viewPipelineTab}</span> tab to seed keys or add a
|
|
806
|
+
pipeline step — the palette unlocks as soon as a widget's contract is satisfied.
|
|
807
|
+
</p>
|
|
808
|
+
<Button variant="outline" size="sm" onClick={onGotoPipeline} className="mt-1">
|
|
809
|
+
<SlidersHorizontal /> Open {L.viewPipelineTab}
|
|
810
|
+
</Button>
|
|
811
|
+
</div>
|
|
812
|
+
)
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// -------------------------------------------------------------------------- //
|
|
816
|
+
// Builder banners — catalogue-fetch failure + pipeline validation warning
|
|
817
|
+
// -------------------------------------------------------------------------- //
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Inline feedback strip under the toolbar. Surfaces two things the builder
|
|
821
|
+
* previously swallowed:
|
|
822
|
+
* - a `get-builder-catalogue` fetch failure (Finding [3]) — only console-
|
|
823
|
+
* logged before, so the user couldn't tell the palette was stale, and
|
|
824
|
+
* - pipeline validation issues (Finding [10]/[6]) the catalogue reports
|
|
825
|
+
* fail-soft but `render-view` would reject fail-hard.
|
|
826
|
+
*
|
|
827
|
+
* Renders nothing when there's no error and no issues, so the happy path is
|
|
828
|
+
* visually unchanged.
|
|
829
|
+
*/
|
|
830
|
+
function BuilderBanners({
|
|
831
|
+
L,
|
|
832
|
+
catalogueToolName,
|
|
833
|
+
refreshError,
|
|
834
|
+
validationIssues,
|
|
835
|
+
}: {
|
|
836
|
+
L: Required<LayoutBuilderLabels>
|
|
837
|
+
catalogueToolName: string
|
|
838
|
+
refreshError: string | null
|
|
839
|
+
validationIssues: string[]
|
|
840
|
+
}) {
|
|
841
|
+
if (!refreshError && validationIssues.length === 0) return null
|
|
842
|
+
return (
|
|
843
|
+
<div className="flex flex-col gap-2" role="status" aria-live="polite">
|
|
844
|
+
{refreshError && (
|
|
845
|
+
<div className="border-destructive/30 bg-destructive/10 text-destructive flex items-start gap-2 rounded-md border px-3 py-2 text-xs">
|
|
846
|
+
<AlertTriangle className="mt-0.5 size-3.5 shrink-0" aria-hidden="true" />
|
|
847
|
+
<div className="min-w-0">
|
|
848
|
+
<p className="font-medium">{L.catalogueError}</p>
|
|
849
|
+
<p className="text-destructive/80 mt-0.5 break-words">
|
|
850
|
+
<span className="font-mono">{catalogueToolName}</span>: {refreshError}
|
|
851
|
+
</p>
|
|
852
|
+
</div>
|
|
853
|
+
</div>
|
|
854
|
+
)}
|
|
855
|
+
{validationIssues.length > 0 && (
|
|
856
|
+
<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">
|
|
857
|
+
<AlertTriangle className="mt-0.5 size-3.5 shrink-0" aria-hidden="true" />
|
|
858
|
+
<div className="min-w-0">
|
|
859
|
+
<p className="font-medium">{L.validationWarning}</p>
|
|
860
|
+
<ul className="mt-0.5 list-inside list-disc space-y-0.5">
|
|
861
|
+
{validationIssues.map((issue, idx) => (
|
|
862
|
+
<li key={idx} className="break-words">
|
|
863
|
+
{issue}
|
|
864
|
+
</li>
|
|
865
|
+
))}
|
|
866
|
+
</ul>
|
|
867
|
+
</div>
|
|
868
|
+
</div>
|
|
869
|
+
)}
|
|
870
|
+
</div>
|
|
871
|
+
)
|
|
872
|
+
}
|