sg-paisou 0.0.0 → 0.0.1

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 (79) hide show
  1. package/README.npm.md +322 -0
  2. package/dist/ai-chat-sdk.es.js +22610 -0
  3. package/dist/ai-chat-sdk.es.js.map +1 -0
  4. package/dist/ai-chat-sdk.umd.js +300 -0
  5. package/dist/ai-chat-sdk.umd.js.map +1 -0
  6. package/dist/style.css +1 -0
  7. package/dist/types/index.d.ts +48 -0
  8. package/package.json +58 -11
  9. package/.idea/GitCommitMessageStorage.xml +0 -8
  10. package/.idea/modules.xml +0 -8
  11. package/.idea/sg-paisou-web.iml +0 -12
  12. package/.idea/vcs.xml +0 -6
  13. package/.vscode/settings.json +0 -2
  14. package/auto-imports.d.ts +0 -10
  15. package/components.d.ts +0 -18
  16. package/index.html +0 -21
  17. package/src/App.vue +0 -38
  18. package/src/assets/images/Camera-icon.png +0 -0
  19. package/src/assets/images/anger.png +0 -0
  20. package/src/assets/images/answer-icon.png +0 -0
  21. package/src/assets/images/back.png +0 -0
  22. package/src/assets/images/bg-img.png +0 -0
  23. package/src/assets/images/collect.png +0 -0
  24. package/src/assets/images/collected.png +0 -0
  25. package/src/assets/images/empty-bookmark.png +0 -0
  26. package/src/assets/images/empty-history.png +0 -0
  27. package/src/assets/images/feedback-thinkie-img.png +0 -0
  28. package/src/assets/images/history.png +0 -0
  29. package/src/assets/images/image 5.png +0 -0
  30. package/src/assets/images/insolubility.png +0 -0
  31. package/src/assets/images/key-points.png +0 -0
  32. package/src/assets/images/photograph-icon.png +0 -0
  33. package/src/assets/images/praise-icon.png +0 -0
  34. package/src/assets/images/praiseing-icon.png +0 -0
  35. package/src/assets/images/question.png +0 -0
  36. package/src/assets/images/smiling.png +0 -0
  37. package/src/assets/images/solution-icon.png +0 -0
  38. package/src/assets/images/star-icon.png +0 -0
  39. package/src/assets/images/trample-icon.png +0 -0
  40. package/src/assets/images/trampleing-icon.png +0 -0
  41. package/src/assets/images/volume-icon.png +0 -0
  42. package/src/assets/images/volumeing-icon.png +0 -0
  43. package/src/components/AIChatSDK/AIChatComponent.vue +0 -963
  44. package/src/components/AIChatSDK/component/Dialogs.vue +0 -340
  45. package/src/components/AIChatSDK/component/SpecialQuestions.vue +0 -208
  46. package/src/components/AIChatSDK/index.ts +0 -146
  47. package/src/components/AIChatSDK/style.scss +0 -432
  48. package/src/components/AIChatSDK/utils/imageUtils.ts +0 -61
  49. package/src/components/AIChatSDK/utils/latex.ts +0 -34
  50. package/src/components/AIChatSDK/utils/mergeConfig.ts +0 -125
  51. package/src/components/ImagePreview.vue +0 -62
  52. package/src/components/PageHeader/index.vue +0 -121
  53. package/src/config.ts +0 -11
  54. package/src/env.d.ts +0 -11
  55. package/src/main.ts +0 -12
  56. package/src/router.ts +0 -20
  57. package/src/style.css +0 -33
  58. package/src/type.ts +0 -106
  59. package/src/utils/TTS_README.md +0 -232
  60. package/src/utils/bridge.ts +0 -42
  61. package/src/utils/index.ts +0 -8
  62. package/src/utils/listenOsEvent.ts +0 -3
  63. package/src/utils/messageToast.ts +0 -43
  64. package/src/utils/render.ts +0 -81
  65. package/src/utils/request.ts +0 -87
  66. package/src/utils/tts.ts +0 -319
  67. package/src/utils/typewriter.ts +0 -61
  68. package/src/utils/useSSE.ts +0 -113
  69. package/src/views/History/index.vue +0 -419
  70. package/src/views/QuestionChatPage/index.vue +0 -480
  71. package/src/vite-env.d.ts +0 -1
  72. package/tsconfig.app.json +0 -24
  73. package/tsconfig.json +0 -7
  74. package/tsconfig.node.json +0 -22
  75. package/vite.config.ts +0 -41
  76. /package/{public → dist}/mathjax/all-packages.js +0 -0
  77. /package/{public → dist}/mathjax/talEditorConfig.js +0 -0
  78. /package/{public → dist}/mathjax/tex-svg.js +0 -0
  79. /package/{src/components/AIChatSDK/types.ts → dist/types/types.d.ts} +0 -0
