@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.
- package/docs/components/AIDashboard.md +472 -0
- package/docs/components/WebPageBuilder.md +125 -0
- package/package.json +7 -30
- package/src/components/AIChat.vue +479 -0
- package/src/components/AIChatItem.vue +168 -0
- package/src/components/AIChatTextbox.vue +56 -0
- package/src/components/AIDashboard.vue +1113 -0
- package/src/components/Card.vue +3 -1
- package/src/components/Chart.vue +2 -1
- package/src/components/CodeEditor.vue +121 -13
- package/src/components/ColorPicker.vue +6 -2
- package/src/components/Dashboard.vue +493 -212
- package/src/components/Dropdown.vue +0 -1
- package/src/components/HTMLEditor.vue +9 -1
- package/src/components/List.vue +226 -58
- package/src/components/MarkdownEdit.vue +123 -4
- package/src/components/PresetSelectorFilterItem.vue +1 -0
- package/src/components/TextWithTag.vue +3 -3
- package/src/components/TreeView.vue +1 -1
- package/src/components/VirtualTable.vue +166 -75
- package/src/index.js +3 -55
- package/src/instructions/dashboard.js +203 -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 +321 -0
- package/src/utils/ai-list-chat.mjs +407 -0
- package/src/utils/dashboard.js +13 -0
- package/src/utils/list.mjs +146 -91
- package/src/utils/preset-selector.mjs +183 -27
- package/src/utils/wss.js +0 -2
- package/src/widgets/ColumnSelector.vue +22 -33
- package/src/widgets/LLMChat.vue +327 -0
- package/src/widgets/PresetBar.vue +31 -8
- package/src/widgets/PresetBarPivot.vue +147 -15
- package/src/widgets/PresetBarPivotColumnEdit.vue +8 -19
- package/src/widgets/PresetBarPivotValueEdit.vue +172 -0
- package/src/widgets/PresetSelector.vue +8 -4
- package/src/widgets/WebPageBuilder.vue +153 -352
- 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/Tooltip.vue +0 -143
- 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/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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mixd-id/web-scaffold",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.2.
|
|
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
|
-
"./
|
|
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>
|