126 lines
4.3 KiB
Vue
126 lines
4.3 KiB
Vue
<script setup lang="ts">
|
||
import { ElMessage } from "element-plus";
|
||
import { ref, watch } from "vue";
|
||
import { api } from "../services/api";
|
||
import type { AiLogRecord } from "../types/api";
|
||
|
||
const props = defineProps<{ modelValue: boolean; logId: number | null }>();
|
||
const emit = defineEmits<{ "update:modelValue": [value: boolean] }>();
|
||
const detail = ref<AiLogRecord | null>(null);
|
||
const loading = ref(false);
|
||
|
||
watch(
|
||
() => [props.modelValue, props.logId] as const,
|
||
async ([open, logId]) => {
|
||
if (!open || !logId) return;
|
||
detail.value = null;
|
||
loading.value = true;
|
||
try {
|
||
detail.value = await api.aiLogDetail(logId);
|
||
} catch (error) {
|
||
ElMessage.error(
|
||
error instanceof Error ? error.message : "AI 请求详情加载失败",
|
||
);
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
},
|
||
);
|
||
|
||
function formatMoney(value?: number | null, currency = "CNY") {
|
||
if (typeof value !== "number") return "-";
|
||
return `${currency || "CNY"} ${value.toFixed(6)}`;
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<el-drawer
|
||
:model-value="modelValue"
|
||
size="820px"
|
||
title="AI 请求详情"
|
||
@update:model-value="emit('update:modelValue', $event)"
|
||
>
|
||
<div v-loading="loading">
|
||
<template v-if="detail">
|
||
<section class="chat-summary">
|
||
<div>
|
||
<span>请求ID</span><strong>{{ detail.id }}</strong>
|
||
</div>
|
||
<div>
|
||
<span>会话ID</span><strong>{{ detail.sessionId || "-" }}</strong>
|
||
</div>
|
||
<div>
|
||
<span>消息ID</span><strong>{{ detail.messageId || "-" }}</strong>
|
||
</div>
|
||
<div>
|
||
<span>用户ID</span><strong>{{ detail.userId || "-" }}</strong>
|
||
</div>
|
||
<div>
|
||
<span>模型</span><strong>{{ detail.modelName || "-" }}</strong>
|
||
</div>
|
||
<div>
|
||
<span>状态</span><strong>{{ detail.status }}</strong>
|
||
</div>
|
||
</section>
|
||
<section class="audit-detail-grid">
|
||
<span>知识库:{{ detail.knowledgeIds || "-" }}</span
|
||
><span>命中数:{{ detail.retrieveCount }}</span
|
||
><span>片段数:{{ detail.retrievedChunks?.length || 0 }}</span
|
||
><span>输入 Token:{{ detail.inputToken || "-" }}</span
|
||
><span>输出 Token:{{ detail.outputToken || "-" }}</span
|
||
><span>总 Token:{{ detail.totalToken || "-" }}</span
|
||
><span
|
||
>估算成本:{{
|
||
formatMoney(detail.estimatedCost, detail.currency || "CNY")
|
||
}}</span
|
||
><span>问题类型:{{ detail.questionType || "-" }}</span
|
||
><span>知识命中:{{ detail.knowledgeHit ? "是" : "否" }}</span
|
||
><span>路由原因:{{ detail.routeReason || "-" }}</span
|
||
><span>耗时:{{ detail.costMs || "-" }}ms</span
|
||
><span>时间:{{ detail.createdAt }}</span>
|
||
</section>
|
||
<p v-if="detail.errorMessage" class="error-text">
|
||
{{ detail.errorMessage }}
|
||
</p>
|
||
<h3 class="detail-title">召回知识片段</h3>
|
||
<section v-if="detail.retrievedChunks?.length" class="retrieval-chunks">
|
||
<article
|
||
v-for="chunk in detail.retrievedChunks"
|
||
:key="`${detail.id}-${chunk.index}`"
|
||
class="retrieval-chunk"
|
||
>
|
||
<div class="chunk-head">
|
||
<strong
|
||
>片段 {{ chunk.index }}/{{
|
||
detail.retrievedChunks.length
|
||
}}</strong
|
||
><span>{{ chunk.knowledgeName || "未知知识库" }}</span
|
||
><span v-if="chunk.knowledgeId"
|
||
>知识库ID:{{ chunk.knowledgeId }}</span
|
||
>
|
||
</div>
|
||
<div class="chunk-title">{{ chunk.title || "未命名片段" }}</div>
|
||
<pre>{{ chunk.content }}</pre>
|
||
<a
|
||
v-if="chunk.sourceUrl"
|
||
:href="chunk.sourceUrl"
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
>查看来源</a
|
||
>
|
||
</article>
|
||
</section>
|
||
<el-empty
|
||
v-else
|
||
description="本条请求未保存召回片段"
|
||
:image-size="72"
|
||
/>
|
||
<h3 class="detail-title">完整 Prompt</h3>
|
||
<pre class="prompt-preview">{{
|
||
detail.prompt || "无 Prompt 记录"
|
||
}}</pre>
|
||
</template>
|
||
</div>
|
||
</el-drawer>
|
||
</template>
|