|
@@ -485,13 +485,20 @@
|
|
|
v-model="inputContent"
|
|
|
@keyup.enter="sendMessage"
|
|
|
placeholder="Type a message..."
|
|
|
- /><!-- @input="handleInputChange" -->
|
|
|
+ /><!-- @input="handleInputChange" :disabled="(!inputContent && attachedFiles.length === 0)"-->
|
|
|
<button
|
|
|
class="send-btn"
|
|
|
@click="sendMessage"
|
|
|
- :disabled="!inputContent && attachedFiles.length === 0"
|
|
|
+
|
|
|
>
|
|
|
- <img src="../assets/svg/up.svg" alt="send" />
|
|
|
+ <template v-if="isTyping">
|
|
|
+ <span class="loading-btn">
|
|
|
+ <span class="loading-square"></span>
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <img src="../assets/svg/up.svg" alt="send" />
|
|
|
+ </template>
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
@@ -807,6 +814,7 @@ const conversations = ref([
|
|
|
// 添加打字效果相关的状态
|
|
|
const typingSpeed = 50; // 打字速度(毫秒/字符)
|
|
|
const isTyping = ref(false);
|
|
|
+const typingTimeout = ref(null); // 添加打字超时引用
|
|
|
|
|
|
// 添加音频播放相关的响应式变量
|
|
|
const audioPlayer = ref(null);
|
|
@@ -1194,6 +1202,9 @@ const scrollToBottom = () => {
|
|
|
|
|
|
// 添加 session_id 的响应式引用
|
|
|
const session_id = ref("");
|
|
|
+// 添加取消控制器
|
|
|
+const abortController = ref(null);
|
|
|
+
|
|
|
// 修改发送消息方法
|
|
|
const sendMessage = async () => {
|
|
|
// 检查登录状态
|
|
@@ -1202,13 +1213,26 @@ const sendMessage = async () => {
|
|
|
router.push('/login');
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
+ if(isTyping.value){
|
|
|
+ isTyping.value = false;
|
|
|
+ // 如果存在正在进行的请求,取消它
|
|
|
+ if (abortController.value) {
|
|
|
+ abortController.value.abort();
|
|
|
+ abortController.value = null;
|
|
|
+ }
|
|
|
+ // 清除打字效果的定时器
|
|
|
+ if (typingTimeout.value) {
|
|
|
+ clearTimeout(typingTimeout.value);
|
|
|
+ typingTimeout.value = null;
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
if (
|
|
|
(!inputContent.value.trim() && attachedFiles.value.length === 0) ||
|
|
|
isTyping.value
|
|
|
)
|
|
|
return;
|
|
|
-
|
|
|
+ isTyping.value = true;
|
|
|
// 保存当前输入内容
|
|
|
const currentMessage = inputContent.value.trim();
|
|
|
|
|
@@ -1279,6 +1303,9 @@ const sendMessage = async () => {
|
|
|
/* scrollToBottom(); */
|
|
|
|
|
|
try {
|
|
|
+ // 创建新的 AbortController
|
|
|
+ abortController.value = new AbortController();
|
|
|
+
|
|
|
// 根据开关状态选择不同的 API 端点
|
|
|
const apiEndpoint = enableWebSearch.value
|
|
|
? `${import.meta.env.VITE_BASE_AI_API}/api/chat/web-search-llm/`
|
|
@@ -1300,7 +1327,8 @@ const sendMessage = async () => {
|
|
|
{
|
|
|
headers: {
|
|
|
'Authorization': `JWT ${localStorage.getItem('token')}`
|
|
|
- }
|
|
|
+ },
|
|
|
+ signal: abortController.value.signal
|
|
|
}
|
|
|
);
|
|
|
|
|
@@ -1339,18 +1367,32 @@ const sendMessage = async () => {
|
|
|
await typeMessage(aiMessage);
|
|
|
|
|
|
} catch (error) {
|
|
|
- console.error("发送消息失败:", error);
|
|
|
- messages.value = messages.value.filter(
|
|
|
- (msg) => msg.id !== tempAiMessage.id
|
|
|
- );
|
|
|
- messages.value.push({
|
|
|
- id: Date.now() + 2,
|
|
|
- role: "assistant",
|
|
|
- content: "Sorry, there was an error processing your request.",
|
|
|
- displayContent: "Sorry, there was an error processing your request.",
|
|
|
- });
|
|
|
+ if (error.name === 'CanceledError') {
|
|
|
+ console.log('请求已被取消');
|
|
|
+ messages.value = messages.value.filter(
|
|
|
+ (msg) => msg.id !== tempAiMessage.id
|
|
|
+ );
|
|
|
+ messages.value.push({
|
|
|
+ id: Date.now() + 2,
|
|
|
+ role: "assistant",
|
|
|
+ content: "生成已取消",
|
|
|
+ displayContent: "生成已取消",
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ console.error("发送消息失败:", error);
|
|
|
+ messages.value = messages.value.filter(
|
|
|
+ (msg) => msg.id !== tempAiMessage.id
|
|
|
+ );
|
|
|
+ messages.value.push({
|
|
|
+ id: Date.now() + 2,
|
|
|
+ role: "assistant",
|
|
|
+ content: "Sorry, there was an error processing your request.",
|
|
|
+ displayContent: "Sorry, there was an error processing your request.",
|
|
|
+ });
|
|
|
+ }
|
|
|
} finally {
|
|
|
isTyping.value = false;
|
|
|
+ abortController.value = null;
|
|
|
await nextTick();
|
|
|
/* scrollToBottom(); */
|
|
|
}
|
|
@@ -1398,14 +1440,15 @@ const typeMessage = async (message) => {
|
|
|
message.displayContent = ""; // 确保开始时是空的
|
|
|
|
|
|
const typeChar = () => {
|
|
|
- if (i < message.content.length) {
|
|
|
+ if (i < message.content.length && isTyping.value) {
|
|
|
message.displayContent = message.content.substring(0, i + 1);
|
|
|
i++;
|
|
|
- nextTick(() => {
|
|
|
- /* scrollToBottom(); */
|
|
|
- setTimeout(typeChar, delay);
|
|
|
- });
|
|
|
+ typingTimeout.value = setTimeout(typeChar, delay);
|
|
|
} else {
|
|
|
+ if (!isTyping.value) {
|
|
|
+ // 如果打字被取消,直接显示完整内容
|
|
|
+ message.displayContent = message.content;
|
|
|
+ }
|
|
|
resolve();
|
|
|
}
|
|
|
};
|
|
@@ -2190,7 +2233,6 @@ const updateCollapsedState = () => {
|
|
|
const handleDocClick = (doc) => {
|
|
|
currentDoc.value = doc;
|
|
|
};
|
|
|
-
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
@@ -2569,6 +2611,7 @@ const updateCollapsedState = () => {
|
|
|
cursor: pointer;
|
|
|
color: rgba(0, 0, 0, 0.45);
|
|
|
border-radius: 4px;
|
|
|
+ padding: 0;
|
|
|
}
|
|
|
|
|
|
.attachment-btn:hover,
|
|
@@ -2835,6 +2878,32 @@ const updateCollapsedState = () => {
|
|
|
transition: border 0.3s, background 0.3s; /* 替换 var(--ant-motion-duration-slow) */
|
|
|
border: 1px solid #f0f0f0; /* 替换合的 border 属性 */
|
|
|
}
|
|
|
+
|
|
|
+.loading-btn {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ background: #4d7cff;
|
|
|
+ border-radius: 50%;
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+
|
|
|
+ .loading-square {
|
|
|
+ width: 14px;
|
|
|
+ height: 14px;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 3px;
|
|
|
+ display: block;
|
|
|
+ animation: loading-square-blink 1s infinite;
|
|
|
+ }
|
|
|
+
|
|
|
+ @keyframes loading-square-blink {
|
|
|
+ 0%, 100% { opacity: 1; }
|
|
|
+ 50% { opacity: 0.5; }
|
|
|
+ }
|
|
|
+
|
|
|
:root {
|
|
|
--ant-color-bg-container: #ffffff;
|
|
|
--ant-border-radius-lg: 8px;
|