feat: 模型思维链展示
This commit is contained in:
@@ -19,7 +19,8 @@ MODEL_DATA = [
|
|||||||
{
|
{
|
||||||
"vendor": "Anthropic",
|
"vendor": "Anthropic",
|
||||||
"models": [
|
"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"},
|
{"model_id": "claude-sonnet-4", "model_name": "Claude Sonnet 4", "model_type": "text"},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -27,6 +28,7 @@ MODEL_DATA = [
|
|||||||
"vendor": "硅基流动",
|
"vendor": "硅基流动",
|
||||||
"models": [
|
"models": [
|
||||||
{"model_id": "deepseek-v3", "model_name": "DeepSeek V3", "model_type": "text"},
|
{"model_id": "deepseek-v3", "model_name": "DeepSeek V3", "model_type": "text"},
|
||||||
|
{"model_id": "deepseek-r1", "model_name": "DeepSeek R1", "model_type": "reasoning"},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
2
web/components.d.ts
vendored
2
web/components.d.ts
vendored
@@ -10,6 +10,8 @@ declare module 'vue' {
|
|||||||
Avatar: typeof import('./src/components/avatar.vue')['default']
|
Avatar: typeof import('./src/components/avatar.vue')['default']
|
||||||
Markdown: typeof import('./src/components/markdown.vue')['default']
|
Markdown: typeof import('./src/components/markdown.vue')['default']
|
||||||
NButton: typeof import('naive-ui')['NButton']
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
|
NCollapse: typeof import('naive-ui')['NCollapse']
|
||||||
|
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
|
||||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||||
NDivider: typeof import('naive-ui')['NDivider']
|
NDivider: typeof import('naive-ui')['NDivider']
|
||||||
NImage: typeof import('naive-ui')['NImage']
|
NImage: typeof import('naive-ui')['NImage']
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface IChatWithLLMRequest {
|
|||||||
|
|
||||||
export interface Message {
|
export interface Message {
|
||||||
content?: string;
|
content?: string;
|
||||||
|
thinking?: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
usage?: UsageInfo;
|
usage?: UsageInfo;
|
||||||
[property: string]: any;
|
[property: string]: any;
|
||||||
|
|||||||
@@ -11,11 +11,14 @@ export class ChatService {
|
|||||||
accessToken: string,
|
accessToken: string,
|
||||||
request: IChatWithLLMRequest,
|
request: IChatWithLLMRequest,
|
||||||
onProgress: (content: string) => void,
|
onProgress: (content: string) => void,
|
||||||
getUsageInfo: (object: UsageInfo) => void = () => {}
|
getUsageInfo: (object: UsageInfo) => void = () => {},
|
||||||
|
getThinking: (thinkingContent: string) => void
|
||||||
) {
|
) {
|
||||||
let response;
|
let response;
|
||||||
let buffer = "";
|
let buffer = "";
|
||||||
let accumulatedContent = "";
|
let accumulatedContent = "";
|
||||||
|
let thinking = false;
|
||||||
|
let thinkingContent = "";
|
||||||
try {
|
try {
|
||||||
response = await fetch("/v1/chat/completions", {
|
response = await fetch("/v1/chat/completions", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -67,10 +70,46 @@ export class ChatService {
|
|||||||
|
|
||||||
// 处理内容
|
// 处理内容
|
||||||
if (data.choices?.[0]?.delta?.content) {
|
if (data.choices?.[0]?.delta?.content) {
|
||||||
// 累积内容
|
const content = data.choices[0].delta.content;
|
||||||
accumulatedContent += data.choices[0].delta.content;
|
|
||||||
// 触发回调
|
if (content.includes("<think>")) {
|
||||||
onProgress(accumulatedContent);
|
thinking = true;
|
||||||
|
// 只处理<think>前的内容
|
||||||
|
const [beforeThink, afterThink] = content.split("<think>");
|
||||||
|
if (beforeThink) {
|
||||||
|
accumulatedContent += beforeThink;
|
||||||
|
onProgress(accumulatedContent);
|
||||||
|
}
|
||||||
|
thinkingContent = "";
|
||||||
|
if (afterThink) {
|
||||||
|
thinkingContent += afterThink;
|
||||||
|
getThinking(thinkingContent);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.includes("</think>")) {
|
||||||
|
thinking = false;
|
||||||
|
// 只处理</think>后的内容
|
||||||
|
const [beforeEndThink, afterEndThink] =
|
||||||
|
content.split("</think>");
|
||||||
|
thinkingContent += beforeEndThink;
|
||||||
|
getThinking(thinkingContent);
|
||||||
|
if (afterEndThink) {
|
||||||
|
accumulatedContent += afterEndThink;
|
||||||
|
onProgress(accumulatedContent);
|
||||||
|
}
|
||||||
|
thinkingContent = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thinking) {
|
||||||
|
thinkingContent += content;
|
||||||
|
getThinking(thinkingContent);
|
||||||
|
} else {
|
||||||
|
accumulatedContent += content;
|
||||||
|
onProgress(accumulatedContent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理使用信息
|
// 处理使用信息
|
||||||
|
|||||||
@@ -14,14 +14,17 @@ export const useChatStore = defineStore("chat", () => {
|
|||||||
const historyMessages = ref<IChatWithLLMRequest["messages"]>([]);
|
const historyMessages = ref<IChatWithLLMRequest["messages"]>([]);
|
||||||
// 是否正在响应
|
// 是否正在响应
|
||||||
const completing = ref<boolean>(false);
|
const completing = ref<boolean>(false);
|
||||||
|
// 是否正在思考
|
||||||
|
const thinking = ref<boolean>(false);
|
||||||
// 在线人数
|
// 在线人数
|
||||||
const onlineCount = ref<number>(0);
|
const onlineCount = ref<number>(0);
|
||||||
|
|
||||||
// 与 LLM 聊天
|
// 与 LLM 聊天
|
||||||
const chatWithLLM = async (
|
const chatWithLLM = async (
|
||||||
request: IChatWithLLMRequest,
|
request: IChatWithLLMRequest,
|
||||||
onProgress: (content: string) => void, // 接收进度回调
|
onProgress: (content: string) => void, // 接收内容进度回调
|
||||||
getUsageInfo: (object: UsageInfo) => void = () => {}
|
getUsageInfo: (object: UsageInfo) => void = () => {}, // 接收使用信息回调
|
||||||
|
getThinking: (thinkingContent: string) => void = () => {} // 接收思维链内容回调
|
||||||
) => {
|
) => {
|
||||||
if (completing.value) throw new Error("正在响应中");
|
if (completing.value) throw new Error("正在响应中");
|
||||||
|
|
||||||
@@ -35,6 +38,9 @@ export const useChatStore = defineStore("chat", () => {
|
|||||||
},
|
},
|
||||||
(object: UsageInfo) => {
|
(object: UsageInfo) => {
|
||||||
getUsageInfo(object);
|
getUsageInfo(object);
|
||||||
|
},
|
||||||
|
(thinkingContent: string) => {
|
||||||
|
getThinking(thinkingContent);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -60,6 +66,20 @@ export const useChatStore = defineStore("chat", () => {
|
|||||||
historyMessages.value = [];
|
historyMessages.value = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 确保最后一条消息是助手消息,如果最后一条消息不是,就加一条空的占位,不然后面的思维链会丢失
|
||||||
|
const ensureAssistantMessage = () => {
|
||||||
|
if (
|
||||||
|
historyMessages.value.length === 0 ||
|
||||||
|
historyMessages.value[historyMessages.value.length - 1].role !==
|
||||||
|
"assistant"
|
||||||
|
) {
|
||||||
|
historyMessages.value.push({
|
||||||
|
role: "assistant",
|
||||||
|
content: ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
historyMessages,
|
historyMessages,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
@@ -72,23 +92,15 @@ export const useChatStore = defineStore("chat", () => {
|
|||||||
messages: newVal,
|
messages: newVal,
|
||||||
model: modelInfo.value?.model_id
|
model: modelInfo.value?.model_id
|
||||||
},
|
},
|
||||||
|
// 处理进度回调,文本
|
||||||
(content) => {
|
(content) => {
|
||||||
// 处理进度回调
|
ensureAssistantMessage();
|
||||||
if (
|
thinking.value = false;
|
||||||
historyMessages.value.length === 0 ||
|
|
||||||
historyMessages.value[historyMessages.value.length - 1].role !==
|
|
||||||
"assistant"
|
|
||||||
) {
|
|
||||||
historyMessages.value.push({
|
|
||||||
role: "assistant",
|
|
||||||
content: ""
|
|
||||||
});
|
|
||||||
}
|
|
||||||
historyMessages.value[historyMessages.value.length - 1].content =
|
historyMessages.value[historyMessages.value.length - 1].content =
|
||||||
content;
|
content;
|
||||||
},
|
},
|
||||||
|
// 处理使用usage信息回调
|
||||||
(usageInfo: UsageInfo) => {
|
(usageInfo: UsageInfo) => {
|
||||||
// 处理使用usage信息回调
|
|
||||||
// 如果最后一条消息是助手的回复,则更新使用信息
|
// 如果最后一条消息是助手的回复,则更新使用信息
|
||||||
if (
|
if (
|
||||||
historyMessages.value.length > 0 &&
|
historyMessages.value.length > 0 &&
|
||||||
@@ -98,6 +110,13 @@ export const useChatStore = defineStore("chat", () => {
|
|||||||
historyMessages.value[historyMessages.value.length - 1].usage =
|
historyMessages.value[historyMessages.value.length - 1].usage =
|
||||||
usageInfo;
|
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,
|
getModelList,
|
||||||
modelList,
|
modelList,
|
||||||
modelInfo,
|
modelInfo,
|
||||||
onlineCount
|
onlineCount,
|
||||||
|
thinking
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import markdown from "@/components/markdown.vue";
|
|||||||
import { useAsrStore, useChatStore, useLayoutStore } from "@/stores";
|
import { useAsrStore, useChatStore, useLayoutStore } from "@/stores";
|
||||||
|
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
const { historyMessages, completing, modelList, modelInfo } =
|
const { historyMessages, completing, modelList, modelInfo, thinking } =
|
||||||
storeToRefs(chatStore);
|
storeToRefs(chatStore);
|
||||||
const asrStore = useAsrStore();
|
const asrStore = useAsrStore();
|
||||||
const { isRecording } = storeToRefs(asrStore);
|
const { isRecording } = storeToRefs(asrStore);
|
||||||
@@ -23,6 +23,38 @@ const { hiddenLeftSidebar, simpleMode } = storeToRefs(layoutStore);
|
|||||||
const inputData = ref("");
|
const inputData = ref("");
|
||||||
const scrollbarRef = ref<HTMLElement | null>(null);
|
const scrollbarRef = ref<HTMLElement | null>(null);
|
||||||
const options = ref<Array<SelectGroupOption | SelectOption>>([]);
|
const options = ref<Array<SelectGroupOption | SelectOption>>([]);
|
||||||
|
// NCollapse 组件的折叠状态
|
||||||
|
const collapseActive = ref<string[]>(
|
||||||
|
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
|
// 处理选中模型的 ID
|
||||||
const selectedModelId = computed({
|
const selectedModelId = computed({
|
||||||
@@ -111,11 +143,13 @@ onMounted(() => {
|
|||||||
<NDivider />
|
<NDivider />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 默认消息↑ 历史消息↓ -->
|
||||||
<div
|
<div
|
||||||
v-for="(msg, idx) in historyMessages"
|
v-for="(msg, idx) in historyMessages"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
class="flex items-start mb-4"
|
class="flex items-start mb-4"
|
||||||
>
|
>
|
||||||
|
<!-- 头像↓ -->
|
||||||
<span
|
<span
|
||||||
v-if="msg.role === 'user'"
|
v-if="msg.role === 'user'"
|
||||||
class="rounded-lg overflow-hidden !w-16 !min-w-16 !h-16"
|
class="rounded-lg overflow-hidden !w-16 !min-w-16 !h-16"
|
||||||
@@ -125,10 +159,12 @@ onMounted(() => {
|
|||||||
<span v-else class="rounded-lg overflow-hidden">
|
<span v-else class="rounded-lg overflow-hidden">
|
||||||
<avatar :avatar="AIAvatar" />
|
<avatar :avatar="AIAvatar" />
|
||||||
</span>
|
</span>
|
||||||
|
<!-- 头像↑ 名称↓ -->
|
||||||
<div class="text-base w-full max-w-full ml-2 flex flex-col items-start">
|
<div class="text-base w-full max-w-full ml-2 flex flex-col items-start">
|
||||||
<span class="text-base font-bold">{{
|
<span class="text-base font-bold">{{
|
||||||
msg.role === "user" ? "你:" : "助手:"
|
msg.role === "user" ? "你:" : "助手:"
|
||||||
}}</span>
|
}}</span>
|
||||||
|
<!-- 使用信息 -->
|
||||||
<div
|
<div
|
||||||
v-if="msg.role !== 'user'"
|
v-if="msg.role !== 'user'"
|
||||||
class="text-[12px] text-[#7A7A7A] mb-[2px]"
|
class="text-[12px] text-[#7A7A7A] mb-[2px]"
|
||||||
@@ -136,6 +172,23 @@ onMounted(() => {
|
|||||||
Tokens: <span class="mr-1">{{ msg.usage?.total_tokens }}</span>
|
Tokens: <span class="mr-1">{{ msg.usage?.total_tokens }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full max-w-full">
|
<div class="w-full max-w-full">
|
||||||
|
<NCollapse
|
||||||
|
v-if="msg.thinking?.trim()"
|
||||||
|
:expanded-names="collapseActive[idx]"
|
||||||
|
>
|
||||||
|
<NCollapseItem
|
||||||
|
:title="thinking && idx === historyMessages.length - 1 ? '思考中...' : '已深度思考'"
|
||||||
|
:name="getName(idx)"
|
||||||
|
@item-header-click="() => handleItemHeaderClick(getName(idx))"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="text-[#7A7A7A] mb-4 border-l-2 border-[#E5E5E5] ml-2 pl-2"
|
||||||
|
>
|
||||||
|
<markdown :content="msg.thinking || ''" />
|
||||||
|
</div>
|
||||||
|
</NCollapseItem>
|
||||||
|
</NCollapse>
|
||||||
|
<!-- 内容↓ 思维链↑ -->
|
||||||
<markdown :content="msg.content || ''" />
|
<markdown :content="msg.content || ''" />
|
||||||
<NDivider />
|
<NDivider />
|
||||||
</div>
|
</div>
|
||||||
@@ -184,7 +237,7 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NButton :disabled="isRecording || completing" type="warning">
|
<NButton :disabled="isRecording || completing" type="warning">
|
||||||
<template v-if="!simpleMode">清除历史</template>
|
<template v-if="!simpleMode"> 清除历史 </template>
|
||||||
<TrashIcon
|
<TrashIcon
|
||||||
class="!w-4 !h-4"
|
class="!w-4 !h-4"
|
||||||
:class="{
|
:class="{
|
||||||
|
|||||||
Reference in New Issue
Block a user