fix(chat): preserve intermediate reasoning in segments via phase-based splitting

This commit is contained in:
matevip 2026-04-10 07:58:19 +08:00
parent dafdcb4182
commit ba34f1eed3
3 changed files with 89 additions and 16 deletions

View File

@ -1248,6 +1248,9 @@ public class ChatController {
String phase = String.valueOf(delta.eventData().getOrDefault("phase", ""));
if (!phase.isBlank()) {
streamTracker.updatePhase(conversationId, phase);
// 关键phase 变化时关闭 running content/thinking segments
// 让每个阶段reasoning tool summarize final answer的文字成为独立 segment
closeRunningSegments("content", "thinking");
}
}
// 累积工具调用事件用于持久化到消息历史

View File

@ -1,33 +1,103 @@
<script setup lang="ts">
import { computed } from 'vue'
import { ref, computed } from 'vue'
import { ArrowDown, InfoFilled } from '@element-plus/icons-vue'
import { useMarkdownRenderer } from '@/composables/useMarkdownRenderer'
import TypingCursor from './TypingCursor.vue'
import type { MessageSegment } from '@/types'
const props = defineProps<{
const props = withDefaults(defineProps<{
segment: MessageSegment
showCursor?: boolean
}>()
const { renderMarkdown } = useMarkdownRenderer()
const renderedContent = computed(() => {
return renderMarkdown(props.segment.text || '')
/** 是否是最后一个 content segment最终回答 */
isLast?: boolean
}>(), {
showCursor: false,
isLast: true,
})
const { renderMarkdown } = useMarkdownRenderer()
const expanded = ref(props.isLast) //
const renderedContent = computed(() => renderMarkdown(props.segment.text || ''))
const isRunning = computed(() => props.segment.status === 'running')
const isIntermediate = computed(() => !props.isLast)
const textLength = computed(() => {
const len = props.segment.text?.length || 0
return len < 1000 ? `${len}` : `${(len / 1000).toFixed(1)}k 字`
})
</script>
<template>
<div class="segment segment--content">
<div class="markdown-body" v-html="renderedContent"></div>
<TypingCursor v-if="isRunning && showCursor" />
<div class="seg-content" :class="{ 'is-intermediate': isIntermediate }">
<!-- 中间推理摘要折叠式显示 -->
<div v-if="isIntermediate" class="seg-content__header" @click="expanded = !expanded">
<el-icon class="seg-content__info-icon" :size="13"><InfoFilled /></el-icon>
<span class="seg-content__label">中间推理总结</span>
<span class="seg-content__hint">{{ textLength }}</span>
<el-icon class="seg-content__arrow" :class="{ 'is-open': expanded }" :size="11"><ArrowDown /></el-icon>
</div>
<!-- 内容体 -->
<Transition name="seg-slide">
<div v-if="expanded || isLast" class="seg-content__body">
<div class="markdown-body" v-html="renderedContent"></div>
<TypingCursor v-if="isRunning && showCursor" />
</div>
</Transition>
</div>
</template>
<style scoped>
.segment--content {
padding: 4px 0;
margin-top: 4px;
.seg-content {
padding: 2px 0;
}
.seg-content.is-intermediate {
margin: 3px 0;
border-radius: 8px;
border: 1px solid var(--mc-border-light);
background: var(--mc-bg-muted);
overflow: hidden;
}
.seg-content__header {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
font-size: 13px;
cursor: pointer;
color: var(--mc-text-tertiary);
user-select: none;
transition: color 0.15s;
}
.seg-content__header:hover {
color: var(--mc-text-secondary);
}
.seg-content__info-icon {
color: var(--mc-primary-light);
}
.seg-content__label {
font-weight: 500;
flex: 1;
}
.seg-content__hint {
font-size: 11px;
}
.seg-content__arrow {
transition: transform 0.2s;
}
.seg-content__arrow.is-open {
transform: rotate(180deg);
}
.is-intermediate .seg-content__body {
padding: 0 12px 8px;
}
.seg-slide-enter-active, .seg-slide-leave-active {
transition: all 0.2s ease;
}
.seg-slide-enter-from, .seg-slide-leave-to {
opacity: 0;
transform: translateY(-4px);
}
</style>

View File

@ -21,10 +21,10 @@
<!-- ===== 分段式渲染模式Claude Code 风格===== -->
<template v-if="useSegmentedView">
<div class="segments-view">
<template v-for="seg in segments" :key="seg.id">
<template v-for="(seg, index) in segments" :key="seg.id">
<ThinkingSegment v-if="seg.type === 'thinking'" :segment="seg" />
<ToolCallSegment v-if="seg.type === 'tool_call'" :segment="seg" />
<ContentSegment v-if="seg.type === 'content'" :segment="seg" :show-cursor="showCursor && seg.status === 'running'" />
<ContentSegment v-if="seg.type === 'content'" :segment="seg" :is-last="index === segments.length - 1" :show-cursor="showCursor && seg.status === 'running'" />
</template>
</div>
</template>