@flexem/chat-box 1.0.0

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 (46) hide show
  1. package/README.md +638 -0
  2. package/miniprogram_dist/TEST_CASES.md +256 -0
  3. package/miniprogram_dist/assets/icons/icon-arrow-down.svg +1 -0
  4. package/miniprogram_dist/assets/icons/icon-arrow-up.svg +1 -0
  5. package/miniprogram_dist/assets/icons/icon-avatar-default.svg +1 -0
  6. package/miniprogram_dist/assets/icons/icon-back.svg +1 -0
  7. package/miniprogram_dist/assets/icons/icon-camera.svg +1 -0
  8. package/miniprogram_dist/assets/icons/icon-close.svg +1 -0
  9. package/miniprogram_dist/assets/icons/icon-copy.svg +1 -0
  10. package/miniprogram_dist/assets/icons/icon-delete.svg +1 -0
  11. package/miniprogram_dist/assets/icons/icon-edit-msg.svg +1 -0
  12. package/miniprogram_dist/assets/icons/icon-edit.svg +1 -0
  13. package/miniprogram_dist/assets/icons/icon-file.svg +1 -0
  14. package/miniprogram_dist/assets/icons/icon-image.svg +1 -0
  15. package/miniprogram_dist/assets/icons/icon-keyboard.svg +1 -0
  16. package/miniprogram_dist/assets/icons/icon-menu.svg +1 -0
  17. package/miniprogram_dist/assets/icons/icon-play-voice.svg +1 -0
  18. package/miniprogram_dist/assets/icons/icon-plus.svg +1 -0
  19. package/miniprogram_dist/assets/icons/icon-regenerate.svg +1 -0
  20. package/miniprogram_dist/assets/icons/icon-thinking.svg +1 -0
  21. package/miniprogram_dist/assets/icons/icon-voice.svg +1 -0
  22. package/miniprogram_dist/components/attachment/index.js +169 -0
  23. package/miniprogram_dist/components/attachment/index.json +4 -0
  24. package/miniprogram_dist/components/attachment/index.wxml +40 -0
  25. package/miniprogram_dist/components/attachment/index.wxss +119 -0
  26. package/miniprogram_dist/components/input-bar/index.js +934 -0
  27. package/miniprogram_dist/components/input-bar/index.json +6 -0
  28. package/miniprogram_dist/components/input-bar/index.wxml +132 -0
  29. package/miniprogram_dist/components/input-bar/index.wxss +324 -0
  30. package/miniprogram_dist/components/message/index.js +988 -0
  31. package/miniprogram_dist/components/message/index.json +4 -0
  32. package/miniprogram_dist/components/message/index.wxml +285 -0
  33. package/miniprogram_dist/components/message/index.wxss +575 -0
  34. package/miniprogram_dist/components/sidebar/index.js +506 -0
  35. package/miniprogram_dist/components/sidebar/index.json +4 -0
  36. package/miniprogram_dist/components/sidebar/index.wxml +137 -0
  37. package/miniprogram_dist/components/sidebar/index.wxss +264 -0
  38. package/miniprogram_dist/index.js +1316 -0
  39. package/miniprogram_dist/index.json +8 -0
  40. package/miniprogram_dist/index.wxml +172 -0
  41. package/miniprogram_dist/index.wxss +291 -0
  42. package/miniprogram_dist/package.json +5 -0
  43. package/miniprogram_dist/utils/api.js +474 -0
  44. package/miniprogram_dist/utils/audio.js +860 -0
  45. package/miniprogram_dist/utils/storage.js +168 -0
  46. package/package.json +27 -0
