优化: ai-chat 对话流布局与轨迹信息密度改造
- 分组行去统计化,调用统计归并轨迹卡头部 - 相对时间改时间分隔线,跨日显绝对时间 - 思考过程单行 pill 化,答案卡加 accent 边 - 工具卡短路径展示与耗时提示统一 - 高风险工具名单启动时同步后端注册表 - 自动重试改琥珀细条,红卡与手动重试仅终态 - 轨迹卡头并时间线行,显轮次号与绝对时间 - 运行中摘要补当前工具名与失败计数 - 执行中工具卡加实时计时,耗时统一人话口径 - 重试残留清理改标记匹配,替代中文子串硬编码
This commit is contained in:
@@ -14,10 +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="isFailed" 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>
|
||||
<span v-else-if="isFailed" class="ai-tool-sub ai-tool-sub--failed" :title="summaryTitle">{{ $t('aiTool.executionFailed') }}</span>
|
||||
<span v-else-if="toolResultSummary(tc)" class="ai-tool-sub" :title="summaryTitle">{{ toolResultSummary(tc) }}</span>
|
||||
</div>
|
||||
<span class="ai-tool-status-dot" :class="'ai-tool-status-dot--' + (isFailed ? 'failed' : tc.status)"></span>
|
||||
<span class="ai-tool-status-dot" :class="'ai-tool-status-dot--' + (isFailed ? 'failed' : tc.status)" :title="statusTitle"></span>
|
||||
<!-- 统一卡形态第4要素:耗时(前端 startedAt/completedAt 差;历史消息无时间戳隐藏不显 0)。
|
||||
hover 显绝对起止时刻,相对时间不再占卡片行(B 聚合卡统一)。 -->
|
||||
<span
|
||||
@@ -144,7 +144,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, toRef } from 'vue'
|
||||
import { computed, ref, toRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
shouldKeepOpen,
|
||||
@@ -152,12 +152,14 @@ import {
|
||||
toolCategory,
|
||||
toolIcon,
|
||||
toolResultSummary,
|
||||
argString,
|
||||
} from '@/composables/ai/useToolCard'
|
||||
import { parseDiffLines } from '@/composables/ai/useToolCardRender'
|
||||
import { useToolCardHeader } from '@/composables/ai/useToolCardHeader'
|
||||
import { useToolApproval } from '@/composables/ai/useToolApproval'
|
||||
import { useTimerOwnership } from '@/composables/useTimerOwnership'
|
||||
import { callDurationText } from '@/composables/ai/useTurnTrace'
|
||||
import { formatDate } from '@/utils/time'
|
||||
import { formatDate, formatDuration } from '@/utils/time'
|
||||
import type { AiToolCallInfo } from '@/api/types'
|
||||
import ConfirmDialog from './ConfirmDialog.vue'
|
||||
import ToolResultBody from './ToolResultBody.vue'
|
||||
@@ -241,14 +243,57 @@ const isFailed = computed(() => {
|
||||
/** write_file 审批 diff 行解析(复用 parseDiffLines 纯函数,数据源 props.tc.diff) */
|
||||
const diffLines = computed(() => parseDiffLines(props.tc.diff))
|
||||
|
||||
/** 单卡耗时(前端时间差;历史消息无 startedAt/completedAt 返空串隐藏) */
|
||||
const durationText = computed(() => callDurationText(props.tc.startedAt, props.tc.completedAt))
|
||||
/** hover 提示:绝对起止时刻(相对时长 hover 可溯具体时间) */
|
||||
// 执行中实时走表:进入 running 每秒刷新 nowTick,离开时清理(定时器经 useTimerOwnership
|
||||
// 注册,onUnmounted 自动全清,防组件卸载后计时器仍触发)
|
||||
const { setOwnInterval, clearOwnInterval } = useTimerOwnership()
|
||||
const nowTick = ref(Date.now())
|
||||
let runTimerId: ReturnType<typeof setInterval> | null = null
|
||||
watch(() => props.tc.status, (s) => {
|
||||
if (s === 'running') {
|
||||
nowTick.value = Date.now()
|
||||
if (runTimerId === null) {
|
||||
runTimerId = setOwnInterval(() => { nowTick.value = Date.now() }, 1000)
|
||||
}
|
||||
} else if (runTimerId !== null) {
|
||||
clearOwnInterval(runTimerId)
|
||||
runTimerId = null
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
/** 单卡耗时(前端时间差;历史消息无 startedAt/completedAt 返空串隐藏);
|
||||
* running 态以 nowTick 实时走表(需 startedAt,与完成态共用同一时间差口径) */
|
||||
const durationText = computed(() => {
|
||||
const tc = props.tc
|
||||
if (tc.status === 'running' && tc.startedAt != null) {
|
||||
return formatDuration(nowTick.value - tc.startedAt, true)
|
||||
}
|
||||
return callDurationText(tc.startedAt, tc.completedAt)
|
||||
})
|
||||
/** hover 提示:后端实测毫秒 + 绝对起止时刻(run_command 取 duration_ms、http_request 取
|
||||
* elapsed_ms,与前端时间差口径互证;无后端耗时的工具只显起止时刻) */
|
||||
const durationTitle = computed(() => {
|
||||
const tc = props.tc
|
||||
if (tc.startedAt == null || tc.completedAt == null) return ''
|
||||
return `${formatDate(tc.startedAt)} ~ ${formatDate(tc.completedAt)}`
|
||||
const r = parsed.value
|
||||
const backendMs = tc.name === 'run_command'
|
||||
? (typeof r?.duration_ms === 'number' ? r.duration_ms : null)
|
||||
: (tc.name === 'http_request' && typeof r?.elapsed_ms === 'number' ? r.elapsed_ms : null)
|
||||
const span = `${formatDate(tc.startedAt)} ~ ${formatDate(tc.completedAt)}`
|
||||
return backendMs != null ? `${backendMs}ms · ${span}` : span
|
||||
})
|
||||
|
||||
/** 状态点 tooltip:成功/失败/进行中/待审批/已拒绝(绿红蓝点 hover 可读,不再靠颜色猜) */
|
||||
const statusTitle = computed(() => {
|
||||
const s = props.tc.status
|
||||
if (s === 'running') return t('aiTool.statusRunning')
|
||||
if (s === 'pending_approval') return t('aiTool.statusPending')
|
||||
if (s === 'rejected') return t('aiTool.statusRejected')
|
||||
if (s === 'completed') return isFailed.value ? t('aiTool.statusFailed') : t('aiTool.statusOk')
|
||||
return ''
|
||||
})
|
||||
|
||||
/** 头部摘要 hover 回看完整路径(摘要统一缩略尾段,全路径走 title;路径类工具才有值) */
|
||||
const summaryTitle = computed(() => argString(props.tc.args, 'path'))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user