@mixd-id/web-scaffold 0.2.250801012 → 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 (171) hide show
  1. package/docs/components/AIDashboard.md +472 -0
  2. package/docs/components/WebPageBuilder.md +125 -0
  3. package/package.json +8 -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/AccountIcon.vue +19 -0
  9. package/src/components/Ahref.vue +200 -0
  10. package/src/components/Alert.vue +257 -0
  11. package/src/components/Article.vue +387 -0
  12. package/src/components/BarChart.vue +87 -0
  13. package/src/components/Block.vue +31 -0
  14. package/src/components/Button.vue +258 -0
  15. package/src/components/Card.vue +255 -0
  16. package/src/components/Carousel.vue +404 -0
  17. package/src/components/Cart.vue +192 -0
  18. package/src/components/CartIcon.vue +89 -0
  19. package/src/components/Chart.vue +486 -0
  20. package/src/components/ChatTyping.vue +69 -0
  21. package/src/components/Checkbox.vue +200 -0
  22. package/src/components/CodeEditor.vue +158 -0
  23. package/src/components/CollapsiblePanel.vue +70 -0
  24. package/src/components/ColorPicker.vue +653 -0
  25. package/src/components/Confirm.vue +180 -0
  26. package/src/components/ContextMenu.vue +265 -0
  27. package/src/components/ContextMenuItem.vue +53 -0
  28. package/src/components/CopyToClipboard.vue +34 -0
  29. package/src/components/Countdown.vue +264 -0
  30. package/src/components/Dashboard.vue +1151 -0
  31. package/src/components/Datepicker.vue +412 -0
  32. package/src/components/Datetimepicker.vue +355 -0
  33. package/src/components/DoughnutChart.vue +68 -0
  34. package/src/components/Dropdown.vue +314 -0
  35. package/src/components/FilterCard.vue +177 -0
  36. package/src/components/Flex.vue +44 -0
  37. package/src/components/Gmaps.vue +251 -0
  38. package/src/components/Grid.vue +35 -0
  39. package/src/components/HTMLEditor.vue +786 -0
  40. package/src/components/IconMenu2.vue +89 -0
  41. package/src/components/Image.vue +311 -0
  42. package/src/components/ImagePreview.vue +93 -0
  43. package/src/components/ImageUploader.vue +111 -0
  44. package/src/components/LineChart.vue +104 -0
  45. package/src/components/Link.vue +95 -0
  46. package/src/components/List.vue +1549 -0
  47. package/src/components/ListItem.vue +160 -0
  48. package/src/components/MarkdownEdit.vue +346 -0
  49. package/src/components/MarkdownPreview.vue +102 -0
  50. package/src/components/MenuItem1.vue +36 -0
  51. package/src/components/Metric.vue +105 -0
  52. package/src/components/MultiDropdown.vue +124 -0
  53. package/src/components/OTPField.vue +139 -0
  54. package/src/components/Paragraph.vue +40 -0
  55. package/src/components/PieChart.vue +67 -0
  56. package/src/components/PresetSelectorFilterItem.vue +204 -0
  57. package/src/components/Radio.vue +109 -0
  58. package/src/components/SearchModal.vue +153 -0
  59. package/src/components/Slider.vue +407 -0
  60. package/src/components/Svg.vue +33 -0
  61. package/src/components/SvgEditor.vue +173 -0
  62. package/src/components/Switch.vue +101 -0
  63. package/src/components/Table.vue +56 -0
  64. package/src/components/Tabs.vue +131 -0
  65. package/src/components/Testimonial.vue +86 -0
  66. package/src/components/Text.vue +34 -0
  67. package/src/components/TextBlock.vue +50 -0
  68. package/src/components/TextWithTag.vue +298 -0
  69. package/src/components/Textarea.vue +195 -0
  70. package/src/components/Textbox.vue +216 -0
  71. package/src/components/Thumbnail.vue +27 -0
  72. package/src/components/Timepicker.vue +223 -0
  73. package/src/components/Toast.vue +117 -0
  74. package/src/components/TreeMenu.vue +122 -0
  75. package/src/components/TreeView.vue +107 -0
  76. package/src/components/TreeViewItem.vue +452 -0
  77. package/src/components/Uploader.vue +45 -0
  78. package/src/components/Video.vue +119 -0
  79. package/src/components/VirtualGrid.vue +296 -0
  80. package/src/components/VirtualTable.vue +1047 -0
  81. package/src/components/YoutubeVideo.vue +196 -0
  82. package/src/configs/web-page-builder.js +118 -0
  83. package/src/directives/intersect.js +26 -0
  84. package/src/hooks/device.js +14 -0
  85. package/src/index.css +3 -0
  86. package/src/index.js +635 -0
  87. package/src/instructions/dashboard.js +203 -0
  88. package/src/mixin/component.js +306 -0
  89. package/src/themes/default/index.js +303 -0
  90. package/src/utils/ai-chat-answer.mjs +240 -0
  91. package/src/utils/ai-chat.js +167 -0
  92. package/src/utils/ai-dashboard-chat.mjs +371 -0
  93. package/src/utils/ai-dashboard.js +321 -0
  94. package/src/utils/ai-list-chat.mjs +407 -0
  95. package/src/utils/dashboard.js +317 -0
  96. package/src/utils/helpers.js +635 -0
  97. package/src/utils/helpers.mjs +722 -0
  98. package/src/utils/list.mjs +1564 -0
  99. package/src/utils/preset-selector.mjs +390 -0
  100. package/src/utils/selection.js +43 -0
  101. package/src/utils/wss.js +311 -0
  102. package/src/utils/wss.mjs +286 -0
  103. package/src/widgets/AhrefSetting.vue +97 -0
  104. package/src/widgets/ArticleSetting.vue +457 -0
  105. package/src/widgets/BackgroundColorSetting.vue +149 -0
  106. package/src/widgets/BlockSetting.vue +49 -0
  107. package/src/widgets/BorderColorSetting.vue +53 -0
  108. package/src/widgets/ButtonSetting.vue +113 -0
  109. package/src/widgets/CarouselSetting.vue +181 -0
  110. package/src/widgets/CartSetting.vue +46 -0
  111. package/src/widgets/CollapsiblePanelSetting.vue +46 -0
  112. package/src/widgets/ColumnSelector.vue +93 -0
  113. package/src/widgets/ComponentSetting.vue +2220 -0
  114. package/src/widgets/ComponentSetting2.vue +927 -0
  115. package/src/widgets/ContactForm.vue +193 -0
  116. package/src/widgets/ContactFormSetting.vue +395 -0
  117. package/src/widgets/CountdownSetting.vue +76 -0
  118. package/src/widgets/DataListSetting.vue +102 -0
  119. package/src/widgets/EmbeddedVideo.vue +60 -0
  120. package/src/widgets/EmbeddedVideoSetting.vue +50 -0
  121. package/src/widgets/FAQ.vue +98 -0
  122. package/src/widgets/FAQSetting.vue +94 -0
  123. package/src/widgets/FeatureList.vue +131 -0
  124. package/src/widgets/FeatureListSetting.vue +233 -0
  125. package/src/widgets/FlexSetting.vue +174 -0
  126. package/src/widgets/GridSetting.vue +165 -0
  127. package/src/widgets/Header2.vue +65 -0
  128. package/src/widgets/Header2Setting.vue +193 -0
  129. package/src/widgets/IconList.vue +51 -0
  130. package/src/widgets/IconListSetting.vue +188 -0
  131. package/src/widgets/ImageSetting.vue +119 -0
  132. package/src/widgets/LLMChat.vue +327 -0
  133. package/src/widgets/LinkSetting.vue +209 -0
  134. package/src/widgets/LinkSettingModal.vue +173 -0
  135. package/src/widgets/MenuItem1Setting.vue +78 -0
  136. package/src/widgets/ModalSetting.vue +94 -0
  137. package/src/widgets/MultiValueSetting.vue +121 -0
  138. package/src/widgets/MultiValueSetting2.vue +147 -0
  139. package/src/widgets/OGSettingModal.vue +103 -0
  140. package/src/widgets/ParagraphSetting.vue +89 -0
  141. package/src/widgets/PositionSetting.vue +209 -0
  142. package/src/widgets/PresetBar.vue +1011 -0
  143. package/src/widgets/PresetBarPivot.vue +330 -0
  144. package/src/widgets/PresetBarPivotColumnEdit.vue +186 -0
  145. package/src/widgets/PresetBarPivotValueEdit.vue +172 -0
  146. package/src/widgets/PresetSelector.vue +713 -0
  147. package/src/widgets/Review.vue +131 -0
  148. package/src/widgets/ReviewSetting.vue +201 -0
  149. package/src/widgets/SearchModalSetting.vue +70 -0
  150. package/src/widgets/Share.vue +164 -0
  151. package/src/widgets/ShareSetting.vue +99 -0
  152. package/src/widgets/SvgSetting.vue +133 -0
  153. package/src/widgets/TableSetting.vue +172 -0
  154. package/src/widgets/TestimonialSetting.vue +161 -0
  155. package/src/widgets/TextBlockSetting.vue +81 -0
  156. package/src/widgets/ThumbnailSetting.vue +49 -0
  157. package/src/widgets/WebComponentSelector.vue +205 -0
  158. package/src/widgets/WebDatasourceSelector.vue +153 -0
  159. package/src/widgets/WebLayoutSelector.vue +139 -0
  160. package/src/widgets/WebPageBuilder.vue +1962 -0
  161. package/src/widgets/YoutubeVideoSetting.vue +68 -0
  162. package/docs/components/ArrayList.md +0 -39
  163. package/docs/components/Box.md +0 -15
  164. package/docs/components/ChartBar.md +0 -34
  165. package/docs/components/DayTimeRange.md +0 -20
  166. package/docs/components/ErrorText.md +0 -15
  167. package/docs/components/FBLogin.md +0 -21
  168. package/docs/components/GHeatMaps.md +0 -16
  169. package/docs/components/GSignIn.md +0 -21
  170. package/docs/components/GmapsDirection.md +0 -20
  171. package/docs/components/IconMenu.md +0 -20
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.250801012",
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",
@@ -154,6 +131,7 @@
154
131
  "author": "mixd.id",
155
132
  "license": "ISC",
156
133
  "files": [
134
+ "src",
157
135
  "docs/components"
158
136
  ],
159
137
  "bugs": {
@@ -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>