@mixd-id/web-scaffold 0.2.250801013 → 0.2.250801014

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/docs/components/AIDashboard.md +472 -0
  2. package/docs/components/WebPageBuilder.md +125 -0
  3. package/package.json +7 -30
  4. package/src/components/AIChat.vue +479 -0
  5. package/src/components/AIChatItem.vue +168 -0
  6. package/src/components/AIChatTextbox.vue +56 -0
  7. package/src/components/AIDashboard.vue +1113 -0
  8. package/src/components/Card.vue +3 -1
  9. package/src/components/Chart.vue +2 -1
  10. package/src/components/CodeEditor.vue +121 -13
  11. package/src/components/ColorPicker.vue +6 -2
  12. package/src/components/Dashboard.vue +493 -212
  13. package/src/components/Dropdown.vue +0 -1
  14. package/src/components/HTMLEditor.vue +9 -1
  15. package/src/components/List.vue +226 -58
  16. package/src/components/MarkdownEdit.vue +123 -4
  17. package/src/components/PresetSelectorFilterItem.vue +1 -0
  18. package/src/components/TextWithTag.vue +3 -3
  19. package/src/components/TreeView.vue +1 -1
  20. package/src/components/VirtualTable.vue +166 -75
  21. package/src/index.js +3 -55
  22. package/src/instructions/dashboard.js +203 -0
  23. package/src/utils/ai-chat-answer.mjs +240 -0
  24. package/src/utils/ai-chat.js +167 -0
  25. package/src/utils/ai-dashboard-chat.mjs +371 -0
  26. package/src/utils/ai-dashboard.js +321 -0
  27. package/src/utils/ai-list-chat.mjs +407 -0
  28. package/src/utils/dashboard.js +13 -0
  29. package/src/utils/list.mjs +146 -91
  30. package/src/utils/preset-selector.mjs +183 -27
  31. package/src/utils/wss.js +0 -2
  32. package/src/widgets/ColumnSelector.vue +22 -33
  33. package/src/widgets/LLMChat.vue +327 -0
  34. package/src/widgets/PresetBar.vue +31 -8
  35. package/src/widgets/PresetBarPivot.vue +147 -15
  36. package/src/widgets/PresetBarPivotColumnEdit.vue +8 -19
  37. package/src/widgets/PresetBarPivotValueEdit.vue +172 -0
  38. package/src/widgets/PresetSelector.vue +8 -4
  39. package/src/widgets/WebPageBuilder.vue +153 -352
  40. package/docs/components/ArrayList.md +0 -39
  41. package/docs/components/Box.md +0 -15
  42. package/docs/components/ChartBar.md +0 -34
  43. package/docs/components/DayTimeRange.md +0 -20
  44. package/docs/components/ErrorText.md +0 -15
  45. package/docs/components/FBLogin.md +0 -21
  46. package/docs/components/GHeatMaps.md +0 -16
  47. package/docs/components/GSignIn.md +0 -21
  48. package/docs/components/GmapsDirection.md +0 -20
  49. package/docs/components/IconMenu.md +0 -20
  50. package/src/components/ArrayList.vue +0 -49
  51. package/src/components/Box.vue +0 -34
  52. package/src/components/ChartBar.vue +0 -192
  53. package/src/components/DayTimeRange.vue +0 -126
  54. package/src/components/ErrorText.vue +0 -57
  55. package/src/components/FBLogin.vue +0 -194
  56. package/src/components/GHeatMaps.vue +0 -317
  57. package/src/components/GSignIn.vue +0 -166
  58. package/src/components/GmapsDirection.vue +0 -191
  59. package/src/components/IconMenu.vue +0 -95
  60. package/src/components/IconPlus.vue +0 -89
  61. package/src/components/Image360.vue +0 -140
  62. package/src/components/ImageFullScreen.vue +0 -101
  63. package/src/components/ImportModal.vue +0 -260
  64. package/src/components/ListContextMenu.vue +0 -88
  65. package/src/components/LogViewerItem.vue +0 -116
  66. package/src/components/MultilineText.vue +0 -174
  67. package/src/components/PageBuilder.vue +0 -449
  68. package/src/components/SplitPane.vue +0 -129
  69. package/src/components/TabView.vue +0 -105
  70. package/src/components/TableView.vue +0 -200
  71. package/src/components/TableViewHead.vue +0 -159
  72. package/src/components/Test.vue +0 -23
  73. package/src/components/TextEditor.vue +0 -85
  74. package/src/components/Tooltip.vue +0 -143
  75. package/src/components/TreeViewItem1.vue +0 -282
  76. package/src/components/VirtualScroll.vue +0 -282
  77. package/src/configs/dashboard/bar.js +0 -10
  78. package/src/configs/dashboard/collection-1.js +0 -5
  79. package/src/configs/dashboard/data-table.js +0 -9
  80. package/src/configs/dashboard/doughnut.js +0 -7
  81. package/src/configs/dashboard/gheatmaps.js +0 -9
  82. package/src/configs/dashboard/grid-2.js +0 -34
  83. package/src/configs/dashboard/grid-3.js +0 -34
  84. package/src/configs/dashboard/grid-4.js +0 -34
  85. package/src/configs/dashboard/grid.js +0 -15
  86. package/src/configs/dashboard/metric.js +0 -10
  87. package/src/configs/dashboard/pie.js +0 -7
  88. package/src/configs/dashboard/polar-area.js +0 -7
  89. package/src/configs/dashboard/virtual-table.js +0 -9
  90. package/src/configs/icons-fa.json +0 -12
  91. package/src/middleware/http/trim-string.js +0 -20
  92. package/src/mixin/ready-state.js +0 -37
  93. package/src/stores/components.js +0 -36
  94. package/src/stores/datasource.js +0 -11
  95. package/src/utils/event-bus.js +0 -8
  96. package/src/utils/helpers.cjs +0 -635
  97. package/src/utils/importer.js +0 -209
  98. package/src/utils/listview.js +0 -1287
  99. package/src/utils/preset-selector.cjs +0 -1455
  100. package/src/utils/preset-selector.js +0 -1452
  101. package/src/utils/queue.js +0 -63
  102. package/src/utils/web.js +0 -50
  103. package/src/utils/web.mjs +0 -120
  104. package/src/widgets/BackgroundSetting.vue +0 -132
  105. package/src/widgets/BorderSetting.vue +0 -201
  106. package/src/widgets/BotEditor/BotEditorActions.vue +0 -163
  107. package/src/widgets/BotEditor/BotEditorSettings.vue +0 -21
  108. package/src/widgets/BotEditor.vue +0 -248
  109. package/src/widgets/BoxSetting.vue +0 -53
  110. package/src/widgets/CheckoutSetting.vue +0 -46
  111. package/src/widgets/ComponentPropertyLabel.vue +0 -35
  112. package/src/widgets/ComponentPropertySelector.vue +0 -115
  113. package/src/widgets/ComponentSetting3.vue +0 -231
  114. package/src/widgets/EffectSetting.vue +0 -94
  115. package/src/widgets/FiltersSetting.vue +0 -88
  116. package/src/widgets/Header.vue +0 -15
  117. package/src/widgets/Header0.vue +0 -41
  118. package/src/widgets/Header0Setting.vue +0 -75
  119. package/src/widgets/HeaderSetting.vue +0 -102
  120. package/src/widgets/LayoutSetting.vue +0 -251
  121. package/src/widgets/LogViewer.vue +0 -85
  122. package/src/widgets/MarginPaddingSetting.vue +0 -58
  123. package/src/widgets/MarginSetting.vue +0 -142
  124. package/src/widgets/MenuEditor.vue +0 -251
  125. package/src/widgets/PaddingSetting.vue +0 -142
  126. package/src/widgets/SizingSetting.vue +0 -306
  127. package/src/widgets/SpacingSetting.vue +0 -77
  128. package/src/widgets/StyleSetting.vue +0 -543
  129. package/src/widgets/StyleSetting2.vue +0 -90
  130. package/src/widgets/TransitionSetting.vue +0 -64
  131. package/src/widgets/TypographySetting.vue +0 -334
  132. package/src/widgets/UserActionBuilder/UserActionCondition.vue +0 -99
  133. package/src/widgets/UserActionBuilder/UserActionConsole.vue +0 -97
  134. package/src/widgets/UserActionBuilder/UserActionItem.vue +0 -273
  135. package/src/widgets/UserActionBuilder/UserActionOutput.vue +0 -150
  136. package/src/widgets/UserActionBuilder/UserActionOutputDelay.vue +0 -27
  137. package/src/widgets/UserActionBuilder/UserActionOutputLog.vue +0 -28
  138. package/src/widgets/UserActionBuilder/UserActionOutputReply.vue +0 -112
  139. package/src/widgets/UserActionBuilder/UserActionProps.vue +0 -213
  140. package/src/widgets/UserActionBuilder.vue +0 -314
  141. package/src/widgets/WebPageBuilder4/BackgroundColorSetting.vue +0 -54
  142. package/src/widgets/WebPageBuilder4/BackgroundPositionSetting.vue +0 -75
  143. package/src/widgets/WebPageBuilder4/BackgroundRepeatSetting.vue +0 -72
  144. package/src/widgets/WebPageBuilder4/BackgroundSizeSetting.vue +0 -72
  145. package/src/widgets/WebPageBuilder4/BlurSetting.vue +0 -72
  146. package/src/widgets/WebPageBuilder4/BorderColorSetting.vue +0 -54
  147. package/src/widgets/WebPageBuilder4/BorderRadiusSetting.vue +0 -75
  148. package/src/widgets/WebPageBuilder4/BorderSizeSetting.vue +0 -69
  149. package/src/widgets/WebPageBuilder4/BorderStyleSetting.vue +0 -72
  150. package/src/widgets/WebPageBuilder4/BoxShadowSetting.vue +0 -73
  151. package/src/widgets/WebPageBuilder4/ButtonSetting.vue +0 -65
  152. package/src/widgets/WebPageBuilder4/CarouselSetting.vue +0 -178
  153. package/src/widgets/WebPageBuilder4/DisplaySetting.vue +0 -76
  154. package/src/widgets/WebPageBuilder4/FlexAlignSetting.vue +0 -71
  155. package/src/widgets/WebPageBuilder4/FlexDirectionSetting.vue +0 -70
  156. package/src/widgets/WebPageBuilder4/FlexJustifySetting.vue +0 -74
  157. package/src/widgets/WebPageBuilder4/FlexPropertySetting.vue +0 -71
  158. package/src/widgets/WebPageBuilder4/FlexSetting.vue +0 -96
  159. package/src/widgets/WebPageBuilder4/FlexWrapSetting.vue +0 -69
  160. package/src/widgets/WebPageBuilder4/GapSetting.vue +0 -75
  161. package/src/widgets/WebPageBuilder4/GrayscaleSetting.vue +0 -68
  162. package/src/widgets/WebPageBuilder4/GridSetting.vue +0 -216
  163. package/src/widgets/WebPageBuilder4/HeightSetting.vue +0 -98
  164. package/src/widgets/WebPageBuilder4/ImageSetting.vue +0 -102
  165. package/src/widgets/WebPageBuilder4/MarginSetting.vue +0 -73
  166. package/src/widgets/WebPageBuilder4/MaxHeightSetting.vue +0 -82
  167. package/src/widgets/WebPageBuilder4/MaxWidthSetting.vue +0 -90
  168. package/src/widgets/WebPageBuilder4/MinHeightSetting.vue +0 -81
  169. package/src/widgets/WebPageBuilder4/MinWidthSetting.vue +0 -101
  170. package/src/widgets/WebPageBuilder4/MultiValueSetting.vue +0 -163
  171. package/src/widgets/WebPageBuilder4/OpacitySetting.vue +0 -77
  172. package/src/widgets/WebPageBuilder4/OverflowSetting.vue +0 -81
  173. package/src/widgets/WebPageBuilder4/PaddingSetting.vue +0 -57
  174. package/src/widgets/WebPageBuilder4/PositionSetting.vue +0 -191
  175. package/src/widgets/WebPageBuilder4/PropertySetting.vue +0 -176
  176. package/src/widgets/WebPageBuilder4/TextSetting.vue +0 -72
  177. package/src/widgets/WebPageBuilder4/TreeView.vue +0 -46
  178. package/src/widgets/WebPageBuilder4/TreeViewItem.vue +0 -303
  179. package/src/widgets/WebPageBuilder4/WebPageComponentSelector.vue +0 -141
  180. package/src/widgets/WebPageBuilder4/WebPagePropertySelector.vue +0 -168
  181. package/src/widgets/WebPageBuilder4/WidthSetting.vue +0 -105
  182. package/src/widgets/WebPageBuilder4/ZIndexSetting.vue +0 -72
  183. package/src/widgets/WebPagePreview.vue +0 -295
  184. package/src/widgets/WebPageSelector.vue +0 -173
  185. package/src/widgets/WebTemplateCreator.vue +0 -156
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mixd-id/web-scaffold",
3
3
  "private": false,
