feat(agent): control reasoning visibility
This commit is contained in:
@@ -150,9 +150,16 @@ async function send(message: string, complete: (success: boolean) => void) {
|
||||
currentAssistant().content += chunk;
|
||||
if (followingOutput.value) await messageList.value?.scrollToBottom();
|
||||
},
|
||||
async (statusMessage, statusType) => {
|
||||
async (chunk) => {
|
||||
currentAssistant().reasoning = (currentAssistant().reasoning || "") + chunk;
|
||||
if (followingOutput.value) await messageList.value?.scrollToBottom();
|
||||
},
|
||||
async (statusMessage, statusType, reasoningVisible) => {
|
||||
if (statusType === "queued") currentAssistant().content = statusMessage || "当前请求较多,正在排队中。";
|
||||
if (statusType === "generating" && !hasContent) currentAssistant().content = "";
|
||||
if (statusType === "generating") {
|
||||
currentAssistant().showReasoning = reasoningVisible;
|
||||
if (!hasContent) currentAssistant().content = "";
|
||||
}
|
||||
if (followingOutput.value) await messageList.value?.scrollToBottom();
|
||||
},
|
||||
activeAbortController.value.signal,
|
||||
@@ -256,6 +263,7 @@ function toUiMessage(message: ApiMessage): DisplayMessage {
|
||||
id: String(message.id),
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
showReasoning: message.role === "assistant" && /<think(?:\s[^>]*)?>/i.test(message.content),
|
||||
createdAt: message.created_at,
|
||||
streaming: message.message_status === "GENERATING",
|
||||
};
|
||||
|
||||
@@ -7,58 +7,63 @@ const props = defineProps<{
|
||||
messageId: string;
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
reasoning?: string;
|
||||
showReasoning?: boolean;
|
||||
createdAt: string;
|
||||
streaming?: boolean;
|
||||
}>();
|
||||
|
||||
const markdown = new MarkdownIt({ breaks: true, html: false, linkify: true });
|
||||
|
||||
const parsed = computed(() => splitReasoning(props.content));
|
||||
const answer = computed(() => {
|
||||
if (props.role === "user") return props.content;
|
||||
return stripStreamingReasoning(props.content).replace(/^\s+/, "");
|
||||
return parsed.value.answer.replace(/^\s+/, "");
|
||||
});
|
||||
|
||||
const reasoning = computed(() => props.reasoning || parsed.value.reasoning);
|
||||
const hasAnswer = computed(() => answer.value.trim().length > 0);
|
||||
|
||||
const renderedContent = computed(() => {
|
||||
if (props.role !== "assistant") return answer.value;
|
||||
return hasAnswer.value ? markdown.render(answer.value) : "";
|
||||
});
|
||||
|
||||
function stripStreamingReasoning(content: string) {
|
||||
function splitReasoning(content: string) {
|
||||
const lower = content.toLowerCase();
|
||||
let visible = "";
|
||||
let answer = "";
|
||||
let reasoning = "";
|
||||
let cursor = 0;
|
||||
let reasoningDepth = 0;
|
||||
let depth = 0;
|
||||
while (cursor < content.length) {
|
||||
const tagStart = content.indexOf("<", cursor);
|
||||
if (tagStart === -1) {
|
||||
if (reasoningDepth === 0) visible += content.slice(cursor);
|
||||
if (depth) reasoning += content.slice(cursor);
|
||||
else answer += content.slice(cursor);
|
||||
break;
|
||||
}
|
||||
if (reasoningDepth === 0) visible += content.slice(cursor, tagStart);
|
||||
const text = content.slice(cursor, tagStart);
|
||||
if (depth) reasoning += text;
|
||||
else answer += text;
|
||||
const tail = lower.slice(tagStart);
|
||||
const openTag = tail.match(/^<think(?:\s[^>]*)?>/);
|
||||
if (openTag) {
|
||||
reasoningDepth += 1;
|
||||
depth += 1;
|
||||
cursor = tagStart + openTag[0].length;
|
||||
continue;
|
||||
}
|
||||
const closeTag = tail.match(/^<\/think\s*>/);
|
||||
if (closeTag) {
|
||||
reasoningDepth = Math.max(0, reasoningDepth - 1);
|
||||
depth = Math.max(0, depth - 1);
|
||||
cursor = tagStart + closeTag[0].length;
|
||||
continue;
|
||||
}
|
||||
if ("<think".startsWith(tail) || "</think>".startsWith(tail)) break;
|
||||
if (reasoningDepth === 0) visible += "<";
|
||||
if (depth) reasoning += "<";
|
||||
else answer += "<";
|
||||
cursor = tagStart + 1;
|
||||
}
|
||||
return visible;
|
||||
return { answer, reasoning };
|
||||
}
|
||||
|
||||
const displayTime = computed(() => {
|
||||
// 消息服务返回的是 UTC 无时区字符串,补齐时区后再按用户本地时间展示。
|
||||
const normalized = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(props.createdAt) ? props.createdAt : `${props.createdAt}Z`;
|
||||
const date = new Date(normalized);
|
||||
if (Number.isNaN(date.getTime())) return "";
|
||||
@@ -73,6 +78,10 @@ const displayTime = computed(() => {
|
||||
</div>
|
||||
<div class="message-bubble">
|
||||
<template v-if="role === 'assistant'">
|
||||
<details v-if="showReasoning && reasoning.trim()" class="reasoning-panel" :open="streaming">
|
||||
<summary>思考过程</summary>
|
||||
<div class="reasoning-content">{{ reasoning }}</div>
|
||||
</details>
|
||||
<div v-if="renderedContent" class="message-content markdown-content" v-html="renderedContent"></div>
|
||||
<div v-if="streaming && !renderedContent" class="generation-state">
|
||||
<span></span><span></span><span></span>
|
||||
|
||||
@@ -7,6 +7,8 @@ export interface DisplayMessage {
|
||||
id: string;
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
reasoning?: string;
|
||||
showReasoning?: boolean;
|
||||
createdAt: string;
|
||||
streaming?: boolean;
|
||||
}
|
||||
@@ -57,6 +59,8 @@ defineExpose({ scrollToBottom, scrollToMessage });
|
||||
:message-id="message.id"
|
||||
:role="message.role"
|
||||
:content="message.content"
|
||||
:reasoning="message.reasoning"
|
||||
:show-reasoning="message.showReasoning"
|
||||
:created-at="message.createdAt"
|
||||
:streaming="message.streaming"
|
||||
/>
|
||||
|
||||
@@ -83,7 +83,8 @@ export async function streamChat(
|
||||
sessionId: number,
|
||||
message: string,
|
||||
onChunk: (chunk: string) => void,
|
||||
onStatus?: (message: string, type: string) => void,
|
||||
onReasoning?: (chunk: string) => void,
|
||||
onStatus?: (message: string, type: string, reasoningVisible: boolean) => void,
|
||||
signal?: AbortSignal,
|
||||
) {
|
||||
const token = getToken();
|
||||
@@ -114,15 +115,21 @@ export async function streamChat(
|
||||
if (!dataLines.length) continue;
|
||||
const payload = dataLines.map((line) => line.slice(5).trimStart()).join("\n").trim();
|
||||
if (payload === "[DONE]") return;
|
||||
const parsed = JSON.parse(payload) as { type?: string; content?: string; message?: string };
|
||||
const parsed = JSON.parse(payload) as {
|
||||
type?: string;
|
||||
content?: string;
|
||||
message?: string;
|
||||
reasoningVisible?: boolean;
|
||||
};
|
||||
if (parsed.type === "queued" || parsed.type === "generating") {
|
||||
onStatus?.(parsed.message || "", parsed.type);
|
||||
onStatus?.(parsed.message || "", parsed.type, Boolean(parsed.reasoningVisible));
|
||||
continue;
|
||||
}
|
||||
if (parsed.type === "error") {
|
||||
throw new Error(parsed.message || "AI 回复失败");
|
||||
}
|
||||
if (parsed.content) onChunk(parsed.content);
|
||||
if (parsed.type === "reasoning" && parsed.content) onReasoning?.(parsed.content);
|
||||
if (parsed.type === "content" && parsed.content) onChunk(parsed.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -818,6 +818,9 @@ textarea:focus-visible {
|
||||
padding: 0 10px 10px;
|
||||
color: #5d7169;
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.markdown-content > :first-child {
|
||||
|
||||
Reference in New Issue
Block a user