@@ -1,963 +0,0 @@
1
- <template>
2
- <div
3
- :class="['ai-chat-container', config.customClasses?.container]"
4
- :style="mergedStyles.container as StyleValue"
5
- >
6
- <!-- 普通题 -->
7
- <template v-if="questionInfo?.questionType !== 1">
8
- <!-- AI头部区域 -->
9
- <div
10
- :class="['ai-sticky-header', config.customClasses?.header]"
11
- :style="mergedStyles.header as StyleValue"
12
- >
13
- <!-- AI头像 -->
14
- <div class="ai-avatar">
15
- <img
16
- :src="config.avatar?.src || DEFAULT_CONFIG.avatar.src"
17
- :alt="config.avatar?.alt || DEFAULT_CONFIG.avatar.alt"
18
- :style="{
19
- width: config.avatar?.width || DEFAULT_CONFIG.avatar.width,
20
- height: config.avatar?.height || DEFAULT_CONFIG.avatar.height,
21
- borderRadius: config.avatar?.borderRadius || DEFAULT_CONFIG.avatar.borderRadius
22
- }"
23
- />
24
- </div>
25
-
26
- <!-- AI信息 -->
27
- <div class="ai-info">
28
- <h3 :style="mergedAIInfoStyles.name">
29
- {{ config.aiInfo?.name || DEFAULT_CONFIG.aiInfo.name }}
30
- </h3>
31
- <p :style="mergedAIInfoStyles.title">
32
- {{ config.aiInfo?.title || DEFAULT_CONFIG.aiInfo.title }}
33
- </p>
34
- </div>
35
-
36
- <!-- 静音按钮 -->
37
- <div class="mute-button" @click="handleMuteToggle">
38
- <img
39
- v-if="isSoundOn"
40
- :src="config.muteButton?.soundOnIcon || DEFAULT_CONFIG.muteButton.soundOnIcon"
41
- alt="声音开启"
42
- :style="{
43
- width: config.muteButton?.width || DEFAULT_CONFIG.muteButton.width,
44
- height: config.muteButton?.height || DEFAULT_CONFIG.muteButton.height
45
- }"
46
- />
47
- <img
48
- v-else
49
- :src="config.muteButton?.soundOffIcon || DEFAULT_CONFIG.muteButton.soundOffIcon"
50
- alt="声音关闭"
51
- :style="{
52
- width: config.muteButton?.width || DEFAULT_CONFIG.muteButton.width,
53
- height: config.muteButton?.height || DEFAULT_CONFIG.muteButton.height
54
- }"
55
- />
56
- </div>
57
- </div>
58
-
59
- <!-- 聊天内容区域 -->
60
- <div
61
- :class="['chat-content', config.customClasses?.chatContent]"
62
- :style="mergedStyles.chatContent as StyleValue"
63
- >
64
- <!-- 消息列表 -->
65
- <div class="chat-messages" ref="messagesContainer">
66
- <div
67
- v-for="message in messages"
68
- :key="message.id"
69
- :class="['message', `${message.type}-message`]"
70
- :style="{ marginBottom: mergedStyles.messages?.messageGap }"
71
- >
72
- <template v-if="message.type === 'ai'">
73
- <div class="message-wrapper">
74
- <div
75
- class="message-content"
76
- :style="mergedStyles.messages?.aiMessage"
77
- @touchstart="!isAIResponding && !message.disableLongPress && handleLongPressStart($event, message)"
78
- @touchend="!message.disableLongPress && handleLongPressEnd"
79
- @mousedown="!isAIResponding && !message.disableLongPress && handleLongPressStart($event, message)"
80
- @mouseup="!message.disableLongPress && handleLongPressEnd"
81
- @mouseleave="!message.disableLongPress && handleLongPressEnd"
82
- >
83
- <div class="message-text-wrapper">
84
- <span v-html="renderContent(message.content)"></span>
85
- <!-- 打字/流式接收时的呼吸灯 - 紧跟在文字后面 -->
86
- <span v-if="message.isTyping || message.isStreaming" class="typing-indicator"></span>
87
- </div>
88
- <!-- 操作按钮 (Not my question / Start Explaining) - AI消息 -->
89
- <div v-if="message.showButtons" class="message-buttons">
90
- <button class="btn-secondary" @click="handleNotMyQuestion">Not my question</button>
91
- <button class="btn-primary" @click="handleStartExplaining">Start Explaining</button>
92
- </div>
93
-
94
- <!-- 只显示反馈按钮(追问后的回复) -->
95
- <div v-if="message.showFeedbackOnly" class="feedback-actions-only">
96
- <img :src="message.isPlayingTTS ? volumeingIcon : volumeIcon"
97
- @click="handleMessageFeedback(message.isPlayingTTS ? 'volumeing' : 'volume', message)"
98
- :alt="message.isPlayingTTS ? '播放中' : '播放'" />
99
- <img :src="message.vote === 'upvote' ? praiseingIcon : praiseIcon"
100
- @click="handleMessageFeedback('upvote', message)"
101
- :alt="message.vote === 'upvote' ? '已点赞' : '点赞'" />
102
- <img :src="message.vote === 'downvote' ? trampleingIcon : trampleIcon"
103
- @click="handleMessageFeedback('downvote', message)"
104
- :alt="message.vote === 'downvote' ? '已不喜欢' : '不喜欢'" />
105
- </div>
106
-
107
- <!-- 反馈按钮和 Continue 按钮 - 流式消息完成后显示 -->
108
- <div v-if="message.showFeedbackAndContinue" class="feedback-and-continue">
109
- <div class="feedback-actions">
110
- <img :src="message.isPlayingTTS ? volumeingIcon : volumeIcon"
111
- @click="handleMessageFeedback(message.isPlayingTTS ? 'volumeing' : 'volume', message)"
112
- :alt="message.isPlayingTTS ? '播放中' : '播放'" />
113
- <img :src="message.vote === 'upvote' ? praiseingIcon : praiseIcon"
114
- @click="handleMessageFeedback('upvote', message)"
115
- :alt="message.vote === 'upvote' ? '已点赞' : '点赞'" />
116
- <img :src="message.vote === 'downvote' ? trampleingIcon : trampleIcon"
117
- @click="handleMessageFeedback('downvote', message)"
118
- :alt="message.vote === 'downvote' ? '已不喜欢' : '不喜欢'" />
119
- </div>
120
- <button class="btn-continue" @click="handleContinueClick(message.id)">Continue</button>
121
- </div>
122
-
123
- </div>
124
- </div>
125
- </template>
126
-
127
- <!-- 用户消息:头像在右侧 -->
128
- <template v-else>
129
- <div class="message-wrapper user-message-wrapper">
130
- <div
131
- class="message-content"
132
- :style="mergedStyles.messages?.userMessage"
133
- :class="{ 'background-transparent': message.imageUrl }"
134
- >
135
- <!-- 图片消息 -->
136
- <div v-if="message.imageUrl" class="message-image">
137
- <img :src="message.imageUrl" alt="Question Image" @click="openImagePreview(message.imageUrl)" />
138
- </div>
139
-
140
- <!-- 文本消息 -->
141
- <div v-html="renderContent(message.content)"></div>
142
-
143
- <!-- 用户消息的选项 -->
144
- <div v-if="message.options && message.options.length > 0" class="options">
145
- <span
146
- v-for="(option, index) in message.options"
147
- :key="index"
148
- v-html="renderContent(option)"
149
- ></span>
150
- </div>
151
- </div>
152
- <div class="message-avatar user-avatar">
153
- <img
154
- :src="questionInfo?.avatar || config.userAvatar?.src || DEFAULT_CONFIG.userAvatar.src"
155
- :alt="config.userAvatar?.alt || DEFAULT_CONFIG.userAvatar.alt"
156
- :style="{
157
- width: config.userAvatar?.width || DEFAULT_CONFIG.userAvatar.width,
158
- height: config.userAvatar?.height || DEFAULT_CONFIG.userAvatar.height,
159
- borderRadius: config.userAvatar?.borderRadius || DEFAULT_CONFIG.userAvatar.borderRadius
160
- }"
161
- />
162
- </div>
163
- </div>
164
- </template>
165
-
166
- <!-- 讲题完成区域 - Was this helpful + 追问 + Snap another one -->
167
-
168
- <div v-if="message.showCompletionSection" class="completion-section">
169
- <!-- Was this solution helpful? -->
170
- <div class="helpful-section">
171
- <span class="helpful-text">Was this solution helpful?</span>
172
- <img :src="smilingIcon"
173
- @click="handleHelpfulReaction('upvote')"
174
- :class="{ 'voted': message.vote === 'upvote' }"
175
- :alt="message.vote === 'upvote' ? '已点赞' : '赞'" />
176
- <img :src="angerIcon"
177
- @click="handleHelpfulReaction('downvote')"
178
- :class="{ 'voted': message.vote === 'downvote' }"
179
- :alt="message.vote === 'downvote' ? '已踩' : '踩'" />
180
- </div>
181
-
182
- <!-- 追问问题列表 -->
183
- <div v-if="message.followUpQuestions && message.followUpQuestions.length > 0" class="follow-up-questions">
184
- <div
185
- v-for="(question, index) in message.followUpQuestions"
186
- :key="index"
187
- class="follow-up-item"
188
- @click="handleFollowUpClick(question, message.id)"
189
- >
190
- <img :src="starIcon" alt="追问" />
191
- <span class="question-text">{{ question }}</span>
192
- </div>
193
- </div>
194
-
195
- <!-- Snap Another One 按钮 -->
196
- <button class="btn-snap-another" @click="handleSnapAnother">
197
- <img :src="cameraIcon" alt="拍照" />
198
- Snap Another One
199
- </button>
200
- </div>
201
- </div>
202
- </div>
203
- </div>
204
-
205
- <!-- 长按反馈弹窗遮罩 -->
206
- <div
207
- v-if="showMessageFeedback"
208
- class="feedback-overlay"
209
- @click="closeFeedbackPopup"
210
- ></div>
211
-
212
- <!-- 长按反馈弹窗 -->
213
- <div
214
- v-if="showMessageFeedback"
215
- class="message-feedback-popup"
216
- :class="{ 'show-above': feedbackPosition.showAbove }"
217
- :style="{
218
- top: feedbackPosition.top + 'px',
219
- left: feedbackPosition.left + 'px'
220
- }"
221
- >
222
- <!-- 气泡小三角 -->
223
- <div class="popup-arrow"></div>
224
-
225
- <!-- 反馈按钮 -->
226
- <div class="popup-buttons">
227
- <img :src="getCurrentMessage()?.isPlayingTTS ? volumeingIcon : volumeIcon"
228
- @click="handleMessageFeedback(getCurrentMessage()?.isPlayingTTS ? 'volumeing' : 'volume', currentLongPressMessage)"
229
- :alt="getCurrentMessage()?.isPlayingTTS ? '播放中' : '播放'" />
230
- <img :src="getCurrentMessage()?.vote === 'upvote' ? praiseingIcon : praiseIcon"
231
- @click="handleMessageFeedback('upvote',currentLongPressMessage)"
232
- :alt="getCurrentMessage()?.vote === 'upvote' ? '已点赞' : '点赞'" />
233
- <img :src="getCurrentMessage()?.vote === 'downvote' ? trampleingIcon : trampleIcon"
234
- @click="handleMessageFeedback('downvote',currentLongPressMessage)"
235
- :alt="getCurrentMessage()?.vote === 'downvote' ? '已不喜欢' : '不喜欢'" />
236
- </div>
237
- </div>
238
-
239
- <!-- 输入区域 -->
240
- <div
241
- :class="['chat-input-container', config.customClasses?.inputContainer]"
242
- :style="mergedStyles.inputContainer as StyleValue"
243
- >
244
- <!-- 快捷回复按钮 -->
245
- <div
246
- v-if="config.feedback?.show !== false && feedbackButtons.length > 0 && isQuickReply"
247
- class="feedback-buttons"
248
- :class="{ disabled: isAIResponding }"
249
- :style="(config.feedback?.containerStyle || {}) as StyleValue"
250
- >
251
- <button
252
- v-for="button in feedbackButtons"
253
- :key="button.id"
254
- class="feedback-btn"
255
- :style="button.style as StyleValue"
256
- @click="handleQuickReply(button.text)"
257
- :disabled="isAIResponding"
258
- >
259
- <img
260
- v-if="button.icon"
261
- :src="button.icon"
262
- alt=""
263
- style="width: 24px; height: 24px; vertical-align: middle; margin-right: 8px;"
264
- />
265
- {{ button.text }}
266
- </button>
267
- </div>
268
-
269
- <!-- 输入框 -->
270
- <div class="input-wrapper" :style="inputWrapperStyle">
271
- <input
272
- type="text"
273
- v-model="inputMessage"
274
- :placeholder="config.input?.placeholder || DEFAULT_CONFIG.input.placeholder"
275
- @keyup.enter="handleSendMessage"
276
- class="chat-input"
277
- :style="inputStyle"
278
- :disabled="isAIResponding"
279
- />
280
- <div
281
- class="send-button"
282
- @click="handleSendMessage"
283
- :class="{ disabled: !inputMessage.trim() || isAIResponding }"
284
- :style="sendButtonStyle"
285
- >
286
- <img
287
- :src="config.input?.sendIcon || DEFAULT_CONFIG.input.sendIcon"
288
- alt="发送"
289
- :style="{
290
- width: config.input?.sendIconWidth || DEFAULT_CONFIG.input.sendIconWidth,
291
- height: config.input?.sendIconHeight || DEFAULT_CONFIG.input.sendIconHeight
292
- }"
293
- />
294
- </div>
295
- </div>
296
- </div>
297
- </template>
298
- <!--特殊题-->
299
- <SpecialQuestions v-else-if="questionInfo?.questionType === 1" @feedback="handleFeedback" :isDownVoted="isDownVoted" :solutionAnswer="solutionAnswerData" />
300
- </div>
301
-
302
- <ImagePreview :visible="showImagePreview" :image-url="previewImageUrl" @close="closeImagePreview" />
303
- <Dialogs :dialogType="dialogType" :visible="isDialogVisible" :feedbackType="feedbackType" :currentLongPressMessageId="currentLongPressMessageId" @close="handleClose" />
304
- </template>
305
-
306
- <script lang="ts" setup>
307
- import { ref, computed, nextTick, onMounted, watch } from 'vue'
308
- import type { StyleValue } from 'vue'
309
- import { renderLatex } from './utils/latex'
310
- import {
311
- mergeConfig,
312
- mergeStyles,
313
- getMergedAIInfoStyles,
314
- getFeedbackButtons,
315
- getInputWrapperStyle,
316
- getInputStyle,
317
- getSendButtonStyle
318
- } from './utils/mergeConfig'
319
- import {
320
- IMessage,
321
- IAIChatConfig,
322
- IFeedbackButton, DEFAULT_CONFIG
323
- } from './types'
324
- import SpecialQuestions from './component/SpecialQuestions.vue'
325
- import Dialogs from './component/Dialogs.vue'
326
- import ImagePreview from '../ImagePreview.vue'
327
- import { Typewriter } from '../../utils/typewriter'
328
- import { explainQuestionSSE } from '../../utils/useSSE'
329
- import { feedbackApi } from '../../utils/request'
330
- import { showMessageToast } from '../../utils/messageToast'
331
- import { ttsPlayer, playTTSAudio, playMessageTTS, stopMessageTTS } from '../../utils/tts'
332
-
333
- import volumeIcon from '@/assets/images/volume-icon.png'
334
- import volumeingIcon from '@/assets/images/volumeing-icon.png'
335
- import praiseIcon from '@/assets/images/praise-icon.png'
336
- import praiseingIcon from '@/assets/images/praiseing-icon.png'
337
- import trampleIcon from '@/assets/images/trample-icon.png'
338
- import trampleingIcon from '@/assets/images/trampleing-icon.png'
339
- import smilingIcon from '@/assets/images/smiling.png'
340
- import angerIcon from '@/assets/images/anger.png'
341
- import starIcon from '@/assets/images/star-icon.png'
342
- import cameraIcon from '@/assets/images/Camera-icon.png'
343
-
344
- // Props定义
345
- const props = withDefaults(defineProps<{
346
- config: IAIChatConfig
347
- questionInfo: any
348
- }>(), {
349
- config: () => ({}),
350
- questionInfo: () => ({})
351
- })
352
-
353
- // Emits定义
354
- const emit = defineEmits<{
355
- messageSend: [message: string]
356
- feedback: [feedbackId: string]
357
- muteToggle: [isMuted: boolean]
358
- 'not-my-question': []
359
- 'snap-another': []
360
- }>()
361
-
362
- // 响应式数据
363
- const isSoundOn = ref(true) // 默认开启声音
364
- const inputMessage = ref('') // 输入框内容
365
- const messages = ref<IMessage[]>([]) // 消息列表
366
- const messagesContainer = ref<HTMLElement>() // 消息容器
367
- const isDialogVisible = ref(false) // 弹窗是否显示
368
- const dialogType = ref<'insolubility' | 'feedback'>('insolubility') // 弹窗类型
369
- const feedbackType = ref<'angry' | 'thumbsDown'>('angry') // 反馈类型
370
- const currentTypewriter = ref<Typewriter | null>(null) // 打字机
371
- const currentStreamAbort = ref<(() => void) | null>(null) // 当前流的中止函数
372
- const questionConfirmMessageId = ref<string>('') // 用于存储需要确认的问题消息ID
373
- const isDownVoted = ref(false) // 是否已不喜欢
374
- const isQuickReply = ref(false) // 是否是快捷回复
375
-
376
- // 长按反馈相关
377
- const showMessageFeedback = ref(false)
378
- const feedbackPosition = ref({ top: 0, left: 0, showAbove: false })
379
- const currentLongPressMessage = ref<any>(null)
380
- const currentLongPressMessageId = ref<string>('')
381
-
382
- // 图片预览相关
383
- const showImagePreview = ref(false)
384
- const previewImageUrl = ref('')
385
- let longPressTimer: any = null
386
-
387
- // AI响应状态(用于禁用输入、长按等操作)
388
- const isAIResponding = ref(true)
389
- const solutionAnswerData = ref<{ solution: any, answer: any }>({ solution: {}, answer: '' })
390
-
391
- const questionInfo = computed(() => {
392
- sessionStorage.setItem('questionInfo', JSON.stringify(props.questionInfo))
393
- return props.questionInfo
394
- })
395
-
396
- // 合并配置
397
- const config = computed<IAIChatConfig>(() => mergeConfig(props.config))
398
-
399
- // 合并样式
400
- const mergedStyles = computed(() => mergeStyles(props.config))
401
-
402
- // AI信息样式
403
- const mergedAIInfoStyles = computed(() => getMergedAIInfoStyles(mergeConfig(props.config)))
404
-
405
- // 快捷回复按钮
406
- const feedbackButtons = computed<IFeedbackButton[]>(() => getFeedbackButtons(mergeConfig(props.config)))
407
-
408
- // 输入框样式
409
- const inputWrapperStyle = computed(() => getInputWrapperStyle(mergeConfig(props.config)))
410
-
411
- const inputStyle = computed(() => getInputStyle())
412
-
413
- const sendButtonStyle = computed(() => getSendButtonStyle())
414
-
415
- // 内容渲染
416
- const renderContent = (content: string): string => {
417
- return config.value.enableLatex ? renderLatex(content) : content
418
- }
419
-
420
- // 全局声音开关
421
- const handleMuteToggle = () => {
422
- isSoundOn.value = !isSoundOn.value
423
-
424
- // 控制TTS播放
425
- ttsPlayer.setMuted(!isSoundOn.value)
426
-
427
- // 调用配置中的回调
428
- config.value.muteButton?.onClick?.(isSoundOn.value)
429
- console.log('AI聊天声音状态:', isSoundOn.value ? '静音' : '开启')
430
- }
431
-
432
- const handleSendMessage = () => {
433
- if (!inputMessage.value.trim()) return
434
- const message = inputMessage.value.trim()
435
-
436
- // 添加用户消息
437
- addMessage({
438
- id: `user_${Date.now()}`,
439
- type: 'user',
440
- content: message,
441
- timestamp: Date.now()
442
- })
443
-
444
- // 清空输入框
445
- inputMessage.value = ''
446
-
447
- // 调用配置中的回调
448
- config.value.input?.onSend?.(message)
449
-
450
- // 触发事件
451
- emit('messageSend', message)
452
- // 发送消息
453
- setTimeout(() => sendStreamMessage(message), 300)
454
- }
455
-
456
- // 辅助函数:查找消息
457
- const findMessage = (id: string) => messages.value.find(m => m.id === id)
458
-
459
- // 获取当前长按消息
460
- const getCurrentMessage = () => findMessage(currentLongPressMessageId.value)
461
-
462
- // 通用流式消息处理函数
463
- const sendStreamMessage = (content: string, isFollowUp = false) => {
464
- if (!props.questionInfo?.sessionId) {
465
- console.error('Missing sessionId for streaming')
466
- return
467
- }
468
-
469
- isAIResponding.value = true
470
-
471
- const streamMessageId = `ai_${Date.now()}`
472
- const streamMessage: IMessage = {
473
- id: streamMessageId,
474
- type: 'ai',
475
- content: '',
476
- showFeedbackAndContinue: false,
477
- isStreaming: true,
478
- timestamp: Date.now()
479
- }
480
-
481
- messages.value.push(streamMessage)
482
- nextTick(() => scrollToBottom())
483
-
484
- const handleSSEMessage = (e: any) => {
485
- const msg = findMessage(streamMessageId)
486
- if (!msg) return
487
-
488
- if (e.event === 'finish') {
489
- msg.isStreaming = false
490
- msg.showFeedbackAndContinue = !isFollowUp
491
- currentStreamAbort.value = null
492
- isAIResponding.value = false
493
- } else if (e.event === 'complete') {
494
- msg.isStreaming = false
495
- msg.showCompletionSection = true
496
- if (e.data?.followUpQuestions) {
497
- msg.followUpQuestions = e.data.followUpQuestions
498
- }
499
- currentStreamAbort.value = null
500
- isAIResponding.value = false
501
- } else if (e.data) {
502
- const newContent = typeof e.data === 'string' ? e.data : e.data.content || ''
503
- msg.content += newContent
504
-
505
- // 请求并播放TTS音频
506
- if (newContent && isSoundOn.value) {
507
- const sessionDetailId = e.msgId || e.id || streamMessageId
508
- playTTSAudio(
509
- props.questionInfo.sessionId,
510
- sessionDetailId,
511
- newContent,
512
- !isSoundOn.value
513
- ).catch(err => console.error('TTS playback error:', err))
514
- }
515
-
516
- nextTick(() => scrollToBottom())
517
- }
518
- }
519
-
520
- const handleSSEError = (error: any) => {
521
- console.error('SSE Error:', error)
522
- const msg = findMessage(streamMessageId)
523
- if (msg) {
524
- msg.isStreaming = false
525
- msg.content += '\n\n[Error: Failed to receive response]'
526
- }
527
- currentStreamAbort.value = null
528
- isAIResponding.value = false
529
- }
530
-
531
- const handleSSEClose = () => {
532
- const msg = findMessage(streamMessageId)
533
- if (msg) {
534
- msg.isStreaming = false
535
- if (!isFollowUp) {
536
- msg.showFeedbackAndContinue = false
537
- } else {
538
- msg.showFeedbackOnly = true
539
- }
540
- }
541
- currentStreamAbort.value = null
542
- isAIResponding.value = false
543
- }
544
-
545
- const { startSSE, abort } = explainQuestionSSE(
546
- {
547
- sessionId: props.questionInfo.sessionId,
548
- content,
549
- recommendQuestions: false
550
- },
551
- handleSSEMessage,
552
- handleSSEError,
553
- handleSSEClose
554
- )
555
-
556
- currentStreamAbort.value = abort
557
- startSSE()
558
- }
559
-
560
- // 公共方法
561
- const addMessage = (message: IMessage) => {
562
- messages.value.push(message)
563
- nextTick(() => {
564
- scrollToBottom()
565
- })
566
- }
567
-
568
- // 添加AI消息
569
- const addAIMessage = (content: string, options?: string[]) => {
570
- addMessage({
571
- id: `ai_${Date.now()}`,
572
- type: 'ai',
573
- content,
574
- options,
575
- timestamp: Date.now()
576
- })
577
- }
578
-
579
- // 添加用户消息
580
- const addUserMessage = (content: string, options?: string[]) => {
581
- addMessage({
582
- id: `user_${Date.now()}`,
583
- type: 'user',
584
- content,
585
- options,
586
- timestamp: Date.now()
587
- })
588
- }
589
-
590
- // 清空消息
591
- const clearMessages = () => messages.value = []
592
-
593
- // 滚动到底部
594
- const scrollToBottom = () => {
595
- if (messagesContainer.value) {
596
- messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
597
- }
598
- }
599
-
600
- // 打字机效果发送AI消息
601
- const typeAIMessage = (content: string, onComplete?: () => void, disableLongPress?: boolean) => {
602
- const messageId = `ai_${Date.now()}`
603
- const message: IMessage = {
604
- id: messageId,
605
- type: 'ai',
606
- content: '',
607
- isTyping: true,
608
- timestamp: Date.now(),
609
- disableLongPress: disableLongPress || false
610
- }
611
-
612
- messages.value.push(message)
613
- nextTick(() => scrollToBottom())
614
-
615
- // 创建打字机实例
616
- currentTypewriter.value = new Typewriter({
617
- text: content,
618
- wordDelay: 10, // 每个单词10ms
619
- onUpdate: (displayedText) => {
620
- const msg = findMessage(messageId)
621
- if (msg) {
622
- msg.content = displayedText
623
- nextTick(() => scrollToBottom())
624
- }
625
- },
626
- onComplete: () => {
627
- const msg = findMessage(messageId)
628
- if (msg) {
629
- msg.isTyping = false
630
- }
631
- currentTypewriter.value = null
632
- onComplete?.()
633
- }
634
- })
635
-
636
- currentTypewriter.value.start()
637
- }
638
-
639
- // 图片预览相关函数
640
- const openImagePreview = (imageUrl: string) => { // 打开图片预览
641
- previewImageUrl.value = imageUrl
642
- showImagePreview.value = true
643
- }
644
-
645
- const closeImagePreview = () => { // 关闭图片预览
646
- showImagePreview.value = false
647
- previewImageUrl.value = ''
648
- }
649
-
650
- // 处理 Continue 按钮点击
651
- const handleContinueClick = (messageId: string) => {
652
- const msg = findMessage(messageId)
653
- if (msg) msg.showFeedbackAndContinue = false
654
-
655
- addUserMessage('Continue Explaining')
656
- setTimeout(() => sendStreamMessage('Continue Explaining'), 300)
657
- }
658
-
659
- // 处理快捷回复按钮
660
- const handleQuickReply = (reply: string) => {
661
- // 查找最后一条AI消息并隐藏反馈按钮
662
- for (let i = messages.value.length - 1; i >= 0; i--) {
663
- if (messages.value[i].type === 'ai') {
664
- messages.value[i].showFeedbackAndContinue = false
665
- messages.value[i].showCompletionSection = false
666
- break
667
- }
668
- }
669
-
670
- addUserMessage(reply)
671
- setTimeout(() => sendStreamMessage(reply), 300)
672
- }
673
-
674
- // 处理追问点击
675
- const handleFollowUpClick = (question: string, messageId: string) => {
676
- const msg = findMessage(messageId)
677
- if (msg) msg.showCompletionSection = false
678
-
679
- addUserMessage(question)
680
- setTimeout(() => sendStreamMessage(question, true), 300)
681
- }
682
-
683
- // 处理 Snap Another One 按钮
684
- const handleSnapAnother = () => emit('snap-another')
685
-
686
- // 处理"Start Explaining"按钮
687
- const handleStartExplaining = () => {
688
- // 隐藏AI消息上的按钮
689
- const msg = findMessage(questionConfirmMessageId.value)
690
- if (msg) msg.showButtons = false
691
- isAIResponding.value = false
692
- // 发送用户消息
693
- addUserMessage('Start Explaining')
694
- // 开始流式解释
695
- setTimeout(() => sendStreamMessage('Start Explaining'), 300)
696
- isQuickReply.value = true
697
- }
698
-
699
- // 初始化普通题的对话流程
700
- const initNormalQuestionFlow = () => {
701
- const isNormalQuestion = props.questionInfo?.questionType === 0 || props.questionInfo?.questionType === '0'
702
- if (isNormalQuestion) {
703
- }
704
-
705
- // 清空消息
706
- messages.value = []
707
-
708
- // 1. 用户消息1:文本 "Could you help me solve this question?"
709
- const userTextMessage: IMessage = {
710
- id: `user_${Date.now()}`,
711
- type: 'user',
712
- content: 'Could you help me solve this question?',
713
- timestamp: Date.now()
714
- }
715
- messages.value.push(userTextMessage)
716
- nextTick(() => scrollToBottom())
717
-
718
- // 2. 用户消息2:图片
719
- setTimeout(() => {
720
- const imageUrl = props.questionInfo?.imgUrl || props.questionInfo?.imagePath
721
-
722
- if (imageUrl) {
723
- const userImageMessage: IMessage = {
724
- id: `user_${Date.now()}`,
725
- type: 'user',
726
- content: '',
727
- imageUrl: imageUrl,
728
- timestamp: Date.now()
729
- }
730
- messages.value.push(userImageMessage)
731
- nextTick(() => scrollToBottom())
732
- }
733
- if (isNormalQuestion) {
734
- dialogType.value = 'insolubility'
735
- isDialogVisible.value = true
736
- return
737
- }
738
- // 3. AI打字机回复:Sure, Please confirm the question.
739
- setTimeout(() => {
740
- typeAIMessage('Sure, Please confirm the question.', () => {
741
- playTTSAudio(
742
- props.questionInfo.sessionId,
743
- questionConfirmMessageId.value,
744
- 'Sure, Please confirm the question.',
745
- !isSoundOn.value
746
- ).catch(err => console.error('TTS playback error:', err))
747
- // 4. AI消息:orcText 打字机效果 + 操作按钮(打字机完成后显示)
748
- setTimeout(() => {
749
- const orcText = props.questionInfo?.orcText || 'Question text here'
750
- questionConfirmMessageId.value = `ai_${Date.now()}`
751
-
752
- // 先添加不带按钮的AI消息
753
- const confirmMessage: IMessage = {
754
- id: questionConfirmMessageId.value,
755
- type: 'ai',
756
- content: '',
757
- showButtons: false,
758
- isTyping: true,
759
- timestamp: Date.now(),
760
- disableLongPress: true
761
- }
762
- messages.value.push(confirmMessage)
763
- nextTick(() => scrollToBottom())
764
-
765
- // 启动打字机效果显示 orcText
766
- currentTypewriter.value = new Typewriter({
767
- text: orcText,
768
- wordDelay: 10,
769
- onUpdate: (displayedText) => {
770
- const msg = findMessage(questionConfirmMessageId.value)
771
- if (msg) {
772
- msg.content = displayedText
773
- nextTick(() => scrollToBottom())
774
- }
775
- },
776
- onComplete: () => {
777
- const msg = findMessage(questionConfirmMessageId.value)
778
- if (msg) {
779
- msg.isTyping = false
780
- // 打字机完成后,显示按钮
781
- msg.showButtons = true
782
- nextTick(() => scrollToBottom())
783
- }
784
- currentTypewriter.value = null
785
- }
786
- })
787
-
788
- currentTypewriter.value.start()
789
- }, 50)
790
- }, true)
791
- }, 50)
792
- }, 10)
793
- }
794
-
795
-
796
- // 反馈相关方法
797
-
798
- const handleClose = (messageId?: string) => {
799
- if (messageId) {
800
- const message = findMessage(messageId)
801
- if (message) {
802
- message.vote = 'downvote'
803
- message.isDownvote = 1
804
- }
805
- }
806
- if (feedbackType.value === 'angry') isDownVoted.value = true
807
- isDialogVisible.value = false
808
- }
809
-
810
- // 特殊题反馈
811
- const handleFeedback = async (type?: 'upvote' | 'downvote', isDownVoted?: boolean) => {
812
- if (!type) return
813
- if (type === 'upvote') {
814
- await feedbackApi({ subjectId: props.questionInfo?.sessionId, vote: 'upvote' })
815
- } else if (type === 'downvote') {
816
- if (isDownVoted) {
817
- await feedbackApi({ subjectId: props.questionInfo?.sessionId, vote: 'downvote' })
818
- return
819
- }
820
- isDialogVisible.value = true
821
- dialogType.value = 'feedback'
822
- feedbackType.value = 'angry'
823
- }
824
- }
825
-
826
- // 关闭长按反馈弹窗
827
- const closeFeedbackPopup = () => {
828
- showMessageFeedback.value = false
829
- currentLongPressMessageId.value = ''
830
- }
831
-
832
- // 处理消息下方的反馈按钮
833
- const handleMessageFeedback = async (type: 'upvote' | 'downvote' | 'volume' | 'volumeing', message: any) => {
834
- const targetMessageId = message.id || currentLongPressMessageId.value
835
- const msg = findMessage(targetMessageId)
836
-
837
- if (type === 'volume' || type === 'volumeing') { // 播放/停止TTS
838
- if (!msg) return
839
-
840
- // 如果正在播放,则停止
841
- if (msg.isPlayingTTS) {
842
- stopMessageTTS(targetMessageId)
843
- msg.isPlayingTTS = false
844
- } else {
845
- // 开始播放
846
- msg.isPlayingTTS = true
847
- await playMessageTTS(
848
- props.questionInfo.sessionId,
849
- targetMessageId,
850
- msg.content,
851
- () => {
852
- // 播放开始
853
- console.log('[TTS] Started playing message:', targetMessageId)
854
- },
855
- () => {
856
- // 播放结束
857
- if (msg) {
858
- msg.isPlayingTTS = false
859
- }
860
- console.log('[TTS] Finished playing message:', targetMessageId)
861
- }
862
- )
863
- }
864
- } else if (type === 'upvote') { // 点赞
865
- await feedbackApi({ subjectId: targetMessageId, vote: 'upvote' })
866
- if (msg) {
867
- msg.vote = 'upvote'
868
- }
869
- } else if (type === 'downvote') { // 不喜欢
870
- if(message.isDownvote === 1) {
871
- await feedbackApi({ subjectId: targetMessageId, vote: 'downvote' })
872
- if (msg) {
873
- msg.vote = 'downvote'
874
- }
875
- return
876
- }
877
- isDialogVisible.value = true
878
- dialogType.value = 'feedback'
879
- feedbackType.value = 'thumbsDown'
880
- currentLongPressMessageId.value = targetMessageId
881
- }
882
- }
883
-
884
- // 处理"Not my question"按钮
885
- const handleNotMyQuestion = async () => {
886
- await feedbackApi({ subjectId: props.questionInfo?.sessionId, question: 'Not my question' })
887
- showMessageToast()
888
- }
889
-
890
- // 处理 "Was this helpful" 的反馈
891
- const handleHelpfulReaction = async (type: 'upvote' | 'downvote') => {
892
-
893
- if (type === 'upvote') { // 点赞
894
- await feedbackApi({ subjectId: props.questionInfo?.sessionId, vote: 'upvote' })
895
- } else if (type === 'downvote') { // 不喜欢
896
- if (isDownVoted.value) {
897
- await feedbackApi({ subjectId: props.questionInfo?.sessionId, vote: 'downvote' })
898
- return
899
- }
900
- isDialogVisible.value = true
901
- dialogType.value = 'feedback'
902
- feedbackType.value = 'angry'
903
- }
904
- }
905
-
906
- // 长按开始
907
- const handleLongPressStart = (event: TouchEvent | MouseEvent, message: any) => {
908
- longPressTimer = setTimeout(() => showFeedbackPopup(event, message), 500)
909
- }
910
-
911
- // 长按结束
912
- const handleLongPressEnd = () => {
913
- if (longPressTimer) {
914
- clearTimeout(longPressTimer)
915
- longPressTimer = null
916
- }
917
- }
918
-
919
- // 显示反馈弹窗
920
- const showFeedbackPopup = (event: TouchEvent | MouseEvent, message: any) => {
921
- const target = event.target as HTMLElement
922
- const messageElement = target.closest('.message-content')
923
- if (!messageElement) return
924
-
925
- const rect = messageElement.getBoundingClientRect()
926
- const showAbove = rect.bottom > window.innerHeight / 2
927
- const popupWidth = 176
928
- const centerLeft = rect.left + (rect.width / 2) - (popupWidth / 2)
929
-
930
- feedbackPosition.value = {
931
- top: showAbove ? rect.top - 60 : rect.bottom + 5,
932
- left: Math.max(10, Math.min(centerLeft, window.innerWidth - popupWidth - 10)),
933
- showAbove
934
- }
935
- currentLongPressMessage.value = message
936
- currentLongPressMessageId.value = message.id
937
- showMessageFeedback.value = true
938
- }
939
-
940
- onMounted(() => {})
941
-
942
- // 监听 questionInfo 变化,初始化普通题流程
943
- watch(() => props.questionInfo, (newVal) => {
944
- if (newVal) {
945
- initNormalQuestionFlow()
946
- }
947
- }, { immediate: true, deep: true })
948
-
949
- // 暴露方法
950
- defineExpose({
951
- addMessage,
952
- addAIMessage,
953
- addUserMessage,
954
- clearMessages,
955
- scrollToBottom,
956
- messages: messages.value
957
- })
958
- </script>
959
-
960
- <style scoped lang="scss">
961
- @import './style.scss';
962
- </style>
963
-