feat(ui): 实现深度思考折叠功能,提升用户界面信息密度与交互体验 [优化:支持用户展开/收起思考过程,减少信息干扰]
This commit is contained in:
parent
a289db56c7
commit
d80b17050d
|
|
@ -442,21 +442,16 @@ async def chat_endpoint_handler(body: dict):
|
||||||
# 处理思考过程片段
|
# 处理思考过程片段
|
||||||
if reasoning_content:
|
if reasoning_content:
|
||||||
if not full_reasoning_content:
|
if not full_reasoning_content:
|
||||||
# 第一个思考片段,加标题前缀
|
# 第一个思考片段,添加 <think> 开始标签
|
||||||
delta_str += (
|
delta_str += "<think>"
|
||||||
"> **💭 深度思考过程:**\n> \n> "
|
|
||||||
)
|
|
||||||
full_reasoning_content += reasoning_content
|
full_reasoning_content += reasoning_content
|
||||||
# markdown 引用块内换行需加 >
|
delta_str += reasoning_content
|
||||||
delta_str += reasoning_content.replace(
|
|
||||||
"\n", "\n> "
|
|
||||||
)
|
|
||||||
|
|
||||||
# 处理正式回复片段
|
# 处理正式回复片段
|
||||||
if content:
|
if content:
|
||||||
if not full_content and full_reasoning_content:
|
if not full_content and full_reasoning_content:
|
||||||
# 思考结束后首个正式回复,加分隔线
|
# 思考结束后首个正式回复,关闭 </think> 标签
|
||||||
delta_str += "\n\n---\n\n"
|
delta_str += "</think>\n\n"
|
||||||
full_content += content
|
full_content += content
|
||||||
delta_str += content
|
delta_str += content
|
||||||
|
|
||||||
|
|
@ -581,8 +576,7 @@ async def chat_endpoint_handler(body: dict):
|
||||||
content = msg_dict.get("content", "")
|
content = msg_dict.get("content", "")
|
||||||
rc = msg_dict.get("reasoning_content", "")
|
rc = msg_dict.get("reasoning_content", "")
|
||||||
if rc:
|
if rc:
|
||||||
rc_formatted = rc.replace("\n", "\n> ")
|
content = f"<think>{rc}</think>\n\n{content}"
|
||||||
content = f"> **💭 深度思考过程:**\n> \n> {rc_formatted}\n\n---\n\n{content}"
|
|
||||||
# 否则尝试从 output.text 获取内容(DashScope特定格式)
|
# 否则尝试从 output.text 获取内容(DashScope特定格式)
|
||||||
elif (
|
elif (
|
||||||
hasattr(response, "output")
|
hasattr(response, "output")
|
||||||
|
|
|
||||||
|
|
@ -364,17 +364,16 @@ async def glm_stream_generator(
|
||||||
# ── 思考过程(reasoning_content)────────────────────────
|
# ── 思考过程(reasoning_content)────────────────────────
|
||||||
if reasoning:
|
if reasoning:
|
||||||
if not full_reasoning:
|
if not full_reasoning:
|
||||||
# 首个思考片段:加 Markdown 引用块标题
|
# 首个思考片段:添加 <think> 开始标签
|
||||||
delta_str += "> **💭 深度思考过程:**\n> \n> "
|
delta_str += "<think>"
|
||||||
full_reasoning += reasoning
|
full_reasoning += reasoning
|
||||||
# 引用块内换行需在每行前加 `> `
|
delta_str += reasoning
|
||||||
delta_str += reasoning.replace("\n", "\n> ")
|
|
||||||
|
|
||||||
# ── 正式回答(content)──────────────────────────────────
|
# ── 正式回答(content)──────────────────────────────────
|
||||||
if text:
|
if text:
|
||||||
if not full_content and full_reasoning:
|
if not full_content and full_reasoning:
|
||||||
# 思考结束后首次出现正式回答:加分隔线
|
# 思考结束后首次出现正式回答:关闭 </think> 标签
|
||||||
delta_str += "\n\n---\n\n"
|
delta_str += "</think>\n\n"
|
||||||
full_content += text
|
full_content += text
|
||||||
delta_str += text
|
delta_str += text
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { MarkdownRender } from "markstream-vue";
|
import { MarkdownRender } from "markstream-vue";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
node: {
|
node: {
|
||||||
type: "think";
|
type: "think";
|
||||||
content: string;
|
content: string;
|
||||||
|
|
@ -12,6 +13,21 @@ defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { copy } = useClipboard({ legacy: true });
|
const { copy } = useClipboard({ legacy: true });
|
||||||
|
const collapsed = ref(false);
|
||||||
|
|
||||||
|
// 当思考完成时(loading 从 true 变为 false),自动折叠
|
||||||
|
watch(
|
||||||
|
() => props.node.loading,
|
||||||
|
(newVal, oldVal) => {
|
||||||
|
if (oldVal === true && newVal === false) {
|
||||||
|
collapsed.value = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
function toggleCollapse() {
|
||||||
|
collapsed.value = !collapsed.value;
|
||||||
|
}
|
||||||
|
|
||||||
async function textCopy(data: any) {
|
async function textCopy(data: any) {
|
||||||
if (typeof data === "string") {
|
if (typeof data === "string") {
|
||||||
|
|
@ -22,64 +38,67 @@ async function textCopy(data: any) {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="thinking-node p-4 my-4 bg-blue-50 dark:bg-blue-900/40 rounded-md border-l-4 border-blue-400 flex items-start gap-3"
|
class="thinking-node p-4 my-4 bg-blue-50 dark:bg-blue-900/40 rounded-md border-l-4 border-blue-400"
|
||||||
>
|
>
|
||||||
<div class="flex-shrink-0 mt-1">
|
<!-- 可点击的标题栏 -->
|
||||||
<!-- decorative thinking SVG icon -->
|
<div class="thinking-header" @click="toggleCollapse">
|
||||||
<div
|
<div class="flex-shrink-0">
|
||||||
class="w-9 h-9 rounded-full bg-blue-200 dark:bg-blue-700 flex items-center justify-center text-blue-700 dark:text-blue-100"
|
<!-- 思考图标 -->
|
||||||
>
|
<div
|
||||||
|
class="w-8 h-8 rounded-full bg-blue-200 dark:bg-blue-700 flex items-center justify-center text-blue-700 dark:text-blue-100"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12 3C7.03 3 3 6.58 3 11c0 1.86.66 3.57 1.77 4.98L4 21l5.2-1.9C10.06 19.35 11 19.5 12 19.5c4.97 0 9-3.58 9-8.5S16.97 3 12 3z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="0.8"
|
||||||
|
fill="currentColor"
|
||||||
|
opacity="0.9"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="thinking-title">
|
||||||
|
<strong class="text-sm">💭 深度思考</strong>
|
||||||
|
<!-- 加载动画 -->
|
||||||
|
<span v-if="node.loading" class="thinking-dots visible" aria-hidden="true">
|
||||||
|
<span class="dot dot-1" />
|
||||||
|
<span class="dot dot-2" />
|
||||||
|
<span class="dot dot-3" />
|
||||||
|
</span>
|
||||||
|
<span v-else class="thinking-status text-xs text-slate-500 dark:text-slate-300">
|
||||||
|
已完成
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- 折叠/展开箭头 -->
|
||||||
|
<div class="collapse-arrow" :class="{ collapsed }">
|
||||||
<svg
|
<svg
|
||||||
class="w-5 h-5"
|
width="16"
|
||||||
|
height="16"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
stroke="currentColor"
|
||||||
aria-hidden="true"
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<path
|
<polyline points="6 9 12 15 18 9" />
|
||||||
d="M12 3C7.03 3 3 6.58 3 11c0 1.86.66 3.57 1.77 4.98L4 21l5.2-1.9C10.06 19.35 11 19.5 12 19.5c4.97 0 9-3.58 9-8.5S16.97 3 12 3z"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="0.8"
|
|
||||||
fill="currentColor"
|
|
||||||
opacity="0.9"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
|
||||||
<div class="flex items-baseline gap-3">
|
<!-- 可折叠的内容区域 -->
|
||||||
<strong class="text-sm">Thinking</strong>
|
<div class="thinking-content" :class="{ collapsed }">
|
||||||
<span class="text-xs text-slate-500 dark:text-slate-300"
|
|
||||||
>(assistant)</span
|
|
||||||
>
|
|
||||||
<!-- keep dots in DOM to avoid layout shift; toggle visibility with classes -->
|
|
||||||
<span class="ml-2" aria-hidden="true">
|
|
||||||
<span
|
|
||||||
class="thinking-dots"
|
|
||||||
:class="[node.loading ? 'visible' : 'hidden']"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<span class="dot dot-1" />
|
|
||||||
<span class="dot dot-2" />
|
|
||||||
<span class="dot dot-3" />
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="mt-1 text-sm leading-relaxed text-slate-800 dark:text-slate-100"
|
class="mt-3 text-sm leading-relaxed text-slate-800 dark:text-slate-100"
|
||||||
>
|
>
|
||||||
<!-- sr-only live region only present when loading to announce change -->
|
<MarkdownRender :content="node.content" @copy="textCopy" />
|
||||||
<span v-if="node.loading" class="sr-only" aria-live="polite"
|
|
||||||
>Thinking…</span
|
|
||||||
>
|
|
||||||
<transition name="fade" mode="out-in">
|
|
||||||
<div
|
|
||||||
key="{{ node.loading ? 'loading' : 'ready' }}"
|
|
||||||
class="content-area"
|
|
||||||
>
|
|
||||||
<MarkdownRender :content="node.content" @copy="textCopy" />
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -93,33 +112,72 @@ async function textCopy(data: any) {
|
||||||
color: #e6f0ff;
|
color: #e6f0ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animated dots for thinking state */
|
/* 标题栏 */
|
||||||
|
.thinking-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-title {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thinking-status {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 折叠箭头 */
|
||||||
|
.collapse-arrow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
color: #64748b;
|
||||||
|
transition: transform 0.25s ease;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.collapse-arrow:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
.dark .collapse-arrow:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
.collapse-arrow.collapsed {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 可折叠内容 */
|
||||||
|
.thinking-content {
|
||||||
|
max-height: 2000px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.35s ease, opacity 0.25s ease;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.thinking-content.collapsed {
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载动画 */
|
||||||
.thinking-dots {
|
.thinking-dots {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
width: 36px;
|
height: 12px;
|
||||||
justify-content: flex-start;
|
|
||||||
height: 12px; /* reserve vertical space so toggling doesn't collapse layout */
|
|
||||||
transition:
|
|
||||||
opacity 160ms linear,
|
|
||||||
transform 160ms linear;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
}
|
||||||
.thinking-dots .dot {
|
.thinking-dots .dot {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
background: #1e3a8a; /* blue-800 */
|
background: #1e3a8a;
|
||||||
opacity: 0.25;
|
opacity: 0.25;
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
.thinking-dots.visible {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.thinking-dots.hidden {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
}
|
||||||
.thinking-dots.visible .dot-1 {
|
.thinking-dots.visible .dot-1 {
|
||||||
animation: think-bounce 1s infinite ease-in-out;
|
animation: think-bounce 1s infinite ease-in-out;
|
||||||
|
|
@ -150,33 +208,4 @@ async function textCopy(data: any) {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ensure content area doesn't shift when dots appear */
|
|
||||||
.content-area {
|
|
||||||
min-height: 1.25rem;
|
|
||||||
}
|
|
||||||
.partial-content,
|
|
||||||
.full-content {
|
|
||||||
transition: opacity 140ms ease;
|
|
||||||
}
|
|
||||||
.partial-content {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
.full-content {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Vue transition classes for fade */
|
|
||||||
.fade-enter-from,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.fade-enter-active,
|
|
||||||
.fade-leave-active {
|
|
||||||
transition: opacity 160ms ease;
|
|
||||||
}
|
|
||||||
.fade-enter-to,
|
|
||||||
.fade-leave-from {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue