优化: 聊天体验批与Web规范合规

This commit is contained in:
lxy
2026-10-05 23:41:32 +08:00
parent 9e81182b4f
commit 28e6acbf3f
41 changed files with 1518 additions and 570 deletions
+29 -6
View File
@@ -26,6 +26,8 @@
<div v-if="group.calls.length >= 2" class="ai-tool-group-header" @click="toggleGroupCollapse(gi)">
<span class="ai-tool-group-icon" v-html="groupIcon(group.name)"></span>
<span class="ai-tool-group-name">{{ groupDisplayName(group) }}</span>
<!-- R7 组头失败徽标:折叠组内失败卡不展开也能从组头发现(红字计数) -->
<span v-if="groupFailCounts[gi] > 0" class="ai-tool-group-fail">{{ $t('aiTool.groupFailCount', { n: groupFailCounts[gi] }) }}</span>
<span class="ai-tool-group-chevron" :class="{ 'is-open': !isGroupCollapsed(gi) }">▸</span>
</div>
<ToolCard
@@ -61,6 +63,7 @@ import { listen, type UnlistenFn } from '@tauri-apps/api/event'
import type { AiToolCallInfo } from '@/api/types'
import { useConfirm } from '@/composables/useConfirm'
import { toolRiskLevel } from '@/composables/ai/useToolApproval'
import { isToolFailure } from '@/composables/ai/useToolCard'
// useI18n needed for $t() in template (collapseAll / expandAll) + groupDisplayName(i18n key)
const { t } = useI18n()
@@ -110,6 +113,16 @@ const collapsedGroups = ref<Set<number>>(new Set())
// 用户手动展开过的组不再被自动收回——名字比索引稳(分组只追加不重排,但语义上名字更直白)。
const userTouchedGroups = ref(new Set<string>())
// ── R7 组头失败徽标:折叠组内失败卡扫组头即可发现 ──
// 病灶(R7 单行折叠信息量缺口):工具失败信号散在单卡红边,同名工具组默认折叠后
// 失败卡被 display:none 压住,10+ 卡需逐组展开逐行扫。轨迹卡聚合行/执行中步骤行
// 已有「失败 N」计数,唯一盲区是组头。此处补:组内失败卡 ≥1 时组头显红徽标
// 「{n} 失败」,折叠展开都可见。失败判定复用 useToolCard.isToolFailure 单点(与
// 单卡红边/轨迹卡统计同源,防双写判定漂移)。
const groupFailCounts = computed<number[]>(() =>
groupedToolCalls.value.map(g => g.calls.reduce((n, tc) => n + (isToolFailure(tc) ? 1 : 0), 0)),
)
interface ToolCallGroup {
name: string
calls: AiToolCallInfo[]
@@ -415,12 +428,12 @@ defineExpose({ collapseInactive, scrollToFirstPending })
/* -- 分组图标(复用 ToolCard 的 toolCategory 逻辑) -- */
function groupIcon(name: string): string {
if (name === 'read_file') return '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>'
if (name === 'write_file') return '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>'
if (name === 'list_directory') return '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>'
if (name.includes('create')) return '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>'
if (name.includes('delete')) return '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>'
return '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>'
if (name === 'read_file') return '<svg aria-hidden="true" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>'
if (name === 'write_file') return '<svg aria-hidden="true" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>'
if (name === 'list_directory') return '<svg aria-hidden="true" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/></svg>'
if (name.includes('create')) return '<svg aria-hidden="true" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>'
if (name.includes('delete')) return '<svg aria-hidden="true" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>'
return '<svg aria-hidden="true" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>'
}
</script>
@@ -501,6 +514,16 @@ function groupIcon(name: string): string {
.ai-tool-group-header:hover {
background: rgba(255,255,255,0.03);
}
/* R7 组头失败徽标:失败计数红字,margin-left auto 推到 chevron 之前右对齐 */
.ai-tool-group-fail {
margin-left: auto;
margin-right: 2px;
font-size: 10px;
font-weight: 600;
color: var(--df-danger);
opacity: 0.9;
white-space: nowrap;
}
.ai-tool-group-icon {
display: flex;
align-items: center;