@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,4 @@
1
+ {
2
+ "component": true,
3
+ "usingComponents": {}
4
+ }
@@ -0,0 +1,285 @@
1
+ <view class="message-item {{message.role === 'user' ? 'user-message' : 'ai-message'}}">
2
+ <!-- 消息内容区域 -->
3
+ <view class="message-content">
4
+ <!-- 用户消息 -->
5
+ <view wx:if="{{message.role === 'user'}}" class="message-bubble user-bubble">
6
+ <text class="message-text">{{message.content}}</text>
7
+
8
+ <!-- 附件预览 -->
9
+ <view wx:if="{{message.attachments && message.attachments.length > 0}}" class="attachments">
10
+ <block wx:for="{{message.attachments}}" wx:key="url">
11
+ <image
12
+ wx:if="{{item.type === 'image'}}"
13
+ class="attachment-image"
14
+ src="{{item.url}}"
15
+ mode="widthFix"
16
+ bindtap="onPreviewImage"
17
+ bindload="onImageLoad"
18
+ data-url="{{item.url}}"
19
+ />
20
+ <view wx:else class="attachment-file" bindtap="onOpenFile" data-file="{{item}}">
21
+ <text class="file-icon">📄</text>
22
+ <text class="file-name">{{item.name}}</text>
23
+ </view>
24
+ </block>
25
+ </view>
26
+
27
+ <!-- 用户消息元信息 -->
28
+ <view class="message-meta">
29
+ <view class="meta-left">
30
+ <text class="message-time">{{message.timeText}}</text>
31
+ </view>
32
+ <view wx:if="{{showUserActions}}" class="meta-right">
33
+ <!-- 重新生成按钮 -->
34
+ <image class="action-icon-img" src="../../assets/icons/icon-regenerate.svg" mode="aspectFit" bindtap="onRegenerate" />
35
+ <!-- 编辑按钮 -->
36
+ <image class="action-icon-img" src="../../assets/icons/icon-edit-msg.svg" mode="aspectFit" bindtap="onEdit" />
37
+ </view>
38
+ </view>
39
+ </view>
40
+
41
+ <!-- AI 消息 -->
42
+ <view wx:else class="message-bubble ai-bubble {{message.isError ? 'error-bubble' : ''}}">
43
+ <!-- 思考过程(可折叠) -->
44
+ <view wx:if="{{message.thinkingText}}" class="thinking-section">
45
+ <view class="thinking-header" bindtap="toggleThinking">
46
+ <image class="thinking-icon" src="../../assets/icons/icon-thinking.svg" mode="aspectFit" />
47
+ <text class="thinking-title">思考过程</text>
48
+ <text class="thinking-toggle">{{showThinking ? '收起' : '展开'}}</text>
49
+ </view>
50
+ <view wx:if="{{showThinking}}" class="thinking-content">
51
+ <text>{{message.thinkingText}}</text>
52
+ </view>
53
+ </view>
54
+
55
+ <!-- Markdown 内容 -->
56
+ <view class="markdown-content">
57
+ <block wx:for="{{parsedContent}}" wx:key="id">
58
+ <!-- 普通段落 -->
59
+ <view wx:if="{{item.type === 'paragraph'}}" class="md-paragraph">
60
+ <block wx:for="{{item.text}}" wx:for-item="part" wx:key="idx">
61
+ <text wx:if="{{part.type === 'text' || !part.type}}" class="{{part.bold ? 'md-bold' : ''}} {{part.italic ? 'md-italic' : ''}}">{{part.text}}</text>
62
+ <text wx:elif="{{part.type === 'link'}}" class="md-link" bindtap="onOpenLink" data-url="{{part.url}}">{{part.text}}</text>
63
+ <text wx:elif="{{part.type === 'code'}}" class="md-inline-code">{{part.text}}</text>
64
+ <image
65
+ wx:elif="{{part.type === 'image'}}"
66
+ class="md-image md-inline-image"
67
+ src="{{part.src}}"
68
+ mode="widthFix"
69
+ bindtap="onPreviewImage"
70
+ data-url="{{part.src}}"
71
+ />
72
+ </block>
73
+ </view>
74
+
75
+ <!-- 标题 -->
76
+ <view wx:elif="{{item.type === 'heading'}}" class="md-heading md-h{{item.level}}">
77
+ <text>{{item.text}}</text>
78
+ </view>
79
+
80
+ <!-- 代码块 -->
81
+ <view wx:elif="{{item.type === 'code'}}" class="md-code-block">
82
+ <view class="code-header">
83
+ <text class="code-lang">{{item.lang || 'code'}}</text>
84
+ <text class="code-copy" bindtap="onCopyCode" data-code="{{item.text}}">复制</text>
85
+ </view>
86
+ <!-- 使用 view + CSS overflow 替代 scroll-view,避免内容更新时跳动 -->
87
+ <view class="code-content-wrapper">
88
+ <text class="code-text" user-select="{{true}}">{{item.text}}</text>
89
+ </view>
90
+ </view>
91
+
92
+ <!-- 行内代码 -->
93
+ <text wx:elif="{{item.type === 'inline-code'}}" class="md-inline-code">{{item.text}}</text>
94
+
95
+ <!-- 列表(支持嵌套) -->
96
+ <view wx:elif="{{item.type === 'list'}}" class="md-list">
97
+ <view wx:for="{{item.items}}" wx:for-item="listItem" wx:for-index="listIndex" wx:key="listIndex" class="md-list-item-wrapper">
98
+ <!-- 第一级列表项 -->
99
+ <view class="md-list-item" style="padding-left: 0;">
100
+ <text class="list-marker">{{listItem.isOrdered ? (listIndex + 1) + '.' : '•'}}</text>
101
+ <view class="list-text">
102
+ <block wx:for="{{listItem.content}}" wx:for-item="part" wx:key="idx">
103
+ <text wx:if="{{part.type === 'text' || !part.type}}" class="{{part.bold ? 'md-bold' : ''}} {{part.italic ? 'md-italic' : ''}}">{{part.text}}</text>
104
+ <text wx:elif="{{part.type === 'link'}}" class="md-link" bindtap="onOpenLink" data-url="{{part.url}}">{{part.text}}</text>
105
+ <text wx:elif="{{part.type === 'code'}}" class="md-inline-code">{{part.text}}</text>
106
+ <image
107
+ wx:elif="{{part.type === 'image'}}"
108
+ class="md-image md-inline-image"
109
+ src="{{part.src}}"
110
+ mode="widthFix"
111
+ bindtap="onPreviewImage"
112
+ data-url="{{part.src}}"
113
+ />
114
+ </block>
115
+ </view>
116
+ </view>
117
+
118
+ <!-- 第二级嵌套列表 -->
119
+ <block wx:if="{{listItem.children && listItem.children.length > 0}}">
120
+ <view wx:for="{{listItem.children}}" wx:for-item="childList" wx:key="idx" class="md-nested-list">
121
+ <view wx:for="{{childList.items}}" wx:for-item="childItem" wx:for-index="childIndex" wx:key="childIndex" class="md-list-item-wrapper">
122
+ <view class="md-list-item" style="padding-left: 32rpx;">
123
+ <text class="list-marker">{{childItem.isOrdered ? (childIndex + 1) + '.' : '•'}}</text>
124
+ <view class="list-text">
125
+ <block wx:for="{{childItem.content}}" wx:for-item="part" wx:key="partIdx">
126
+ <text wx:if="{{part.type === 'text' || !part.type}}" class="{{part.bold ? 'md-bold' : ''}} {{part.italic ? 'md-italic' : ''}}">{{part.text}}</text>
127
+ <text wx:elif="{{part.type === 'link'}}" class="md-link" bindtap="onOpenLink" data-url="{{part.url}}">{{part.text}}</text>
128
+ <text wx:elif="{{part.type === 'code'}}" class="md-inline-code">{{part.text}}</text>
129
+ <image
130
+ wx:elif="{{part.type === 'image'}}"
131
+ class="md-image md-inline-image"
132
+ src="{{part.src}}"
133
+ mode="widthFix"
134
+ bindtap="onPreviewImage"
135
+ data-url="{{part.src}}"
136
+ />
137
+ </block>
138
+ </view>
139
+ </view>
140
+
141
+ <!-- 第三级嵌套列表 -->
142
+ <block wx:if="{{childItem.children && childItem.children.length > 0}}">
143
+ <view wx:for="{{childItem.children}}" wx:for-item="grandChildList" wx:key="gcIdx" class="md-nested-list">
144
+ <view wx:for="{{grandChildList.items}}" wx:for-item="grandChildItem" wx:for-index="grandChildIndex" wx:key="grandChildIndex" class="md-list-item-wrapper">
145
+ <view class="md-list-item" style="padding-left: 64rpx;">
146
+ <text class="list-marker">{{grandChildItem.isOrdered ? (grandChildIndex + 1) + '.' : '•'}}</text>
147
+ <view class="list-text">
148
+ <block wx:for="{{grandChildItem.content}}" wx:for-item="part" wx:key="gcPartIdx">
149
+ <text wx:if="{{part.type === 'text' || !part.type}}" class="{{part.bold ? 'md-bold' : ''}} {{part.italic ? 'md-italic' : ''}}">{{part.text}}</text>
150
+ <text wx:elif="{{part.type === 'link'}}" class="md-link" bindtap="onOpenLink" data-url="{{part.url}}">{{part.text}}</text>
151
+ <text wx:elif="{{part.type === 'code'}}" class="md-inline-code">{{part.text}}</text>
152
+ <image
153
+ wx:elif="{{part.type === 'image'}}"
154
+ class="md-image md-inline-image"
155
+ src="{{part.src}}"
156
+ mode="widthFix"
157
+ bindtap="onPreviewImage"
158
+ data-url="{{part.src}}"
159
+ />
160
+ </block>
161
+ </view>
162
+ </view>
163
+ </view>
164
+ </view>
165
+ </block>
166
+ </view>
167
+ </view>
168
+ </block>
169
+ </view>
170
+ </view>
171
+
172
+ <!-- 表格 -->
173
+ <scroll-view wx:elif="{{item.type === 'table'}}" class="md-table-wrapper" scroll-x="true">
174
+ <view class="md-table">
175
+ <!-- 表头 -->
176
+ <view class="md-table-row md-table-header">
177
+ <view wx:for="{{item.headers}}" wx:for-item="header" wx:for-index="colIndex" wx:key="colIndex"
178
+ class="md-table-cell" style="text-align: {{item.alignments[colIndex] || 'left'}}">
179
+ <block wx:for="{{header}}" wx:for-item="part" wx:key="idx">
180
+ <text wx:if="{{part.type === 'text' || !part.type}}" class="{{part.bold ? 'md-bold' : ''}}">{{part.text}}</text>
181
+ <text wx:elif="{{part.type === 'link'}}" class="md-link" bindtap="onOpenLink" data-url="{{part.url}}">{{part.text}}</text>
182
+ <text wx:elif="{{part.type === 'code'}}" class="md-inline-code">{{part.text}}</text>
183
+ </block>
184
+ </view>
185
+ </view>
186
+ <!-- 数据行 -->
187
+ <view wx:for="{{item.rows}}" wx:for-item="row" wx:for-index="rowIndex" wx:key="rowIndex" class="md-table-row">
188
+ <view wx:for="{{row}}" wx:for-item="cell" wx:for-index="colIndex" wx:key="colIndex"
189
+ class="md-table-cell" style="text-align: {{item.alignments[colIndex] || 'left'}}">
190
+ <block wx:for="{{cell}}" wx:for-item="part" wx:key="idx">
191
+ <text wx:if="{{part.type === 'text' || !part.type}}" class="{{part.bold ? 'md-bold' : ''}}">{{part.text}}</text>
192
+ <text wx:elif="{{part.type === 'link'}}" class="md-link" bindtap="onOpenLink" data-url="{{part.url}}">{{part.text}}</text>
193
+ <text wx:elif="{{part.type === 'code'}}" class="md-inline-code">{{part.text}}</text>
194
+ </block>
195
+ </view>
196
+ </view>
197
+ </view>
198
+ </scroll-view>
199
+
200
+ <!-- 引用 -->
201
+ <view wx:elif="{{item.type === 'blockquote'}}" class="md-blockquote">
202
+ <block wx:for="{{item.text}}" wx:for-item="part" wx:key="idx">
203
+ <text wx:if="{{part.type === 'text' || !part.type}}" class="{{part.bold ? 'md-bold' : ''}}">{{part.text}}</text>
204
+ <text wx:elif="{{part.type === 'link'}}" class="md-link" bindtap="onOpenLink" data-url="{{part.url}}">{{part.text}}</text>
205
+ <text wx:elif="{{part.type === 'code'}}" class="md-inline-code">{{part.text}}</text>
206
+ </block>
207
+ </view>
208
+
209
+ <!-- 分隔线 -->
210
+ <view wx:elif="{{item.type === 'hr'}}" class="md-hr"></view>
211
+
212
+ <!-- 快捷回复 -->
213
+ <view wx:elif="{{item.type === 'quick-replies'}}" class="quick-replies">
214
+ <view
215
+ wx:for="{{item.options}}"
216
+ wx:for-item="option"
217
+ wx:key="*this"
218
+ class="quick-reply-btn {{isStreaming ? 'disabled' : ''}}"
219
+ bindtap="onQuickReply"
220
+ data-option="{{option}}"
221
+ >
222
+ <text>{{option}}</text>
223
+ </view>
224
+ </view>
225
+
226
+ <!-- 图片 -->
227
+ <image
228
+ wx:elif="{{item.type === 'image'}}"
229
+ class="md-image"
230
+ src="{{item.src}}"
231
+ mode="widthFix"
232
+ bindtap="onPreviewImage"
233
+ data-url="{{item.src}}"
234
+ />
235
+
236
+ <!-- 链接(块级) -->
237
+ <text
238
+ wx:elif="{{item.type === 'link'}}"
239
+ class="md-link"
240
+ bindtap="onOpenLink"
241
+ data-url="{{item.url}}"
242
+ >{{item.text}}</text>
243
+ </block>
244
+ </view>
245
+
246
+ <!-- 正在输入指示器 -->
247
+ <view wx:if="{{message.isStreaming}}" class="typing-indicator">
248
+ <!-- 语音播放中时显示播放动画 -->
249
+ <block wx:if="{{isPlaying}}">
250
+ <view class="voice-bar"></view>
251
+ <view class="voice-bar"></view>
252
+ <view class="voice-bar"></view>
253
+ </block>
254
+ <!-- 未播放时显示打字动画 -->
255
+ <block wx:else>
256
+ <view class="typing-dot"></view>
257
+ <view class="typing-dot"></view>
258
+ <view class="typing-dot"></view>
259
+ </block>
260
+ </view>
261
+
262
+ <!-- AI消息元信息 -->
263
+ <view wx:if="{{!message.isStreaming}}" class="message-meta">
264
+ <view class="meta-left">
265
+ <!-- 语音合成中 -->
266
+ <view wx:if="{{isSynthesizing}}" class="voice-synthesizing">
267
+ <view class="synthesizing-spinner"></view>
268
+ <text class="synthesizing-text">语音合成中</text>
269
+ </view>
270
+ <!-- 语音播放按钮 -->
271
+ <view wx:elif="{{isPlaying}}" class="voice-playing" bindtap="onPlayVoice" data-id="{{message.id}}">
272
+ <view class="voice-bar"></view>
273
+ <view class="voice-bar"></view>
274
+ <view class="voice-bar"></view>
275
+ </view>
276
+ <image wx:else class="action-icon-img" src="../../assets/icons/icon-play-voice.svg" mode="aspectFit" bindtap="onPlayVoice" data-id="{{message.id}}" />
277
+ </view>
278
+ <view class="meta-right">
279
+ <!-- 复制按钮 -->
280
+ <image class="action-icon-img" src="../../assets/icons/icon-copy.svg" mode="aspectFit" bindtap="onCopy" />
281
+ </view>
282
+ </view>
283
+ </view>
284
+ </view>
285
+ </view>