4
- "version": "0.2.250801013",
4
+ "version": "0.2.250801014",
5
5
  "scripts": {
6
6
  "dev": "vite serve",
7
7
  "build": "vite build",
@@ -14,85 +14,61 @@
14
14
  "./components/*": "./src/components/*",
15
15
  "./widgets/*": "./src/widgets/*",
16
16
  "./mixin/component": "./src/mixin/component.js",
17
- "./mixin/edit-mode": "./src/mixin/edit-mode.js",
18
- "./middleware/http/trim-string": "./src/middleware/http/trim-string.js",
19
17
  "./helpers": {
20
18
  "require": "./src/utils/helpers.js",
21
19
  "import": "./src/utils/helpers.mjs"
22
20
  },
23
21
  "./helpers.js": "./src/utils/helpers.js",
24
22
  "./helpers.mjs": "./src/utils/helpers.mjs",
25
- "./queue": "./src/utils/queue.js",
26
23
  "./wss": {
27
24
  "require": "./src/utils/wss.js",
28
25
  "import": "./src/utils/wss.mjs"
29
26
  },
30
27
  "./wss.js": "./src/utils/wss.js",
31
28
  "./wss.mjs": "./src/utils/wss.mjs",
32
- "./importer": "./src/utils/importer.js",
33
- "./listpage1": "./src/utils/listpage1.js",
34
29
  "./dashboard": "./src/utils/dashboard.js",
35
- "./listview": "./src/utils/listview.js",
30
+ "./ai-dashboard": "./src/utils/ai-dashboard.js",
31
+ "./ai-dashboard-chat": "./src/utils/ai-dashboard-chat.mjs",
32
+ "./ai-chat": "./src/utils/ai-chat.js",
33
+ "./ai-chat-answer": "./src/utils/ai-chat-answer.mjs",
34
+ "./ai-list-chat": "./src/utils/ai-list-chat.mjs",
36
35
  "./preset-selector": {
37
- "require": "./src/utils/preset-selector.cjs",
38
36
  "import": "./src/utils/preset-selector.mjs"
39
37
  },
40
38
  "./hooks/*": "./src/hooks/*",
41
- "./preset-selector.cjs": "./src/utils/preset-selector.cjs",
42
- "./preset-selector.js": "./src/utils/preset-selector.js",
43
39
  "./preset-selector.mjs": "./src/utils/preset-selector.mjs",
44
40
  "./list": {
45
41
  "import": "./src/utils/list.mjs"
46
42
  },
47
43
  "./list.mjs": "./src/utils/list.mjs",
48
- "./web": {
49
- "require": "./src/utils/web.js",
50
- "import": "./src/utils/web.mjs"
51
- },
52
- "./docs/components/404": "./docs/components/404.md",
53
44
  "./docs/components/AccountIcon": "./docs/components/AccountIcon.md",
54
45
  "./docs/components/Ahref": "./docs/components/Ahref.md",
55
46
  "./docs/components/Alert": "./docs/components/Alert.md",
56
- "./docs/components/ArrayList": "./docs/components/ArrayList.md",
57
47
  "./docs/components/Article": "./docs/components/Article.md",
58
48
  "./docs/components/Block": "./docs/components/Block.md",
59
- "./docs/components/Box": "./docs/components/Box.md",
60
49
  "./docs/components/Button": "./docs/components/Button.md",
61
50
  "./docs/components/Card": "./docs/components/Card.md",
62
51
  "./docs/components/Carousel": "./docs/components/Carousel.md",
63
52
  "./docs/components/Cart": "./docs/components/Cart.md",
64
53
  "./docs/components/CartIcon": "./docs/components/CartIcon.md",
65
54
  "./docs/components/Chart": "./docs/components/Chart.md",
66
- "./docs/components/ChartBar": "./docs/components/ChartBar.md",
67
55
  "./docs/components/ChatTyping": "./docs/components/ChatTyping.md",
68
56
  "./docs/components/Checkbox": "./docs/components/Checkbox.md",
69
- "./docs/components/Checkout": "./docs/components/Checkout.md",
70
- "./docs/components/CheckoutDelivery": "./docs/components/CheckoutDelivery.md",
71
57
  "./docs/components/CodeEditor": "./docs/components/CodeEditor.md",
72
58
  "./docs/components/CollapsiblePanel": "./docs/components/CollapsiblePanel.md",
73
59
  "./docs/components/ColorPicker": "./docs/components/ColorPicker.md",
74
- "./docs/components/ColorPicker2": "./docs/components/ColorPicker2.md",
75
- "./docs/components/ColorPicker3": "./docs/components/ColorPicker3.md",
76
60
  "./docs/components/Confirm": "./docs/components/Confirm.md",
77
61
  "./docs/components/ContextMenu": "./docs/components/ContextMenu.md",
78
62
  "./docs/components/ContextMenuItem": "./docs/components/ContextMenuItem.md",
79
63
  "./docs/components/CopyToClipboard": "./docs/components/CopyToClipboard.md",
80
64
  "./docs/components/Countdown": "./docs/components/Countdown.md",
81
- "./docs/components/DataList": "./docs/components/DataList.md",
82
65
  "./docs/components/Datepicker": "./docs/components/Datepicker.md",
83
66
  "./docs/components/Datetimepicker": "./docs/components/Datetimepicker.md",
84
- "./docs/components/DayTimeRange": "./docs/components/DayTimeRange.md",
85
67
  "./docs/components/Dropdown": "./docs/components/Dropdown.md",
86
- "./docs/components/ErrorText": "./docs/components/ErrorText.md",
87
- "./docs/components/FBLogin": "./docs/components/FBLogin.md",
88
68
  "./docs/components/Flex": "./docs/components/Flex.md",
89
- "./docs/components/GHeatMaps": "./docs/components/GHeatMaps.md",
90
69
  "./docs/components/Gmaps": "./docs/components/Gmaps.md",
91
- "./docs/components/GmapsDirection": "./docs/components/GmapsDirection.md",
92
70
  "./docs/components/Grid": "./docs/components/Grid.md",
93
- "./docs/components/GSignIn": "./docs/components/GSignIn.md",
94
71
  "./docs/components/HTMLEditor": "./docs/components/HTMLEditor.md",
95
- "./docs/components/IconMenu": "./docs/components/IconMenu.md",
96
72
  "./docs/components/Image": "./docs/components/Image.md",
97
73
  "./docs/components/Link": "./docs/components/Link.md",
98
74
  "./docs/components/MultiDropdown": "./docs/components/MultiDropdown.md",
@@ -125,6 +101,7 @@
125
101
  "exceljs": "^4.3.0",
126
102
  "file-type": "^18.2.1",
127
103
  "glob": "^8.0.3",
104
+ "highlight.js": "^11.11.1",
128
105
  "lodash": "^4.17.21",
129
106
  "lodash-es": "^4.17.21",
130
107
  "marked": "^16.4.1",
@@ -0,0 +1,479 @@
1
+ <template>
2
+ <div class="flex h-full w-full overflow-hidden">
3
+
4
+ <!-- Mobile drawer backdrop -->
5
+ <div v-if="showSidebar && !isDesktop" class="fixed inset-0 z-20 bg-black/40 md:hidden" @click="showSidebar = false" />
6
+
7
+ <!-- Left panel: sessions -->
8
+ <div class="relative shrink-0 flex flex-col bg-base-300 border-r-[1px] border-border-50
9
+ fixed inset-y-0 left-0 z-30 max-md:w-[280px] transition-transform
10
+ md:static md:z-auto md:transition-none"
11
+ :class="(showSidebar || isDesktop) ? 'translate-x-0' : '-translate-x-full'"
12
+ :style="isDesktop ? { width: leftWidth + 'px' } : null">
13
+
14
+ <div class="p-3 pb-0">
15
+ <button type="button"
16
+ class="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg bg-base-500 hover:bg-base-200 transition-colors"
17
+ @click="newSession">
18
+ <svg width="13" height="13" class="fill-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32V224H48c-17.7 0-32 14.3-32 32s14.3 32 32 32H192V432c0 17.7 14.3 32 32 32s32-14.3 32-32V288H400c17.7 0 32-14.3 32-32s-14.3-32-32-32H256V80z"/></svg>
19
+ New Chat
20
+ </button>
21
+ </div>
22
+
23
+ <div class="flex-1 overflow-y-auto p-3 flex flex-col gap-1">
24
+ <div v-for="s in sessions" :key="s.uid"
25
+ class="group flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer transition-colors"
26
+ :class="s.uid === activeUid ? 'bg-base-500' : 'hover:bg-base-400'"
27
+ @click="selectSession(s.uid)">
28
+ <span class="flex-1 truncate">{{ s.name || 'New Chat' }}</span>
29
+ <button type="button"
30
+ class="shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
31
+ @click.stop="removeSession(s.uid)">
32
+ <svg width="11" height="11" class="fill-text-300 hover:fill-red-600" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z"/></svg>
33
+ </button>
34
+ </div>
35
+ </div>
36
+
37
+ <!-- Resize handle (desktop only) -->
38
+ <div v-if="isDesktop" :class="$style.resize1" @mousedown="(e) => $util.dragResize(e, resizeLeft)" />
39
+ </div>
40
+
41
+ <!-- Main: chat -->
42
+ <div class="flex-1 flex flex-col min-w-0 bg-base-400">
43
+
44
+ <!-- Header -->
45
+ <div class="flex items-center gap-3 px-4 md:px-6 py-3 border-b-[1px] border-border-50 ">
46
+ <button type="button" class="md:hidden p-1" @click="showSidebar = true">
47
+ <svg width="18" height="18" class="fill-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M0 96C0 78.3 14.3 64 32 64H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32C14.3 128 0 113.7 0 96zM0 256c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 416c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"/></svg>
48
+ </button>
49
+ <h3 class="flex-1 truncate md:text-lg">{{ activeSession?.name || 'New Chat' }}</h3>
50
+ </div>
51
+
52
+ <!-- Messages (flex-col-reverse keeps view pinned to the latest message) -->
53
+ <div ref="scroller" class="flex-1 overflow-y-auto flex flex-col-reverse">
54
+ <div class="w-full max-w-[780px] mx-auto px-4 md:px-6 py-6 flex flex-col gap-6">
55
+
56
+ <!-- Empty state with sample prompts -->
57
+ <div v-if="!messages.length" class="flex-1 flex flex-col items-center justify-center text-center py-16 gap-5">
58
+ <svg width="40" height="40" class="fill-text-300" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 448c141.4 0 256-93.1 256-208S397.4 32 256 32S0 125.1 0 240c0 45.1 17.7 86.8 47.7 120.9c-1.9 24.5-11.4 46.3-21.4 62.9c-5.5 9.2-11.1 16.6-15.2 21.6c-2.1 2.5-3.7 4.4-4.9 5.7c-.6 .6-1 1.1-1.3 1.4l-.3 .3c-4.6 4.6-5.9 11.4-3.4 17.4c2.5 6 8.3 9.9 14.8 9.9c28.7 0 57.6-8.9 81.6-19.3c22.9-10 42.4-21.9 54.3-30.6c31.8 11.5 67 17.9 104.1 17.9z"/></svg>
59
+ <p class="text-text-300 text-lg">How can I help you today?</p>
60
+ <div class="flex flex-wrap gap-2 justify-center max-w-md">
61
+ <button v-for="(p, n) in samplePrompts" :key="n" type="button"
62
+ class="px-4 py-2 rounded-full bg-base-300 hover:bg-base-500 transition-colors text-sm"
63
+ @click="send(p)">
64
+ {{ p }}
65
+ </button>
66
+ </div>
67
+ </div>
68
+
69
+ <!-- Message list -->
70
+ <AIChatItem v-for="(message, i) in messages" :key="i" :message="message" />
71
+
72
+ </div>
73
+ </div>
74
+
75
+ <!-- Composer -->
76
+ <div class="w-full max-w-[780px] mx-auto px-4 md:px-6 pb-4 md:pb-6 pt-2">
77
+ <AIChatTextbox :sending="sending" @send="send" @cancel="cancel" />
78
+ <p class="text-center text-[11px] text-text-300 mt-2 hidden md:block">Press Enter to send · Shift+Enter for newline</p>
79
+ </div>
80
+
81
+ </div>
82
+ </div>
83
+ </template>
84
+
85
+ <script setup>
86
+ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
87
+ import AIChatItem from './AIChatItem.vue'
88
+ import AIChatTextbox from './AIChatTextbox.vue'
89
+
90
+ // AIChat mirrors the UserConfig model (UserConfig hasMany UserPreset):
91
+ // config.params — UI state (leftWidth, sessionUid)
92
+ // config.presets[] — chat sessions: { uid, name, params }
93
+ // config.presets[].params — { messages: [{ direction, body, attachments?, model?, ellapsed? }] }
94
+ //
95
+ // It runs in two modes (same convention as AIDashboard):
96
+ // • Offline — no `socket`/`controller` (or `server-chat` off): assistant
97
+ // replies are canned samples that "stream" in locally to show off each
98
+ // supported message type. Demo sessions are seeded on first run.
99
+ // • Connected — `socket` + `controller` given: mirrors the server AIChat
100
+ // mixin. `${controller}.ai-chat` receives { messages } and returns
101
+ // { body, attachments?, model? }; `${controller}.save` persists the whole
102
+ // config (debounced, when `auto-save` is on) and `${controller}.remove-preset`
103
+ // drops a deleted session. Turn `auto-save` off when the host page owns
104
+ // persistence (e.g. via useConfig) to avoid a double write.
105
+ const { controller, socket, config, autoSave, serverChat, samplePrompts } = defineProps({
106
+ controller: { type: String, default: '' },
107
+ socket: { type: Object, default: null },
108
+ config: { type: Object, default: () => ({ params: {} }) },
109
+ autoSave: { type: Boolean, default: true },
110
+ serverChat: { type: Boolean, default: true },
111
+ samplePrompts: {
112
+ type: Array,
113
+ default: () => [ 'Show me markdown', 'Write some code', 'Send me an image', 'Share a file' ],
114
+ },
115
+ })
116
+
117
+ const connected = computed(() => !!(socket && controller))
118
+ const useServerChat = computed(() => connected.value && serverChat)
119
+
120
+ /* ---------- Persisted state (UserConfig shape) ---------- */
121
+ if (!config.params) config.params = {}
122
+ if (config.params.leftWidth == null) config.params.leftWidth = 260
123
+ if (!Array.isArray(config.presets)) config.presets = []
124
+
125
+ const leftWidth = computed(() => config.params.leftWidth)
126
+
127
+ function resizeLeft(w) {
128
+ const next = config.params.leftWidth + w
129
+ if (next >= 200 && next <= 480) config.params.leftWidth = next
130
+ }
131
+
132
+ /* ---------- Responsive ---------- */
133
+ const isDesktop = ref(true)
134
+ const showSidebar = ref(false)
135
+ let mq = null
136
+ function onMq(e) { isDesktop.value = e.matches }
137
+
138
+ /* ---------- Sessions (UserPreset shape) ---------- */
139
+ let seq = 0
140
+ function uid(prefix = 's') {
141
+ return `${prefix}-${Date.now().toString(36)}-${seq++}`
142
+ }
143
+
144
+ function newSessionObj(name = '') {
145
+ return { uid: uid(), name, params: { messages: [] } }
146
+ }
147
+
148
+ function normalizeSession(session) {
149
+ if (!session.uid) session.uid = uid()
150
+ if (!session.params || typeof session.params !== 'object') session.params = {}
151
+ if (!Array.isArray(session.params.messages)) session.params.messages = []
152
+ // a reload can leave a message stuck mid-stream — freeze it
153
+ for (const m of session.params.messages) {
154
+ if (m.isLoading) {
155
+ m.isLoading = false
156
+ m.body = m.display || m.body || 'Cancelled.'
157
+ m.display = m.body
158
+ }
159
+ }
160
+ return session
161
+ }
162
+
163
+ config.presets.forEach(normalizeSession)
164
+
165
+ const sessions = computed(() => config.presets)
166
+ const activeUid = computed(() => config.params.sessionUid)
167
+ const activeSession = computed(() => config.presets.find(s => s.uid === activeUid.value) ?? null)
168
+ const messages = computed(() => activeSession.value?.params.messages ?? [])
169
+
170
+ function selectSession(sessionUid) {
171
+ config.params.sessionUid = sessionUid
172
+ showSidebar.value = false
173
+ }
174
+
175
+ function newSession() {
176
+ // Reuse an existing empty session rather than piling up blanks.
177
+ const blank = config.presets.find(s => !s.params.messages.length)
178
+ const target = blank ?? newSessionObj()
179
+ if (!blank) config.presets.unshift(target)
180
+ selectSession(target.uid)
181
+ persist()
182
+ }
183
+
184
+ function removeSession(sessionUid) {
185
+ const i = config.presets.findIndex(s => s.uid === sessionUid)
186
+ if (i === -1) return
187
+ config.presets.splice(i, 1)
188
+ if (!config.presets.length) config.presets.push(newSessionObj())
189
+ if (activeUid.value === sessionUid) config.params.sessionUid = config.presets[0].uid
190
+ if (connected.value) socket.send(`${controller}.remove-preset`, { uid: sessionUid }).catch(() => {})
191
+ persist()
192
+ }
193
+
194
+ /* ---------- Server: persist ---------- */
195
+ // Strip ephemeral streaming fields; only the finished transcript is saved.
196
+ function cleanMessage(m) {
197
+ const { display, isLoading, streamDone, pendingAttachments, ...rest } = m
198
+ return { ...rest, body: m.body ?? m.display ?? '' }
199
+ }
200
+
201
+ function cleanConfig() {
202
+ return {
203
+ key: config.key,
204
+ name: config.name,
205
+ params: config.params,
206
+ presets: config.presets.map(s => ({
207
+ uid: s.uid,
208
+ name: s.name,
209
+ params: { ...s.params, messages: (s.params.messages || []).map(cleanMessage) },
210
+ })),
211
+ }
212
+ }
213
+
214
+ let saveTimer = null
215
+ function persist() {
216
+ if (!connected.value || !autoSave) return
217
+ clearTimeout(saveTimer)
218
+ saveTimer = setTimeout(() => {
219
+ socket.send(`${controller}.save`, { config: cleanConfig() }).catch(() => {})
220
+ }, 700)
221
+ }
222
+
223
+ /* ---------- Sample assistant replies (offline mode) ---------- */
224
+ const SAMPLES = {
225
+ markdown: {
226
+ body: `Sure! Here's a taste of **markdown** rendering:
227
+
228
+ ## Headings & emphasis
229
+ You can use *italics*, **bold**, \`inline code\`, and [links](https://example.com).
230
+
231
+ ### Lists
232
+ - First item
233
+ - Second item
234
+ - Nested item
235
+ 1. Ordered one
236
+ 2. Ordered two
237
+
238
+ ### Table
239
+ | Feature | Supported |
240
+ | ------- | --------- |
241
+ | Tables | ✅ |
242
+ | Code | ✅ |
243
+ | Images | ✅ |
244
+
245
+ > Blockquotes work too — handy for callouts.`,
246
+ },
247
+ code: {
248
+ body: `Here's a code block with syntax highlighting:
249
+
250
+ \`\`\`js
251
+ // Debounce a function
252
+ function debounce(fn, wait = 300) {
253
+ let t
254
+ return (...args) => {
255
+ clearTimeout(t)
256
+ t = setTimeout(() => fn(...args), wait)
257
+ }
258
+ }
259
+ \`\`\`
260
+
261
+ And some \`inline code\` mixed into a sentence.`,
262
+ },
263
+ image: {
264
+ body: `Of course — here's a sample image attachment:`,
265
+ attachments: [
266
+ { type: 'image', url: 'https://picsum.photos/seed/aichat/640/400' },
267
+ ],
268
+ },
269
+ images: {
270
+ body: `I can also return multiple images in a grid:`,
271
+ attachments: [
272
+ { type: 'image', url: 'https://picsum.photos/seed/one/320/320' },
273
+ { type: 'image', url: 'https://picsum.photos/seed/two/320/320' },
274
+ ],
275
+ },
276
+ file: {
277
+ body: `Here's a downloadable file attachment:`,
278
+ attachments: [
279
+ { type: 'file', url: 'https://example.com/files/quarterly-report.pdf' },
280
+ { type: 'file', url: 'https://example.com/files/data-export.xlsx' },
281
+ ],
282
+ },
283
+ text: {
284
+ body: `I'm a client-only demo, so this reply is canned — but the text still streams in with a smooth typing animation, just like a real model response.`,
285
+ },
286
+ }
287
+
288
+ // Pick a sample reply based on simple keyword matching.
289
+ function pickReply(text) {
290
+ const t = text.toLowerCase()
291
+ if (t.includes('image') && (t.includes('multiple') || t.includes('grid'))) return SAMPLES.images
292
+ if (t.includes('image') || t.includes('photo') || t.includes('picture')) return SAMPLES.image
293
+ if (t.includes('file') || t.includes('download') || t.includes('attach')) return SAMPLES.file
294
+ if (t.includes('code') || t.includes('function') || t.includes('snippet')) return SAMPLES.code
295
+ if (t.includes('markdown') || t.includes('table') || t.includes('format')) return SAMPLES.markdown
296
+ return SAMPLES.text
297
+ }
298
+
299
+ // Build a finished assistant message from a sample (used for seeded sessions).
300
+ function botMsg(sample) {
301
+ return {
302
+ direction: 2,
303
+ body: sample.body,
304
+ display: sample.body,
305
+ attachments: sample.attachments ?? null,
306
+ model: 'demo-client',
307
+ ellapsed: 1240,
308
+ }
309
+ }
310
+
311
+ // Offline first run: seed demo sessions. Connected first run: one blank session.
312
+ function seed() {
313
+ if (!config.presets.length) {
314
+ if (useServerChat.value) {
315
+ config.presets.push(newSessionObj())
316
+ } else {
317
+ config.presets.push(
318
+ newSessionObj(),
319
+ { uid: uid(), name: 'Markdown & tables', params: { messages: [{ direction: 1, body: 'Show me markdown' }, botMsg(SAMPLES.markdown)] } },
320
+ { uid: uid(), name: 'Code snippet', params: { messages: [{ direction: 1, body: 'Write some code' }, botMsg(SAMPLES.code)] } },
321
+ { uid: uid(), name: 'Image attachment', params: { messages: [{ direction: 1, body: 'Send me an image' }, botMsg(SAMPLES.image)] } },
322
+ )
323
+ }
324
+ }
325
+ if (!config.presets.find(s => s.uid === config.params.sessionUid)) {
326
+ config.params.sessionUid = config.presets[0].uid
327
+ }
328
+ }
329
+
330
+ /* ---------- Streaming text animation ----------
331
+ The assistant message keeps a full `body` and an animated `display` that
332
+ catches up a few characters per frame — the smooth claude.ai typing reveal. */
333
+ const sending = ref(false)
334
+ let raf = null
335
+
336
+ function tick() {
337
+ let active = false
338
+ for (const s of config.presets) {
339
+ for (const m of s.params.messages) {
340
+ if (m.display === undefined) continue
341
+ const target = m.body ?? ''
342
+ if (m.display.length < target.length) {
343
+ const remaining = target.length - m.display.length
344
+ const step = Math.max(2, Math.ceil(remaining / 10))
345
+ m.display = target.slice(0, m.display.length + step)
346
+ active = true
347
+ } else if (m.streamDone && m.isLoading) {
348
+ // Fully revealed — reveal attachments and finalize.
349
+ m.isLoading = false
350
+ m.attachments = m.pendingAttachments ?? m.attachments ?? null
351
+ sending.value = false
352
+ persist()
353
+ }
354
+ }
355
+ }
356
+ raf = active ? requestAnimationFrame(tick) : null
357
+ }
358
+
359
+ function animate() {
360
+ if (!raf) raf = requestAnimationFrame(tick)
361
+ }
362
+
363
+ /* ---------- Send / cancel ---------- */
364
+ let streamTimer = null
365
+
366
+ // The transcript sent to `${controller}.ai-chat` — finished messages only.
367
+ function chatPayload(session) {
368
+ return {
369
+ messages: session.params.messages
370
+ .filter(m => !m.isLoading)
371
+ .map(m => ({ direction: m.direction, body: m.body ?? '' })),
372
+ }
373
+ }
374
+
375
+ async function serverSend(session, assistant) {
376
+ const started = Date.now()
377
+ try {
378
+ const res = await socket.send(`${controller}.ai-chat`, chatPayload(session))
379
+ if (!assistant.isLoading) return // cancelled while waiting
380
+ assistant.body = res?.body ?? res?.reply ?? ''
381
+ assistant.pendingAttachments = res?.attachments ?? null
382
+ if (res?.model) assistant.model = res.model
383
+ } catch (e) {
384
+ if (!assistant.isLoading) return
385
+ assistant.body = 'Sorry, something went wrong — please try again.'
386
+ }
387
+ assistant.ellapsed = Date.now() - started
388
+ assistant.streamDone = true
389
+ animate()
390
+ }
391
+
392
+ function offlineSend(session, assistant, text) {
393
+ const reply = pickReply(text)
394
+ const started = Date.now()
395
+ let i = 0
396
+ const full = reply.body
397
+ assistant.pendingAttachments = reply.attachments ?? null
398
+ // Simulate network latency + token streaming.
399
+ streamTimer = setInterval(() => {
400
+ const step = Math.max(3, Math.ceil((full.length - i) / 30))
401
+ i = Math.min(full.length, i + step)
402
+ assistant.body = full.slice(0, i)
403
+ animate()
404
+ if (i >= full.length) {
405
+ clearInterval(streamTimer)
406
+ streamTimer = null
407
+ assistant.model = 'demo-client'
408
+ assistant.ellapsed = Date.now() - started
409
+ assistant.streamDone = true
410
+ animate()
411
+ }
412
+ }, 45)
413
+ }
414
+
415
+ function send(content) {
416
+ const text = `${content ?? ''}`.trim()
417
+ if (!text || sending.value) return
418
+ const session = activeSession.value
419
+ if (!session) return
420
+ sending.value = true
421
+
422
+ if (!session.name) session.name = text.slice(0, 40)
423
+ session.params.messages.push({ direction: 1, body: text })
424
+
425
+ const assistant = {
426
+ direction: 2,
427
+ body: '',
428
+ display: '',
429
+ isLoading: true,
430
+ streamDone: false,
431
+ pendingAttachments: null,
432
+ }
433
+ session.params.messages.push(assistant)
434
+
435
+ if (useServerChat.value) serverSend(session, assistant)
436
+ else offlineSend(session, assistant, text)
437
+ }
438
+
439
+ function cancel() {
440
+ if (streamTimer) { clearInterval(streamTimer); streamTimer = null }
441
+ for (const s of config.presets) {
442
+ const target = s.params.messages.find(m => m.isLoading)
443
+ if (target) {
444
+ Object.assign(target, {
445
+ body: target.display || target.body || 'Cancelled.',
446
+ streamDone: true,
447
+ isLoading: false,
448
+ })
449
+ }
450
+ }
451
+ sending.value = false
452
+ persist()
453
+ }
454
+
455
+ /* ---------- Lifecycle ---------- */
456
+ onMounted(() => {
457
+ seed()
458
+ mq = window.matchMedia('(min-width: 768px)')
459
+ isDesktop.value = mq.matches
460
+ mq.addEventListener('change', onMq)
461
+ })
462
+
463
+ // persist UI-state edits (no-op unless connected + autoSave)
464
+ watch(() => config.params, () => persist(), { deep: true })
465
+
466
+ onUnmounted(() => {
467
+ if (streamTimer) clearInterval(streamTimer)
468
+ if (raf) cancelAnimationFrame(raf)
469
+ clearTimeout(saveTimer)
470
+ mq?.removeEventListener('change', onMq)
471
+ })
472
+ </script>
473
+
474
+ <style module>
475
+ .resize1 {
476
+ @apply w-[4px] cursor-ew-resize absolute top-0 right-0 bottom-0;
477
+ @apply border-r-[1px] border-border-50 hover:border-primary;
478
+ }
479
+ </style>