优化: UX错误气泡区分重试类型+工具卡按轮次自动收起
This commit is contained in:
@@ -401,13 +401,18 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- UX-03:错误气泡操作入口(重试 / 去设置)。
|
||||
仅 isError 消息显示;「去设置」仅 error_type ∈ {auth, provider_config} 显示。 -->
|
||||
<!-- UX-03 / UX-260616-01:错误气泡操作入口(重试 / 去设置)。
|
||||
仅 isError 消息显示;「重试」仅可重试错误(network/timeout/unknown)显示,
|
||||
Fatal 类(auth/provider_config — 鉴权/4xx/参数错)重试必败故隐藏;
|
||||
「去设置」仅 error_type ∈ {auth, provider_config} 显示;
|
||||
Fatal 时附 notRetryable 提示,说明为何无重试按钮。 -->
|
||||
<div
|
||||
v-if="msg.isError && !store.state.streaming"
|
||||
class="ai-msg-actions ai-msg-actions--error"
|
||||
>
|
||||
<span v-if="!canRetry(msg)" class="ai-msg-not-retryable">{{ $t('ai.notRetryable') }}</span>
|
||||
<button
|
||||
v-if="canRetry(msg)"
|
||||
class="ai-msg-act ai-msg-act--primary"
|
||||
:disabled="store.state.streaming"
|
||||
@click.stop="handleRetry"
|
||||
@@ -1632,6 +1637,31 @@ onBeforeUnmount(() => {
|
||||
// (deep watch 检测仍每帧跑,但 body 跳过)。剔除 len 后流式 delta 不再触发收起重算,
|
||||
// 只在 toolCall 状态翻转(新卡片/完成/拒绝)时触发,与折叠语义精确对齐。
|
||||
// snap/MESSAGE_CAP 兜底机制保持不变(deep watch 仍做变化检测,只是 body 短路掉无意义重算)。
|
||||
//
|
||||
// UX-260616-03: 触发时机扩展。除原 messages deep watch(toolCall 状态翻转/新消息条数),
|
||||
// 另加 agentRound watch(AiAgentRound 新轮次 → 收起上一轮工具卡,聚焦当前轮)。
|
||||
// active Set 推导逻辑复用 buildActiveToolIds,两 watch 共用。
|
||||
function buildActiveToolIds(msgs: typeof store.state.messages): Set<string> {
|
||||
const active = new Set<string>()
|
||||
for (const m of msgs) {
|
||||
for (const t of (m.toolCalls || [])) {
|
||||
// 与 ToolCard.shouldKeepOpen 同源:running/pending_approval/rejected/write_file 保持展开
|
||||
if (t.status === 'running' || t.status === 'pending_approval'
|
||||
|| t.status === 'rejected' || t.name === 'write_file') {
|
||||
active.add(t.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
return active
|
||||
}
|
||||
|
||||
/** 逐实例调 collapseInactive(v-for 内 ref 收集为数组,逐实例调否则报 not a function) */
|
||||
function collapseAllToolLists(active: Set<string>): void {
|
||||
const tcl = toolCardListRef.value as unknown
|
||||
const refs = Array.isArray(tcl) ? tcl : (tcl ? [tcl] : [])
|
||||
refs.forEach((r: any) => r?.collapseInactive?.(active))
|
||||
}
|
||||
|
||||
let lastMsgSnapshot = ''
|
||||
watch(
|
||||
() => store.state.messages,
|
||||
@@ -1644,25 +1674,21 @@ watch(
|
||||
const isFirst = lastMsgSnapshot === ''
|
||||
lastMsgSnapshot = snap
|
||||
if (isFirst) return // 首次加载/切换对话不触发收起
|
||||
const active = new Set<string>()
|
||||
for (const m of msgs) {
|
||||
for (const t of (m.toolCalls || [])) {
|
||||
// 与 ToolCard.shouldKeepOpen 同源:running/pending_approval/rejected/write_file 保持展开
|
||||
if (t.status === 'running' || t.status === 'pending_approval'
|
||||
|| t.status === 'rejected' || t.name === 'write_file') {
|
||||
active.add(t.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
// ref 绑定在 v-for 内 → Vue 3 收集为数组;逐实例调用 collapseInactive,
|
||||
// 否则数组上访问 .collapseInactive 报 not a function,中断组件更新致乐观状态不渲染
|
||||
const tcl = toolCardListRef.value as unknown
|
||||
const refs = Array.isArray(tcl) ? tcl : (tcl ? [tcl] : [])
|
||||
refs.forEach((r: any) => r?.collapseInactive?.(active))
|
||||
collapseAllToolLists(buildActiveToolIds(msgs))
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// UX-260616-03: AiAgentRound 新轮次到达时收起上一轮工具卡(round 递增)。
|
||||
// 首次加载不触发(等真到达更"新"的轮次才收起,避免历史会话加载即折叠)。
|
||||
watch(
|
||||
() => store.state.agentRound,
|
||||
(cur, prev) => {
|
||||
if (cur <= (prev ?? 0)) return
|
||||
collapseAllToolLists(buildActiveToolIds(store.state.messages))
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
// 全局快捷键(UX-2025-07):window 级 listener,卸载时移除(对齐 _unlistenToolSlow 生命周期)
|
||||
window.addEventListener('keydown', onGlobalKeydown)
|
||||
@@ -1905,6 +1931,19 @@ function canOpenSettings(msg: AiMessage): boolean {
|
||||
return et === 'auth' || et === 'provider_config'
|
||||
}
|
||||
|
||||
/**
|
||||
* UX-260616-01: 错误是否可重试(显隐「重试」按钮)。
|
||||
* Fatal 类错误(鉴权/Provider 配置错——4xx/401/403/invalid_api_key)重试必再败,
|
||||
* 隐藏重试按钮,引导用户走「去设置」。network/timeout/unknown 为瞬时错误,可重试。
|
||||
* retryable 推断依据后端 error_type(本批前端推断;后端 AiStreamRetry/AiError 加 retryable 字段下批 df-ai 增强)。
|
||||
*/
|
||||
function canRetry(msg: AiMessage): boolean {
|
||||
const et = (msg as AiMessageWithErrorType).errorType
|
||||
// auth/provider_config = Fatal(鉴权/配置错),重试无意义
|
||||
if (et === 'auth' || et === 'provider_config') return false
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误气泡「重试」:取错误前的末尾 user 消息重发。
|
||||
* 复用 regenerate 链路(后端弹末尾 AI 回复——错误消息本身——取触发它的 user 消息重跑)。
|
||||
@@ -2654,6 +2693,14 @@ body.ai-sidebar-resizing * {
|
||||
.ai-msg-actions--error {
|
||||
opacity: 1;
|
||||
}
|
||||
/* UX-260616-01: Fatal 错误(不可重试)的提示文案——替代隐藏的「重试」按钮,
|
||||
说明为何无重试入口,引导走「去设置」。 */
|
||||
.ai-msg-not-retryable {
|
||||
font-size: 10.5px;
|
||||
color: var(--df-text-dim);
|
||||
font-style: italic;
|
||||
align-self: center;
|
||||
}
|
||||
.ai-msg-act {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -348,7 +348,13 @@ function groupDisplayName(group: ToolCallGroup): string {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* -- 分组隐藏态的卡片 -- */
|
||||
/* -- 分组隐藏态的卡片。
|
||||
UX-260616-03: 保持 display:none(原行为)。
|
||||
不改 max-height/opacity 动画的原因:flex 容器 gap:8px 对零高 item 仍贡献 gap,
|
||||
会留双倍间距致布局回归;display:none 移出流才无 gap。
|
||||
真正的高度动画需 <Transition> JS 钩子(高度 auto→0 不可纯 CSS transition),
|
||||
本轮野人 lite 不重构 ToolCard 单卡级折叠(v-show 同因)。
|
||||
触发时机扩展(新消息/新轮次)已落地,见 AiChat.vue watch。 -- */
|
||||
.ai-tool-card--group-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user