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.
- package/README.npm.md +322 -0
- package/dist/ai-chat-sdk.es.js +22610 -0
- package/dist/ai-chat-sdk.es.js.map +1 -0
- package/dist/ai-chat-sdk.umd.js +300 -0
- package/dist/ai-chat-sdk.umd.js.map +1 -0
- package/dist/style.css +1 -0
- package/dist/types/index.d.ts +48 -0
- package/package.json +58 -11
- package/.idea/GitCommitMessageStorage.xml +0 -8
- package/.idea/modules.xml +0 -8
- package/.idea/sg-paisou-web.iml +0 -12
- package/.idea/vcs.xml +0 -6
- package/.vscode/settings.json +0 -2
- package/auto-imports.d.ts +0 -10
- package/components.d.ts +0 -18
- package/index.html +0 -21
- package/src/App.vue +0 -38
- package/src/assets/images/Camera-icon.png +0 -0
- package/src/assets/images/anger.png +0 -0
- package/src/assets/images/answer-icon.png +0 -0
- package/src/assets/images/back.png +0 -0
- package/src/assets/images/bg-img.png +0 -0
- package/src/assets/images/collect.png +0 -0
- package/src/assets/images/collected.png +0 -0
- package/src/assets/images/empty-bookmark.png +0 -0
- package/src/assets/images/empty-history.png +0 -0
- package/src/assets/images/feedback-thinkie-img.png +0 -0
- package/src/assets/images/history.png +0 -0
- package/src/assets/images/image 5.png +0 -0
- package/src/assets/images/insolubility.png +0 -0
- package/src/assets/images/key-points.png +0 -0
- package/src/assets/images/photograph-icon.png +0 -0
- package/src/assets/images/praise-icon.png +0 -0
- package/src/assets/images/praiseing-icon.png +0 -0
- package/src/assets/images/question.png +0 -0
- package/src/assets/images/smiling.png +0 -0
- package/src/assets/images/solution-icon.png +0 -0
- package/src/assets/images/star-icon.png +0 -0
- package/src/assets/images/trample-icon.png +0 -0
- package/src/assets/images/trampleing-icon.png +0 -0
- package/src/assets/images/volume-icon.png +0 -0
- package/src/assets/images/volumeing-icon.png +0 -0
- package/src/components/AIChatSDK/AIChatComponent.vue +0 -963
- package/src/components/AIChatSDK/component/Dialogs.vue +0 -340
- package/src/components/AIChatSDK/component/SpecialQuestions.vue +0 -208
- package/src/components/AIChatSDK/index.ts +0 -146
- package/src/components/AIChatSDK/style.scss +0 -432
- package/src/components/AIChatSDK/utils/imageUtils.ts +0 -61
- package/src/components/AIChatSDK/utils/latex.ts +0 -34
- package/src/components/AIChatSDK/utils/mergeConfig.ts +0 -125
- package/src/components/ImagePreview.vue +0 -62
- package/src/components/PageHeader/index.vue +0 -121
- package/src/config.ts +0 -11
- package/src/env.d.ts +0 -11
- package/src/main.ts +0 -12
- package/src/router.ts +0 -20
- package/src/style.css +0 -33
- package/src/type.ts +0 -106
- package/src/utils/TTS_README.md +0 -232
- package/src/utils/bridge.ts +0 -42
- package/src/utils/index.ts +0 -8
- package/src/utils/listenOsEvent.ts +0 -3
- package/src/utils/messageToast.ts +0 -43
- package/src/utils/render.ts +0 -81
- package/src/utils/request.ts +0 -87
- package/src/utils/tts.ts +0 -319
- package/src/utils/typewriter.ts +0 -61
- package/src/utils/useSSE.ts +0 -113
- package/src/views/History/index.vue +0 -419
- package/src/views/QuestionChatPage/index.vue +0 -480
- package/src/vite-env.d.ts +0 -1
- package/tsconfig.app.json +0 -24
- package/tsconfig.json +0 -7
- package/tsconfig.node.json +0 -22
- package/vite.config.ts +0 -41
- /package/{public → dist}/mathjax/all-packages.js +0 -0
- /package/{public → dist}/mathjax/talEditorConfig.js +0 -0
- /package/{public → dist}/mathjax/tex-svg.js +0 -0
- /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
|
-
|