From 867454eb841caffe3a627eb347b44dd2206a7d50 Mon Sep 17 00:00:00 2001 From: Marcus <1922576605@qq.com> Date: Sun, 29 Jun 2025 16:09:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=A8=A1=E5=9E=8B=E6=80=9D=E7=BB=B4?= =?UTF-8?q?=E9=93=BE=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/constants/model_data.py | 4 +- web/components.d.ts | 2 + web/src/interfaces/chat_service.ts | 1 + web/src/services/chat_service.ts | 49 ++++++++++++++++++++++--- web/src/stores/chat_store.ts | 50 +++++++++++++++++-------- web/src/views/CommunityView.vue | 57 ++++++++++++++++++++++++++++- 6 files changed, 140 insertions(+), 23 deletions(-) diff --git a/backend/app/constants/model_data.py b/backend/app/constants/model_data.py index c2761bd..cf1884c 100644 --- a/backend/app/constants/model_data.py +++ b/backend/app/constants/model_data.py @@ -19,7 +19,8 @@ MODEL_DATA = [ { "vendor": "Anthropic", "models": [ - {"model_id": "claude-sonnet-4-thinking", "model_name": "Claude Sonnet 4 thinking", "model_type": "reasoning"}, + {"model_id": "claude-sonnet-4-thinking", "model_name": "Claude Sonnet 4 thinking", + "model_type": "reasoning"}, {"model_id": "claude-sonnet-4", "model_name": "Claude Sonnet 4", "model_type": "text"}, ] }, @@ -27,6 +28,7 @@ MODEL_DATA = [ "vendor": "硅基流动", "models": [ {"model_id": "deepseek-v3", "model_name": "DeepSeek V3", "model_type": "text"}, + {"model_id": "deepseek-r1", "model_name": "DeepSeek R1", "model_type": "reasoning"}, ] } ] diff --git a/web/components.d.ts b/web/components.d.ts index 48e64e4..66d4ee6 100644 --- a/web/components.d.ts +++ b/web/components.d.ts @@ -10,6 +10,8 @@ declare module 'vue' { Avatar: typeof import('./src/components/avatar.vue')['default'] Markdown: typeof import('./src/components/markdown.vue')['default'] NButton: typeof import('naive-ui')['NButton'] + NCollapse: typeof import('naive-ui')['NCollapse'] + NCollapseItem: typeof import('naive-ui')['NCollapseItem'] NConfigProvider: typeof import('naive-ui')['NConfigProvider'] NDivider: typeof import('naive-ui')['NDivider'] NImage: typeof import('naive-ui')['NImage'] diff --git a/web/src/interfaces/chat_service.ts b/web/src/interfaces/chat_service.ts index 7d62063..82de380 100644 --- a/web/src/interfaces/chat_service.ts +++ b/web/src/interfaces/chat_service.ts @@ -8,6 +8,7 @@ export interface IChatWithLLMRequest { export interface Message { content?: string; + thinking?: string; role?: string; usage?: UsageInfo; [property: string]: any; diff --git a/web/src/services/chat_service.ts b/web/src/services/chat_service.ts index 8d6afc5..4957670 100644 --- a/web/src/services/chat_service.ts +++ b/web/src/services/chat_service.ts @@ -11,11 +11,14 @@ export class ChatService { accessToken: string, request: IChatWithLLMRequest, onProgress: (content: string) => void, - getUsageInfo: (object: UsageInfo) => void = () => {} + getUsageInfo: (object: UsageInfo) => void = () => {}, + getThinking: (thinkingContent: string) => void ) { let response; let buffer = ""; let accumulatedContent = ""; + let thinking = false; + let thinkingContent = ""; try { response = await fetch("/v1/chat/completions", { method: "POST", @@ -67,10 +70,46 @@ export class ChatService { // 处理内容 if (data.choices?.[0]?.delta?.content) { - // 累积内容 - accumulatedContent += data.choices[0].delta.content; - // 触发回调 - onProgress(accumulatedContent); + const content = data.choices[0].delta.content; + + if (content.includes("")) { + thinking = true; + // 只处理前的内容 + const [beforeThink, afterThink] = content.split(""); + if (beforeThink) { + accumulatedContent += beforeThink; + onProgress(accumulatedContent); + } + thinkingContent = ""; + if (afterThink) { + thinkingContent += afterThink; + getThinking(thinkingContent); + } + continue; + } + + if (content.includes("")) { + thinking = false; + // 只处理后的内容 + const [beforeEndThink, afterEndThink] = + content.split(""); + thinkingContent += beforeEndThink; + getThinking(thinkingContent); + if (afterEndThink) { + accumulatedContent += afterEndThink; + onProgress(accumulatedContent); + } + thinkingContent = ""; + continue; + } + + if (thinking) { + thinkingContent += content; + getThinking(thinkingContent); + } else { + accumulatedContent += content; + onProgress(accumulatedContent); + } } // 处理使用信息 diff --git a/web/src/stores/chat_store.ts b/web/src/stores/chat_store.ts index a5cd616..f171825 100644 --- a/web/src/stores/chat_store.ts +++ b/web/src/stores/chat_store.ts @@ -14,14 +14,17 @@ export const useChatStore = defineStore("chat", () => { const historyMessages = ref([]); // 是否正在响应 const completing = ref(false); + // 是否正在思考 + const thinking = ref(false); // 在线人数 const onlineCount = ref(0); // 与 LLM 聊天 const chatWithLLM = async ( request: IChatWithLLMRequest, - onProgress: (content: string) => void, // 接收进度回调 - getUsageInfo: (object: UsageInfo) => void = () => {} + onProgress: (content: string) => void, // 接收内容进度回调 + getUsageInfo: (object: UsageInfo) => void = () => {}, // 接收使用信息回调 + getThinking: (thinkingContent: string) => void = () => {} // 接收思维链内容回调 ) => { if (completing.value) throw new Error("正在响应中"); @@ -35,6 +38,9 @@ export const useChatStore = defineStore("chat", () => { }, (object: UsageInfo) => { getUsageInfo(object); + }, + (thinkingContent: string) => { + getThinking(thinkingContent); } ); } catch (error) { @@ -60,6 +66,20 @@ export const useChatStore = defineStore("chat", () => { historyMessages.value = []; }; + // 确保最后一条消息是助手消息,如果最后一条消息不是,就加一条空的占位,不然后面的思维链会丢失 + const ensureAssistantMessage = () => { + if ( + historyMessages.value.length === 0 || + historyMessages.value[historyMessages.value.length - 1].role !== + "assistant" + ) { + historyMessages.value.push({ + role: "assistant", + content: "" + }); + } + }; + watch( historyMessages, (newVal) => { @@ -72,23 +92,15 @@ export const useChatStore = defineStore("chat", () => { messages: newVal, model: modelInfo.value?.model_id }, + // 处理进度回调,文本 (content) => { - // 处理进度回调 - if ( - historyMessages.value.length === 0 || - historyMessages.value[historyMessages.value.length - 1].role !== - "assistant" - ) { - historyMessages.value.push({ - role: "assistant", - content: "" - }); - } + ensureAssistantMessage(); + thinking.value = false; historyMessages.value[historyMessages.value.length - 1].content = content; }, + // 处理使用usage信息回调 (usageInfo: UsageInfo) => { - // 处理使用usage信息回调 // 如果最后一条消息是助手的回复,则更新使用信息 if ( historyMessages.value.length > 0 && @@ -98,6 +110,13 @@ export const useChatStore = defineStore("chat", () => { historyMessages.value[historyMessages.value.length - 1].usage = usageInfo; } + }, + // 处理思维链内容回调 + (thinkingContent: string) => { + ensureAssistantMessage(); + thinking.value = true; + historyMessages.value[historyMessages.value.length - 1].thinking = + thinkingContent; } ); } @@ -129,6 +148,7 @@ export const useChatStore = defineStore("chat", () => { getModelList, modelList, modelInfo, - onlineCount + onlineCount, + thinking }; }); diff --git a/web/src/views/CommunityView.vue b/web/src/views/CommunityView.vue index efb4876..ba5d1a8 100644 --- a/web/src/views/CommunityView.vue +++ b/web/src/views/CommunityView.vue @@ -13,7 +13,7 @@ import markdown from "@/components/markdown.vue"; import { useAsrStore, useChatStore, useLayoutStore } from "@/stores"; const chatStore = useChatStore(); -const { historyMessages, completing, modelList, modelInfo } = +const { historyMessages, completing, modelList, modelInfo, thinking } = storeToRefs(chatStore); const asrStore = useAsrStore(); const { isRecording } = storeToRefs(asrStore); @@ -23,6 +23,38 @@ const { hiddenLeftSidebar, simpleMode } = storeToRefs(layoutStore); const inputData = ref(""); const scrollbarRef = ref(null); const options = ref>([]); +// NCollapse 组件的折叠状态 +const collapseActive = ref( + historyMessages.value.map((_, idx) => String(idx)) +); + +const getName = (idx: number) => String(idx); + +watch( + historyMessages, + (newVal, oldVal) => { + // 取所有name + const newNames = newVal.map((_, idx) => getName(idx)); + const oldNames = oldVal ? oldVal.map((_, idx) => getName(idx)) : []; + // 找出新增的name + const addedNames = newNames.filter((name) => !oldNames.includes(name)); + // 保留原有已展开项 + const currentActive = collapseActive.value.filter((name) => + newNames.includes(name) + ); + // 新增的默认展开 + collapseActive.value = [...currentActive, ...addedNames]; + }, + { deep: true } +); + +const handleItemHeaderClick = (name: string) => { + if (collapseActive.value.includes(name)) { + collapseActive.value = collapseActive.value.filter((n) => n !== name); + } else { + collapseActive.value.push(name); + } +}; // 处理选中模型的 ID const selectedModelId = computed({ @@ -111,11 +143,13 @@ onMounted(() => { +
+ { +
{{ msg.role === "user" ? "你:" : "助手:" }} +
{ Tokens: {{ msg.usage?.total_tokens }}
+ + +
+ +
+
+
+
@@ -184,7 +237,7 @@ onMounted(() => {