优化: 工具失败视觉区分(UX-01)+旧卡自动收起增强(UX-03)
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<div
|
||||
class="ai-tool-card"
|
||||
:class="['ai-tool-card--' + tc.status, { 'ai-tool-card--collapsed': !isExpanded && !shouldKeepOpen(tc) }]"
|
||||
:class="[
|
||||
'ai-tool-card--' + tc.status,
|
||||
{ 'ai-tool-card--collapsed': !isExpanded && !shouldKeepOpen(tc), 'ai-tool-card--failed': isToolFailure(tc) },
|
||||
]"
|
||||
>
|
||||
<!-- 卡片头部:图标 + 名称 + 状态 -->
|
||||
<div class="ai-tool-header" @click="emit('toggle', tc.id)">
|
||||
@@ -11,9 +14,10 @@
|
||||
<div class="ai-tool-header-text">
|
||||
<span class="ai-tool-name">{{ toolDisplayName(tc) }}</span>
|
||||
<span v-if="tc.status === 'running'" class="ai-tool-sub">{{ $t('aiTool.running') }}</span>
|
||||
<span v-else-if="isToolFailure(tc)" class="ai-tool-sub ai-tool-sub--failed">{{ $t('aiTool.executionFailed') }}</span>
|
||||
<span v-else-if="toolResultSummary(tc)" class="ai-tool-sub">{{ toolResultSummary(tc) }}</span>
|
||||
</div>
|
||||
<span class="ai-tool-status-dot" :class="'ai-tool-status-dot--' + tc.status"></span>
|
||||
<span class="ai-tool-status-dot" :class="'ai-tool-status-dot--' + (isToolFailure(tc) ? 'failed' : tc.status)"></span>
|
||||
<span v-if="!shouldKeepOpen(tc)" class="ai-tool-collapse-hint" :class="{ 'is-open': isExpanded }">▸</span>
|
||||
</div>
|
||||
|
||||
@@ -107,7 +111,12 @@
|
||||
</div>
|
||||
|
||||
<!-- 通用 CRUD 结果 -->
|
||||
<div v-else-if="tc.result && tc.status === 'completed'" class="ai-tool-result">
|
||||
<div
|
||||
v-else-if="tc.result && tc.status === 'completed'"
|
||||
class="ai-tool-result"
|
||||
:class="{ 'ai-tool-result--failed': isToolFailure(tc) }"
|
||||
>
|
||||
<div v-if="isToolFailure(tc)" class="ai-tool-failed-banner">⚠ {{ $t('aiTool.executionFailedHint') }}</div>
|
||||
<code>{{ formatToolResult(tc) }}</code>
|
||||
</div>
|
||||
</div><!-- /.ai-tool-body -->
|
||||
@@ -184,6 +193,38 @@ function shouldKeepOpen(tc: AiToolCallInfo): boolean {
|
||||
|| tc.name === 'write_file'
|
||||
}
|
||||
|
||||
/**
|
||||
* UX-260616-01: 工具执行失败检测(completed 状态下的失败语义)。
|
||||
*
|
||||
* 后端 AR-6:Low 风险工具失败不 emit AiError,改 emit AiToolCallCompleted(result=错误),
|
||||
* 故失败工具 status 仍为 'completed',与成功工具视觉同态(原绿色边框+绿色结果框)。
|
||||
* 此函数区分两类失败:
|
||||
* - run_command exit_code≠0:结果 JSON 合法,parseResult 命中,看 exit_code 判失败
|
||||
* - 通用工具失败:AR-6 路径后端塞 serde_json::Value::String("工具 X 执行失败: ..."),
|
||||
* 前端 parseResult 返回 null(非 JSON 对象)→ 走 raw 文本兜底,无结构信号
|
||||
* 此时按字符串前缀 "执行失败"/"failed" 启发式判定(对齐后端 err_msg 格式)。
|
||||
*
|
||||
* 注意:status==='rejected'(用户拒绝)走独立分支,不算工具失败,不进此函数。
|
||||
* running/pending_approval 自然 false(无 result 或 result 未稳定)。
|
||||
*/
|
||||
function isToolFailure(tc: AiToolCallInfo): boolean {
|
||||
if (tc.status !== 'completed') return false
|
||||
// run_command:结构化 exit_code 判定(可靠)
|
||||
if (tc.name === 'run_command') {
|
||||
const r = parseResult(tc.result)
|
||||
if (r && typeof r.exit_code === 'number') return r.exit_code !== 0
|
||||
// run_command 但无 exit_code(异常)→ 保守判失败
|
||||
if (r) return true
|
||||
}
|
||||
// 通用工具:parseResult 返回 null(非 JSON,AR-6 错误字符串路径)→ 看文本前缀
|
||||
if (!parseResult(tc.result)) {
|
||||
const raw = typeof tc.result === 'string' ? tc.result : ''
|
||||
if (!raw) return false
|
||||
return /执行失败|failed|error[:\s]/i.test(raw)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function formatToolName(name: string): string {
|
||||
if (!name) return ''
|
||||
return name.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
|
||||
@@ -666,7 +707,10 @@ function toolResultSummary(tc: AiToolCallInfo): string {
|
||||
overflow: hidden;
|
||||
max-width: var(--df-msg-max-width);
|
||||
animation: fadeInUp 0.2s var(--df-ease);
|
||||
transition: border-color 0.2s var(--df-ease);
|
||||
/* UX-260616-03: border/shadow/opacity 过渡让折叠/失败态切换不突兀。
|
||||
不做 body 高度动画(v-show display 切换 + flex gap 会留双倍间距致布局跳变,
|
||||
见 ToolCardList 注释);opacity 过渡覆盖自动收起时的视觉跳变(约 200ms)。 */
|
||||
transition: border-color 0.2s var(--df-ease), box-shadow 0.2s var(--df-ease), opacity 0.2s var(--df-ease);
|
||||
}
|
||||
.ai-tool-card--pending_approval {
|
||||
border-color: rgba(240,199,94,0.3);
|
||||
@@ -679,6 +723,32 @@ function toolResultSummary(tc: AiToolCallInfo): string {
|
||||
border-color: rgba(240,101,101,0.2);
|
||||
opacity: 0.55;
|
||||
}
|
||||
/* UX-260616-01: 工具失败态——AR-6 下 status 仍为 completed,故单独覆盖边框/底色与 completed 区分 */
|
||||
.ai-tool-card--failed {
|
||||
border-color: rgba(240,101,101,0.35);
|
||||
box-shadow: 0 0 0 1px rgba(240,101,101,0.06);
|
||||
}
|
||||
.ai-tool-status-dot--failed {
|
||||
background: var(--df-danger);
|
||||
}
|
||||
.ai-tool-sub--failed {
|
||||
color: var(--df-danger);
|
||||
}
|
||||
.ai-tool-failed-banner {
|
||||
padding: 4px 10px 2px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--df-danger);
|
||||
background: var(--df-danger-bg);
|
||||
border-bottom: 0.5px solid rgba(240,101,101,0.2);
|
||||
}
|
||||
.ai-tool-result--failed {
|
||||
background: var(--df-danger-bg);
|
||||
border-top-color: rgba(240,101,101,0.2);
|
||||
}
|
||||
.ai-tool-result--failed code {
|
||||
color: var(--df-danger);
|
||||
}
|
||||
.ai-tool-rejected {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user