@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.
- package/README.md +25 -1
- package/docs/components/AIDashboard.md +557 -0
- package/docs/components/Alert.md +3 -1
- package/docs/components/Card.md +5 -0
- package/docs/components/PivotList.md +207 -0
- package/docs/components/Textbox.md +20 -1
- package/docs/components/Tooltip.md +148 -0
- package/docs/components/WebPageBuilder.md +125 -0
- package/package.json +22 -89
- package/src/components/AIChat.vue +488 -0
- package/src/components/AIChatItem.vue +199 -0
- package/src/components/AIChatTextbox.vue +56 -0
- package/src/components/AIDashboard.vue +1159 -0
- package/src/components/Alert.vue +58 -35
- package/src/components/Card.vue +58 -7
- package/src/components/Carousel.vue +1 -1
- package/src/{widgets → components}/CartSetting.vue +1 -0
- package/src/components/Chart.vue +4 -3
- package/src/components/Checkbox.vue +22 -24
- package/src/components/CodeEditor.vue +123 -13
- package/src/{widgets → components}/CollapsiblePanelSetting.vue +1 -0
- package/src/components/ColorPicker.vue +8 -4
- package/src/components/ColumnSelector.vue +93 -0
- package/src/{widgets → components}/ComponentSetting2.vue +1 -1
- package/src/components/Confirm.vue +2 -0
- package/src/{widgets → components}/ContactForm.vue +1 -1
- package/src/components/CopyToClipboard.vue +0 -1
- package/src/components/Dashboard.vue +493 -212
- package/src/components/Datepicker.vue +2 -0
- package/src/components/Datetimepicker.vue +1 -1
- package/src/components/Dropdown.vue +1 -2
- package/src/{widgets → components}/FeatureList.vue +1 -1
- package/src/components/Gmaps.vue +1 -1
- package/src/components/HTMLEditor.vue +9 -1
- package/src/{widgets → components}/IconList.vue +1 -1
- package/src/components/Image.vue +1 -1
- package/src/components/ImagePreview.vue +32 -51
- package/src/components/LLMChat.vue +327 -0
- package/src/components/List.vue +267 -62
- package/src/components/MarkdownEdit.vue +124 -5
- package/src/components/MarkdownPreview.vue +4 -1
- package/src/{widgets → components}/MenuItem1Setting.vue +1 -0
- package/src/components/Metric.vue +30 -7
- package/src/components/MultiDropdown.vue +3 -1
- package/src/{widgets → components}/MultiValueSetting.vue +2 -2
- package/src/{widgets → components}/MultiValueSetting2.vue +2 -2
- package/src/components/OTPField.vue +3 -1
- package/src/components/PivotList.vue +1239 -0
- package/src/{widgets → components}/PresetBar.vue +32 -9
- package/src/{widgets → components}/PresetBarPivot.vue +147 -15
- package/src/{widgets → components}/PresetBarPivotColumnEdit.vue +8 -19
- package/src/components/PresetBarPivotValueEdit.vue +172 -0
- package/src/{widgets → components}/PresetSelector.vue +8 -4
- package/src/components/PresetSelectorFilterItem.vue +2 -1
- package/src/components/Radio.vue +1 -1
- package/src/{widgets → components}/Share.vue +1 -1
- package/src/components/SvgEditor.vue +1 -1
- package/src/components/Switch.vue +1 -1
- package/src/components/TextWithTag.vue +5 -5
- package/src/components/Textbox.vue +63 -5
- package/src/components/Toast.vue +2 -1
- package/src/components/Tooltip.vue +579 -96
- package/src/components/TreeView.vue +1 -1
- package/src/components/VirtualGrid.vue +2 -2
- package/src/components/VirtualTable.vue +170 -78
- package/src/{widgets → components}/WebDatasourceSelector.vue +2 -0
- package/src/{widgets → components}/WebLayoutSelector.vue +1 -1
- package/src/{widgets → components}/WebPageBuilder.vue +153 -352
- package/src/directives/tooltip.js +131 -0
- package/src/index.js +11 -287
- package/src/instructions/dashboard.js +203 -0
- package/src/themes/default/index.js +16 -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 +446 -0
- package/src/utils/ai-list-chat.mjs +407 -0
- package/src/utils/dashboard.js +13 -0
- package/src/utils/helpers.mjs +52 -5
- package/src/utils/list.mjs +150 -92
- package/src/utils/pivot-formula.mjs +358 -0
- package/src/utils/pivot-list.mjs +557 -0
- package/src/utils/preset-selector.mjs +183 -27
- package/src/utils/tooltip.mjs +243 -0
- package/src/utils/wss.js +26 -8
- package/src/utils/wss.mjs +29 -2
- 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/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/ColumnSelector.vue +0 -104
- 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
- /package/src/{widgets → components}/AhrefSetting.vue +0 -0
- /package/src/{widgets → components}/ArticleSetting.vue +0 -0
- /package/src/{widgets → components}/BackgroundColorSetting.vue +0 -0
- /package/src/{widgets → components}/BlockSetting.vue +0 -0
- /package/src/{widgets → components}/BorderColorSetting.vue +0 -0
- /package/src/{widgets → components}/ButtonSetting.vue +0 -0
- /package/src/{widgets → components}/CarouselSetting.vue +0 -0
- /package/src/{widgets → components}/ComponentSetting.vue +0 -0
- /package/src/{widgets → components}/ContactFormSetting.vue +0 -0
- /package/src/{widgets → components}/CountdownSetting.vue +0 -0
- /package/src/{widgets → components}/DataListSetting.vue +0 -0
- /package/src/{widgets → components}/EmbeddedVideo.vue +0 -0
- /package/src/{widgets → components}/EmbeddedVideoSetting.vue +0 -0
- /package/src/{widgets → components}/FAQ.vue +0 -0
- /package/src/{widgets → components}/FAQSetting.vue +0 -0
- /package/src/{widgets → components}/FeatureListSetting.vue +0 -0
- /package/src/{widgets → components}/FlexSetting.vue +0 -0
- /package/src/{widgets → components}/GridSetting.vue +0 -0
- /package/src/{widgets → components}/Header2.vue +0 -0
- /package/src/{widgets → components}/Header2Setting.vue +0 -0
- /package/src/{widgets → components}/IconListSetting.vue +0 -0
- /package/src/{widgets → components}/ImageSetting.vue +0 -0
- /package/src/{widgets → components}/LinkSetting.vue +0 -0
- /package/src/{widgets → components}/LinkSettingModal.vue +0 -0
- /package/src/{widgets → components}/ModalSetting.vue +0 -0
- /package/src/{widgets → components}/OGSettingModal.vue +0 -0
- /package/src/{widgets → components}/ParagraphSetting.vue +0 -0
- /package/src/{widgets → components}/PositionSetting.vue +0 -0
- /package/src/{widgets → components}/Review.vue +0 -0
- /package/src/{widgets → components}/ReviewSetting.vue +0 -0
- /package/src/{widgets → components}/SearchModalSetting.vue +0 -0
- /package/src/{widgets → components}/ShareSetting.vue +0 -0
- /package/src/{widgets → components}/SvgSetting.vue +0 -0
- /package/src/{widgets → components}/TableSetting.vue +0 -0
- /package/src/{widgets → components}/TestimonialSetting.vue +0 -0
- /package/src/{widgets → components}/TextBlockSetting.vue +0 -0
- /package/src/{widgets → components}/ThumbnailSetting.vue +0 -0
- /package/src/{widgets → components}/WebComponentSelector.vue +0 -0
- /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
|
-
|
|
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`). |
|
package/docs/components/Alert.md
CHANGED
|
@@ -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
|
|------|-------------|
|
package/docs/components/Card.md
CHANGED
|
@@ -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
|
|--------|------------|-------------|
|