feat(agent): control reasoning visibility

This commit is contained in:
2026-07-17 14:05:54 +08:00
parent bfaf2ebf67
commit a879dc2ff1
18 changed files with 368 additions and 51 deletions

View File

@@ -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>

View File

@@ -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"
/>