ibc-ai-web-sdk 2.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.
- package/README.md +607 -0
- package/dist/index.cjs.js +6256 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.esm.js +6245 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.umd.js +6262 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/index.umd.min.js +1 -0
- package/index.d.ts +96 -0
- package/miniprogram/README.md +220 -0
- package/miniprogram/ai-chat-dialog/ai-chat-dialog.js +433 -0
- package/miniprogram/ai-chat-dialog/ai-chat-dialog.json +4 -0
- package/miniprogram/ai-chat-dialog/ai-chat-dialog.wxml +82 -0
- package/miniprogram/ai-chat-dialog/ai-chat-dialog.wxss +88 -0
- package/miniprogram/config/defaultConfig.js +40 -0
- package/miniprogram/core/AIChatClient.js +326 -0
- package/miniprogram/types.ts +178 -0
- package/miniprogram/utils/errorCodes.js +25 -0
- package/miniprogram/utils/logger.js +51 -0
- package/miniprogram/utils/markdownParser.js +97 -0
- package/miniprogram/utils/messageIdGenerator.js +23 -0
- package/package.json +68 -0
- package/src/types.ts +599 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
// AI Chat Dialog 微信小程序组件
|
|
2
|
+
const defaultConfig = require('../config/defaultConfig')
|
|
3
|
+
const AIChatClient = require('../core/AIChatClient')
|
|
4
|
+
const { parseMarkdown, clearMarkdownCache } = require('../utils/markdownParser')
|
|
5
|
+
const { generateMessageId } = require('../utils/messageIdGenerator')
|
|
6
|
+
|
|
7
|
+
Component({
|
|
8
|
+
options: {
|
|
9
|
+
styleIsolation: 'apply-shared'
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
properties: {
|
|
13
|
+
config: {
|
|
14
|
+
type: Object,
|
|
15
|
+
value: {}
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
data: {
|
|
20
|
+
dialogVisible: false,
|
|
21
|
+
inputMessage: '',
|
|
22
|
+
messages: [],
|
|
23
|
+
loading: false,
|
|
24
|
+
isExpanded: false,
|
|
25
|
+
conversationId: null,
|
|
26
|
+
isDragging: false,
|
|
27
|
+
dialogX: 0,
|
|
28
|
+
dialogY: 0,
|
|
29
|
+
isMobile: true,
|
|
30
|
+
touchStartX: 0,
|
|
31
|
+
touchStartY: 0,
|
|
32
|
+
touchStartDialogX: 0,
|
|
33
|
+
touchStartDialogY: 0,
|
|
34
|
+
screenWidth: 0,
|
|
35
|
+
mergedConfig: {},
|
|
36
|
+
dialogStyle: 'width: 100vw; height: 100vh; left: 0; top: 0;',
|
|
37
|
+
maxMessages: 50,
|
|
38
|
+
lastSendTime: 0,
|
|
39
|
+
scrollToView: '',
|
|
40
|
+
messageLength: 0,
|
|
41
|
+
maxLength: 500
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
lifetimes: {
|
|
45
|
+
attached() {
|
|
46
|
+
this.detectEnvironment()
|
|
47
|
+
this.updateMergedConfig()
|
|
48
|
+
this.updateDialogStyle()
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
observers: {
|
|
53
|
+
'config': function(newVal) {
|
|
54
|
+
this.updateMergedConfig()
|
|
55
|
+
this.initClient()
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
methods: {
|
|
60
|
+
// 更新合并配置
|
|
61
|
+
updateMergedConfig() {
|
|
62
|
+
const merged = {
|
|
63
|
+
...defaultConfig,
|
|
64
|
+
...(this.data.config || {})
|
|
65
|
+
}
|
|
66
|
+
this.setData({ mergedConfig: merged })
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// 更新弹窗样式
|
|
70
|
+
updateDialogStyle() {
|
|
71
|
+
let style = ''
|
|
72
|
+
if (this.data.isMobile) {
|
|
73
|
+
style = 'width: 100vw; height: 100vh; left: 0; top: 0;'
|
|
74
|
+
} else if (this.data.isExpanded) {
|
|
75
|
+
style = 'width: 800px; height: calc(100vh - 100px); left: calc(50% - 400px); top: 50px;'
|
|
76
|
+
} else {
|
|
77
|
+
const left = this.data.dialogX ? `${this.data.dialogX}px` : 'auto'
|
|
78
|
+
const top = this.data.dialogY ? `${this.data.dialogY}px` : 'auto'
|
|
79
|
+
const right = this.data.dialogX ? 'auto' : '0'
|
|
80
|
+
const bottom = this.data.dialogY ? 'auto' : '60px'
|
|
81
|
+
style = `width: 380px; height: 520px; left: ${left}; top: ${top}; right: ${right}; bottom: ${bottom};`
|
|
82
|
+
}
|
|
83
|
+
this.setData({ dialogStyle: style })
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// 环境检测
|
|
87
|
+
detectEnvironment() {
|
|
88
|
+
try {
|
|
89
|
+
const systemInfo = wx.getSystemInfoSync()
|
|
90
|
+
this.setData({
|
|
91
|
+
screenWidth: systemInfo.windowWidth || systemInfo.screenWidth,
|
|
92
|
+
isMobile: true // 微信小程序默认移动端
|
|
93
|
+
})
|
|
94
|
+
console.log('[Environment] WeChat Mini Program detected, screenWidth:', systemInfo.windowWidth)
|
|
95
|
+
} catch (e) {
|
|
96
|
+
console.warn('[Environment] Failed to get system info:', e)
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// 初始化客户端
|
|
101
|
+
initClient() {
|
|
102
|
+
const merged = this.data.mergedConfig
|
|
103
|
+
const config = {
|
|
104
|
+
apiEndpoint: merged.apiEndpoint,
|
|
105
|
+
agentAppId: merged.agentAppId,
|
|
106
|
+
userId: merged.userId,
|
|
107
|
+
bizType: merged.bizType,
|
|
108
|
+
token: merged.token,
|
|
109
|
+
timeout: merged.timeout,
|
|
110
|
+
headers: merged.headers,
|
|
111
|
+
extendInfo: merged.extendInfo,
|
|
112
|
+
authFn: merged.authFn
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 保存 chatClient 到组件实例(不在 data 中)
|
|
116
|
+
this.chatClient = new AIChatClient(config)
|
|
117
|
+
console.log('[Init] AI Chat Client initialized')
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
// 输入处理
|
|
121
|
+
onInput(e) {
|
|
122
|
+
const value = e.detail.value
|
|
123
|
+
const length = value.length
|
|
124
|
+
|
|
125
|
+
// 字数限制检查
|
|
126
|
+
if (length > this.data.maxLength) {
|
|
127
|
+
wx.showToast({
|
|
128
|
+
title: `最多输入${this.data.maxLength}个字`,
|
|
129
|
+
icon: 'none',
|
|
130
|
+
duration: 1500
|
|
131
|
+
})
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.setData({
|
|
136
|
+
inputMessage: value,
|
|
137
|
+
messageLength: length
|
|
138
|
+
})
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
// 发送消息
|
|
142
|
+
sendMessage() {
|
|
143
|
+
const message = this.data.inputMessage.trim()
|
|
144
|
+
if (!message || this.data.loading) return
|
|
145
|
+
|
|
146
|
+
const now = Date.now()
|
|
147
|
+
if (now - this.data.lastSendTime < 500) return
|
|
148
|
+
this.setData({ lastSendTime: now })
|
|
149
|
+
|
|
150
|
+
const msgId = generateMessageId()
|
|
151
|
+
const newMessages = [...this.data.messages, {
|
|
152
|
+
type: 'user',
|
|
153
|
+
content: message,
|
|
154
|
+
renderedContent: this.renderMarkdown(message),
|
|
155
|
+
time: this.formatTime(),
|
|
156
|
+
id: msgId
|
|
157
|
+
}]
|
|
158
|
+
|
|
159
|
+
if (newMessages.length > this.data.maxMessages) {
|
|
160
|
+
newMessages.splice(0, newMessages.length - this.data.maxMessages)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.setData({
|
|
164
|
+
messages: newMessages,
|
|
165
|
+
inputMessage: '',
|
|
166
|
+
loading: true,
|
|
167
|
+
scrollToView: `msg-${msgId}`
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
this.triggerEvent('message-send', { message, index: newMessages.length - 1 })
|
|
171
|
+
this.callAI(message)
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
// 滚动到底部
|
|
175
|
+
scrollToBottom() {
|
|
176
|
+
const lastMsg = this.data.messages[this.data.messages.length - 1]
|
|
177
|
+
if (lastMsg) {
|
|
178
|
+
this.setData({
|
|
179
|
+
scrollToView: `msg-${lastMsg.id}`
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
// 调用 AI
|
|
185
|
+
callAI(message) {
|
|
186
|
+
if (!this.chatClient) {
|
|
187
|
+
this.initClient()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log('[AI] Calling AI with message:', message)
|
|
191
|
+
|
|
192
|
+
this.chatClient.sendMessage(message, this.data.mergedConfig.token)
|
|
193
|
+
.then(response => {
|
|
194
|
+
console.log('[AI] Response:', response)
|
|
195
|
+
|
|
196
|
+
if (response && response.success) {
|
|
197
|
+
const content = (response.data && (response.data.content || response.data.replyText)) || '暂无回复'
|
|
198
|
+
const aiMsgId = generateMessageId()
|
|
199
|
+
|
|
200
|
+
if (response.data && response.data.conversationId) {
|
|
201
|
+
this.chatClient.setConversationId(response.data.conversationId)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 修复:使用最新的 messages(包含用户消息),而不是闭包中的旧快照
|
|
205
|
+
const currentMessages = this.data.messages
|
|
206
|
+
const newMessages = [...currentMessages, {
|
|
207
|
+
type: 'ai',
|
|
208
|
+
content: content,
|
|
209
|
+
time: this.formatTime(),
|
|
210
|
+
id: aiMsgId,
|
|
211
|
+
renderedContent: this.renderMarkdown(content)
|
|
212
|
+
}]
|
|
213
|
+
|
|
214
|
+
this.setData({
|
|
215
|
+
messages: newMessages,
|
|
216
|
+
loading: false,
|
|
217
|
+
scrollToView: `msg-${aiMsgId}`
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
this.triggerEvent('message-receive', { response, index: newMessages.length - 1 })
|
|
221
|
+
this.triggerEvent('loading-change', false)
|
|
222
|
+
} else {
|
|
223
|
+
const errorMsg = (response && response.message) || '请求失败'
|
|
224
|
+
const aiMsgId = generateMessageId()
|
|
225
|
+
// 修复:使用最新的 messages(包含用户消息),而不是闭包中的旧快照
|
|
226
|
+
const currentMessages = this.data.messages
|
|
227
|
+
const newMessages = [...currentMessages, {
|
|
228
|
+
type: 'ai',
|
|
229
|
+
content: '抱歉,请求失败:' + errorMsg,
|
|
230
|
+
time: this.formatTime(),
|
|
231
|
+
id: aiMsgId,
|
|
232
|
+
error: true,
|
|
233
|
+
renderedContent: this.renderMarkdown('抱歉,请求失败:' + errorMsg)
|
|
234
|
+
}]
|
|
235
|
+
|
|
236
|
+
this.setData({
|
|
237
|
+
messages: newMessages,
|
|
238
|
+
loading: false,
|
|
239
|
+
scrollToView: `msg-${aiMsgId}`
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
this.triggerEvent('loading-change', false)
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
.catch(error => {
|
|
246
|
+
console.error('[AI] Error:', error)
|
|
247
|
+
|
|
248
|
+
const errorMsg = (error && error.message) || error || '网络请求失败'
|
|
249
|
+
const aiMsgId = generateMessageId()
|
|
250
|
+
// 修复:使用最新的 messages(包含用户消息),而不是闭包中的旧快照
|
|
251
|
+
const currentMessages = this.data.messages
|
|
252
|
+
const newMessages = [...currentMessages, {
|
|
253
|
+
type: 'ai',
|
|
254
|
+
content: '抱歉,请求失败:' + errorMsg,
|
|
255
|
+
time: this.formatTime(),
|
|
256
|
+
id: aiMsgId,
|
|
257
|
+
error: true,
|
|
258
|
+
renderedContent: this.renderMarkdown('抱歉,请求失败:' + errorMsg)
|
|
259
|
+
}]
|
|
260
|
+
|
|
261
|
+
this.setData({
|
|
262
|
+
messages: newMessages,
|
|
263
|
+
loading: false,
|
|
264
|
+
scrollToView: `msg-${aiMsgId}`
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
this.triggerEvent('loading-change', false)
|
|
268
|
+
|
|
269
|
+
const merged = this.data.mergedConfig
|
|
270
|
+
if (merged && merged.onError) {
|
|
271
|
+
merged.onError(error)
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
formatTime() {
|
|
277
|
+
const now = new Date()
|
|
278
|
+
const hours = now.getHours().toString().padStart(2, '0')
|
|
279
|
+
const minutes = now.getMinutes().toString().padStart(2, '0')
|
|
280
|
+
return `${hours}:${minutes}`
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
renderMarkdown(text) {
|
|
284
|
+
if (!text) return ''
|
|
285
|
+
return parseMarkdown(text)
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
// 重试失败的请求
|
|
289
|
+
retryMessage(e) {
|
|
290
|
+
const { index } = e.currentTarget.dataset
|
|
291
|
+
if (this.data.loading) return
|
|
292
|
+
|
|
293
|
+
// 查找失败消息的前一条用户消息
|
|
294
|
+
let userQuestion = ''
|
|
295
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
296
|
+
if (this.data.messages[i] && this.data.messages[i].type === 'user') {
|
|
297
|
+
userQuestion = this.data.messages[i].content
|
|
298
|
+
break
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (!userQuestion) {
|
|
303
|
+
wx.showToast({ title: '找不到原始消息', icon: 'none' })
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 删除失败的 AI 消息
|
|
308
|
+
const newMessages = this.data.messages.slice(0, index)
|
|
309
|
+
this.setData({
|
|
310
|
+
messages: newMessages,
|
|
311
|
+
loading: true
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
// 重新调用 AI
|
|
315
|
+
this.callAI(userQuestion)
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
startTouchDrag(e) {
|
|
319
|
+
if (this.data.isExpanded) return
|
|
320
|
+
const touch = e.touches[0]
|
|
321
|
+
this.setData({
|
|
322
|
+
isDragging: true,
|
|
323
|
+
touchStartX: touch.clientX,
|
|
324
|
+
touchStartY: touch.clientY,
|
|
325
|
+
touchStartDialogX: this.data.dialogX || 0,
|
|
326
|
+
touchStartDialogY: this.data.dialogY || 0
|
|
327
|
+
})
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
onTouchDrag(e) {
|
|
331
|
+
if (!this.data.isDragging) return
|
|
332
|
+
e.preventDefault()
|
|
333
|
+
const touch = e.touches[0]
|
|
334
|
+
this.setData({
|
|
335
|
+
dialogX: this.data.touchStartDialogX + (touch.clientX - this.data.touchStartX),
|
|
336
|
+
dialogY: this.data.touchStartDialogY + (touch.clientY - this.data.touchStartY)
|
|
337
|
+
})
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
endTouchDrag() {
|
|
341
|
+
this.setData({
|
|
342
|
+
isDragging: false
|
|
343
|
+
})
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
toggleExpand() {
|
|
347
|
+
const isExpanded = !this.data.isExpanded
|
|
348
|
+
this.setData({
|
|
349
|
+
isExpanded: isExpanded,
|
|
350
|
+
dialogX: isExpanded ? 0 : this.data.dialogX,
|
|
351
|
+
dialogY: isExpanded ? 0 : this.data.dialogY
|
|
352
|
+
})
|
|
353
|
+
this.updateDialogStyle()
|
|
354
|
+
this.triggerEvent('expand', isExpanded)
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
handleClose() {
|
|
358
|
+
this.setData({
|
|
359
|
+
dialogVisible: false,
|
|
360
|
+
isExpanded: false,
|
|
361
|
+
dialogX: 0,
|
|
362
|
+
dialogY: 0
|
|
363
|
+
})
|
|
364
|
+
this.triggerEvent('close')
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
openDialog() {
|
|
368
|
+
this.setData({
|
|
369
|
+
dialogVisible: true
|
|
370
|
+
})
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
copyMessage(e) {
|
|
374
|
+
const { content, index } = e.currentTarget.dataset
|
|
375
|
+
if (!content || index === undefined) return
|
|
376
|
+
|
|
377
|
+
wx.setClipboardData({
|
|
378
|
+
data: content,
|
|
379
|
+
success: () => {
|
|
380
|
+
wx.showToast({
|
|
381
|
+
title: '已复制',
|
|
382
|
+
icon: 'success',
|
|
383
|
+
duration: 2000
|
|
384
|
+
})
|
|
385
|
+
},
|
|
386
|
+
fail: (err) => {
|
|
387
|
+
console.error('[Copy] Failed:', err)
|
|
388
|
+
}
|
|
389
|
+
})
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
regenerate(e) {
|
|
393
|
+
const { index } = e.currentTarget.dataset
|
|
394
|
+
if (this.data.loading) return
|
|
395
|
+
|
|
396
|
+
let userQuestion = ''
|
|
397
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
398
|
+
if (this.data.messages[i] && this.data.messages[i].type === 'user') {
|
|
399
|
+
userQuestion = this.data.messages[i].content
|
|
400
|
+
break
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (!userQuestion) return
|
|
405
|
+
|
|
406
|
+
const newMessages = this.data.messages.slice(0, index)
|
|
407
|
+
|
|
408
|
+
if (newMessages.length > this.data.maxMessages) {
|
|
409
|
+
newMessages.splice(0, newMessages.length - this.data.maxMessages)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
this.setData({
|
|
413
|
+
messages: newMessages,
|
|
414
|
+
loading: true
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
this.callAI(userQuestion)
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
clearChat() {
|
|
421
|
+
this.setData({
|
|
422
|
+
messages: [],
|
|
423
|
+
conversationId: null
|
|
424
|
+
})
|
|
425
|
+
if (this.chatClient) {
|
|
426
|
+
this.chatClient.clearConversation()
|
|
427
|
+
}
|
|
428
|
+
// 清除 Markdown 缓存
|
|
429
|
+
clearMarkdownCache()
|
|
430
|
+
this.triggerEvent('clear')
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
})
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<view class="ai-float-container" wx:if="{{dialogVisible}}">
|
|
2
|
+
<view class="ai-chat-dialog {{isExpanded ? 'expanded' : ''}} {{isDragging ? 'dragging' : ''}}"
|
|
3
|
+
style="{{dialogStyle}}">
|
|
4
|
+
<view class="dialog-header" bindtouchstart="startTouchDrag" bindtouchmove="onTouchDrag" bindtouchend="endTouchDrag">
|
|
5
|
+
<view class="header-left">
|
|
6
|
+
<text class="robot-icon">🤖</text>
|
|
7
|
+
<text class="header-title">{{mergedConfig.title}}</text>
|
|
8
|
+
</view>
|
|
9
|
+
<view class="header-right">
|
|
10
|
+
<view class="header-icon close-btn" bindtap="handleClose">
|
|
11
|
+
<text>✕</text>
|
|
12
|
+
</view>
|
|
13
|
+
</view>
|
|
14
|
+
</view>
|
|
15
|
+
|
|
16
|
+
<scroll-view class="dialog-body" scroll-y="true" scroll-into-view="{{scrollToView}}" scroll-with-animation>
|
|
17
|
+
<view wx:if="{{messages.length === 0}}" class="empty-state">
|
|
18
|
+
<view class="welcome-content">
|
|
19
|
+
<view class="welcome-icon">AI</view>
|
|
20
|
+
<text class="welcome-text">{{mergedConfig.welcomeText}}</text>
|
|
21
|
+
<text class="welcome-desc">{{mergedConfig.welcomeDesc}}</text>
|
|
22
|
+
</view>
|
|
23
|
+
</view>
|
|
24
|
+
|
|
25
|
+
<view wx:else class="message-list">
|
|
26
|
+
<view wx:for="{{messages}}" wx:key="id" class="message-item {{item.type}} {{item.error ? 'error' : ''}}">
|
|
27
|
+
<view class="message-avatar">
|
|
28
|
+
<text class="avatar-icon">{{item.type === 'user' ? '👤' : '🤖'}}</text>
|
|
29
|
+
</view>
|
|
30
|
+
<view class="message-content">
|
|
31
|
+
<view class="message-bubble {{item.error ? 'error' : ''}}">
|
|
32
|
+
<rich-text nodes="{{item.renderedContent}}"></rich-text>
|
|
33
|
+
</view>
|
|
34
|
+
<view class="message-actions-bar" wx:if="{{item.type === 'ai'}}">
|
|
35
|
+
<view class="action-icons">
|
|
36
|
+
<view class="action-icon" bindtap="copyMessage" data-content="{{item.content}}" data-index="{{index}}">
|
|
37
|
+
<text>⎘</text>
|
|
38
|
+
</view>
|
|
39
|
+
<view class="action-icon" bindtap="regenerate" data-index="{{index}}" wx:if="{{!item.error}}">
|
|
40
|
+
<text>↻</text>
|
|
41
|
+
</view>
|
|
42
|
+
<view class="action-icon retry" bindtap="retryMessage" data-index="{{index}}" wx:if="{{item.error}}">
|
|
43
|
+
<text>🔄</text>
|
|
44
|
+
<text class="retry-text">重试</text>
|
|
45
|
+
</view>
|
|
46
|
+
</view>
|
|
47
|
+
<text class="message-time">{{item.time}}</text>
|
|
48
|
+
</view>
|
|
49
|
+
</view>
|
|
50
|
+
</view>
|
|
51
|
+
|
|
52
|
+
<view wx:if="{{loading}}" class="message-item ai">
|
|
53
|
+
<view class="message-avatar"><text class="avatar-icon">🤖</text></view>
|
|
54
|
+
<view class="message-content">
|
|
55
|
+
<view class="message-bubble loading">
|
|
56
|
+
<text>⏳</text><text>思考中...</text>
|
|
57
|
+
</view>
|
|
58
|
+
</view>
|
|
59
|
+
</view>
|
|
60
|
+
</view>
|
|
61
|
+
</scroll-view>
|
|
62
|
+
|
|
63
|
+
<view class="dialog-footer">
|
|
64
|
+
<view class="input-container">
|
|
65
|
+
<input
|
|
66
|
+
class="message-input"
|
|
67
|
+
placeholder="请输入您的问题..."
|
|
68
|
+
value="{{inputMessage}}"
|
|
69
|
+
bindinput="onInput"
|
|
70
|
+
bindconfirm="sendMessage"
|
|
71
|
+
disabled="{{loading}}"
|
|
72
|
+
/>
|
|
73
|
+
<view class="input-footer">
|
|
74
|
+
<text class="message-count">{{messageLength}}/{{maxLength}}</text>
|
|
75
|
+
<view class="send-btn {{loading || !inputMessage.trim() ? 'disabled' : ''}}" bindtap="sendMessage">
|
|
76
|
+
<text>➤</text>
|
|
77
|
+
</view>
|
|
78
|
+
</view>
|
|
79
|
+
</view>
|
|
80
|
+
</view>
|
|
81
|
+
</view>
|
|
82
|
+
</view>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/* 基础样式 */
|
|
2
|
+
.ai-float-container { position: fixed; bottom: 24rpx; right: 24rpx; z-index: 10000; }
|
|
3
|
+
.ai-chat-dialog { position: fixed; background: rgba(255, 255, 255, 0.98); border-radius: 32rpx; border: 2rpx solid rgba(24, 144, 255, 0.4); box-shadow: 0 0 60rpx rgba(24, 144, 255, 0.12), 0 16rpx 64rpx rgba(0, 0, 0, 0.1); display: flex; flex-direction: column; overflow: hidden; }
|
|
4
|
+
.ai-chat-dialog:not(.expanded) { cursor: move; }
|
|
5
|
+
.ai-chat-dialog.dragging { transition: none; }
|
|
6
|
+
|
|
7
|
+
/* Header */
|
|
8
|
+
.dialog-header { display: flex; justify-content: space-between; align-items: center; padding: 28rpx 36rpx; background: linear-gradient(90deg, rgba(24, 144, 255, 0.08), rgba(114, 46, 209, 0.08)); border-bottom: 2rpx solid rgba(24, 144, 255, 0.15); position: relative; }
|
|
9
|
+
.dialog-header::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4rpx; background: linear-gradient(90deg, #1890ff, #722ed1); }
|
|
10
|
+
.header-left { display: flex; align-items: center; gap: 20rpx; }
|
|
11
|
+
.robot-icon { font-size: 44rpx; color: #1890ff; }
|
|
12
|
+
.header-title { font-size: 30rpx; font-weight: 600; background: linear-gradient(90deg, #1890ff, #722ed1); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
|
13
|
+
.header-right { display: flex; gap: 24rpx; align-items: center; }
|
|
14
|
+
.header-icon { display: flex; align-items: center; justify-content: center; font-size: 32rpx; color: #64748b; min-width: 88rpx; min-height: 88rpx; }
|
|
15
|
+
.close-btn { color: #ef4444; }
|
|
16
|
+
|
|
17
|
+
/* Body */
|
|
18
|
+
.dialog-body { flex: 1; overflow-y: auto; padding: 32rpx; background: linear-gradient(180deg, #f8fafc, #ffffff); min-height: 200rpx; box-sizing: border-box;}
|
|
19
|
+
.empty-state { height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; }
|
|
20
|
+
.welcome-content { margin-bottom: 48rpx; display: flex; flex-direction: column; align-items: center; }
|
|
21
|
+
.welcome-icon { width: 180rpx; height: 180rpx; border-radius: 50%; background: linear-gradient(135deg, #1890ff, #722ed1); display: flex; align-items: center; justify-content: center; font-size: 80rpx; font-weight: bold; color: #ffffff; margin-bottom: 32rpx; box-shadow: 0 8rpx 24rpx rgba(24, 144, 255, 0.3); }
|
|
22
|
+
.welcome-text { font-size: 34rpx; color: #1e293b; margin-bottom: 16rpx; font-weight: 500; }
|
|
23
|
+
.welcome-desc { font-size: 26rpx; color: #64748b; line-height: 1.6; }
|
|
24
|
+
|
|
25
|
+
/* Messages */
|
|
26
|
+
.message-list { display: flex; flex-direction: column; gap: 32rpx; }
|
|
27
|
+
.message-item { display: flex; gap: 20rpx; max-width: 85%; }
|
|
28
|
+
.message-item.user { flex-direction: row-reverse; align-self: flex-end; }
|
|
29
|
+
.message-item.ai { flex-direction: row; align-self: flex-start; }
|
|
30
|
+
.message-avatar { width: 68rpx; height: 68rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32rpx; flex-shrink: 0; }
|
|
31
|
+
.message-item.user .message-avatar { background: linear-gradient(135deg, #1890ff, #40a9ff); color: #fff; }
|
|
32
|
+
.message-item.ai .message-avatar { background: linear-gradient(135deg, #722ed1, #9254de); color: #fff; }
|
|
33
|
+
.message-content { display: flex; flex-direction: column; gap: 8rpx; }
|
|
34
|
+
.message-item.user .message-content { align-items: flex-end; }
|
|
35
|
+
.message-bubble { padding: 24rpx 32rpx; border-radius: 24rpx; font-size: 28rpx; line-height: 1.6; }
|
|
36
|
+
.message-item.user .message-bubble { background: linear-gradient(135deg, #1890ff, #40a9ff); color: #fff; border-bottom-right-radius: 8rpx; box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.25); }
|
|
37
|
+
.message-item.ai .message-bubble { background: #ffffff; color: #334155; border: 2rpx solid rgba(114, 46, 209, 0.15); border-bottom-left-radius: 8rpx; box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06); }
|
|
38
|
+
.message-bubble.loading { display: flex; align-items: center; gap: 16rpx; color: #64748b; }
|
|
39
|
+
.message-bubble.loading text:first-child { animation: hourglass-spin 1.5s ease-in-out infinite; }
|
|
40
|
+
@keyframes hourglass-spin {
|
|
41
|
+
0% { transform: rotate(0deg); }
|
|
42
|
+
25% { transform: rotate(180deg); }
|
|
43
|
+
50% { transform: rotate(180deg); }
|
|
44
|
+
75% { transform: rotate(360deg); }
|
|
45
|
+
100% { transform: rotate(360deg); }
|
|
46
|
+
}
|
|
47
|
+
.message-actions-bar { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; margin-top: 16rpx; padding-top: 16rpx; border-top: 2rpx solid rgba(0, 0, 0, 0.04); }
|
|
48
|
+
.message-time { font-size: 22rpx; color: #94a3b8; }
|
|
49
|
+
.action-icons { display: flex; gap: 16rpx; }
|
|
50
|
+
.action-icon { display: flex; align-items: center; justify-content: center; width: 56rpx; height: 56rpx; border-radius: 12rpx; color: #94a3b8; min-width: 88rpx; min-height: 88rpx; }
|
|
51
|
+
.action-icon.copied { color: #52c41a; }
|
|
52
|
+
.action-icon.retry { color: #f59e0b; background: rgba(245, 158, 11, 0.1); }
|
|
53
|
+
.action-icon.retry .retry-text { font-size: 20rpx; margin-left: 4rpx; }
|
|
54
|
+
.message-item.error .message-bubble { border: 2rpx solid rgba(239, 68, 68, 0.3); background: rgba(239, 68, 68, 0.05); }
|
|
55
|
+
|
|
56
|
+
/* Footer */
|
|
57
|
+
.dialog-footer { padding: 32rpx 36rpx 36rpx; background: #ffffff; }
|
|
58
|
+
.input-container { display: flex; flex-direction: column; align-items: center; background: #f8fafc; border: 2rpx solid #e2e8f0; border-radius: 56rpx; padding: 8rpx 8rpx 16rpx 36rpx; }
|
|
59
|
+
.message-input { flex: 1; border: none; background: transparent; font-size: 28rpx; color: #1e293b; padding: 16rpx 0; width: 100%; }
|
|
60
|
+
.input-footer { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 0 8rpx; }
|
|
61
|
+
.message-count { font-size: 22rpx; color: #94a3b8; }
|
|
62
|
+
.send-btn { flex-shrink: 0; width: 88rpx; height: 88rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #1890ff, #40a9ff); color: #fff; }
|
|
63
|
+
.send-btn.disabled { background: #e2e8f0; color: #94a3b8; }
|
|
64
|
+
|
|
65
|
+
/* Float Button */
|
|
66
|
+
.float-button { width: 112rpx; height: 112rpx; border-radius: 50%; background: linear-gradient(135deg, rgba(24, 144, 255, 0.1), rgba(114, 46, 209, 0.08)); border: 4rpx solid rgba(24, 144, 255, 0.25); display: flex; align-items: center; justify-content: center; box-shadow: 0 4rpx 24rpx rgba(24, 144, 255, 0.12); }
|
|
67
|
+
.float-icon { width: 84rpx; height: auto; }
|
|
68
|
+
|
|
69
|
+
/* 移动端适配 */
|
|
70
|
+
@media screen and (max-width: 768px) {
|
|
71
|
+
.ai-float-container { bottom: 32rpx; right: 32rpx; }
|
|
72
|
+
.ai-chat-dialog { border-radius: 0 !important; }
|
|
73
|
+
.dialog-header { padding: 32rpx; padding-top: max(32rpx, env(safe-area-inset-top)); }
|
|
74
|
+
.header-title { font-size: 32rpx; }
|
|
75
|
+
.header-icon { min-width: 88rpx; min-height: 88rpx; }
|
|
76
|
+
.dialog-body { padding: 24rpx; }
|
|
77
|
+
.message-list { gap: 24rpx; }
|
|
78
|
+
.message-item { max-width: 90%; }
|
|
79
|
+
.message-bubble { padding: 20rpx 28rpx; font-size: 30rpx; }
|
|
80
|
+
.message-avatar { width: 64rpx; height: 64rpx; font-size: 30rpx; }
|
|
81
|
+
.welcome-icon { width: 140rpx; height: 140rpx; font-size: 70rpx; }
|
|
82
|
+
.welcome-text { font-size: 32rpx; }
|
|
83
|
+
.welcome-desc { font-size: 28rpx; }
|
|
84
|
+
.dialog-footer { padding: 24rpx; padding-bottom: max(24rpx, env(safe-area-inset-bottom)); }
|
|
85
|
+
.input-container { padding: 8rpx 8rpx 8rpx 28rpx; border-radius: 48rpx; }
|
|
86
|
+
.message-input { font-size: 32rpx; padding: 20rpx 0; }
|
|
87
|
+
.send-btn { width: 88rpx; height: 88rpx; min-width: 88rpx; min-height: 88rpx; }
|
|
88
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 默认配置(微信小程序版本)
|
|
3
|
+
*/
|
|
4
|
+
const defaultConfig = {
|
|
5
|
+
// API 配置(必须)
|
|
6
|
+
apiEndpoint: '/v1/app/completion',
|
|
7
|
+
appListEndpoint: '/v1/app/list',
|
|
8
|
+
runtimePermissionReplyEndpoint: '/v1/app/runtime/permission/reply',
|
|
9
|
+
runtimeQuestionReplyEndpoint: '/v1/app/runtime/question/reply',
|
|
10
|
+
runtimeQuestionRejectEndpoint: '/v1/app/runtime/question/reject',
|
|
11
|
+
appId: '',
|
|
12
|
+
agentAppId: '',
|
|
13
|
+
userId: '',
|
|
14
|
+
bizType: '',
|
|
15
|
+
requestId: '',
|
|
16
|
+
token: '',
|
|
17
|
+
timeout: 60000,
|
|
18
|
+
headers: {},
|
|
19
|
+
bizParams: null,
|
|
20
|
+
extendInfo: null,
|
|
21
|
+
|
|
22
|
+
// UI 配置
|
|
23
|
+
title: 'AI 智能助理',
|
|
24
|
+
welcomeText: '您好!我是 AI 智能助理',
|
|
25
|
+
welcomeDesc: '我可以帮您解答系统使用问题',
|
|
26
|
+
|
|
27
|
+
// 功能开关
|
|
28
|
+
enableDrag: true,
|
|
29
|
+
|
|
30
|
+
// 可选鉴权函数:SDK 将 token 和 userId 传入用户系统校验,通过后才允许调用智能体
|
|
31
|
+
// 返回 Promise<boolean> 或 Promise<void>,resolve 表示通过,reject/throw 表示拒绝
|
|
32
|
+
// 如果不配置此函数,则跳过鉴权直接调用
|
|
33
|
+
authFn: null,
|
|
34
|
+
|
|
35
|
+
// 事件回调
|
|
36
|
+
onLoad: null,
|
|
37
|
+
onError: null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = defaultConfig
|