优化: AI Chat 渲染与无障碍 + 文件查看 + 同步 IO 收口
- 渲染: 结果/过程分离(气泡只放最终结果,过程轮叙述+工具卡收进轨迹卡默认收起可展开——治『流式后正文消失』且不再 N 气泡打扰) - 渲染 P0: v-memo 定向 dep(只有真正渲染流式块的 item 失效,其余取占位) + renderItems 桶 memo(签名覆盖全消费输入,item 引用稳定) + 指纹 watch 降为 O(1) toolEpoch - TopBar 三处 O(n) messages computed 合并单次遍历;md 缓存改有界 LRU(除满即全清抖动);useTurnTrace 补 never 穷尽 + default;MessageItem 模板去重 - 无障碍(四批累计): 可点容器键盘化(含 role/tabindex/keydown/aria-expanded) + icon-only aria-label 约 60 处 + 装饰图标 aria-hidden + 52 个滚动容器 overscroll-behavior:contain + 设置开关聚焦可见环 + outline 零补偿站点补 :focus-visible - UX 走查项: 灵感 IDEA-10/11、知识 KNOW-6/7/9、项目详情 O8、审计 SA-4、设置 SA-6/26/27/28(脏表单/冗余 save/clamp/删除二次确认) - 文件查看专项: 切工程状态残留 F5/F6、FileTree 去重与展开反馈 F7/F15/F17、切文件头部残留 F18、R 状态着色 F19;FilePreview diff 文件头(---/+++)误当增删渲染修复 + 大纲行高亮死样式修复 + 图片失败文案 i18n 化 - 同步 IO 收口(异步上下文内阻塞): unattended 看板读写改 tokio::fs、fetch_url 探活改 spawn_blocking、http/generate_image 清理改 tokio::fs - 子代理 P3 批量: 任务书钉扎上限/截断文案区分父子语境/环境画像日期/temperature 可覆盖/model 回退上报/lang 透传 - crate 清理: df-execute·df-types 注释与格式、df-ideas 过时注释、df-nodes subflow 夸大描述、df-project 死变量
This commit is contained in:
@@ -209,7 +209,7 @@
|
||||
<span v-if="g.status === 'active'" class="ai-goal-dot"></span>
|
||||
<span v-else class="ai-goal-check">✓</span>
|
||||
<span class="ai-goal-text">{{ g.text }}</span>
|
||||
<button class="ai-goal-remove" @click.stop="removeGoal(i)">×</button>
|
||||
<button class="ai-goal-remove" @click.stop="removeGoal(i)" :aria-label="$t('common.delete')">×</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -222,7 +222,7 @@
|
||||
</button>
|
||||
<Transition name="ai-pop">
|
||||
<div v-if="historyExpanded" class="ai-history-inline-list">
|
||||
<div v-for="h in historyMsgs" :key="h.id" class="ai-history-inline-item" @click="scrollToMessage(h.id)">
|
||||
<div v-for="h in historyMsgs" :key="h.id" class="ai-history-inline-item" role="button" tabindex="0" @click="scrollToMessage(h.id)" @keydown.enter.prevent="scrollToMessage(h.id)" @keydown.space.prevent="scrollToMessage(h.id)">
|
||||
<span class="ai-history-index">{{ h.num }}</span>
|
||||
<span class="ai-history-text">{{ h.text }}</span>
|
||||
<span class="ai-history-time">{{ h.time }}</span>
|
||||
@@ -238,7 +238,7 @@
|
||||
</button>
|
||||
<Transition name="ai-pop">
|
||||
<div v-if="summaryExpanded" class="ai-summary-inline-list">
|
||||
<div v-for="(s, i) in summaryMsgs" :key="i" class="ai-summary-inline-item" @click="scrollToMessage(s.id)">
|
||||
<div v-for="(s, i) in summaryMsgs" :key="i" class="ai-summary-inline-item" role="button" tabindex="0" @click="scrollToMessage(s.id)" @keydown.enter.prevent="scrollToMessage(s.id)" @keydown.space.prevent="scrollToMessage(s.id)">
|
||||
<span class="ai-summary-label">{{ $t('aiChat.compressedSummaryLabel') }} #{{ summaryMsgs.length - i }}</span>
|
||||
<p class="ai-summary-preview">{{ s.text }}</p>
|
||||
</div>
|
||||
@@ -309,7 +309,7 @@ import { aiApi } from '../../api/ai'
|
||||
import { pendingTaskProgress } from '../../composables/ai/useAiPendingState'
|
||||
import { convRounds } from '../../composables/ai/aiShared'
|
||||
import { switchConversation } from '../../composables/ai/useAiConversations'
|
||||
import type { AiMessage, ModelConfig, GoalEntry } from '../../api/types'
|
||||
import type { ModelConfig, GoalEntry } from '../../api/types'
|
||||
|
||||
defineProps<{
|
||||
/** 分离窗口模式(嵌入 vs 分离决定顶部按钮组) */
|
||||
@@ -595,6 +595,7 @@ const goalsExpanded = ref(false)
|
||||
|
||||
// ── 📋 历史消息(当前会话 user 消息列表) ──
|
||||
interface HistoryItem { id: string; text: string; time: string; num: number }
|
||||
interface SummaryItem { id: string; text: string }
|
||||
/** 时间显示:今日 HH:mm / 昨天 HH:mm / MM-DD HH:mm */
|
||||
function fmtTime(ts: number): string {
|
||||
const d = new Date(ts)
|
||||
@@ -616,17 +617,45 @@ function fmtTime(ts: number): string {
|
||||
}
|
||||
const MAX_HISTORY = 20
|
||||
const MAX_CHARS = 80
|
||||
const historyMsgs = computed<HistoryItem[]>(() => {
|
||||
const allUserMsgs = store.state.messages
|
||||
.filter((m): m is AiMessage & { timestamp: number } => m.role === 'user' && !!m.content && !!m.timestamp)
|
||||
.map((m, idx) => ({
|
||||
id: m.id,
|
||||
text: m.content.length > MAX_CHARS ? m.content.slice(0, MAX_CHARS) + '…' : m.content,
|
||||
time: fmtTime(m.timestamp),
|
||||
num: idx + 1,
|
||||
}))
|
||||
return allUserMsgs.slice(-MAX_HISTORY).reverse()
|
||||
// 单次遍历 messages,一次产出「历史(user 消息)/摘要(system 消息)/项目名」三路派生,
|
||||
// 替代原先三处 computed 各自全量扫描(historyMsgs/summaryMsgs/contextInfo)。
|
||||
// 三路的筛选条件、收集顺序、截断与反转语义与拆分前逐项一致。
|
||||
const msgDerived = computed(() => {
|
||||
const history: HistoryItem[] = []
|
||||
const summaries: SummaryItem[] = []
|
||||
const projectLabels = new Set<string>()
|
||||
for (const m of store.state.messages) {
|
||||
if (m.role === 'user') {
|
||||
// 项目名收集只认 user 的 mentionSpans(不设 content/timestamp 门槛),
|
||||
// 与原 contextInfo 的独立遍历同条件,故与下面的历史筛选互不干扰。
|
||||
if (m.mentionSpans?.length) {
|
||||
for (const sp of m.mentionSpans) {
|
||||
if (sp.kind === 'project' && sp.label) projectLabels.add(sp.label)
|
||||
}
|
||||
}
|
||||
if (m.content && m.timestamp) {
|
||||
history.push({
|
||||
id: m.id,
|
||||
text: m.content.length > MAX_CHARS ? m.content.slice(0, MAX_CHARS) + '…' : m.content,
|
||||
time: fmtTime(m.timestamp),
|
||||
num: history.length + 1,
|
||||
})
|
||||
}
|
||||
} else if (m.role === 'system' && m.content) {
|
||||
summaries.push({
|
||||
id: m.id,
|
||||
text: m.content.length > 120 ? m.content.slice(0, 120) + '…' : m.content,
|
||||
})
|
||||
}
|
||||
}
|
||||
return {
|
||||
history: history.slice(-MAX_HISTORY).reverse(),
|
||||
summaries,
|
||||
projectNames: Array.from(projectLabels).join(', '),
|
||||
}
|
||||
})
|
||||
const historyMsgs = computed<HistoryItem[]>(() => msgDerived.value.history)
|
||||
const summaryMsgs = computed<SummaryItem[]>(() => msgDerived.value.summaries)
|
||||
const historyExpanded = ref(false)
|
||||
|
||||
/** 点击历史消息项 → 滚动定位到对应的消息气泡 */
|
||||
@@ -672,15 +701,7 @@ function onBottomToolsClickOutside(e: MouseEvent) {
|
||||
}
|
||||
}
|
||||
// ── 📄 上下文摘要(压缩/归档的系统消息) ──
|
||||
interface SummaryItem { id: string; text: string }
|
||||
const summaryMsgs = computed<SummaryItem[]>(() => {
|
||||
return store.state.messages
|
||||
.filter(m => m.role === 'system' && !!m.content)
|
||||
.map(m => ({
|
||||
id: m.id,
|
||||
text: m.content.length > 120 ? m.content.slice(0, 120) + '…' : m.content,
|
||||
}))
|
||||
})
|
||||
// SummaryItem 类型与 summaryMsgs 派生已上移至历史区(与 historyMsgs 共享单次遍历)。
|
||||
const summaryExpanded = ref(false)
|
||||
|
||||
// ── L3 完整上下文 ──
|
||||
@@ -707,22 +728,11 @@ function togglePanel(panel: 'goals' | 'history' | 'summary' | 'context' | 'runni
|
||||
const contextInfo = computed<ContextInfo>(() => {
|
||||
const convId = store.state.activeConversationId
|
||||
const conv = convId ? store.state.conversations.find(c => c.id === convId) : undefined
|
||||
// 从所有 user 消息中收集 mentionSpans(kind === 'project'),去重
|
||||
const projectLabels = new Set<string>()
|
||||
for (const m of store.state.messages) {
|
||||
if (m.role === 'user' && m.mentionSpans?.length) {
|
||||
for (const sp of m.mentionSpans) {
|
||||
if (sp.kind === 'project' && sp.label) {
|
||||
projectLabels.add(sp.label)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
goals: conv?.pinned_goals ?? [],
|
||||
summaryCount: summaryMsgs.value.length,
|
||||
summaryCount: msgDerived.value.summaries.length,
|
||||
osInfo: 'Windows 11 / PowerShell',
|
||||
projectNames: Array.from(projectLabels).join(', ') || t('aiChat.none'),
|
||||
projectNames: msgDerived.value.projectNames || t('aiChat.none'),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -916,6 +926,8 @@ onBeforeUnmount(() => {
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* 聚焦可见环:聚焦态无边框变化,且 scoped 选择器特异性高于全局 :focus-visible 兜底,须就地补环,仅键盘聚焦显示 */
|
||||
.ai-model-select:focus-visible { outline: 2px solid var(--df-accent); outline-offset: 1px; }
|
||||
.ai-model-select--compact {
|
||||
max-width: 130px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user