@mixd-id/web-scaffold 0.2.250801013 → 0.2.260801002

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 (271) hide show
  1. package/README.md +25 -1
  2. package/docs/components/AIDashboard.md +557 -0
  3. package/docs/components/Alert.md +3 -1
  4. package/docs/components/Card.md +5 -0
  5. package/docs/components/PivotList.md +207 -0
  6. package/docs/components/Textbox.md +20 -1
  7. package/docs/components/Tooltip.md +148 -0
  8. package/docs/components/WebPageBuilder.md +125 -0
  9. package/package.json +22 -89
  10. package/src/components/AIChat.vue +488 -0
  11. package/src/components/AIChatItem.vue +199 -0
  12. package/src/components/AIChatTextbox.vue +56 -0
  13. package/src/components/AIDashboard.vue +1159 -0
  14. package/src/components/Alert.vue +58 -35
  15. package/src/components/Card.vue +58 -7
  16. package/src/components/Carousel.vue +1 -1
  17. package/src/{widgets → components}/CartSetting.vue +1 -0
  18. package/src/components/Chart.vue +4 -3
  19. package/src/components/Checkbox.vue +22 -24
  20. package/src/components/CodeEditor.vue +123 -13
  21. package/src/{widgets → components}/CollapsiblePanelSetting.vue +1 -0
  22. package/src/components/ColorPicker.vue +8 -4
  23. package/src/components/ColumnSelector.vue +93 -0
  24. package/src/{widgets → components}/ComponentSetting2.vue +1 -1
  25. package/src/components/Confirm.vue +2 -0
  26. package/src/{widgets → components}/ContactForm.vue +1 -1
  27. package/src/components/CopyToClipboard.vue +0 -1
  28. package/src/components/Dashboard.vue +493 -212
  29. package/src/components/Datepicker.vue +2 -0
  30. package/src/components/Datetimepicker.vue +1 -1
  31. package/src/components/Dropdown.vue +1 -2
  32. package/src/{widgets → components}/FeatureList.vue +1 -1
  33. package/src/components/Gmaps.vue +1 -1
  34. package/src/components/HTMLEditor.vue +9 -1
  35. package/src/{widgets → components}/IconList.vue +1 -1
  36. package/src/components/Image.vue +1 -1
  37. package/src/components/ImagePreview.vue +32 -51
  38. package/src/components/LLMChat.vue +327 -0
  39. package/src/components/List.vue +267 -62
  40. package/src/components/MarkdownEdit.vue +124 -5
  41. package/src/components/MarkdownPreview.vue +4 -1
  42. package/src/{widgets → components}/MenuItem1Setting.vue +1 -0
  43. package/src/components/Metric.vue +30 -7
  44. package/src/components/MultiDropdown.vue +3 -1
  45. package/src/{widgets → components}/MultiValueSetting.vue +2 -2
  46. package/src/{widgets → components}/MultiValueSetting2.vue +2 -2
  47. package/src/components/OTPField.vue +3 -1
  48. package/src/components/PivotList.vue +1239 -0
  49. package/src/{widgets → components}/PresetBar.vue +32 -9
  50. package/src/{widgets → components}/PresetBarPivot.vue +147 -15
  51. package/src/{widgets → components}/PresetBarPivotColumnEdit.vue +8 -19
  52. package/src/components/PresetBarPivotValueEdit.vue +172 -0
  53. package/src/{widgets → components}/PresetSelector.vue +8 -4
  54. package/src/components/PresetSelectorFilterItem.vue +2 -1
  55. package/src/components/Radio.vue +1 -1
  56. package/src/{widgets → components}/Share.vue +1 -1
  57. package/src/components/SvgEditor.vue +1 -1
  58. package/src/components/Switch.vue +1 -1
  59. package/src/components/TextWithTag.vue +5 -5
  60. package/src/components/Textbox.vue +63 -5
  61. package/src/components/Toast.vue +2 -1
  62. package/src/components/Tooltip.vue +579 -96
  63. package/src/components/TreeView.vue +1 -1
  64. package/src/components/VirtualGrid.vue +2 -2
  65. package/src/components/VirtualTable.vue +170 -78
  66. package/src/{widgets → components}/WebDatasourceSelector.vue +2 -0
  67. package/src/{widgets → components}/WebLayoutSelector.vue +1 -1
  68. package/src/{widgets → components}/WebPageBuilder.vue +153 -352
  69. package/src/directives/tooltip.js +131 -0
  70. package/src/index.js +11 -287
  71. package/src/instructions/dashboard.js +203 -0
  72. package/src/themes/default/index.js +16 -0
  73. package/src/utils/ai-chat-answer.mjs +240 -0
  74. package/src/utils/ai-chat.js +167 -0
  75. package/src/utils/ai-dashboard-chat.mjs +371 -0
  76. package/src/utils/ai-dashboard.js +446 -0
  77. package/src/utils/ai-list-chat.mjs +407 -0
  78. package/src/utils/dashboard.js +13 -0
  79. package/src/utils/helpers.mjs +52 -5
  80. package/src/utils/list.mjs +150 -92
  81. package/src/utils/pivot-formula.mjs +358 -0
  82. package/src/utils/pivot-list.mjs +557 -0
  83. package/src/utils/preset-selector.mjs +183 -27
  84. package/src/utils/tooltip.mjs +243 -0
  85. package/src/utils/wss.js +26 -8
  86. package/src/utils/wss.mjs +29 -2
  87. package/docs/components/ArrayList.md +0 -39
  88. package/docs/components/Box.md +0 -15
  89. package/docs/components/ChartBar.md +0 -34
  90. package/docs/components/DayTimeRange.md +0 -20
  91. package/docs/components/ErrorText.md +0 -15
  92. package/docs/components/FBLogin.md +0 -21
  93. package/docs/components/GHeatMaps.md +0 -16
  94. package/docs/components/GSignIn.md +0 -21
  95. package/docs/components/GmapsDirection.md +0 -20
  96. package/docs/components/IconMenu.md +0 -20
  97. package/src/components/ArrayList.vue +0 -49
  98. package/src/components/Box.vue +0 -34
  99. package/src/components/ChartBar.vue +0 -192
  100. package/src/components/DayTimeRange.vue +0 -126
  101. package/src/components/ErrorText.vue +0 -57
  102. package/src/components/FBLogin.vue +0 -194
  103. package/src/components/GHeatMaps.vue +0 -317
  104. package/src/components/GSignIn.vue +0 -166
  105. package/src/components/GmapsDirection.vue +0 -191
  106. package/src/components/IconMenu.vue +0 -95
  107. package/src/components/IconPlus.vue +0 -89
  108. package/src/components/Image360.vue +0 -140
  109. package/src/components/ImageFullScreen.vue +0 -101
  110. package/src/components/ImportModal.vue +0 -260
  111. package/src/components/ListContextMenu.vue +0 -88
  112. package/src/components/LogViewerItem.vue +0 -116
  113. package/src/components/MultilineText.vue +0 -174
  114. package/src/components/PageBuilder.vue +0 -449
  115. package/src/components/SplitPane.vue +0 -129
  116. package/src/components/TabView.vue +0 -105
  117. package/src/components/TableView.vue +0 -200
  118. package/src/components/TableViewHead.vue +0 -159
  119. package/src/components/Test.vue +0 -23
  120. package/src/components/TextEditor.vue +0 -85
  121. package/src/components/TreeViewItem1.vue +0 -282
  122. package/src/components/VirtualScroll.vue +0 -282
  123. package/src/configs/dashboard/bar.js +0 -10
  124. package/src/configs/dashboard/collection-1.js +0 -5
  125. package/src/configs/dashboard/data-table.js +0 -9
  126. package/src/configs/dashboard/doughnut.js +0 -7
  127. package/src/configs/dashboard/gheatmaps.js +0 -9
  128. package/src/configs/dashboard/grid-2.js +0 -34
  129. package/src/configs/dashboard/grid-3.js +0 -34
  130. package/src/configs/dashboard/grid-4.js +0 -34
  131. package/src/configs/dashboard/grid.js +0 -15
  132. package/src/configs/dashboard/metric.js +0 -10
  133. package/src/configs/dashboard/pie.js +0 -7
  134. package/src/configs/dashboard/polar-area.js +0 -7
  135. package/src/configs/dashboard/virtual-table.js +0 -9
  136. package/src/configs/icons-fa.json +0 -12
  137. package/src/middleware/http/trim-string.js +0 -20
  138. package/src/mixin/ready-state.js +0 -37
  139. package/src/stores/components.js +0 -36
  140. package/src/stores/datasource.js +0 -11
  141. package/src/utils/event-bus.js +0 -8
  142. package/src/utils/helpers.cjs +0 -635
  143. package/src/utils/importer.js +0 -209
  144. package/src/utils/listview.js +0 -1287
  145. package/src/utils/preset-selector.cjs +0 -1455
  146. package/src/utils/preset-selector.js +0 -1452
  147. package/src/utils/queue.js +0 -63
  148. package/src/utils/web.js +0 -50
  149. package/src/utils/web.mjs +0 -120
  150. package/src/widgets/BackgroundSetting.vue +0 -132
  151. package/src/widgets/BorderSetting.vue +0 -201
  152. package/src/widgets/BotEditor/BotEditorActions.vue +0 -163
  153. package/src/widgets/BotEditor/BotEditorSettings.vue +0 -21
  154. package/src/widgets/BotEditor.vue +0 -248
  155. package/src/widgets/BoxSetting.vue +0 -53
  156. package/src/widgets/CheckoutSetting.vue +0 -46
  157. package/src/widgets/ColumnSelector.vue +0 -104
  158. package/src/widgets/ComponentPropertyLabel.vue +0 -35
  159. package/src/widgets/ComponentPropertySelector.vue +0 -115
  160. package/src/widgets/ComponentSetting3.vue +0 -231
  161. package/src/widgets/EffectSetting.vue +0 -94
  162. package/src/widgets/FiltersSetting.vue +0 -88
  163. package/src/widgets/Header.vue +0 -15
  164. package/src/widgets/Header0.vue +0 -41
  165. package/src/widgets/Header0Setting.vue +0 -75
  166. package/src/widgets/HeaderSetting.vue +0 -102
  167. package/src/widgets/LayoutSetting.vue +0 -251
  168. package/src/widgets/LogViewer.vue +0 -85
  169. package/src/widgets/MarginPaddingSetting.vue +0 -58
  170. package/src/widgets/MarginSetting.vue +0 -142
  171. package/src/widgets/MenuEditor.vue +0 -251
  172. package/src/widgets/PaddingSetting.vue +0 -142
  173. package/src/widgets/SizingSetting.vue +0 -306
  174. package/src/widgets/SpacingSetting.vue +0 -77
  175. package/src/widgets/StyleSetting.vue +0 -543
  176. package/src/widgets/StyleSetting2.vue +0 -90
  177. package/src/widgets/TransitionSetting.vue +0 -64
  178. package/src/widgets/TypographySetting.vue +0 -334
  179. package/src/widgets/UserActionBuilder/UserActionCondition.vue +0 -99
  180. package/src/widgets/UserActionBuilder/UserActionConsole.vue +0 -97
  181. package/src/widgets/UserActionBuilder/UserActionItem.vue +0 -273
  182. package/src/widgets/UserActionBuilder/UserActionOutput.vue +0 -150
  183. package/src/widgets/UserActionBuilder/UserActionOutputDelay.vue +0 -27
  184. package/src/widgets/UserActionBuilder/UserActionOutputLog.vue +0 -28
  185. package/src/widgets/UserActionBuilder/UserActionOutputReply.vue +0 -112
  186. package/src/widgets/UserActionBuilder/UserActionProps.vue +0 -213
  187. package/src/widgets/UserActionBuilder.vue +0 -314
  188. package/src/widgets/WebPageBuilder4/BackgroundColorSetting.vue +0 -54
  189. package/src/widgets/WebPageBuilder4/BackgroundPositionSetting.vue +0 -75
  190. package/src/widgets/WebPageBuilder4/BackgroundRepeatSetting.vue +0 -72
  191. package/src/widgets/WebPageBuilder4/BackgroundSizeSetting.vue +0 -72
  192. package/src/widgets/WebPageBuilder4/BlurSetting.vue +0 -72
  193. package/src/widgets/WebPageBuilder4/BorderColorSetting.vue +0 -54
  194. package/src/widgets/WebPageBuilder4/BorderRadiusSetting.vue +0 -75
  195. package/src/widgets/WebPageBuilder4/BorderSizeSetting.vue +0 -69
  196. package/src/widgets/WebPageBuilder4/BorderStyleSetting.vue +0 -72
  197. package/src/widgets/WebPageBuilder4/BoxShadowSetting.vue +0 -73
  198. package/src/widgets/WebPageBuilder4/ButtonSetting.vue +0 -65
  199. package/src/widgets/WebPageBuilder4/CarouselSetting.vue +0 -178
  200. package/src/widgets/WebPageBuilder4/DisplaySetting.vue +0 -76
  201. package/src/widgets/WebPageBuilder4/FlexAlignSetting.vue +0 -71
  202. package/src/widgets/WebPageBuilder4/FlexDirectionSetting.vue +0 -70
  203. package/src/widgets/WebPageBuilder4/FlexJustifySetting.vue +0 -74
  204. package/src/widgets/WebPageBuilder4/FlexPropertySetting.vue +0 -71
  205. package/src/widgets/WebPageBuilder4/FlexSetting.vue +0 -96
  206. package/src/widgets/WebPageBuilder4/FlexWrapSetting.vue +0 -69
  207. package/src/widgets/WebPageBuilder4/GapSetting.vue +0 -75
  208. package/src/widgets/WebPageBuilder4/GrayscaleSetting.vue +0 -68
  209. package/src/widgets/WebPageBuilder4/GridSetting.vue +0 -216
  210. package/src/widgets/WebPageBuilder4/HeightSetting.vue +0 -98
  211. package/src/widgets/WebPageBuilder4/ImageSetting.vue +0 -102
  212. package/src/widgets/WebPageBuilder4/MarginSetting.vue +0 -73
  213. package/src/widgets/WebPageBuilder4/MaxHeightSetting.vue +0 -82
  214. package/src/widgets/WebPageBuilder4/MaxWidthSetting.vue +0 -90
  215. package/src/widgets/WebPageBuilder4/MinHeightSetting.vue +0 -81
  216. package/src/widgets/WebPageBuilder4/MinWidthSetting.vue +0 -101
  217. package/src/widgets/WebPageBuilder4/MultiValueSetting.vue +0 -163
  218. package/src/widgets/WebPageBuilder4/OpacitySetting.vue +0 -77
  219. package/src/widgets/WebPageBuilder4/OverflowSetting.vue +0 -81
  220. package/src/widgets/WebPageBuilder4/PaddingSetting.vue +0 -57
  221. package/src/widgets/WebPageBuilder4/PositionSetting.vue +0 -191
  222. package/src/widgets/WebPageBuilder4/PropertySetting.vue +0 -176
  223. package/src/widgets/WebPageBuilder4/TextSetting.vue +0 -72
  224. package/src/widgets/WebPageBuilder4/TreeView.vue +0 -46
  225. package/src/widgets/WebPageBuilder4/TreeViewItem.vue +0 -303
  226. package/src/widgets/WebPageBuilder4/WebPageComponentSelector.vue +0 -141
  227. package/src/widgets/WebPageBuilder4/WebPagePropertySelector.vue +0 -168
  228. package/src/widgets/WebPageBuilder4/WidthSetting.vue +0 -105
  229. package/src/widgets/WebPageBuilder4/ZIndexSetting.vue +0 -72
  230. package/src/widgets/WebPagePreview.vue +0 -295
  231. package/src/widgets/WebPageSelector.vue +0 -173
  232. package/src/widgets/WebTemplateCreator.vue +0 -156
  233. /package/src/{widgets → components}/AhrefSetting.vue +0 -0
  234. /package/src/{widgets → components}/ArticleSetting.vue +0 -0
  235. /package/src/{widgets → components}/BackgroundColorSetting.vue +0 -0
  236. /package/src/{widgets → components}/BlockSetting.vue +0 -0
  237. /package/src/{widgets → components}/BorderColorSetting.vue +0 -0
  238. /package/src/{widgets → components}/ButtonSetting.vue +0 -0
  239. /package/src/{widgets → components}/CarouselSetting.vue +0 -0
  240. /package/src/{widgets → components}/ComponentSetting.vue +0 -0
  241. /package/src/{widgets → components}/ContactFormSetting.vue +0 -0
  242. /package/src/{widgets → components}/CountdownSetting.vue +0 -0
  243. /package/src/{widgets → components}/DataListSetting.vue +0 -0
  244. /package/src/{widgets → components}/EmbeddedVideo.vue +0 -0
  245. /package/src/{widgets → components}/EmbeddedVideoSetting.vue +0 -0
  246. /package/src/{widgets → components}/FAQ.vue +0 -0
  247. /package/src/{widgets → components}/FAQSetting.vue +0 -0
  248. /package/src/{widgets → components}/FeatureListSetting.vue +0 -0
  249. /package/src/{widgets → components}/FlexSetting.vue +0 -0
  250. /package/src/{widgets → components}/GridSetting.vue +0 -0
  251. /package/src/{widgets → components}/Header2.vue +0 -0
  252. /package/src/{widgets → components}/Header2Setting.vue +0 -0
  253. /package/src/{widgets → components}/IconListSetting.vue +0 -0
  254. /package/src/{widgets → components}/ImageSetting.vue +0 -0
  255. /package/src/{widgets → components}/LinkSetting.vue +0 -0
  256. /package/src/{widgets → components}/LinkSettingModal.vue +0 -0
  257. /package/src/{widgets → components}/ModalSetting.vue +0 -0
  258. /package/src/{widgets → components}/OGSettingModal.vue +0 -0
  259. /package/src/{widgets → components}/ParagraphSetting.vue +0 -0
  260. /package/src/{widgets → components}/PositionSetting.vue +0 -0
  261. /package/src/{widgets → components}/Review.vue +0 -0
  262. /package/src/{widgets → components}/ReviewSetting.vue +0 -0
  263. /package/src/{widgets → components}/SearchModalSetting.vue +0 -0
  264. /package/src/{widgets → components}/ShareSetting.vue +0 -0
  265. /package/src/{widgets → components}/SvgSetting.vue +0 -0
  266. /package/src/{widgets → components}/TableSetting.vue +0 -0
  267. /package/src/{widgets → components}/TestimonialSetting.vue +0 -0
  268. /package/src/{widgets → components}/TextBlockSetting.vue +0 -0
  269. /package/src/{widgets → components}/ThumbnailSetting.vue +0 -0
  270. /package/src/{widgets → components}/WebComponentSelector.vue +0 -0
  271. /package/src/{widgets → components}/YoutubeVideoSetting.vue +0 -0
