@mixd-id/web-scaffold 0.2.250801013 → 0.2.250801014
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/docs/components/AIDashboard.md +472 -0
- package/docs/components/WebPageBuilder.md +125 -0
- package/package.json +7 -30
- package/src/components/AIChat.vue +479 -0
- package/src/components/AIChatItem.vue +168 -0
- package/src/components/AIChatTextbox.vue +56 -0
- package/src/components/AIDashboard.vue +1113 -0
- package/src/components/Card.vue +3 -1
- package/src/components/Chart.vue +2 -1
- package/src/components/CodeEditor.vue +121 -13
- package/src/components/ColorPicker.vue +6 -2
- package/src/components/Dashboard.vue +493 -212
- package/src/components/Dropdown.vue +0 -1
- package/src/components/HTMLEditor.vue +9 -1
- package/src/components/List.vue +226 -58
- package/src/components/MarkdownEdit.vue +123 -4
- package/src/components/PresetSelectorFilterItem.vue +1 -0
- package/src/components/TextWithTag.vue +3 -3
- package/src/components/TreeView.vue +1 -1
- package/src/components/VirtualTable.vue +166 -75
- package/src/index.js +3 -55
- package/src/instructions/dashboard.js +203 -0
- package/src/utils/ai-chat-answer.mjs +240 -0
- package/src/utils/ai-chat.js +167 -0
- package/src/utils/ai-dashboard-chat.mjs +371 -0
- package/src/utils/ai-dashboard.js +321 -0
- package/src/utils/ai-list-chat.mjs +407 -0
- package/src/utils/dashboard.js +13 -0
- package/src/utils/list.mjs +146 -91
- package/src/utils/preset-selector.mjs +183 -27
- package/src/utils/wss.js +0 -2
- package/src/widgets/ColumnSelector.vue +22 -33
- package/src/widgets/LLMChat.vue +327 -0
- package/src/widgets/PresetBar.vue +31 -8
- package/src/widgets/PresetBarPivot.vue +147 -15
- package/src/widgets/PresetBarPivotColumnEdit.vue +8 -19
- package/src/widgets/PresetBarPivotValueEdit.vue +172 -0
- package/src/widgets/PresetSelector.vue +8 -4
- package/src/widgets/WebPageBuilder.vue +153 -352
- package/docs/components/ArrayList.md +0 -39
- package/docs/components/Box.md +0 -15
- package/docs/components/ChartBar.md +0 -34
- package/docs/components/DayTimeRange.md +0 -20
- package/docs/components/ErrorText.md +0 -15
- package/docs/components/FBLogin.md +0 -21
- package/docs/components/GHeatMaps.md +0 -16
- package/docs/components/GSignIn.md +0 -21
- package/docs/components/GmapsDirection.md +0 -20
- package/docs/components/IconMenu.md +0 -20
- package/src/components/ArrayList.vue +0 -49
- package/src/components/Box.vue +0 -34
- package/src/components/ChartBar.vue +0 -192
- package/src/components/DayTimeRange.vue +0 -126
- package/src/components/ErrorText.vue +0 -57
- package/src/components/FBLogin.vue +0 -194
- package/src/components/GHeatMaps.vue +0 -317
- package/src/components/GSignIn.vue +0 -166
- package/src/components/GmapsDirection.vue +0 -191
- package/src/components/IconMenu.vue +0 -95
- package/src/components/IconPlus.vue +0 -89
- package/src/components/Image360.vue +0 -140
- package/src/components/ImageFullScreen.vue +0 -101
- package/src/components/ImportModal.vue +0 -260
- package/src/components/ListContextMenu.vue +0 -88
- package/src/components/LogViewerItem.vue +0 -116
- package/src/components/MultilineText.vue +0 -174
- package/src/components/PageBuilder.vue +0 -449
- package/src/components/SplitPane.vue +0 -129
- package/src/components/TabView.vue +0 -105
- package/src/components/TableView.vue +0 -200
- package/src/components/TableViewHead.vue +0 -159
- package/src/components/Test.vue +0 -23
- package/src/components/TextEditor.vue +0 -85
- package/src/components/Tooltip.vue +0 -143
- package/src/components/TreeViewItem1.vue +0 -282
- package/src/components/VirtualScroll.vue +0 -282
- package/src/configs/dashboard/bar.js +0 -10
- package/src/configs/dashboard/collection-1.js +0 -5
- package/src/configs/dashboard/data-table.js +0 -9
- package/src/configs/dashboard/doughnut.js +0 -7
- package/src/configs/dashboard/gheatmaps.js +0 -9
- package/src/configs/dashboard/grid-2.js +0 -34
- package/src/configs/dashboard/grid-3.js +0 -34
- package/src/configs/dashboard/grid-4.js +0 -34
- package/src/configs/dashboard/grid.js +0 -15
- package/src/configs/dashboard/metric.js +0 -10
- package/src/configs/dashboard/pie.js +0 -7
- package/src/configs/dashboard/polar-area.js +0 -7
- package/src/configs/dashboard/virtual-table.js +0 -9
- package/src/configs/icons-fa.json +0 -12
- package/src/middleware/http/trim-string.js +0 -20
- package/src/mixin/ready-state.js +0 -37
- package/src/stores/components.js +0 -36
- package/src/stores/datasource.js +0 -11
- package/src/utils/event-bus.js +0 -8
- package/src/utils/helpers.cjs +0 -635
- package/src/utils/importer.js +0 -209
- package/src/utils/listview.js +0 -1287
- package/src/utils/preset-selector.cjs +0 -1455
- package/src/utils/preset-selector.js +0 -1452
- package/src/utils/queue.js +0 -63
- package/src/utils/web.js +0 -50
- package/src/utils/web.mjs +0 -120
- package/src/widgets/BackgroundSetting.vue +0 -132
- package/src/widgets/BorderSetting.vue +0 -201
- package/src/widgets/BotEditor/BotEditorActions.vue +0 -163
- package/src/widgets/BotEditor/BotEditorSettings.vue +0 -21
- package/src/widgets/BotEditor.vue +0 -248
- package/src/widgets/BoxSetting.vue +0 -53
- package/src/widgets/CheckoutSetting.vue +0 -46
- package/src/widgets/ComponentPropertyLabel.vue +0 -35
- package/src/widgets/ComponentPropertySelector.vue +0 -115
- package/src/widgets/ComponentSetting3.vue +0 -231
- package/src/widgets/EffectSetting.vue +0 -94
- package/src/widgets/FiltersSetting.vue +0 -88
- package/src/widgets/Header.vue +0 -15
- package/src/widgets/Header0.vue +0 -41
- package/src/widgets/Header0Setting.vue +0 -75
- package/src/widgets/HeaderSetting.vue +0 -102
- package/src/widgets/LayoutSetting.vue +0 -251
- package/src/widgets/LogViewer.vue +0 -85
- package/src/widgets/MarginPaddingSetting.vue +0 -58
- package/src/widgets/MarginSetting.vue +0 -142
- package/src/widgets/MenuEditor.vue +0 -251
- package/src/widgets/PaddingSetting.vue +0 -142
- package/src/widgets/SizingSetting.vue +0 -306
- package/src/widgets/SpacingSetting.vue +0 -77
- package/src/widgets/StyleSetting.vue +0 -543
- package/src/widgets/StyleSetting2.vue +0 -90
- package/src/widgets/TransitionSetting.vue +0 -64
- package/src/widgets/TypographySetting.vue +0 -334
- package/src/widgets/UserActionBuilder/UserActionCondition.vue +0 -99
- package/src/widgets/UserActionBuilder/UserActionConsole.vue +0 -97
- package/src/widgets/UserActionBuilder/UserActionItem.vue +0 -273
- package/src/widgets/UserActionBuilder/UserActionOutput.vue +0 -150
- package/src/widgets/UserActionBuilder/UserActionOutputDelay.vue +0 -27
- package/src/widgets/UserActionBuilder/UserActionOutputLog.vue +0 -28
- package/src/widgets/UserActionBuilder/UserActionOutputReply.vue +0 -112
- package/src/widgets/UserActionBuilder/UserActionProps.vue +0 -213
- package/src/widgets/UserActionBuilder.vue +0 -314
- package/src/widgets/WebPageBuilder4/BackgroundColorSetting.vue +0 -54
- package/src/widgets/WebPageBuilder4/BackgroundPositionSetting.vue +0 -75
- package/src/widgets/WebPageBuilder4/BackgroundRepeatSetting.vue +0 -72
- package/src/widgets/WebPageBuilder4/BackgroundSizeSetting.vue +0 -72
- package/src/widgets/WebPageBuilder4/BlurSetting.vue +0 -72
- package/src/widgets/WebPageBuilder4/BorderColorSetting.vue +0 -54
- package/src/widgets/WebPageBuilder4/BorderRadiusSetting.vue +0 -75
- package/src/widgets/WebPageBuilder4/BorderSizeSetting.vue +0 -69
- package/src/widgets/WebPageBuilder4/BorderStyleSetting.vue +0 -72
- package/src/widgets/WebPageBuilder4/BoxShadowSetting.vue +0 -73
- package/src/widgets/WebPageBuilder4/ButtonSetting.vue +0 -65
- package/src/widgets/WebPageBuilder4/CarouselSetting.vue +0 -178
- package/src/widgets/WebPageBuilder4/DisplaySetting.vue +0 -76
- package/src/widgets/WebPageBuilder4/FlexAlignSetting.vue +0 -71
- package/src/widgets/WebPageBuilder4/FlexDirectionSetting.vue +0 -70
- package/src/widgets/WebPageBuilder4/FlexJustifySetting.vue +0 -74
- package/src/widgets/WebPageBuilder4/FlexPropertySetting.vue +0 -71
- package/src/widgets/WebPageBuilder4/FlexSetting.vue +0 -96
- package/src/widgets/WebPageBuilder4/FlexWrapSetting.vue +0 -69
- package/src/widgets/WebPageBuilder4/GapSetting.vue +0 -75
- package/src/widgets/WebPageBuilder4/GrayscaleSetting.vue +0 -68
- package/src/widgets/WebPageBuilder4/GridSetting.vue +0 -216
- package/src/widgets/WebPageBuilder4/HeightSetting.vue +0 -98
- package/src/widgets/WebPageBuilder4/ImageSetting.vue +0 -102
- package/src/widgets/WebPageBuilder4/MarginSetting.vue +0 -73
- package/src/widgets/WebPageBuilder4/MaxHeightSetting.vue +0 -82
- package/src/widgets/WebPageBuilder4/MaxWidthSetting.vue +0 -90
- package/src/widgets/WebPageBuilder4/MinHeightSetting.vue +0 -81
- package/src/widgets/WebPageBuilder4/MinWidthSetting.vue +0 -101
- package/src/widgets/WebPageBuilder4/MultiValueSetting.vue +0 -163
- package/src/widgets/WebPageBuilder4/OpacitySetting.vue +0 -77
- package/src/widgets/WebPageBuilder4/OverflowSetting.vue +0 -81
- package/src/widgets/WebPageBuilder4/PaddingSetting.vue +0 -57
- package/src/widgets/WebPageBuilder4/PositionSetting.vue +0 -191
- package/src/widgets/WebPageBuilder4/PropertySetting.vue +0 -176
- package/src/widgets/WebPageBuilder4/TextSetting.vue +0 -72
- package/src/widgets/WebPageBuilder4/TreeView.vue +0 -46
- package/src/widgets/WebPageBuilder4/TreeViewItem.vue +0 -303
- package/src/widgets/WebPageBuilder4/WebPageComponentSelector.vue +0 -141
- package/src/widgets/WebPageBuilder4/WebPagePropertySelector.vue +0 -168
- package/src/widgets/WebPageBuilder4/WidthSetting.vue +0 -105
- package/src/widgets/WebPageBuilder4/ZIndexSetting.vue +0 -72
- package/src/widgets/WebPagePreview.vue +0 -295
- package/src/widgets/WebPageSelector.vue +0 -173
- package/src/widgets/WebTemplateCreator.vue +0 -156
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
# AIDashboard.vue
|
|
2
|
+
|
|
3
|
+
## TLDR
|
|
4
|
+
|
|
5
|
+
An AI-chat-style **dashboard builder** (think Claude web / ChatGPT, but the main
|
|
6
|
+
content is a dashboard), split into three panels:
|
|
7
|
+
|
|
8
|
+
- **Left** — a tabbed builder: **Presets** (the saved dashboards), **Build** (a
|
|
9
|
+
`TreeView` structure editor + node inspector), and **Data** (the data sources
|
|
10
|
+
available to query).
|
|
11
|
+
- **Middle** — the **dashboard** itself: the active preset's widgets (metrics,
|
|
12
|
+
charts, tables), rendered top-to-bottom, groups as sections laid out per their
|
|
13
|
+
`layout` (grid / flex).
|
|
14
|
+
- **Right** — an **AI chat** panel (same look as `AIChat`) that generates /
|
|
15
|
+
edits widgets. Chat history is kept **per preset**.
|
|
16
|
+
|
|
17
|
+
The `config` object mirrors the server's **UserConfig model** (`UserConfig`
|
|
18
|
+
hasMany `UserPreset`).
|
|
19
|
+
|
|
20
|
+
The component runs in **two modes**, chosen by whether it is given a `socket` +
|
|
21
|
+
`controller`:
|
|
22
|
+
|
|
23
|
+
- **Offline** (no socket/controller) — makes **no** network calls. Sample data
|
|
24
|
+
comes from the `samples` prop, the chat appends canned widgets, and the host
|
|
25
|
+
page owns persistence. This is how `pages/Dashboard.vue` uses it.
|
|
26
|
+
- **Connected** — mirrors the server `@mixd-id/web-scaffold/ai-dashboard`
|
|
27
|
+
mixin + the `ai-dashboard` socket controller: data sources load from
|
|
28
|
+
`<controller>.load-datasource`, widget data is hydrated by
|
|
29
|
+
`<controller>.load-dashboard`, chat calls `<controller>.chat`, and the config
|
|
30
|
+
is persisted through `<controller>.save`.
|
|
31
|
+
|
|
32
|
+
```vue
|
|
33
|
+
<!-- offline (host owns persistence + samples) -->
|
|
34
|
+
<AIDashboard :config="config" :samples="samples" />
|
|
35
|
+
|
|
36
|
+
<!-- connected (server-backed data via the ai-dashboard controller) -->
|
|
37
|
+
<AIDashboard :config="config" :socket="socket" controller="ai-dashboard" />
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Component family
|
|
43
|
+
|
|
44
|
+
| Component | Responsibility |
|
|
45
|
+
|-----------|----------------|
|
|
46
|
+
| `AIDashboard` | Orchestrator. Owns preset CRUD + selection, the tabbed left builder (presets / structure tree / data sources), the three-panel layout + resizing, chat (offline canned **or** server-driven), and server sync (datasource load, hydration, persistence). |
|
|
47
|
+
| `Widget` (inline) | Pure renderer for **one** node, branching on `node.type` (`metric` / `chart` / `table`). Render-function component; emits `remove`, never mutates the node. Charts use **vue-chartjs**; tables use **`VirtualTable`**. |
|
|
48
|
+
| `AIChatItem` | Reused from `AIChat`. Renders one chat message. See `AIChat.md`. |
|
|
49
|
+
| `AIChatTextbox` | Reused from `AIChat`. The composer input; emits the typed string. See `AIChat.md`. |
|
|
50
|
+
| `AIDashboardMixin` (server) | `web-scaffold/src/utils/ai-dashboard.js` — socket-controller mixin persisting the same config shape to `UserConfig`/`UserPreset`, plus `load-dashboard` hydration and overridable `chat` / `analyze-node`. |
|
|
51
|
+
| `AiDashboardController` (server) | `server/src/controllers/sockets/ai-dashboard.js` — the concrete controller: mixes in the above, exposes hardcoded **data sources** via `load-datasource`, and folds query **filters** into a Sequelize `where`. |
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Layout
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
┌──────────────────┬────────────────────────────────────┬────────────────┐
|
|
59
|
+
│ [Presets|Build|Data] Active preset name [Load samples] │ ✦ AI Assistant │ ← headers
|
|
60
|
+
│ ──────────────── ├────────────────────────────────────┼────────────────┤
|
|
61
|
+
│ Presets tab: │ Overview ✕ │ │
|
|
62
|
+
│ ▦ My Dash ⋮ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌────┐ │ assistant ▍ │
|
|
63
|
+
│ ▦ Sales ⋮ │ │metric│ │metric│ │metric│ │ … │ │ reply… │
|
|
64
|
+
│ │ └──────┘ └──────┘ └──────┘ └────┘ │ │
|
|
65
|
+
│ Build tab: │ ┌───────────────────────────────┐ │ ┌──────────┐ │
|
|
66
|
+
│ ▸ Overview │ │ chart (vue-chartjs) ✕ │ │ │ user msg │ │
|
|
67
|
+
│ • Revenue │ └───────────────────────────────┘ │ └──────────┘ │
|
|
68
|
+
│ ▸ Sales table │ ┌───────────────────────────────┐ ├────────────────┤
|
|
69
|
+
│ ── inspector ─ │ │ table (VirtualTable) ✕ │ │ [ input ] [▶] │ ← composer
|
|
70
|
+
└──────────────────┴──┴───────────────────────────────┴──┴────────────────┘
|
|
71
|
+
↑ resizable ↑ resizable
|
|
72
|
+
220–520px 280–560px
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- Three columns separated by hairline dividers. Left and right are fixed-width
|
|
76
|
+
(from `config.params`), the middle flexes to fill.
|
|
77
|
+
- **Left** grab handle on its **right** edge; `leftWidth` clamped **220–520px**
|
|
78
|
+
(default **260**).
|
|
79
|
+
- **Right** grab handle on its **left** edge; `rightWidth` clamped **280–560px**
|
|
80
|
+
(default **360**; delta *subtracted* — the panel grows when its left edge is
|
|
81
|
+
dragged left).
|
|
82
|
+
- The left panel is a `Tabs` bar switching between **Presets**, **Build** and
|
|
83
|
+
**Data** (`config.params.leftTab`).
|
|
84
|
+
- The chat message area uses a **reverse column** (`flex-col-reverse`) so it
|
|
85
|
+
stays pinned to the latest message as text streams in.
|
|
86
|
+
- A floating `Card` handles preset renaming (replaces `window.prompt`).
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Data model — `config` mirrors UserConfig
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
config = {
|
|
94
|
+
key: String, // UserConfig.key (e.g. 'ai-dashboard') — sent on save
|
|
95
|
+
name: String, // UserConfig.name
|
|
96
|
+
params: { // UserConfig.params — UI state
|
|
97
|
+
leftWidth: Number, // builder panel width, px, 220–520, default 260
|
|
98
|
+
rightWidth: Number, // chat panel width, px, 280–560, default 360
|
|
99
|
+
leftTab: String, // 'presets' | 'build' | 'data', default 'presets'
|
|
100
|
+
presetIdx: String|null, // uid of the active preset
|
|
101
|
+
},
|
|
102
|
+
presets: [ Preset, ... ], // UserConfig.presets → UserPreset[] (array order = seq)
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`config` is a **plain object** (not a ref) and is **mutated in place** — access
|
|
107
|
+
as `config.params.X`, never `config.value.params.X`. The parent owns the ref.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Preset
|
|
112
|
+
|
|
113
|
+
A **preset is one saved dashboard**. `config.presets` is the ordered list;
|
|
114
|
+
array order becomes the server `seq`. The active preset is the one whose `uid`
|
|
115
|
+
equals `config.params.presetIdx`.
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
Preset = {
|
|
119
|
+
uid: String, // unique id; client-generated 'p-<ts36>-<n>', server keeps it
|
|
120
|
+
name: String, // shown in the Presets tab + middle header
|
|
121
|
+
params: { // UserPreset.params — the dashboard content
|
|
122
|
+
structure: Node[], // the widget tree (groups + leaf widgets)
|
|
123
|
+
messages: Message[], // chat history for THIS preset
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Preset properties
|
|
129
|
+
|
|
130
|
+
| Property | Type | Required | Description |
|
|
131
|
+
|----------|------|----------|-------------|
|
|
132
|
+
| `uid` | `String` | ✅ | Stable unique id. Created by the component (`p-<timestamp36>-<counter>`) if missing; the server preserves the client value. Used for selection (`presetIdx`), reorder/seq, and targeted `remove-preset`. |
|
|
133
|
+
| `name` | `String` | ✅ | Display name in the Presets tab and the middle header. Edited via the rename `Card`. |
|
|
134
|
+
| `params` | `Object` | ✅ | The dashboard payload. Always normalized to contain `structure` and `messages`. |
|
|
135
|
+
| `params.structure` | `Node[]` | ✅ | The widget tree (see **Node**). Empty array for a blank preset. |
|
|
136
|
+
| `params.messages` | `Message[]` | ✅ | Per-preset chat history. On load, any message left `isLoading` by a reload is frozen (`body = display \|\| 'Cancelled.'`). |
|
|
137
|
+
|
|
138
|
+
> Persistence-only preset fields (`UserPreset` also has `columns`, `filters`,
|
|
139
|
+
> `sorts`, `pivot`, `datasource`, `search`, `views`). The AIDashboard component
|
|
140
|
+
> does not use them; the server `save` passes any present through. Ignore them
|
|
141
|
+
> for dashboards.
|
|
142
|
+
|
|
143
|
+
### Node (recursive) — an entry in `params.structure`
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
Node = {
|
|
147
|
+
uid: String, // unique id, assigned by the component (NOT in samples)
|
|
148
|
+
name: String, // widget title / section heading
|
|
149
|
+
type: 'group' | 'metric' | 'chart' | 'table',
|
|
150
|
+
items: Node[], // children — 'group' only
|
|
151
|
+
layout: { mode, cols }, // 'group' only — how children are laid out
|
|
152
|
+
data: Object, // type-specific render payload (see below)
|
|
153
|
+
query: Object, // connected mode — how the server computes `data`
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
| Property | Type | Applies to | Description |
|
|
158
|
+
|----------|------|-----------|-------------|
|
|
159
|
+
| `uid` | `String` | all | Unique id (`w-<ts36>-<n>`). Assigned on creation / instantiation; drives selection, removal (`removeFromTree`) and hydration merge-by-uid. |
|
|
160
|
+
| `name` | `String` | all | Section heading (group) or widget card title. |
|
|
161
|
+
| `type` | `String` | all | `group`, `metric`, `chart`, or `table`. |
|
|
162
|
+
| `items` | `Node[]` | group | Child nodes. Only groups can contain children. |
|
|
163
|
+
| `layout` | `Object` | group | `{ mode: 'grid' \| 'flex', cols: 1–4 }`. `grid` lays children in `cols` equal columns; `flex` wraps them. Defaults to `{ mode:'grid', cols: metricOnly ? 4 : 2 }`. |
|
|
164
|
+
| `data` | `Object` | leaf | Render payload (see **Per-type `data`**). In offline mode you author it directly; in connected mode it is **derived** (hydrated) and stripped before save. |
|
|
165
|
+
| `query` | `Object` | leaf | Data-source query used by `load-dashboard` / `analyze-node` to compute `data` (see **Per-type `query`**). Present only when a data source is bound. |
|
|
166
|
+
|
|
167
|
+
### Per-type `data` (render payload)
|
|
168
|
+
|
|
169
|
+
```js
|
|
170
|
+
// metric — a single KPI
|
|
171
|
+
data = { label: String, value: Number|String, delta: Number /* optional ±% */ }
|
|
172
|
+
|
|
173
|
+
// chart — rendered as a vue-chartjs Bar
|
|
174
|
+
data = { series: [ { label: String, value: Number }, ... ] } // (also accepts `rows`)
|
|
175
|
+
|
|
176
|
+
// table — rendered with VirtualTable
|
|
177
|
+
data = {
|
|
178
|
+
columns: [ { key: String, label: String }, ... ], // plain string allowed → key & label
|
|
179
|
+
rows: [ { [key]: cellValue, ... }, ... ],
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// group — no data; renders as a section heading + `layout`-driven children
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Per-type `query` (connected mode)
|
|
186
|
+
|
|
187
|
+
Bound via the Build-tab inspector; resolved server-side against the controller's
|
|
188
|
+
data sources. All types also accept a shared **`filters`** array.
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
// metric
|
|
192
|
+
query = { datasource, aggregation: 'count'|'sum'|'avg'|'min'|'max', field, filters }
|
|
193
|
+
|
|
194
|
+
// chart
|
|
195
|
+
query = { datasource, groupBy, dateModifier: ''|'day'|'week'|'month'|'year', limit, filters }
|
|
196
|
+
|
|
197
|
+
// table
|
|
198
|
+
query = { datasource, pageSize, order: [[field,'desc']], columns, filters }
|
|
199
|
+
|
|
200
|
+
// filters (all types) — folded into a Sequelize `where` on the server
|
|
201
|
+
filters = [ { field: String, op: '='|'!='|'>'|'>='|'<'|'<='|'contains'|'in', value }, ... ]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Chat message — an entry in `params.messages`
|
|
205
|
+
|
|
206
|
+
Same shape as `AIChat` (see `AIChat.md`):
|
|
207
|
+
|
|
208
|
+
```js
|
|
209
|
+
{ direction: 1|2, body, display, isLoading, model, ellapsed }
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Messages live in `preset.params.messages`, so each preset has its own
|
|
213
|
+
conversation, persisted with the config.
|
|
214
|
+
|
|
215
|
+
### `samples` prop (offline seed, supplied by the page)
|
|
216
|
+
|
|
217
|
+
```js
|
|
218
|
+
samples = {
|
|
219
|
+
structure: Node[], // dashboard used to seed the first preset; UID-LESS templates
|
|
220
|
+
widgets: { [key]: Node }, // widgets the offline chat can append; UID-LESS templates
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Chat-understood keys: `metric`, `chart`, `table`, `group`. Templates **must
|
|
225
|
+
not** carry `uid` — the component deep-clones each template and assigns fresh
|
|
226
|
+
ids via `instantiate()` on every use.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Modes: offline vs connected
|
|
231
|
+
|
|
232
|
+
`connected = !!(socket && controller)`. Two further flags fine-tune a connected
|
|
233
|
+
mount so a host can keep owning parts it already handles:
|
|
234
|
+
|
|
235
|
+
| Flag | Default | Effect when **on** |
|
|
236
|
+
|------|---------|--------------------|
|
|
237
|
+
| `connected` (derived) | — | Enables the **Data** tab, `load-datasource`, and `load-dashboard` hydration. |
|
|
238
|
+
| `autoSave` | `true` | Persists edits via debounced `<controller>.save`. Turn **off** when the host page persists (avoids a double write). |
|
|
239
|
+
| `serverChat` | `true` | Routes chat through `<controller>.chat`. Turn **off** to keep the offline canned-widget generator even while connected. |
|
|
240
|
+
|
|
241
|
+
`pages/Dashboard.vue` connects for **data only**: it passes `socket` +
|
|
242
|
+
`controller="ai-dashboard"` but sets `:auto-save="false"` and
|
|
243
|
+
`:server-chat="false"`, so it keeps its `loadConfig`/`saveConfig` persistence
|
|
244
|
+
and the offline chat demo while gaining the Data tab and query hydration.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Left panel — the tabbed builder
|
|
249
|
+
|
|
250
|
+
### Presets tab (CRUD)
|
|
251
|
+
|
|
252
|
+
- `activePreset` = the preset whose `uid === config.params.presetIdx`.
|
|
253
|
+
- **Add** — pushes a blank preset (`name:'New Preset'`, empty structure) and
|
|
254
|
+
jumps to the **Build** tab.
|
|
255
|
+
- **Rename** — opens the floating rename `Card` (Textbox + Save/Cancel).
|
|
256
|
+
- **Duplicate** — deep-clones the preset with a fresh preset uid **and** fresh
|
|
257
|
+
uids for every node, inserts after the original, selects it.
|
|
258
|
+
- **Remove** — splices it; if it was active, selection falls back to the first
|
|
259
|
+
preset (or `null`). In connected mode also calls `<controller>.remove-preset`.
|
|
260
|
+
- Each row has a "⋮" button opening a context menu anchored to it.
|
|
261
|
+
|
|
262
|
+
### Build tab (structure editor + inspector)
|
|
263
|
+
|
|
264
|
+
- A **`TreeView`** of the active preset's `structure`. Each row shows a
|
|
265
|
+
type-colored icon + name; groups show their layout mode. Selecting a row sets
|
|
266
|
+
the inspected node; `TreeView` provides drag-reorder, drag-into-group, inline
|
|
267
|
+
add (groups → quick metric), duplicate and remove.
|
|
268
|
+
- **Add component** (toolbar `+`) opens a menu (Section / Metric / Chart /
|
|
269
|
+
Table) that inserts into the selected group, else at the root.
|
|
270
|
+
- **Node inspector** (bottom): edits `name`; for **groups** the `layout`
|
|
271
|
+
(mode + columns); for **leaves** the data source, aggregation / group-by /
|
|
272
|
+
page-size, eligible field, and the **Filters** editor (field / operator /
|
|
273
|
+
value rows). A **Refresh** button (connected) re-hydrates on demand; query
|
|
274
|
+
edits re-hydrate automatically (debounced ~450ms).
|
|
275
|
+
|
|
276
|
+
### Data tab (data sources)
|
|
277
|
+
|
|
278
|
+
- Lists the data sources returned by `<controller>.load-datasource` (or the
|
|
279
|
+
`datasources` prop offline). Each source expands to show its fields + types,
|
|
280
|
+
with a **"+ New metric from this source"** shortcut that drops a bound metric
|
|
281
|
+
into the current section and jumps to Build.
|
|
282
|
+
- Data source descriptor shape:
|
|
283
|
+
|
|
284
|
+
```js
|
|
285
|
+
{ key: String, label: String,
|
|
286
|
+
aggregations: ['count','sum','avg','min','max'], // supported metric aggregations
|
|
287
|
+
fields: [ { key, label, type: 'number'|'string'|'date' }, ... ] }
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## Middle panel — rendering
|
|
293
|
+
|
|
294
|
+
The active preset's `structure` renders top-to-bottom (`flex flex-col gap-6`):
|
|
295
|
+
|
|
296
|
+
- **group** → a section: heading (`node.name`) + its `items` laid out per
|
|
297
|
+
`node.layout` via **inline styles** (grid `repeat(cols, minmax(0,1fr))`, or
|
|
298
|
+
flex-wrap) — inline to survive Tailwind purge of dynamic column counts.
|
|
299
|
+
- **leaf** (metric/chart/table) → a full-width `Widget` card.
|
|
300
|
+
- Every widget card and section heading has a hover-revealed **✕** that removes
|
|
301
|
+
the node.
|
|
302
|
+
- Empty preset / no preset → an empty-state illustration + hint.
|
|
303
|
+
- Header: active preset name + a **Load samples** button that replaces the
|
|
304
|
+
active preset's structure with a fresh instantiation of `samples.structure`.
|
|
305
|
+
|
|
306
|
+
### Widget rendering rules (`Widget`)
|
|
307
|
+
|
|
308
|
+
Card (`bg-base-300 rounded-xl p-5`) with a title row (name + hover ✕), then:
|
|
309
|
+
|
|
310
|
+
1. **metric** — caption (`data.label || node.name`), a large
|
|
311
|
+
`toLocaleString()` number, optional delta line (`▲ n%` green / `▼ n%` red).
|
|
312
|
+
2. **chart** — a **vue-chartjs `Bar`** in a 240px-tall box. Bar/grid/tick colors
|
|
313
|
+
are read from the active theme's CSS variables (`--primary`, `--border-50`,
|
|
314
|
+
`--text-300`) with SSR-safe fallbacks; empty series shows a "No data." hint.
|
|
315
|
+
3. **table** — a **`VirtualTable`** built from `columns` / `rows` (string column
|
|
316
|
+
doubles as key + label); height caps at 320px.
|
|
317
|
+
4. **anything else** — muted placeholder.
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Chat
|
|
322
|
+
|
|
323
|
+
`send(prompt)` routes to server chat when `serverChat` is on and connected,
|
|
324
|
+
otherwise the offline generator.
|
|
325
|
+
|
|
326
|
+
### Offline (canned, generates into the ACTIVE preset)
|
|
327
|
+
|
|
328
|
+
1. Push user message + assistant placeholder into `activePreset.params.messages`.
|
|
329
|
+
2. Compute `reply = pickReply(prompt)` (keyword table below).
|
|
330
|
+
3. **Stream** the reply: every **28ms** advance a cursor by
|
|
331
|
+
`max(2, ceil(remaining/24))` chars. When done, apply the widgets once and
|
|
332
|
+
finalize (`model:'offline-demo'`, `ellapsed`).
|
|
333
|
+
|
|
334
|
+
| Keywords | Result |
|
|
335
|
+
|----------|--------|
|
|
336
|
+
| `overview`, `dashboard`, `build` | rebuild from `samples.structure` |
|
|
337
|
+
| `table`, `customer`, `list` | append `widgets.table` |
|
|
338
|
+
| `chart`, `bar`, `revenue`, `trend`, `graph` | append `widgets.chart` |
|
|
339
|
+
| `metric`, `kpi`, `card`, `number` | append `widgets.metric` (into the last root group) |
|
|
340
|
+
| `group`, `section` | append `widgets.group` |
|
|
341
|
+
| (none) | help message listing available widget keys |
|
|
342
|
+
|
|
343
|
+
`cancel()` stops the timer and freezes the in-flight message; the dashboard only
|
|
344
|
+
changes once a reply fully streams in.
|
|
345
|
+
|
|
346
|
+
### Connected (`serverChat`)
|
|
347
|
+
|
|
348
|
+
`sendConnected` posts `{ messages, structure }` to `<controller>.chat` and
|
|
349
|
+
applies the result: `reply` → assistant body; `structure` → replace the active
|
|
350
|
+
preset's tree; `nodes` → instantiate + append (metrics into the last root
|
|
351
|
+
group). Then re-hydrates and persists. Missing uids are backfilled.
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## Persistence & hydration (connected)
|
|
356
|
+
|
|
357
|
+
- **Hydrate** — on mount, preset switch, data-source change, query edit
|
|
358
|
+
(debounced) and the Refresh button, `hydrateActive()` posts the preset's
|
|
359
|
+
structure to `<controller>.load-dashboard` and **merges** returned `data` into
|
|
360
|
+
the live nodes **by uid**. Derived data is never authored by hand.
|
|
361
|
+
- **Persist** — a deep `watch(config)` calls debounced (**700ms**)
|
|
362
|
+
`<controller>.save` with a cleaned config: node `data` is stripped from any
|
|
363
|
+
node that has a `query` (the server recomputes it), and `messages` are frozen.
|
|
364
|
+
No-op unless `connected && autoSave`.
|
|
365
|
+
|
|
366
|
+
### Offline persistence (host-owned)
|
|
367
|
+
|
|
368
|
+
```js
|
|
369
|
+
// pages/Dashboard.vue
|
|
370
|
+
const config = ref(await loadConfig('dashboard', {
|
|
371
|
+
name: 'Dashboard',
|
|
372
|
+
params: { leftWidth: 260, rightWidth: 360, presetIdx: null },
|
|
373
|
+
presets: [],
|
|
374
|
+
}))
|
|
375
|
+
watch(() => config.value, to => saveConfig('dashboard', to), { deep: true })
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
> Web detail: this project's Tailwind config sets `important: true`, so panel
|
|
379
|
+
> widths and group layouts are applied as **inline styles** with no competing
|
|
380
|
+
> class.
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## Props
|
|
385
|
+
|
|
386
|
+
| Prop | Type | Default | Description |
|
|
387
|
+
|------|------|---------|-------------|
|
|
388
|
+
| `config` | `Object` | required | UserConfig-shaped object (see **Data model**). Mutated in place. |
|
|
389
|
+
| `samples` | `Object` | `{ structure: [], widgets: {} }` | Offline seed layout + generatable widgets. Nodes are uid-less templates. |
|
|
390
|
+
| `socket` | `Object` | `null` | Socket transport. With `controller`, switches the component to **connected** mode. |
|
|
391
|
+
| `controller` | `String` | `''` | Socket namespace prefixing every call (`<controller>.load-datasource`, `.load-dashboard`, `.save`, `.chat`, `.remove-preset`). |
|
|
392
|
+
| `datasources` | `Object` | `{}` | Optional client-side data-source descriptor `{ [key]: { label, fields, aggregations } }`; used only if the server returns none. |
|
|
393
|
+
| `autoSave` | `Boolean` | `true` | Persist edits via `<controller>.save`. Set `false` when the host page owns persistence. |
|
|
394
|
+
| `serverChat` | `Boolean` | `true` | Route chat through `<controller>.chat`. Set `false` to keep the offline generator while connected. |
|
|
395
|
+
|
|
396
|
+
`AIDashboard` emits no events and exposes no methods.
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Server side
|
|
401
|
+
|
|
402
|
+
### `ai-dashboard` socket controller
|
|
403
|
+
|
|
404
|
+
`server/src/controllers/sockets/ai-dashboard.js` (registered as
|
|
405
|
+
`routes['ai-dashboard']`) mixes in `AIDashboardMixin` and adds:
|
|
406
|
+
|
|
407
|
+
- **`load-datasource`** → `loadDatasource()` returns `{ datasources: [...] }`,
|
|
408
|
+
a **hardcoded** list (`order`, `message`, `inbox`, `customer`, `product`)
|
|
409
|
+
with typed fields + supported aggregations.
|
|
410
|
+
- **`get datasources()`** maps each `key` to a real Sequelize model, so
|
|
411
|
+
`load-dashboard` / `analyze-node` can compute widget data.
|
|
412
|
+
- **Filtering** — `buildWhere(filters)` maps `{field, op, value}` rows to a
|
|
413
|
+
Sequelize `where` (`= != > >= < <= contains in` → `Op.eq/ne/gt/gte/lt/lte/like/in`,
|
|
414
|
+
blanks dropped, `in` comma-split). An `analyzeNode` override folds
|
|
415
|
+
`query.filters` into `where` before delegating to the mixin analyzers.
|
|
416
|
+
|
|
417
|
+
### `AIDashboardMixin` (`@mixd-id/web-scaffold/ai-dashboard`)
|
|
418
|
+
|
|
419
|
+
Host-class contract (getters): `Models` `{ UserConfig, UserPreset }`,
|
|
420
|
+
`Sequelize` (for `Op`), `configKey` (the `UserConfig.key`), `conn` and
|
|
421
|
+
`datasources` (for hydration). `this.socket.token.userId` comes from the base
|
|
422
|
+
socket `Controller`.
|
|
423
|
+
|
|
424
|
+
Methods (socket path → method, kebab→camel):
|
|
425
|
+
|
|
426
|
+
| Socket path | Method | Does |
|
|
427
|
+
|-------------|--------|------|
|
|
428
|
+
| `<controller>.load` | `load()` | UserConfig + presets (seq-ordered), or `null` |
|
|
429
|
+
| `<controller>.save` | `save({ config })` | Upsert UserConfig; sync presets (array order → `seq`, missing uids destroyed); returns fresh `load()` |
|
|
430
|
+
| `<controller>.save-preset` | `savePreset({ preset })` | Upsert one preset (appends `seq` for new rows) |
|
|
431
|
+
| `<controller>.remove-preset` | `removePreset({ uid })` | Destroy one preset → `{ removed }` |
|
|
432
|
+
| `<controller>.load-dashboard` | `loadDashboard({ preset })` | Hydrate: walk the structure, compute `data` for every node with a `query`, recurse into groups; returns the hydrated preset |
|
|
433
|
+
| `<controller>.load-datasource` | `loadDatasource()` | *(controller, not mixin)* the available data sources |
|
|
434
|
+
| `<controller>.analyze-node` | `analyzeNode({ node })` | Compute one node's `data`: metric → `{ value }`, chart → `{ series }`, table → `{ columns, rows, hasNext }` |
|
|
435
|
+
| `<controller>.chat` | `chat({ messages, structure })` | **Overridable stub** — real controllers return `{ reply, structure?, nodes?, model? }` |
|
|
436
|
+
|
|
437
|
+
`mixin(Class, overrides)` copies mixin methods onto the prototype **without
|
|
438
|
+
clobbering** methods the class already defines (e.g. the controller's own
|
|
439
|
+
`analyzeNode`); `overrides` always win.
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## Constants & tunables
|
|
444
|
+
|
|
445
|
+
| Name | Value | Meaning |
|
|
446
|
+
|------|-------|---------|
|
|
447
|
+
| Left width clamp | `220–520px` | builder panel resize bounds |
|
|
448
|
+
| Left default width | `260px` | initial `leftWidth` |
|
|
449
|
+
| Right width clamp | `280–560px` | chat panel resize bounds |
|
|
450
|
+
| Right default width | `360px` | initial `rightWidth` |
|
|
451
|
+
| Resize handle width | `3px` | grab strip on each inner panel edge |
|
|
452
|
+
| Default group layout | `{ mode:'grid', cols: metricOnly ? 4 : 2 }` | new / seeded group layout |
|
|
453
|
+
| Chart height | `240px` | vue-chartjs Bar container |
|
|
454
|
+
| Table max height | `320px` | VirtualTable cap |
|
|
455
|
+
| Save debounce | `700ms` | `<controller>.save` |
|
|
456
|
+
| Hydrate debounce | `450ms` | `<controller>.load-dashboard` after query edits |
|
|
457
|
+
| Reveal interval / step | `28ms` / `max(2, ceil(remaining/24))` | offline chat streaming |
|
|
458
|
+
| First preset name | `'My Dashboard'` | auto-created on first run, seeded from samples |
|
|
459
|
+
| Assistant `model` tag (offline) | `'offline-demo'` | footer label on generated replies |
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
## Files
|
|
464
|
+
|
|
465
|
+
| File | Role |
|
|
466
|
+
|------|------|
|
|
467
|
+
| `src/components/AIDashboard.vue` | Orchestrator + inline `Widget` (this doc's subject). |
|
|
468
|
+
| `src/components/AIChatItem.vue` | Reused chat message renderer (see `AIChat.md`). |
|
|
469
|
+
| `src/components/AIChatTextbox.vue` | Reused composer input (see `AIChat.md`). |
|
|
470
|
+
| `src/pages/Dashboard.vue` | Page host; owns `samples`, connects to `ai-dashboard` for data only (auto-save + server-chat off). |
|
|
471
|
+
| `web-scaffold/src/utils/ai-dashboard.js` | Server-side `AIDashboardMixin` + `mixin()` for socket controllers. |
|
|
472
|
+
| `server/src/controllers/sockets/ai-dashboard.js` | Concrete `ai-dashboard` controller: hardcoded `load-datasource` + filter→`where`. |
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# WebPageBuilder
|
|
2
|
+
A full-screen visual page builder widget. It renders a three-pane editor — settings panel (page info / layout / components / chat), a live preview iframe, and a per-component setting panel — and talks to a socket controller for loading, saving and previewing pages.
|
|
3
|
+
|
|
4
|
+
Registered globally by the scaffold plugin as an async component, so it can be used directly without importing.
|
|
5
|
+
|
|
6
|
+
## Requirements
|
|
7
|
+
- `vue-router` must be installed; the page uid is read from `$route.params.uid`, and a navigation guard warns about unsaved changes.
|
|
8
|
+
- The host app must provide the scaffold injections: `alert`, `appStyle`, `confirm`, `useSocket`, `toast` (all provided by the standard scaffold app setup).
|
|
9
|
+
- A preview route (returned by the server as `previewHost`, e.g. `/web-preview`) that renders the page inside the iframe and implements the iframe message protocol below.
|
|
10
|
+
|
|
11
|
+
## Props
|
|
12
|
+
| Property | Type | Default | Description |
|
|
13
|
+
|----------|------|---------|-------------|
|
|
14
|
+
| controller | String | `undefined` | Socket controller namespace. Every server call is prefixed with it, e.g. `web-page.open` |
|
|
15
|
+
| presetKey | String | `undefined` | Key used to persist UI state (tab index, panel widths, zoom level…) via `${controller}.preset` |
|
|
16
|
+
| canManageLayout | Boolean | `false` | Whether the user may manage layouts |
|
|
17
|
+
| excludeComponents | Array | `[]` | Component `type` strings to hide from the component selector |
|
|
18
|
+
| moreComponents | Array | `[]` | Extra component definitions added to the selector (`{ type, name, group, props }`) |
|
|
19
|
+
| uploadConfig | Object | `undefined` | Axios-style config (`{ method, url, data }`) used to upload images when `uploadImageFn` is not given |
|
|
20
|
+
| uploadImageFn | Function | `undefined` | `(image, extra?, opt?) => Promise` — custom image upload handler; takes precedence over `uploadConfig` |
|
|
21
|
+
| useChat | Boolean | `false` | Shows the AI chat tab; requires a `${controller}.chat` handler on the server |
|
|
22
|
+
| useDatasource | Array | `undefined` | Datasource support tuple; the second element is the socket path used to load datasource data (the Datasource tab is currently disabled) |
|
|
23
|
+
|
|
24
|
+
## Events
|
|
25
|
+
| Event | Payload | Description |
|
|
26
|
+
|-------|---------|-------------|
|
|
27
|
+
| close | - | Emitted when the close (✕) button is clicked. The parent decides where to navigate |
|
|
28
|
+
| unmount | `{ uid }` | Emitted when the builder unmounts, with the page uid |
|
|
29
|
+
|
|
30
|
+
## Methods
|
|
31
|
+
| Method | Parameters | Description |
|
|
32
|
+
|--------|------------|-------------|
|
|
33
|
+
| getPage | - | Returns the current (reactive) page object |
|
|
34
|
+
|
|
35
|
+
## Server contract
|
|
36
|
+
The widget expects these socket paths on `controller` (base controller paths included):
|
|
37
|
+
|
|
38
|
+
| Path | Payload | Returns |
|
|
39
|
+
|------|---------|---------|
|
|
40
|
+
| `${controller}.open` | `{ uid }` | `{ page, previewHost, debugMode? }` — `page.app.hosts[0].name` is used to build the public URL |
|
|
41
|
+
| `${controller}.save` | `{ ...page, layout }` | - |
|
|
42
|
+
| `${controller}.load-layouts` | `{}` | Array of layout objects |
|
|
43
|
+
| `${controller}.create-layout` | layout | The created layout |
|
|
44
|
+
| `${controller}.load-templates` | - | `{ items }` — component templates for the selector |
|
|
45
|
+
| `${controller}.subscribe` / `.unsubscribe` | `{ name:'page' }` | - |
|
|
46
|
+
| `${controller}.preset` | `{ key }` / `{ key, config }` | Persisted UI config |
|
|
47
|
+
| `${controller}.chat` | `{ key, prompt, attachments, pageId, input }` | `{ messages }` — only needed when `useChat` is set |
|
|
48
|
+
|
|
49
|
+
## Preview iframe protocol
|
|
50
|
+
Messages are exchanged with the preview iframe via `postMessage`.
|
|
51
|
+
|
|
52
|
+
Sent to the iframe:
|
|
53
|
+
| Message | Description |
|
|
54
|
+
|---------|-------------|
|
|
55
|
+
| `{ action:'update', page }` | Full page JSON (headers, footers, components, stylesheet, data, editMode). Throttled; sent on every page/layout change |
|
|
56
|
+
| `{ action:'select', uid }` | Scrolls/highlights the component with `uid` |
|
|
57
|
+
| `{ action:'reload' }` | Reloads the preview |
|
|
58
|
+
|
|
59
|
+
Received from the iframe:
|
|
60
|
+
| Message | Description |
|
|
61
|
+
|---------|-------------|
|
|
62
|
+
| `{ type:'mounted' }` | Preview is ready; triggers the first update |
|
|
63
|
+
| `{ type:'component-click', uid }` | Selects the clicked component in the editor |
|
|
64
|
+
| `{ type:'setProp', uid, value }` | Merges `value` into the component's props (inline editing) |
|
|
65
|
+
| `{ type:'setSubAction', value }` | Forwarded to the active setting component |
|
|
66
|
+
|
|
67
|
+
## Provided injections
|
|
68
|
+
`*Setting` components rendered in the right panel can inject: `imageUrl`, `imageSrc`, `store`, `postIframe`, `getPage`, `uploadImage`, `openComponentSelector`, `setUid`, `openRightPane2`, `getRightPane`, `pageHistory`, `duplicate`.
|
|
69
|
+
|
|
70
|
+
Each component type `Foo` resolves its setting panel as a globally registered `FooSetting` component.
|
|
71
|
+
|
|
72
|
+
## Keyboard shortcuts
|
|
73
|
+
| Shortcut | Action |
|
|
74
|
+
|----------|--------|
|
|
75
|
+
| Alt+1 | Mobile view |
|
|
76
|
+
| Alt+2 | Tablet view (`md:`) |
|
|
77
|
+
| Alt+3 | Desktop view (`xl:`) |
|
|
78
|
+
| Alt+4 | Wide view (`2xl:`) |
|
|
79
|
+
|
|
80
|
+
## Samples
|
|
81
|
+
|
|
82
|
+
### Page builder page (see `admin/src/pages/WebPageEdit.vue`)
|
|
83
|
+
```vue
|
|
84
|
+
<template>
|
|
85
|
+
<div class="flex-1 flex flex-col">
|
|
86
|
+
<WebPageBuilder class="flex-1"
|
|
87
|
+
controller="web-page"
|
|
88
|
+
preset-key="web-page"
|
|
89
|
+
:more-components="components"
|
|
90
|
+
:upload-image-fn="uploadImage"
|
|
91
|
+
:use-chat="true"
|
|
92
|
+
@close="$router.push({ name:'WebPage' })" />
|
|
93
|
+
</div>
|
|
94
|
+
</template>
|
|
95
|
+
|
|
96
|
+
<script setup>
|
|
97
|
+
import {inject, ref} from 'vue'
|
|
98
|
+
|
|
99
|
+
const socket = inject('useSocket')()
|
|
100
|
+
|
|
101
|
+
const components = ref([
|
|
102
|
+
{ type:'SaasRegister', name:'SAAS Register', group:'SAAS', props:{ items:[] }},
|
|
103
|
+
])
|
|
104
|
+
|
|
105
|
+
async function uploadImage(image){
|
|
106
|
+
return socket.send('web-page.upload-image', { image })
|
|
107
|
+
}
|
|
108
|
+
</script>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The route must carry the page uid, e.g.:
|
|
112
|
+
```js
|
|
113
|
+
{
|
|
114
|
+
name: 'WebPageEdit',
|
|
115
|
+
path: 'web-page/:uid',
|
|
116
|
+
component: () => import('./pages/WebPageEdit.vue')
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Upload via config instead of a function
|
|
121
|
+
```vue
|
|
122
|
+
<WebPageBuilder controller="web-page"
|
|
123
|
+
:upload-config="{ method:'post', url:'/api/upload' }"
|
|
124
|
+
@close="$router.back()" />
|
|
125
|
+
```
|