mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +08:00
fix(chat): preserve intermediate reasoning in segments via phase-based splitting
This commit is contained in:
parent
dafdcb4182
commit
ba34f1eed3
@ -1248,6 +1248,9 @@ public class ChatController {
|
|||||||
String phase = String.valueOf(delta.eventData().getOrDefault("phase", ""));
|
String phase = String.valueOf(delta.eventData().getOrDefault("phase", ""));
|
||||||
if (!phase.isBlank()) {
|
if (!phase.isBlank()) {
|
||||||
streamTracker.updatePhase(conversationId, phase);
|
streamTracker.updatePhase(conversationId, phase);
|
||||||
|
// 关键:phase 变化时关闭 running 的 content/thinking segments
|
||||||
|
// 让每个阶段(reasoning → tool → summarize → final answer)的文字成为独立 segment
|
||||||
|
closeRunningSegments("content", "thinking");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 累积工具调用事件,用于持久化到消息历史
|
// 累积工具调用事件,用于持久化到消息历史
|
||||||
|
|||||||
@ -1,33 +1,103 @@
|
|||||||
<script setup lang="ts">
|
<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 { useMarkdownRenderer } from '@/composables/useMarkdownRenderer'
|
||||||
import TypingCursor from './TypingCursor.vue'
|
import TypingCursor from './TypingCursor.vue'
|
||||||
import type { MessageSegment } from '@/types'
|
import type { MessageSegment } from '@/types'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
segment: MessageSegment
|
segment: MessageSegment
|
||||||
showCursor?: boolean
|
showCursor?: boolean
|
||||||
}>()
|
/** 是否是最后一个 content segment(最终回答) */
|
||||||
|
isLast?: boolean
|
||||||
const { renderMarkdown } = useMarkdownRenderer()
|
}>(), {
|
||||||
|
showCursor: false,
|
||||||
const renderedContent = computed(() => {
|
isLast: true,
|
||||||
return renderMarkdown(props.segment.text || '')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { renderMarkdown } = useMarkdownRenderer()
|
||||||
|
const expanded = ref(props.isLast) // 最终回答默认展开,中间摘要默认折叠
|
||||||
|
|
||||||
|
const renderedContent = computed(() => renderMarkdown(props.segment.text || ''))
|
||||||
const isRunning = computed(() => props.segment.status === 'running')
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="segment segment--content">
|
<div class="seg-content" :class="{ 'is-intermediate': isIntermediate }">
|
||||||
<div class="markdown-body" v-html="renderedContent"></div>
|
<!-- 中间推理摘要:折叠式显示 -->
|
||||||
<TypingCursor v-if="isRunning && showCursor" />
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.segment--content {
|
.seg-content {
|
||||||
padding: 4px 0;
|
padding: 2px 0;
|
||||||
margin-top: 4px;
|
}
|
||||||
|
.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>
|
</style>
|
||||||
|
|||||||
@ -21,10 +21,10 @@
|
|||||||
<!-- ===== 分段式渲染模式(Claude Code 风格)===== -->
|
<!-- ===== 分段式渲染模式(Claude Code 风格)===== -->
|
||||||
<template v-if="useSegmentedView">
|
<template v-if="useSegmentedView">
|
||||||
<div class="segments-view">
|
<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" />
|
<ThinkingSegment v-if="seg.type === 'thinking'" :segment="seg" />
|
||||||
<ToolCallSegment v-if="seg.type === 'tool_call'" :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>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user