优化: UX错误气泡区分重试类型+工具卡按轮次自动收起
This commit is contained in:
@@ -401,13 +401,18 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- UX-03:错误气泡操作入口(重试 / 去设置)。
|
<!-- UX-03 / UX-260616-01:错误气泡操作入口(重试 / 去设置)。
|
||||||
仅 isError 消息显示;「去设置」仅 error_type ∈ {auth, provider_config} 显示。 -->
|
仅 isError 消息显示;「重试」仅可重试错误(network/timeout/unknown)显示,
|
||||||
|
Fatal 类(auth/provider_config — 鉴权/4xx/参数错)重试必败故隐藏;
|
||||||
|
「去设置」仅 error_type ∈ {auth, provider_config} 显示;
|
||||||
|
Fatal 时附 notRetryable 提示,说明为何无重试按钮。 -->
|
||||||
<div
|
<div
|
||||||
v-if="msg.isError && !store.state.streaming"
|
v-if="msg.isError && !store.state.streaming"
|
||||||
class="ai-msg-actions ai-msg-actions--error"
|
class="ai-msg-actions ai-msg-actions--error"
|
||||||
>
|
>
|
||||||
|
<span v-if="!canRetry(msg)" class="ai-msg-not-retryable">{{ $t('ai.notRetryable') }}</span>
|
||||||
<button
|
<button
|
||||||
|
v-if="canRetry(msg)"
|
||||||
class="ai-msg-act ai-msg-act--primary"
|
class="ai-msg-act ai-msg-act--primary"
|
||||||
:disabled="store.state.streaming"
|
:disabled="store.state.streaming"
|
||||||
@click.stop="handleRetry"
|
@click.stop="handleRetry"
|
||||||
@@ -1632,6 +1637,31 @@ onBeforeUnmount(() => {
|
|||||||
// (deep watch 检测仍每帧跑,但 body 跳过)。剔除 len 后流式 delta 不再触发收起重算,
|
// (deep watch 检测仍每帧跑,但 body 跳过)。剔除 len 后流式 delta 不再触发收起重算,
|
||||||
// 只在 toolCall 状态翻转(新卡片/完成/拒绝)时触发,与折叠语义精确对齐。
|
// 只在 toolCall 状态翻转(新卡片/完成/拒绝)时触发,与折叠语义精确对齐。
|
||||||
// snap/MESSAGE_CAP 兜底机制保持不变(deep watch 仍做变化检测,只是 body 短路掉无意义重算)。
|
// 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 = ''
|
let lastMsgSnapshot = ''
|
||||||
watch(
|
watch(
|
||||||
() => store.state.messages,
|
() => store.state.messages,
|
||||||
@@ -1644,25 +1674,21 @@ watch(
|
|||||||
const isFirst = lastMsgSnapshot === ''
|
const isFirst = lastMsgSnapshot === ''
|
||||||
lastMsgSnapshot = snap
|
lastMsgSnapshot = snap
|
||||||
if (isFirst) return // 首次加载/切换对话不触发收起
|
if (isFirst) return // 首次加载/切换对话不触发收起
|
||||||
const active = new Set<string>()
|
collapseAllToolLists(buildActiveToolIds(msgs))
|
||||||
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))
|
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ 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 () => {
|
onMounted(async () => {
|
||||||
// 全局快捷键(UX-2025-07):window 级 listener,卸载时移除(对齐 _unlistenToolSlow 生命周期)
|
// 全局快捷键(UX-2025-07):window 级 listener,卸载时移除(对齐 _unlistenToolSlow 生命周期)
|
||||||
window.addEventListener('keydown', onGlobalKeydown)
|
window.addEventListener('keydown', onGlobalKeydown)
|
||||||
@@ -1905,6 +1931,19 @@ function canOpenSettings(msg: AiMessage): boolean {
|
|||||||
return et === 'auth' || et === 'provider_config'
|
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 消息重发。
|
* 错误气泡「重试」:取错误前的末尾 user 消息重发。
|
||||||
* 复用 regenerate 链路(后端弹末尾 AI 回复——错误消息本身——取触发它的 user 消息重跑)。
|
* 复用 regenerate 链路(后端弹末尾 AI 回复——错误消息本身——取触发它的 user 消息重跑)。
|
||||||
@@ -2654,6 +2693,14 @@ body.ai-sidebar-resizing * {
|
|||||||
.ai-msg-actions--error {
|
.ai-msg-actions--error {
|
||||||
opacity: 1;
|
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 {
|
.ai-msg-act {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -348,7 +348,13 @@ function groupDisplayName(group: ToolCallGroup): string {
|
|||||||
transform: rotate(90deg);
|
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 {
|
.ai-tool-card--group-hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export default {
|
|||||||
errorNetwork: 'Network connection failed. Please check your network.',
|
errorNetwork: 'Network connection failed. Please check your network.',
|
||||||
// CR-30-2 / F-260616-07(d) / decision a1: stream pre-failure retry bubble copy
|
// CR-30-2 / F-260616-07(d) / decision a1: stream pre-failure retry bubble copy
|
||||||
aiStreamRetry: '⚠ Request failed, retrying ({attempt}/{max})…',
|
aiStreamRetry: '⚠ Request failed, retrying ({attempt}/{max})…',
|
||||||
|
// UX-260616-01: Fatal errors (4xx/auth) are not retryable — prompt user to check settings instead
|
||||||
|
notRetryable: 'This error cannot be resolved by retrying. Please check the configuration.',
|
||||||
streamInterruptedAfterTool: '⚠ The tool finished, but the following reply was interrupted (no data stream for a long time). You can click Continue to retry.',
|
streamInterruptedAfterTool: '⚠ The tool finished, but the following reply was interrupted (no data stream for a long time). You can click Continue to retry.',
|
||||||
streamInterrupted: '⚠ The response was interrupted (no data stream for a long time). This may be due to a network disconnection or a backend error. Please try again.',
|
streamInterrupted: '⚠ The response was interrupted (no data stream for a long time). This may be due to a network disconnection or a backend error. Please try again.',
|
||||||
// UX-2025-04 / CR-30-2 / decision a1: system notice bubble after mid-stream failure preserve (frontend mirrors backend session.messages)
|
// UX-2025-04 / CR-30-2 / decision a1: system notice bubble after mid-stream failure preserve (frontend mirrors backend session.messages)
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export default {
|
|||||||
editNotLastUser: 'Only the last user message can be edited',
|
editNotLastUser: 'Only the last user message can be edited',
|
||||||
placeholderEditing: 'Edit message then Enter to regenerate (Esc to cancel)',
|
placeholderEditing: 'Edit message then Enter to regenerate (Esc to cancel)',
|
||||||
|
|
||||||
// ── Error bubble actions (UX-03) ──
|
// ── Error bubble actions (UX-03 / UX-260616-01) ──
|
||||||
retry: 'Retry',
|
retry: 'Retry',
|
||||||
goToSettingsAction: 'Open settings',
|
goToSettingsAction: 'Open settings',
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export default {
|
|||||||
errorNetwork: '网络连接失败,请检查网络',
|
errorNetwork: '网络连接失败,请检查网络',
|
||||||
// CR-30-2 / F-260616-07(d) / 决策 a1: 流前失败重试中错误气泡内更新文案
|
// CR-30-2 / F-260616-07(d) / 决策 a1: 流前失败重试中错误气泡内更新文案
|
||||||
aiStreamRetry: '⚠ 调用失败,正在重试({attempt}/{max})…',
|
aiStreamRetry: '⚠ 调用失败,正在重试({attempt}/{max})…',
|
||||||
|
// UX-260616-01: Fatal 类错误(4xx/鉴权)不可重试,提示用户去设置而非重试
|
||||||
|
notRetryable: '此错误无法通过重试解决,请检查配置',
|
||||||
streamInterruptedAfterTool: '⚠ 工具已执行完成,后续回复中断(长时间无数据流)。可点继续重试。',
|
streamInterruptedAfterTool: '⚠ 工具已执行完成,后续回复中断(长时间无数据流)。可点继续重试。',
|
||||||
streamInterrupted: '⚠ 响应中断(长时间无数据流)。可能是网络断连或后端异常,请重试。',
|
streamInterrupted: '⚠ 响应中断(长时间无数据流)。可能是网络断连或后端异常,请重试。',
|
||||||
// UX-2025-04 / CR-30-2 / 决策 a1: 流中途失败保文后的系统提示气泡(前端镜像后端 session.messages)
|
// UX-2025-04 / CR-30-2 / 决策 a1: 流中途失败保文后的系统提示气泡(前端镜像后端 session.messages)
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export default {
|
|||||||
editNotLastUser: '只能编辑最后一条用户消息',
|
editNotLastUser: '只能编辑最后一条用户消息',
|
||||||
placeholderEditing: '编辑消息后回车重新生成(Esc 取消)',
|
placeholderEditing: '编辑消息后回车重新生成(Esc 取消)',
|
||||||
|
|
||||||
// ── 错误气泡操作入口(UX-03) ──
|
// ── 错误气泡操作入口(UX-03 / UX-260616-01) ──
|
||||||
retry: '重试',
|
retry: '重试',
|
||||||
goToSettingsAction: '去设置',
|
goToSettingsAction: '去设置',
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user