@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.
Files changed (185) hide show
  1. package/docs/components/AIDashboard.md +472 -0
  2. package/docs/components/WebPageBuilder.md +125 -0
  3. package/package.json +7 -30
  4. package/src/components/AIChat.vue +479 -0
  5. package/src/components/AIChatItem.vue +168 -0
  6. package/src/components/AIChatTextbox.vue +56 -0
  7. package/src/components/AIDashboard.vue +1113 -0
  8. package/src/components/Card.vue +3 -1
  9. package/src/components/Chart.vue +2 -1
  10. package/src/components/CodeEditor.vue +121 -13
  11. package/src/components/ColorPicker.vue +6 -2
  12. package/src/components/Dashboard.vue +493 -212
  13. package/src/components/Dropdown.vue +0 -1
  14. package/src/components/HTMLEditor.vue +9 -1
  15. package/src/components/List.vue +226 -58
  16. package/src/components/MarkdownEdit.vue +123 -4
  17. package/src/components/PresetSelectorFilterItem.vue +1 -0
  18. package/src/components/TextWithTag.vue +3 -3
  19. package/src/components/TreeView.vue +1 -1
  20. package/src/components/VirtualTable.vue +166 -75
  21. package/src/index.js +3 -55
  22. package/src/instructions/dashboard.js +203 -0
  23. package/src/utils/ai-chat-answer.mjs +240 -0
  24. package/src/utils/ai-chat.js +167 -0
  25. package/src/utils/ai-dashboard-chat.mjs +371 -0
  26. package/src/utils/ai-dashboard.js +321 -0
  27. package/src/utils/ai-list-chat.mjs +407 -0
  28. package/src/utils/dashboard.js +13 -0
  29. package/src/utils/list.mjs +146 -91
  30. package/src/utils/preset-selector.mjs +183 -27
  31. package/src/utils/wss.js +0 -2
  32. package/src/widgets/ColumnSelector.vue +22 -33
  33. package/src/widgets/LLMChat.vue +327 -0
  34. package/src/widgets/PresetBar.vue +31 -8
  35. package/src/widgets/PresetBarPivot.vue +147 -15
  36. package/src/widgets/PresetBarPivotColumnEdit.vue +8 -19
  37. package/src/widgets/PresetBarPivotValueEdit.vue +172 -0
  38. package/src/widgets/PresetSelector.vue +8 -4
  39. package/src/widgets/WebPageBuilder.vue +153 -352
  40. package/docs/components/ArrayList.md +0 -39
  41. package/docs/components/Box.md +0 -15
  42. package/docs/components/ChartBar.md +0 -34
  43. package/docs/components/DayTimeRange.md +0 -20
  44. package/docs/components/ErrorText.md +0 -15
  45. package/docs/components/FBLogin.md +0 -21
  46. package/docs/components/GHeatMaps.md +0 -16
  47. package/docs/components/GSignIn.md +0 -21
  48. package/docs/components/GmapsDirection.md +0 -20
  49. package/docs/components/IconMenu.md +0 -20
  50. package/src/components/ArrayList.vue +0 -49
  51. package/src/components/Box.vue +0 -34
  52. package/src/components/ChartBar.vue +0 -192
  53. package/src/components/DayTimeRange.vue +0 -126
  54. package/src/components/ErrorText.vue +0 -57
  55. package/src/components/FBLogin.vue +0 -194
  56. package/src/components/GHeatMaps.vue +0 -317
  57. package/src/components/GSignIn.vue +0 -166
  58. package/src/components/GmapsDirection.vue +0 -191
  59. package/src/components/IconMenu.vue +0 -95
  60. package/src/components/IconPlus.vue +0 -89
  61. package/src/components/Image360.vue +0 -140
  62. package/src/components/ImageFullScreen.vue +0 -101
  63. package/src/components/ImportModal.vue +0 -260
  64. package/src/components/ListContextMenu.vue +0 -88
  65. package/src/components/LogViewerItem.vue +0 -116
  66. package/src/components/MultilineText.vue +0 -174
  67. package/src/components/PageBuilder.vue +0 -449
  68. package/src/components/SplitPane.vue +0 -129
  69. package/src/components/TabView.vue +0 -105
  70. package/src/components/TableView.vue +0 -200
  71. package/src/components/TableViewHead.vue +0 -159
  72. package/src/components/Test.vue +0 -23
  73. package/src/components/TextEditor.vue +0 -85
  74. package/src/components/Tooltip.vue +0 -143
  75. package/src/components/TreeViewItem1.vue +0 -282
  76. package/src/components/VirtualScroll.vue +0 -282
  77. package/src/configs/dashboard/bar.js +0 -10
  78. package/src/configs/dashboard/collection-1.js +0 -5
  79. package/src/configs/dashboard/data-table.js +0 -9
  80. package/src/configs/dashboard/doughnut.js +0 -7
  81. package/src/configs/dashboard/gheatmaps.js +0 -9
  82. package/src/configs/dashboard/grid-2.js +0 -34
  83. package/src/configs/dashboard/grid-3.js +0 -34
  84. package/src/configs/dashboard/grid-4.js +0 -34
  85. package/src/configs/dashboard/grid.js +0 -15
  86. package/src/configs/dashboard/metric.js +0 -10
  87. package/src/configs/dashboard/pie.js +0 -7
  88. package/src/configs/dashboard/polar-area.js +0 -7
  89. package/src/configs/dashboard/virtual-table.js +0 -9
  90. package/src/configs/icons-fa.json +0 -12
  91. package/src/middleware/http/trim-string.js +0 -20
  92. package/src/mixin/ready-state.js +0 -37
  93. package/src/stores/components.js +0 -36
  94. package/src/stores/datasource.js +0 -11
  95. package/src/utils/event-bus.js +0 -8
  96. package/src/utils/helpers.cjs +0 -635
  97. package/src/utils/importer.js +0 -209
  98. package/src/utils/listview.js +0 -1287
  99. package/src/utils/preset-selector.cjs +0 -1455
  100. package/src/utils/preset-selector.js +0 -1452
  101. package/src/utils/queue.js +0 -63
  102. package/src/utils/web.js +0 -50
  103. package/src/utils/web.mjs +0 -120
  104. package/src/widgets/BackgroundSetting.vue +0 -132
  105. package/src/widgets/BorderSetting.vue +0 -201
  106. package/src/widgets/BotEditor/BotEditorActions.vue +0 -163
  107. package/src/widgets/BotEditor/BotEditorSettings.vue +0 -21
  108. package/src/widgets/BotEditor.vue +0 -248
  109. package/src/widgets/BoxSetting.vue +0 -53
  110. package/src/widgets/CheckoutSetting.vue +0 -46
  111. package/src/widgets/ComponentPropertyLabel.vue +0 -35
  112. package/src/widgets/ComponentPropertySelector.vue +0 -115
  113. package/src/widgets/ComponentSetting3.vue +0 -231
  114. package/src/widgets/EffectSetting.vue +0 -94
  115. package/src/widgets/FiltersSetting.vue +0 -88
  116. package/src/widgets/Header.vue +0 -15
  117. package/src/widgets/Header0.vue +0 -41
  118. package/src/widgets/Header0Setting.vue +0 -75
  119. package/src/widgets/HeaderSetting.vue +0 -102
  120. package/src/widgets/LayoutSetting.vue +0 -251
  121. package/src/widgets/LogViewer.vue +0 -85
  122. package/src/widgets/MarginPaddingSetting.vue +0 -58
  123. package/src/widgets/MarginSetting.vue +0 -142
  124. package/src/widgets/MenuEditor.vue +0 -251
  125. package/src/widgets/PaddingSetting.vue +0 -142
  126. package/src/widgets/SizingSetting.vue +0 -306
  127. package/src/widgets/SpacingSetting.vue +0 -77
  128. package/src/widgets/StyleSetting.vue +0 -543
  129. package/src/widgets/StyleSetting2.vue +0 -90
  130. package/src/widgets/TransitionSetting.vue +0 -64
  131. package/src/widgets/TypographySetting.vue +0 -334
  132. package/src/widgets/UserActionBuilder/UserActionCondition.vue +0 -99
  133. package/src/widgets/UserActionBuilder/UserActionConsole.vue +0 -97
  134. package/src/widgets/UserActionBuilder/UserActionItem.vue +0 -273
  135. package/src/widgets/UserActionBuilder/UserActionOutput.vue +0 -150
  136. package/src/widgets/UserActionBuilder/UserActionOutputDelay.vue +0 -27
  137. package/src/widgets/UserActionBuilder/UserActionOutputLog.vue +0 -28
  138. package/src/widgets/UserActionBuilder/UserActionOutputReply.vue +0 -112
  139. package/src/widgets/UserActionBuilder/UserActionProps.vue +0 -213
  140. package/src/widgets/UserActionBuilder.vue +0 -314
  141. package/src/widgets/WebPageBuilder4/BackgroundColorSetting.vue +0 -54
  142. package/src/widgets/WebPageBuilder4/BackgroundPositionSetting.vue +0 -75
  143. package/src/widgets/WebPageBuilder4/BackgroundRepeatSetting.vue +0 -72
  144. package/src/widgets/WebPageBuilder4/BackgroundSizeSetting.vue +0 -72
  145. package/src/widgets/WebPageBuilder4/BlurSetting.vue +0 -72
  146. package/src/widgets/WebPageBuilder4/BorderColorSetting.vue +0 -54
  147. package/src/widgets/WebPageBuilder4/BorderRadiusSetting.vue +0 -75
  148. package/src/widgets/WebPageBuilder4/BorderSizeSetting.vue +0 -69
  149. package/src/widgets/WebPageBuilder4/BorderStyleSetting.vue +0 -72
  150. package/src/widgets/WebPageBuilder4/BoxShadowSetting.vue +0 -73
  151. package/src/widgets/WebPageBuilder4/ButtonSetting.vue +0 -65
  152. package/src/widgets/WebPageBuilder4/CarouselSetting.vue +0 -178
  153. package/src/widgets/WebPageBuilder4/DisplaySetting.vue +0 -76
  154. package/src/widgets/WebPageBuilder4/FlexAlignSetting.vue +0 -71
  155. package/src/widgets/WebPageBuilder4/FlexDirectionSetting.vue +0 -70
  156. package/src/widgets/WebPageBuilder4/FlexJustifySetting.vue +0 -74
  157. package/src/widgets/WebPageBuilder4/FlexPropertySetting.vue +0 -71
  158. package/src/widgets/WebPageBuilder4/FlexSetting.vue +0 -96
  159. package/src/widgets/WebPageBuilder4/FlexWrapSetting.vue +0 -69
  160. package/src/widgets/WebPageBuilder4/GapSetting.vue +0 -75
  161. package/src/widgets/WebPageBuilder4/GrayscaleSetting.vue +0 -68
  162. package/src/widgets/WebPageBuilder4/GridSetting.vue +0 -216
  163. package/src/widgets/WebPageBuilder4/HeightSetting.vue +0 -98
  164. package/src/widgets/WebPageBuilder4/ImageSetting.vue +0 -102
  165. package/src/widgets/WebPageBuilder4/MarginSetting.vue +0 -73
  166. package/src/widgets/WebPageBuilder4/MaxHeightSetting.vue +0 -82
  167. package/src/widgets/WebPageBuilder4/MaxWidthSetting.vue +0 -90
  168. package/src/widgets/WebPageBuilder4/MinHeightSetting.vue +0 -81
  169. package/src/widgets/WebPageBuilder4/MinWidthSetting.vue +0 -101
  170. package/src/widgets/WebPageBuilder4/MultiValueSetting.vue +0 -163
  171. package/src/widgets/WebPageBuilder4/OpacitySetting.vue +0 -77
  172. package/src/widgets/WebPageBuilder4/OverflowSetting.vue +0 -81
  173. package/src/widgets/WebPageBuilder4/PaddingSetting.vue +0 -57
  174. package/src/widgets/WebPageBuilder4/PositionSetting.vue +0 -191
  175. package/src/widgets/WebPageBuilder4/PropertySetting.vue +0 -176
  176. package/src/widgets/WebPageBuilder4/TextSetting.vue +0 -72
  177. package/src/widgets/WebPageBuilder4/TreeView.vue +0 -46
  178. package/src/widgets/WebPageBuilder4/TreeViewItem.vue +0 -303
  179. package/src/widgets/WebPageBuilder4/WebPageComponentSelector.vue +0 -141
  180. package/src/widgets/WebPageBuilder4/WebPagePropertySelector.vue +0 -168
  181. package/src/widgets/WebPageBuilder4/WidthSetting.vue +0 -105
  182. package/src/widgets/WebPageBuilder4/ZIndexSetting.vue +0 -72
  183. package/src/widgets/WebPagePreview.vue +0 -295
  184. package/src/widgets/WebPageSelector.vue +0 -173
  185. 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
+ ```