@@ -0,0 +1,8 @@
1
+ {
2
+ "component": true,
3
+ "usingComponents": {
4
+ "sidebar": "./components/sidebar/index",
5
+ "message-item": "./components/message/index",
6
+ "input-bar": "./components/input-bar/index"
7
+ }
8
+ }
@@ -0,0 +1,172 @@
1
+ <view class="chat-box-wrapper">
2
+ <!-- 侧边栏(固定在左侧底层) -->
3
+ <sidebar
4
+ id="sidebar"
5
+ visible="{{showSidebar}}"
6
+ baseUrl="{{serviceUrls.aiChatUrl}}"
7
+ token="{{userToken}}"
8
+ currentSessionId="{{currentSession.id}}"
9
+ username="{{userInfo.name || ''}}"
10
+ userAvatar="{{userInfo.avatar || ''}}"
11
+ bind:select="onSelectSession"
12
+ bind:delete="onDeleteSession"
13
+ bind:close="closeSidebar"
14
+ bind:login="onNeedLogin"
15
+ />
16
+
17
+ <!-- 主聊天容器(会滑动) -->
18
+ <view class="chat-box-container {{showSidebar ? 'sidebar-open' : ''}}">
19
+ <!-- 侧边栏打开时的点击关闭遮罩 -->
20
+ <view wx:if="{{showSidebar}}" class="sidebar-close-overlay" bindtap="closeSidebar"></view>
21
+
22
+ <!-- 标题栏 -->
23
+ <view class="header-bar" style="height: {{navBarHeight}}px; padding-top: {{menuButtonTop}}px;">
24
+ <!-- 左侧胶囊按钮 -->
25
+ <view class="capsule-btn" style="height: {{menuButtonHeight}}px; border-radius: {{menuButtonHeight / 2}}px;">
26
+ <!-- 返回按钮 -->
27
+ <view class="capsule-item" style="height: {{menuButtonHeight}}px;" bindtap="onBack">
28
+ <image class="back-icon" src="/miniprogram_npm/@flexem/chat-box/assets/icons/icon-back.svg" mode="aspectFit" />
29
+ </view>
30
+ <!-- 分割线 -->
31
+ <view class="capsule-divider"></view>
32
+ <!-- 菜单按钮 -->
33
+ <view class="capsule-item" style="height: {{menuButtonHeight}}px;" bindtap="openSidebar">
34
+ <image class="menu-icon" src="/miniprogram_npm/@flexem/chat-box/assets/icons/icon-menu.svg" mode="aspectFit" />
35
+ </view>
36
+ </view>
37
+
38
+ <!-- 中间标题 -->
39
+ <view class="header-center" style="height: {{menuButtonHeight}}px;" bindtap="toggleDropdown">
40
+ <text class="header-title">{{'Chat'}} </text>
41
+ <image
42
+ class="dropdown-arrow"
43
+ src="/miniprogram_npm/@flexem/chat-box/assets/icons/icon-arrow-{{showDropdown ? 'up' : 'down'}}.svg"
44
+ mode="aspectFit"
45
+ />
46
+ </view>
47
+
48
+ <!-- 右侧占位 -->
49
+ <view class="header-right"></view>
50
+
51
+ <!-- 下拉菜单 -->
52
+ <view wx:if="{{showDropdown}}" class="dropdown-menu">
53
+ <view class="dropdown-item" bindtap="onNewChat">
54
+ <text>开始新对话</text>
55
+ </view>
56
+ <view class="dropdown-divider"></view>
57
+ <view class="dropdown-item" bindtap="toggleVoiceAutoPlay">
58
+ <text>语音播放</text>
59
+ <view class="toggle-switch {{settings.voiceAutoPlay ? 'on' : ''}}">
60
+ <view class="toggle-thumb"></view>
61
+ </view>
62
+ </view>
63
+ <!-- 深度思考(暂时隐藏)
64
+ <view class="dropdown-item" bindtap="toggleDeepThinking">
65
+ <text class="dropdown-icon">🧠</text>
66
+ <text>深度思考</text>
67
+ <view class="toggle-switch {{settings.deepThinking ? 'on' : ''}}">
68
+ <view class="toggle-thumb"></view>
69
+ </view>
70
+ </view>
71
+ -->
72
+ <!-- 联网搜索(暂时隐藏)
73
+ <view class="dropdown-item" bindtap="toggleWebSearch">
74
+ <text class="dropdown-icon">🌐</text>
75
+ <text>联网搜索</text>
76
+ <view class="toggle-switch {{settings.webSearch ? 'on' : ''}}">
77
+ <view class="toggle-thumb"></view>
78
+ </view>
79
+ </view>
80
+ -->
81
+ </view>
82
+
83
+ <!-- 下拉菜单遮罩 -->
84
+ <view wx:if="{{showDropdown}}" class="dropdown-mask" bindtap="closeDropdown"></view>
85
+ </view>
86
+
87
+ <!-- 消息列表区域 -->
88
+ <scroll-view
89
+ class="message-list"
90
+ style="margin-top: {{navBarHeight}}px; padding-bottom: {{keyboardHeight > 0 ? (keyboardHeight + inputBarHeight + 40) : (94 + attachmentHeight + inputBarHeight)}}px;"
91
+ scroll-y="true"
92
+ scroll-into-view="{{scrollToMessage}}"
93
+ enhanced="true"
94
+ show-scrollbar="false"
95
+ bindscrolltoupper="onScrollToUpper"
96
+ >
97
+ <!-- 加载更多 -->
98
+ <view wx:if="{{hasMoreHistory}}" class="load-more-history">
99
+ <text bindtap="loadMoreHistory">{{isLoadingHistory ? '加载中...' : '加载更多历史消息'}}</text>
100
+ </view>
101
+
102
+ <!-- 消息列表 -->
103
+ <view class="message-wrapper">
104
+ <block wx:for="{{messages}}" wx:key="id">
105
+ <message-item
106
+ id="msg-{{item.id}}"
107
+ message="{{item}}"
108
+ userAvatar="{{userInfo.avatar}}"
109
+ showUserActions="{{item.role === 'user' && !isStreaming}}"
110
+ isPlaying="{{playingMessageId === item.id}}"
111
+ isSynthesizing="{{synthesizingMessageId === item.id}}"
112
+ isStreaming="{{isStreaming}}"
113
+ bind:playvoice="onPlayVoice"
114
+ bind:regenerate="onRegenerate"
115
+ bind:edit="onEditMessage"
116
+ bind:quickreply="onQuickReply"
117
+ bind:imageload="onImageLoad"
118
+ />
119
+ </block>
120
+
121
+ <!-- 正在生成中的消息 -->
122
+ <message-item
123
+ wx:if="{{streamingMessage}}"
124
+ id="msg-streaming"
125
+ message="{{streamingMessage}}"
126
+ userAvatar="{{userInfo.avatar}}"
127
+ showUserActions="{{false}}"
128
+ isPlaying="{{playingMessageId === streamingMessage.id}}"
129
+ isSynthesizing="{{synthesizingMessageId === streamingMessage.id}}"
130
+ isStreaming="{{true}}"
131
+ />
132
+ </view>
133
+
134
+ <!-- 空状态 -->
135
+ <view wx:if="{{messages.length === 0 && !streamingMessage}}" class="empty-state">
136
+ <view class="empty-icon"></view>
137
+ <text class="empty-text">开始新对话</text>
138
+ <text class="empty-hint">输入问题或按住说话</text>
139
+ </view>
140
+
141
+ <!-- 底部占位 -->
142
+ <view class="scroll-bottom-spacer" id="scroll-bottom"></view>
143
+ </scroll-view>
144
+
145
+ <!-- 底部区域(输入栏 + 免责声明) -->
146
+ <view class="bottom-area {{keyboardHeight > 0 ? 'keyboard-visible' : ''}}" style="bottom: {{keyboardHeight}}px;">
147
+ <!-- 输入栏 -->
148
+ <input-bar
149
+ id="input-bar"
150
+ placeholder="输入消息..."
151
+ voiceToText="{{voiceToText}}"
152
+ speechRecognitionManager="{{speechRecognitionManager}}"
153
+ baseUrl="{{serviceUrls.aiChatUrl}}"
154
+ ossServiceUrl="{{serviceUrls.ossServiceUrl}}"
155
+ blobStorageDownloadServiceUrl="{{serviceUrls.blobStorageDownloadServiceUrl}}"
156
+ token="{{userToken}}"
157
+ defaultValue="{{editingContent}}"
158
+ isGenerating="{{isStreaming}}"
159
+ bind:send="onSendMessage"
160
+ bind:stop="onStopGeneration"
161
+ bind:keyboardheight="onKeyboardHeight"
162
+ bind:attachmentheight="onAttachmentHeight"
163
+ bind:inputheight="onInputHeight"
164
+ />
165
+
166
+ <!-- 免责声明(键盘弹出时隐藏) -->
167
+ <view class="disclaimer" wx:if="{{keyboardHeight === 0}}">
168
+ <text>内容由 AI 生成,请仔细甄别</text>
169
+ </view>
170
+ </view>
171
+ </view>
172
+ </view>
@@ -0,0 +1,291 @@
1
+ /* 外层包裹器 */
2
+ .chat-box-wrapper {
3
+ position: relative;
4
+ width: 100vw;
5
+ height: 100vh;
6
+ overflow: hidden;
7
+ background-color: #F3F3F3;
8
+ }
9
+
10
+ /* 主容器 */
11
+ .chat-box-container {
12
+ display: flex;
13
+ flex-direction: column;
14
+ height: 100vh;
15
+ width: 100vw;
16
+ background-color: #F3F3F3;
17
+ position: relative;
18
+ transition: all 0.3s ease;
19
+ z-index: 10;
20
+ }
21
+
22
+ /* 侧边栏打开时主容器滑到右侧并缩小高度 */
23
+ .chat-box-container.sidebar-open {
24
+ transform: translateX(85vw);
25
+ margin-top: 200rpx;
26
+ margin-bottom: 200rpx;
27
+ height: calc(100vh - 400rpx);
28
+ border-radius: 20rpx;
29
+ overflow: hidden;
30
+ box-shadow: -4rpx 0 20rpx rgba(0, 0, 0, 0.1);
31
+ }
32
+
33
+ /* 侧边栏打开时的点击关闭遮罩 */
34
+ .sidebar-close-overlay {
35
+ position: absolute;
36
+ top: 0;
37
+ left: 0;
38
+ right: 0;
39
+ bottom: 0;
40
+ z-index: 1000;
41
+ background-color: transparent;
42
+ }
43
+
44
+ /* 标题栏 */
45
+ .header-bar {
46
+ display: flex;
47
+ align-items: flex-start;
48
+ justify-content: space-between;
49
+ padding-left: 16rpx;
50
+ padding-right: 30rpx;
51
+ background-color: #F3F3F3;
52
+ position: fixed;
53
+ top: 0;
54
+ left: 0;
55
+ right: 0;
56
+ z-index: 100;
57
+ box-sizing: border-box;
58
+ }
59
+
60
+ .header-right {
61
+ width: 80rpx;
62
+ display: flex;
63
+ align-items: center;
64
+ justify-content: center;
65
+ }
66
+
67
+ /* 胶囊按钮 */
68
+ .capsule-btn {
69
+ display: flex;
70
+ align-items: center;
71
+ background-color: rgba(255, 255, 255, 0.9);
72
+ border: 1rpx solid rgba(0, 0, 0, 0.1);
73
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
74
+ }
75
+
76
+ .capsule-item {
77
+ display: flex;
78
+ align-items: center;
79
+ justify-content: center;
80
+ width: 70rpx;
81
+ }
82
+
83
+ .capsule-item:active {
84
+ background-color: rgba(0, 0, 0, 0.05);
85
+ }
86
+
87
+ .capsule-item:first-child:active {
88
+ border-radius: 50rpx 0 0 50rpx;
89
+ }
90
+
91
+ .capsule-item:last-child:active {
92
+ border-radius: 0 50rpx 50rpx 0;
93
+ }
94
+
95
+ .capsule-divider {
96
+ width: 1rpx;
97
+ height: 32rpx;
98
+ background-color: rgba(0, 0, 0, 0.15);
99
+ }
100
+
101
+ /* 返回图标 */
102
+ .back-icon {
103
+ width: 44rpx;
104
+ height: 44rpx;
105
+ }
106
+
107
+ /* 菜单图标 */
108
+ .menu-icon {
109
+ width: 44rpx;
110
+ height: 44rpx;
111
+ }
112
+
113
+ /* 标题区域 */
114
+ .header-center {
115
+ display: flex;
116
+ align-items: center;
117
+ gap: 8rpx;
118
+ }
119
+
120
+ .header-title {
121
+ font-size: 34rpx;
122
+ font-weight: bold;
123
+ color: #333;
124
+ max-width: 400rpx;
125
+ overflow: hidden;
126
+ text-overflow: ellipsis;
127
+ white-space: nowrap;
128
+ }
129
+
130
+ .dropdown-arrow {
131
+ width: 44rpx;
132
+ height: 44rpx;
133
+ }
134
+
135
+ /* 下拉菜单 */
136
+ .dropdown-menu {
137
+ position: absolute;
138
+ top: 100%;
139
+ left: 50%;
140
+ transform: translateX(-50%);
141
+ background-color: #fff;
142
+ border-radius: 16rpx;
143
+ box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
144
+ min-width: 320rpx;
145
+ overflow: hidden;
146
+ z-index: 200;
147
+ }
148
+
149
+ .dropdown-item {
150
+ display: flex;
151
+ align-items: center;
152
+ padding: 24rpx 30rpx;
153
+ gap: 16rpx;
154
+ }
155
+
156
+ .dropdown-item:active {
157
+ background-color: #f5f5f5;
158
+ }
159
+
160
+ .dropdown-icon {
161
+ font-size: 32rpx;
162
+ }
163
+
164
+ .dropdown-item text:nth-child(2) {
165
+ flex: 1;
166
+ font-size: 30rpx;
167
+ color: #333;
168
+ }
169
+
170
+ .dropdown-divider {
171
+ height: 1rpx;
172
+ background-color: #eee;
173
+ }
174
+
175
+ /* 开关样式 */
176
+ .toggle-switch {
177
+ width: 80rpx;
178
+ height: 44rpx;
179
+ background-color: #ddd;
180
+ border-radius: 22rpx;
181
+ padding: 4rpx;
182
+ transition: background-color 0.2s;
183
+ }
184
+
185
+ .toggle-switch.on {
186
+ background-color: #007aff;
187
+ }
188
+
189
+ .toggle-thumb {
190
+ width: 36rpx;
191
+ height: 36rpx;
192
+ background-color: #fff;
193
+ border-radius: 50%;
194
+ transition: transform 0.2s;
195
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
196
+ }
197
+
198
+ .toggle-switch.on .toggle-thumb {
199
+ transform: translateX(36rpx);
200
+ }
201
+
202
+ /* 下拉菜单遮罩 */
203
+ .dropdown-mask {
204
+ position: fixed;
205
+ top: 0;
206
+ left: 0;
207
+ right: 0;
208
+ bottom: 0;
209
+ z-index: 150;
210
+ }
211
+
212
+ /* 消息列表 */
213
+ .message-list {
214
+ flex: 1;
215
+ overflow-y: auto;
216
+ }
217
+
218
+ .message-wrapper {
219
+ padding-bottom: 20rpx;
220
+ }
221
+
222
+ /* 加载更多历史 */
223
+ .load-more-history {
224
+ display: flex;
225
+ justify-content: center;
226
+ padding: 30rpx;
227
+ }
228
+
229
+ .load-more-history text {
230
+ font-size: 26rpx;
231
+ color: #999;
232
+ }
233
+
234
+ /* 空状态 */
235
+ .empty-state {
236
+ display: flex;
237
+ flex-direction: column;
238
+ align-items: center;
239
+ justify-content: center;
240
+ padding: 100rpx 0;
241
+ }
242
+
243
+ .empty-icon {
244
+ font-size: 100rpx;
245
+ margin-bottom: 30rpx;
246
+ }
247
+
248
+ .empty-text {
249
+ font-size: 34rpx;
250
+ color: #333;
251
+ margin-bottom: 16rpx;
252
+ }
253
+
254
+ .empty-hint {
255
+ font-size: 28rpx;
256
+ color: #999;
257
+ }
258
+
259
+ /* 底部占位 */
260
+ .scroll-bottom-spacer {
261
+ height: 20rpx;
262
+ }
263
+
264
+ /* 底部区域 */
265
+ .bottom-area {
266
+ position: fixed;
267
+ left: 0;
268
+ right: 0;
269
+ bottom: 0;
270
+ background-color: #F3F3F3;
271
+ z-index: 99;
272
+ }
273
+
274
+ /* 键盘弹出时,不需要免责声明的空间,输入栏紧贴底部 */
275
+ .bottom-area.keyboard-visible {
276
+ padding-bottom: 0;
277
+ }
278
+
279
+ /* 免责声明 */
280
+ .disclaimer {
281
+ display: flex;
282
+ justify-content: center;
283
+ padding: 8rpx 16rpx;
284
+ padding-bottom: calc(8rpx + env(safe-area-inset-bottom));
285
+ background-color: #F3F3F3;
286
+ }
287
+
288
+ .disclaimer text {
289
+ font-size: 22rpx;
290
+ color: #999;
291
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "@flexem/chat-box",
3
+ "version": "1.0.0",
4
+ "main": "index.js"
5
+ }