优化: P1-a 长内容折叠(AI文本气泡+多冲突+错误堆栈)
- MessageList: assistant文本气泡>1500字符折叠(max-height 400px+渐变遮罩+展开按钮),流式中不折叠避免delta重排抖动(M1) - ConflictResolver: 多冲突(>=3)默认折叠,点击展开diff(M6) - HelpRequiredCard: context堆栈max-height 200px滚动+等宽字体(M7)
This commit is contained in:
@@ -4,53 +4,94 @@
|
||||
<span class="conflict-title">⚠ {{ t('aiChat.conflictsPending', { n: conflicts.length }) }}</span>
|
||||
</div>
|
||||
<div v-for="c in conflicts" :key="c.id" class="conflict-item">
|
||||
<div class="conflict-file">
|
||||
<div class="conflict-file conflict-file--toggle" @click="toggleConflict(c.id)">
|
||||
<span class="conflict-arrow" :class="{ 'conflict-arrow--open': isExpanded(c.id) }">▸</span>
|
||||
<span class="conflict-type-badge" :class="'badge--' + c.conflict_type">
|
||||
{{ c.conflict_type === 'semantic' ? '语义' : '文件' }}
|
||||
</span>
|
||||
<code>{{ c.file_path }}</code>
|
||||
<code class="conflict-filepath">{{ c.file_path }}</code>
|
||||
<span class="conflict-toggle-text">{{ isExpanded(c.id) ? t('aiChat.collapse') : t('aiChat.expand') }}</span>
|
||||
</div>
|
||||
<div class="conflict-diffs" v-if="c.diff_a || c.diff_b">
|
||||
<div class="diff-panel diff-panel--a">
|
||||
<div class="diff-panel-label">Agent A ({{ c.subtask_a || '?' }})</div>
|
||||
<pre class="diff-content">{{ c.diff_a || '(无改动)' }}</pre>
|
||||
<template v-if="isExpanded(c.id)">
|
||||
<div class="conflict-diffs" v-if="c.diff_a || c.diff_b">
|
||||
<div class="diff-panel diff-panel--a">
|
||||
<div class="diff-panel-label">Agent A ({{ c.subtask_a || '?' }})</div>
|
||||
<pre class="diff-content">{{ c.diff_a || '(无改动)' }}</pre>
|
||||
</div>
|
||||
<div class="diff-panel diff-panel--b">
|
||||
<div class="diff-panel-label">Agent B ({{ c.subtask_b || '?' }})</div>
|
||||
<pre class="diff-content">{{ c.diff_b || '(无改动)' }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="diff-panel diff-panel--b">
|
||||
<div class="diff-panel-label">Agent B ({{ c.subtask_b || '?' }})</div>
|
||||
<pre class="diff-content">{{ c.diff_b || '(无改动)' }}</pre>
|
||||
<div class="conflict-actions">
|
||||
<button class="btn btn-sm btn-ghost" @click="emit('resolve', { id: c.id, resolution: 'a' })">
|
||||
{{ t('aiChat.acceptA') }}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-ghost" @click="emit('resolve', { id: c.id, resolution: 'b' })">
|
||||
{{ t('aiChat.acceptB') }}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-ghost" @click="emit('resolve', { id: c.id, resolution: 'merged' })">
|
||||
{{ t('aiChat.mergeBoth') }}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-ghost" @click="emit('resolve', { id: c.id, resolution: 'manual' })">
|
||||
{{ t('aiChat.manualResolve') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="conflict-actions">
|
||||
<button class="btn btn-sm btn-ghost" @click="emit('resolve', { id: c.id, resolution: 'a' })">
|
||||
{{ t('aiChat.acceptA') }}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-ghost" @click="emit('resolve', { id: c.id, resolution: 'b' })">
|
||||
{{ t('aiChat.acceptB') }}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-ghost" @click="emit('resolve', { id: c.id, resolution: 'merged' })">
|
||||
{{ t('aiChat.mergeBoth') }}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-ghost" @click="emit('resolve', { id: c.id, resolution: 'manual' })">
|
||||
{{ t('aiChat.manualResolve') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { ConflictInfo } from '@/api/types'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
conflicts: ConflictInfo[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'resolve', payload: { id: string; resolution: string }): void
|
||||
}>()
|
||||
|
||||
// 多冲突折叠态:以稳定 id 为 key(避免 index 在列表变化时错位)
|
||||
const expandedConflicts = ref<Set<string>>(new Set())
|
||||
|
||||
// 阈值:冲突数 >= 3 时默认全折叠(M6 撑爆对话区只在多冲突场景发生);
|
||||
// 单/双冲突信息量小,直接展开省一次点击
|
||||
const COLLAPSE_THRESHOLD = 3
|
||||
|
||||
function isExpanded(id: string): boolean {
|
||||
return expandedConflicts.value.has(id)
|
||||
}
|
||||
|
||||
function toggleConflict(id: string): void {
|
||||
const next = new Set(expandedConflicts.value)
|
||||
if (next.has(id)) {
|
||||
next.delete(id)
|
||||
} else {
|
||||
next.add(id)
|
||||
}
|
||||
expandedConflicts.value = next
|
||||
}
|
||||
|
||||
// 冲突列表变化(新增/减少)时重置默认态:< 阈值全展开,否则全折叠
|
||||
const defaultExpanded = computed(() => props.conflicts.length < COLLAPSE_THRESHOLD)
|
||||
|
||||
watch(
|
||||
() => props.conflicts.map((c) => c.id).join('|'),
|
||||
() => {
|
||||
const next = new Set<string>()
|
||||
if (defaultExpanded.value) {
|
||||
props.conflicts.forEach((c) => next.add(c.id))
|
||||
}
|
||||
expandedConflicts.value = next
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -76,6 +117,29 @@ const emit = defineEmits<{
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.conflict-file--toggle {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.conflict-file--toggle:hover .conflict-filepath,
|
||||
.conflict-file--toggle:hover .conflict-toggle-text {
|
||||
color: var(--df-primary);
|
||||
}
|
||||
.conflict-arrow {
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
color: var(--df-text-dim);
|
||||
transition: transform 0.15s ease;
|
||||
width: 10px;
|
||||
}
|
||||
.conflict-arrow--open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.conflict-toggle-text {
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
color: var(--df-text-dim);
|
||||
}
|
||||
.conflict-type-badge {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
|
||||
@@ -82,6 +82,11 @@ function handleSelectOption(opt: string): void {
|
||||
color: var(--df-text-dim);
|
||||
opacity: 0.9;
|
||||
word-break: break-all;
|
||||
/* M7:context 含错误堆栈,长栈撑高卡片。等宽字体利堆栈行对齐;max-height + overflow-y:auto
|
||||
短内容自动不显滚动条,长堆栈滚动扫看(比折叠实用,无需展开按钮)。 */
|
||||
font-family: var(--df-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.ai-help-required-options {
|
||||
display: flex;
|
||||
|
||||
@@ -548,6 +548,31 @@ function toggleSegment(key: string): void {
|
||||
expandedSegmentIds.value = next
|
||||
}
|
||||
|
||||
// M1:assistant 纯文本气泡超长折叠,避免单条 AI 回复撑爆对话区。
|
||||
// 仅对「已完成(非流式)」assistant 文本生效;流式中的消息不折叠(delta 重排抖动)。
|
||||
// 阈值 1500 字符:约 markdown 渲染后一屏(400px@13px/1.6≈35行)的源文本量,超此即判长。
|
||||
// 折叠 max-height 同步 400px(见 CSS .ai-msg-bubble--collapsed,两处一致)。
|
||||
const LONG_MSG_THRESHOLD = 1500
|
||||
/// 展开的长消息 id 集合(点击底部按钮切换);默认折叠
|
||||
const expandedLongIds = ref<Set<string>>(new Set())
|
||||
function toggleLong(id: string): void {
|
||||
const next = new Set(expandedLongIds.value)
|
||||
if (next.has(id)) next.delete(id)
|
||||
else next.add(id)
|
||||
expandedLongIds.value = next
|
||||
}
|
||||
function isLongMsg(msg: AiMessage): boolean {
|
||||
return msg.role === 'assistant' && !!msg.content && msg.content.length > LONG_MSG_THRESHOLD
|
||||
}
|
||||
/// 是否折叠展示该 assistant 文本气泡:长消息 + 未手动展开 + 非流式(当前未在流式生成)。
|
||||
function shouldCollapseLong(msg: AiMessage): boolean {
|
||||
if (!isLongMsg(msg)) return false
|
||||
if (expandedLongIds.value.has(msg.id)) return false
|
||||
// 流式约束:正在生成的当前消息不折叠(避免 delta 重排抖动),参考 L92 同款 streaming 判定。
|
||||
if (isLastAi(msg) && store.state.streaming) return false
|
||||
return true
|
||||
}
|
||||
|
||||
type RenderItem =
|
||||
| { kind: 'msg'; key: string; msg: AiMessage }
|
||||
| { kind: 'sep'; key: string; seg: MessageSegment & { key: string } }
|
||||
@@ -683,7 +708,7 @@ defineExpose({
|
||||
<div
|
||||
v-if="item.msg.content || (isLastAi(item.msg) && store.state.streaming && store.state.currentText)"
|
||||
class="ai-msg-bubble ai-msg-bubble--ai ai-md"
|
||||
:class="{ 'ai-msg-bubble--error': item.msg.isError }"
|
||||
:class="{ 'ai-msg-bubble--error': item.msg.isError, 'ai-msg-bubble--collapsed': shouldCollapseLong(item.msg) }"
|
||||
:key="'md-' + item.msg.id + '-' + (isLastAi(item.msg) ? _mdRenderKey : 0)"
|
||||
>
|
||||
<!-- 流式:分块渲染(已完成块key稳定→DOM不重建) -->
|
||||
@@ -693,6 +718,12 @@ defineExpose({
|
||||
</template>
|
||||
<!-- 完成/历史:整段v-html(不变) -->
|
||||
<div v-else v-html="renderMd(item.msg.content)"></div>
|
||||
<!-- M1:超长 assistant 文本折叠后的底部展开按钮(仅非流式 + 长消息显示) -->
|
||||
<button
|
||||
v-if="isLongMsg(item.msg) && !(isLastAi(item.msg) && store.state.streaming)"
|
||||
class="ai-msg-bubble-toggle"
|
||||
@click.stop="toggleLong(item.msg.id)"
|
||||
>{{ expandedLongIds.has(item.msg.id) ? $t('aiChat.collapse') : $t('aiChat.expand') }}</button>
|
||||
<button
|
||||
v-if="item.msg.content"
|
||||
class="ai-copy-btn ai-copy-btn--ai"
|
||||
@@ -895,6 +926,44 @@ defineExpose({
|
||||
.ai-msg-bubble--ai.ai-md {
|
||||
overflow-x: auto;
|
||||
}
|
||||
/* M1:超长 assistant 文本折叠——max-height 截断 + 底部渐变遮罩 + 展开按钮。
|
||||
背景 var(--df-bg-card) 对齐 .ai-msg-bubble--ai(L876):渐变淡出到气泡背景,
|
||||
视觉上内容「沉入」气泡而非被硬切。overflow:hidden 覆盖 .ai-md 的 overflow-x:auto。 */
|
||||
.ai-msg-bubble--collapsed {
|
||||
max-height: 400px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.ai-msg-bubble--collapsed::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 48px;
|
||||
background: linear-gradient(transparent, var(--df-bg-card));
|
||||
pointer-events: none; /* 让点击穿透到下方展开按钮 */
|
||||
}
|
||||
/* 展开按钮:气泡底部居中,置于渐变遮罩之上 */
|
||||
.ai-msg-bubble-toggle {
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1;
|
||||
padding: 2px 12px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
color: var(--df-accent);
|
||||
background: var(--df-bg-card);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.ai-msg-bubble-toggle:hover {
|
||||
background: var(--df-bg-card-hover);
|
||||
}
|
||||
|
||||
/* ── Input Augmentation: 用户气泡内 mention chip(圆角徽标,复用 ChatInput .ai-skill-chip-name 风格) ──
|
||||
用户气泡背景 = --df-accent(主题强调色,如深色蓝/紫),chip 用半透明白底+深字 保证对比度;
|
||||
|
||||
Reference in New Issue
Block a user