package/README.md CHANGED
@@ -1,3 +1,27 @@
1
1
  # Web Scaffold for mixd.id
2
2
 
3
- This scaffold based on express vitejs vuejs
3
+ `@mixd-id/web-scaffold` the shared Vue 3 + Tailwind UI framework and utility library used by mixd CMS front ends (client: Vue 3 + Vite, server: Node.js ESM).
4
+
5
+ - **[Getting started](docs/GETTING-STARTED.md)** — installation, plugin setup, Tailwind theme, WebSocket client/server, utilities.
6
+ - **[Component docs](docs/components/)** — per-component usage and props.
7
+
8
+ ## Quick start
9
+
10
+ ```js
11
+ // main.js of a Vite + Vue 3 app
12
+ import webScaffold from '@mixd-id/web-scaffold'
13
+ app.use(webScaffold) // registers all components globally (lazy) + directives + $util helpers
14
+ ```
15
+
16
+ ```js
17
+ // tailwind.config.js
18
+ module.exports = {
19
+ content: [
20
+ './src/**/*.{vue,js}',
21
+ './node_modules/@mixd-id/web-scaffold/src/**/*.{vue,js}',
22
+ ],
23
+ plugins: [require('@mixd-id/web-scaffold/themes/default')],
24
+ }
25
+ ```
26
+
27
+ This package ships uncompiled sources — the consuming app's Vite build compiles it. Within the monorepo it is consumed via `"file:../web-scaffold"`, so changes here are picked up without a publish step.
@@ -0,0 +1,557 @@
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 — `aggregation`/`field` size each bar (count of rows when unset)
195
+ query = { datasource, aggregation, field, groupBy,
196
+ dateModifier: ''|'day'|'week'|'month'|'year', limit, filters }
197
+
198
+ // table — `order` is Sequelize's own shape, any number of levels
199
+ query = { datasource, pageSize, page, order: [[field,'asc'|'desc'], ...], columns, filters }
200
+
201
+ // filters (all types) — folded into a Sequelize `where` on the server
202
+ filters = [ { field: String, op: '='|'!='|'>'|'>='|'<'|'<='|'contains'|'in', value }, ... ]
203
+ ```
204
+
205
+ ### Chat message — an entry in `params.messages`
206
+
207
+ Same shape as `AIChat` (see `AIChat.md`):
208
+
209
+ ```js
210
+ { direction: 1|2, body, display, isLoading, model, ellapsed }
211
+ ```
212
+
213
+ Messages live in `preset.params.messages`, so each preset has its own
214
+ conversation, persisted with the config.
215
+
216
+ ### `samples` prop (offline seed, supplied by the page)
217
+
218
+ ```js
219
+ samples = {
220
+ structure: Node[], // dashboard used to seed the first preset; UID-LESS templates
221
+ widgets: { [key]: Node }, // widgets the offline chat can append; UID-LESS templates
222
+ }
223
+ ```
224
+
225
+ Chat-understood keys: `metric`, `chart`, `table`, `group`. Templates **must
226
+ not** carry `uid` — the component deep-clones each template and assigns fresh
227
+ ids via `instantiate()` on every use.
228
+
229
+ ---
230
+
231
+ ## Modes: offline vs connected
232
+
233
+ `connected = !!(socket && controller)`. Two further flags fine-tune a connected
234
+ mount so a host can keep owning parts it already handles:
235
+
236
+ | Flag | Default | Effect when **on** |
237
+ |------|---------|--------------------|
238
+ | `connected` (derived) | — | Enables the **Data** tab, `load-datasource`, and `load-dashboard` hydration. |
239
+ | `autoSave` | `true` | Persists edits via debounced `<controller>.save`. Turn **off** when the host page persists (avoids a double write). |
240
+ | `serverChat` | `true` | Routes chat through `<controller>.chat`. Turn **off** to keep the offline canned-widget generator even while connected. |
241
+
242
+ `pages/Dashboard.vue` connects for **data only**: it passes `socket` +
243
+ `controller="ai-dashboard"` but sets `:auto-save="false"` and
244
+ `:server-chat="false"`, so it keeps its `loadConfig`/`saveConfig` persistence
245
+ and the offline chat demo while gaining the Data tab and query hydration.
246
+
247
+ ---
248
+
249
+ ## Left panel — the tabbed builder
250
+
251
+ ### Presets tab (CRUD)
252
+
253
+ - `activePreset` = the preset whose `uid === config.params.presetIdx`.
254
+ - **Add** — pushes a blank preset (`name:'New Preset'`, empty structure) and
255
+ jumps to the **Build** tab.
256
+ - **Rename** — opens the floating rename `Card` (Textbox + Save/Cancel).
257
+ - **Duplicate** — deep-clones the preset with a fresh preset uid **and** fresh
258
+ uids for every node, inserts after the original, selects it.
259
+ - **Remove** — splices it; if it was active, selection falls back to the first
260
+ preset (or `null`). In connected mode also calls `<controller>.remove-preset`.
261
+ - Each row has a "⋮" button opening a context menu anchored to it.
262
+
263
+ ### Build tab (structure editor + inspector)
264
+
265
+ - A **`TreeView`** of the active preset's `structure`. Each row shows a
266
+ type-colored icon + name; groups show their layout mode. Selecting a row sets
267
+ the inspected node; `TreeView` provides drag-reorder, drag-into-group, inline
268
+ add (groups → quick metric), duplicate and remove.
269
+ - **Add component** (toolbar `+`) opens a menu (Section / Metric / Chart /
270
+ Table) that inserts into the selected group, else at the root.
271
+ - **Node inspector** (bottom): edits `name`; for **groups** the `layout`
272
+ (mode + columns); for **leaves** the authored `data` (caption, and a metric's
273
+ value + delta) followed by the query builder — data source, aggregation +
274
+ eligible field (metric **and** chart), group-by / bucket / limit (chart),
275
+ column checkboxes + multi-level order rows + page size (table), and the
276
+ **Filters** editor (field / operator / value rows). A **Refresh** button
277
+ (connected) re-hydrates on demand; query edits re-hydrate automatically
278
+ (debounced ~450ms).
279
+
280
+ ### Data tab (data sources)
281
+
282
+ - Lists the data sources returned by `<controller>.load-datasource` (or the
283
+ `datasources` prop offline). Each source expands to show its fields + types,
284
+ with a **"+ New metric from this source"** shortcut that drops a bound metric
285
+ into the current section and jumps to Build.
286
+ - Data source descriptor shape:
287
+
288
+ ```js
289
+ { key: String, label: String,
290
+ aggregations: ['count','sum','avg','min','max'], // supported metric aggregations
291
+ fields: [ { key, label, type: 'number'|'string'|'date' }, ... ] }
292
+ ```
293
+
294
+ ---
295
+
296
+ ## Middle panel — rendering
297
+
298
+ The active preset's `structure` renders top-to-bottom (`flex flex-col gap-6`):
299
+
300
+ - **group** → a section: heading (`node.name`) + its `items` laid out per
301
+ `node.layout` via **inline styles** (grid `repeat(cols, minmax(0,1fr))`, or
302
+ flex-wrap) — inline to survive Tailwind purge of dynamic column counts.
303
+ - **leaf** (metric/chart/table) → a full-width `Widget` card.
304
+ - Every widget card and section heading has a hover-revealed **✕** that removes
305
+ the node.
306
+ - Empty preset / no preset → an empty-state illustration + hint.
307
+ - Header: active preset name + a **Load samples** button that replaces the
308
+ active preset's structure with a fresh instantiation of `samples.structure`.
309
+
310
+ ### Widget rendering rules (`Widget`)
311
+
312
+ Card (`bg-base-300 rounded-xl p-5`) with a title row (name + hover ✕), then:
313
+
314
+ 0. The card **caption** is `data.label || node.name` for every type, and a
315
+ node whose hydration failed renders `data.error` in red in place of its body.
316
+ 1. **metric** — a large `toLocaleString()` number, optional delta line
317
+ (`▲ n%` green / `▼ n%` red).
318
+ 2. **chart** — a **vue-chartjs `Bar`** in a 240px-tall box. Bar/grid/tick colors
319
+ are read from the active theme's CSS variables (`--primary`, `--border-50`,
320
+ `--text-300`) with SSR-safe fallbacks; empty series shows a "No data." hint.
321
+ 3. **table** — a **`VirtualTable`** built from `columns` / `rows` (string column
322
+ doubles as key + label); height caps at 320px.
323
+ 4. **anything else** — muted placeholder.
324
+
325
+ ---
326
+
327
+ ## Chat
328
+
329
+ `send(prompt)` routes to server chat when `serverChat` is on and connected,
330
+ otherwise the offline generator.
331
+
332
+ ### Offline (canned, generates into the ACTIVE preset)
333
+
334
+ 1. Push user message + assistant placeholder into `activePreset.params.messages`.
335
+ 2. Compute `reply = pickReply(prompt)` (keyword table below).
336
+ 3. **Stream** the reply: every **28ms** advance a cursor by
337
+ `max(2, ceil(remaining/24))` chars. When done, apply the widgets once and
338
+ finalize (`model:'offline-demo'`, `ellapsed`).
339
+
340
+ | Keywords | Result |
341
+ |----------|--------|
342
+ | `overview`, `dashboard`, `build` | rebuild from `samples.structure` |
343
+ | `table`, `customer`, `list` | append `widgets.table` |
344
+ | `chart`, `bar`, `revenue`, `trend`, `graph` | append `widgets.chart` |
345
+ | `metric`, `kpi`, `card`, `number` | append `widgets.metric` (into the last root group) |
346
+ | `group`, `section` | append `widgets.group` |
347
+ | (none) | help message listing available widget keys |
348
+
349
+ `cancel()` stops the timer and freezes the in-flight message; the dashboard only
350
+ changes once a reply fully streams in.
351
+
352
+ ### Connected (`serverChat`)
353
+
354
+ `sendConnected` posts `{ messages, structure }` to `<controller>.chat` and
355
+ applies the result: `reply` → assistant body; `structure` → replace the active
356
+ preset's tree; `nodes` → instantiate + append (metrics into the last root
357
+ group). Then re-hydrates and persists. Missing uids are backfilled.
358
+
359
+ ---
360
+
361
+ ## Persistence & hydration (connected)
362
+
363
+ - **Hydrate** — on mount, preset switch, data-source change, query edit
364
+ (debounced) and the Refresh button, `hydrateActive()` posts the preset's
365
+ structure to `<controller>.load-dashboard` and **merges** returned `data` into
366
+ the live nodes **by uid**. Derived data is never authored by hand.
367
+ - **Persist** — a deep `watch(config)` calls debounced (**700ms**)
368
+ `<controller>.save` with a cleaned config: on a node that has a `query` only
369
+ the authored `data.label` / `data.delta` are kept (the server recomputes
370
+ `value` / `series` / `rows`), and `messages` are frozen. No-op unless
371
+ `connected && autoSave`.
372
+
373
+ ### Offline persistence (host-owned)
374
+
375
+ ```js
376
+ // pages/Dashboard.vue
377
+ const config = ref(await loadConfig('dashboard', {
378
+ name: 'Dashboard',
379
+ params: { leftWidth: 260, rightWidth: 360, presetIdx: null },
380
+ presets: [],
381
+ }))
382
+ watch(() => config.value, to => saveConfig('dashboard', to), { deep: true })
383
+ ```
384
+
385
+ > Web detail: this project's Tailwind config sets `important: true`, so panel
386
+ > widths and group layouts are applied as **inline styles** with no competing
387
+ > class.
388
+
389
+ ---
390
+
391
+ ## Props
392
+
393
+ | Prop | Type | Default | Description |
394
+ |------|------|---------|-------------|
395
+ | `config` | `Object` | required | UserConfig-shaped object (see **Data model**). Mutated in place. |
396
+ | `samples` | `Object` | `{ structure: [], widgets: {} }` | Offline seed layout + generatable widgets. Nodes are uid-less templates. |
397
+ | `socket` | `Object` | `null` | Socket transport. With `controller`, switches the component to **connected** mode. |
398
+ | `controller` | `String` | `''` | Socket namespace prefixing every call (`<controller>.load-datasource`, `.load-dashboard`, `.save`, `.chat`, `.remove-preset`). |
399
+ | `datasources` | `Object\|Array` | `{}` | Optional client-side data-source descriptor — either `{ [key]: { label, fields, aggregations } }` or the `[{ key, label, fields, aggregations }]` array `load-datasource` returns; used only if the server returns none. |
400
+ | `autoSave` | `Boolean` | `true` | Persist edits via `<controller>.save`. Set `false` when the host page owns persistence. |
401
+ | `serverChat` | `Boolean` | `true` | Route chat through `<controller>.chat`. Set `false` to keep the offline generator while connected. |
402
+
403
+ `AIDashboard` emits no events and exposes no methods.
404
+
405
+ ---
406
+
407
+ ## Server side
408
+
409
+ ### `ai-dashboard` socket controller
410
+
411
+ `server/src/controllers/sockets/ai-dashboard.js` (registered as
412
+ `routes['ai-dashboard']`) mixes in `AIDashboardMixin` and adds only what is
413
+ project-specific:
414
+
415
+ - **`get datasources()`** maps each catalogue `key` to a real Sequelize model,
416
+ so `load-dashboard` / `analyze-node` can compute widget data.
417
+ - **`get datasourceList()`** the catalogue `load-datasource` answers with
418
+ (`order`, `message`, `inbox`, `customer`, `product` — typed fields +
419
+ supported aggregations). Without it the mixin describes the models in
420
+ `datasources` instead.
421
+ - **`aiChat`** the real LLM call.
422
+
423
+ Filtering is **not** the controller's job any more — the mixin folds
424
+ `query.filters` into the `where` of every analyzer.
425
+
426
+ ### `AIDashboardMixin` (`@mixd-id/web-scaffold/ai-dashboard`)
427
+
428
+ Host-class contract (getters): `Models` `{ UserConfig, UserPreset }`,
429
+ `Sequelize` (for `Op`), `configKey` (the `UserConfig.key`), `conn` and
430
+ `datasources` (for hydration), optionally `datasourceList`.
431
+ `this.socket.token.userId` comes from the base socket `Controller`.
432
+
433
+ Every field name a query names — `field`, `groupBy`, `columns[].key`,
434
+ `order[][0]`, `filters[].field` — is checked against the model's attributes
435
+ before it reaches `fn`/`col`/`literal`. An unknown one throws, and
436
+ `load-dashboard` catches per node and returns `data.error` so one broken widget
437
+ cannot blank the dashboard.
438
+
439
+ **Filters → `where`** — `filtersToWhere(model, filters)` maps `{field, op, value}`
440
+ rows to Sequelize (`= != > >= < <= contains in` →
441
+ `Op.eq/ne/gt/gte/lt/lte/like/in`, blanks dropped, `in` comma-split).
442
+
443
+ Methods (socket path → method, kebab→camel):
444
+
445
+ | Socket path | Method | Does |
446
+ |-------------|--------|------|
447
+ | `<controller>.load` | `load()` | UserConfig + presets (seq-ordered), or `null` |
448
+ | `<controller>.save` | `save({ config })` | Upsert UserConfig; sync presets (array order → `seq`, missing uids destroyed); returns fresh `load()` |
449
+ | `<controller>.save-preset` | `savePreset({ preset })` | Upsert one preset (appends `seq` for new rows) |
450
+ | `<controller>.remove-preset` | `removePreset({ uid })` | Destroy one preset → `{ removed }` |
451
+ | `<controller>.load-dashboard` | `loadDashboard({ preset })` | Hydrate: walk the structure, compute `data` for every node with a `query`, recurse into groups; returns the hydrated preset |
452
+ | `<controller>.load-datasource` | `loadDatasource()` | `{ datasources }` — `this.datasourceList`, or the models in `this.datasources` described |
453
+ | `<controller>.analyze-node` | `analyzeNode({ node })` | Compute one node's `data`: metric → `{ value }`, chart → `{ series }`, table → `{ columns, rows, hasNext }`. Each query's `filters` are folded into its `where` here — no override needed |
454
+ | `<controller>.chat` | `chat({ messages, structure })` | **Overridable stub** — real controllers return `{ reply, structure?, nodes?, model? }` |
455
+
456
+ `mixin(Class, overrides)` copies mixin methods onto the prototype **without
457
+ clobbering** methods the class already defines (e.g. a controller's own
458
+ `loadDatasource` or `analyzeNode`); `overrides` always win.
459
+
460
+ `server/docs/dashboard.json` is the worked fixture for all of this — one preset
461
+ exercising every widget type, aggregation, bucket, order and filter operator the
462
+ analyzers understand.
463
+
464
+ ### `apiMixin` (`@mixd-id/web-scaffold/ai-dashboard-api`)
465
+
466
+ The REST counterpart. Same logic, same `UserConfig`/`UserPreset` rows — an API
467
+ client and the component read and write the same dashboards.
468
+
469
+ | Route | Verb → handler | Mixin method |
470
+ |-------|----------------|--------------|
471
+ | `/ai-dashboard` | GET → `index` | `load()` |
472
+ | `/ai-dashboard` | POST → `store` | `save({ config })` |
473
+ | `/ai-dashboard/preset` | POST → `store` | `savePreset({ preset })` |
474
+ | `/ai-dashboard/preset?uid=` | DELETE → `destroy` | `removePreset({ uid })` |
475
+ | `/ai-dashboard/datasource` | GET → `index` | `loadDatasource()` |
476
+ | `/ai-dashboard/dashboard` | POST → `store` | `loadDashboard({ preset })` |
477
+ | `/ai-dashboard/node` | POST → `store` | `analyzeNode({ node })` |
478
+ | `/ai-dashboard/chat` | POST → `store` | `aiChat({ messages, structure })` |
479
+
480
+ `apiMixin(host)` returns those grouped by route (`{ config, preset, datasource,
481
+ dashboard, node, chat }`). `host` is the socket controller's getter contract as
482
+ a plain object — `Models`, `Sequelize`, `conn`, `configKey`, `datasources`,
483
+ optional `datasourceList` / `model` — plus:
484
+
485
+ - **`ValidationError`** — when given, bodies are validated and a bad one is a
486
+ **400** (`uid`+`name` on a preset, `type` on a node, `messages` on chat).
487
+ - **`NotFoundError`** — when given, an unknown preset uid is a **404**.
488
+ - any override the socket controller would make, `aiChat` above all.
489
+
490
+ Notes that bite:
491
+
492
+ - Every body is taken both **wrapped** the way the socket path takes it
493
+ (`{ preset }`, `{ node }`, `{ config }`) and **bare**, so posting a preset as
494
+ the body does what it looks like it does.
495
+ - `POST /ai-dashboard/dashboard` also accepts just `{ uid }` and hydrates the
496
+ stored preset — an API client should not have to post back a structure the
497
+ server already has.
498
+ - **Identity is per request.** The http middleware calls a handler as
499
+ `fn.apply(route, [req, res])`, so `this` is the route object *shared by every
500
+ request*; the mixin builds a fresh context around `req.token` instead and
501
+ never stores a request on the route.
502
+ - **DELETE takes `?uid=`**, not a path segment: wildcard routes match `[a-z0-9]`
503
+ only, which cannot hold a `p-lz4k2h-1`.
504
+ - Controller files must stay **literal** — `npm run postman` reads `name` and
505
+ `examples` out of the source of `export default`, not out of anything a
506
+ function returned. So wire the handlers in, don't generate the object:
507
+
508
+ ```js
509
+ // src/controllers/api/ai-dashboard/index.js
510
+ import { apiMixin } from "@mixd-id/web-scaffold/ai-dashboard-api"
511
+ import host from "./_host.js"
512
+
513
+ const { config } = apiMixin(host)
514
+
515
+ export default {
516
+ index: { name: 'Load the dashboard', fn: config.index },
517
+ store: { name: 'Save the dashboard', fn: config.store, examples: [ ... ] },
518
+ }
519
+ ```
520
+
521
+ Then register the path in `src/controllers/api/index.js` and its auth in
522
+ `src/configs/api.js` (`'/ai-dashboard': { token: true }`).
523
+
524
+ ---
525
+
526
+ ## Constants & tunables
527
+
528
+ | Name | Value | Meaning |
529
+ |------|-------|---------|
530
+ | Left width clamp | `220–520px` | builder panel resize bounds |
531
+ | Left default width | `260px` | initial `leftWidth` |
532
+ | Right width clamp | `280–560px` | chat panel resize bounds |
533
+ | Right default width | `360px` | initial `rightWidth` |
534
+ | Resize handle width | `3px` | grab strip on each inner panel edge |
535
+ | Default group layout | `{ mode:'grid', cols: metricOnly ? 4 : 2 }` | new / seeded group layout |
536
+ | Chart height | `240px` | vue-chartjs Bar container |
537
+ | Table max height | `320px` | VirtualTable cap |
538
+ | Save debounce | `700ms` | `<controller>.save` |
539
+ | Hydrate debounce | `450ms` | `<controller>.load-dashboard` after query edits |
540
+ | Reveal interval / step | `28ms` / `max(2, ceil(remaining/24))` | offline chat streaming |
541
+ | First preset name | `'My Dashboard'` | auto-created on first run, seeded from samples |
542
+ | Assistant `model` tag (offline) | `'offline-demo'` | footer label on generated replies |
543
+
544
+ ---
545
+
546
+ ## Files
547
+
548
+ | File | Role |
549
+ |------|------|
550
+ | `src/components/AIDashboard.vue` | Orchestrator + inline `Widget` (this doc's subject). |
551
+ | `src/components/AIChatItem.vue` | Reused chat message renderer (see `AIChat.md`). |
552
+ | `src/components/AIChatTextbox.vue` | Reused composer input (see `AIChat.md`). |
553
+ | `src/pages/Dashboard.vue` | Page host; owns `samples`, connects to `ai-dashboard` for data only (auto-save + server-chat off). |
554
+ | `web-scaffold/src/utils/ai-dashboard.js` | Server-side `AIDashboardMixin` + `mixin()` for socket controllers. |
555
+ | `web-scaffold/src/utils/ai-dashboard-api.js` | `apiMixin()` — the same logic behind REST handlers for `src/controllers/api`. |
556
+ | `server/src/controllers/sockets/ai-dashboard.js` | Concrete `ai-dashboard` controller: the datasource catalogue, its models, and the real `ai-chat`. |
557
+ | `server/docs/dashboard.json` | Worked fixture of the persisted shape + catalogue (see `server/docs/dashboard.md`). |
@@ -12,9 +12,11 @@ None (controlled via methods)
12
12
  ## Methods
13
13
  | Method | Parameters | Description |
14
14
  |--------|------------|-------------|
15
- | open | `{ title?, message, buttonText?, onConfirm? }` | Opens the alert dialog |
15
+ | open | `{ title?, message, buttonText?, onConfirm?, icon? }` | Opens the alert dialog. Pass `icon: false` to drop the red warning mark when the dialog is not reporting an error |
16
16
  | close | - | Closes the alert dialog |
17
17
 
18
+ The dialog is capped at 80% of the viewport height: the title, message and details scroll inside it while the confirm button stays pinned at the bottom. Pressing Escape confirms and closes it, the same as the button.
19
+
18
20
  ## Slots
19
21
  | Slot | Description |
20
22
  |------|-------------|
@@ -13,6 +13,11 @@ A card component that can be used as a static container or floating modal.
13
13
  | hash | String | `undefined` | URL hash to control card visibility |
14
14
  | query | String | `undefined` | URL query param to control card visibility |
15
15
 
16
+ ## Events
17
+ | Event | Payload | Description |
18
+ |-------|---------|-------------|
19
+ | show | `{ context, caller }` | Emitted when a floating card opens — by `open()`, or by its `hash`/`query` appearing in the route |
20
+
16
21
  ## Methods
17
22
  | Method | Parameters | Description |
18
23
  |--------|------------|-------------|