Files
ai-agent/web/app/dashboard/knowledge/_components/retrieve-log-detail.tsx
T

283 lines
12 KiB
TypeScript
Raw Normal View History

2026-04-09 10:01:23 +08:00
"use client"
import { useCallback, useEffect, useMemo, useState } from "react"
import { toast } from "sonner"
import {
fetchKnowledgeRetrieveLog,
type KnowledgeRetrieveHit,
type KnowledgeRetrieveLogDetail,
} from "@/lib/api/admin"
import { formatDateTime } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle,
} from "@/components/ui/drawer"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Separator } from "@/components/ui/separator"
2026-05-25 12:06:15 +08:00
import { useI18n } from "@/i18n/provider"
import {
getKnowledgeAnswerStatusLabel,
getKnowledgeChunkProviderLabel,
getKnowledgeRetrieveChannelLabel,
getKnowledgeRetrieveSceneLabel,
} from "@/lib/knowledge-i18n"
2026-04-09 10:01:23 +08:00
type RetrieveLogDetailDrawerProps = {
open: boolean
retrieveLogId: number | null
onOpenChange: (open: boolean) => void
}
function safeParseJSON(value: string) {
if (!value) {
return null
}
try {
return JSON.parse(value) as Record<string, unknown>
} catch {
return null
}
}
2026-05-25 12:06:15 +08:00
type TFunction = (key: string, values?: Record<string, string | number>) => string
function CitationList({ hits, t }: { hits: KnowledgeRetrieveHit[]; t: TFunction }) {
2026-04-09 10:01:23 +08:00
const citations = hits.filter((item) => item.isCitation)
if (citations.length === 0) {
2026-05-25 12:06:15 +08:00
return <div className="text-sm text-muted-foreground">{t("knowledge.noCitations")}</div>
2026-04-09 10:01:23 +08:00
}
return (
<div className="space-y-3">
{citations.map((item) => (
<div key={item.id} className="rounded-lg border p-3">
<div className="flex items-center gap-2">
2026-05-25 12:06:15 +08:00
<span className="font-medium">{getHitSourceLabel(item, t)}</span>
2026-04-09 10:01:23 +08:00
<Badge variant="outline">Chunk #{item.chunkNo}</Badge>
</div>
<div className="mt-1 text-xs text-muted-foreground">
2026-05-25 12:06:15 +08:00
{item.sectionPath || item.title || t("knowledge.unrecordedSection")}
2026-04-09 10:01:23 +08:00
</div>
<div className="mt-2 text-sm leading-6 whitespace-pre-wrap text-foreground/90">
{item.snippet || "-"}
</div>
</div>
))}
</div>
)
}
export function RetrieveLogDetailDrawer({
open,
retrieveLogId,
onOpenChange,
}: RetrieveLogDetailDrawerProps) {
2026-05-25 12:06:15 +08:00
const t = useI18n()
2026-04-09 10:01:23 +08:00
const [loading, setLoading] = useState(false)
const [detail, setDetail] = useState<KnowledgeRetrieveLogDetail | null>(null)
const loadDetail = useCallback(async () => {
if (!retrieveLogId) {
setDetail(null)
return
}
setLoading(true)
try {
const data = await fetchKnowledgeRetrieveLog(retrieveLogId)
setDetail(data)
} catch (error) {
2026-05-25 12:06:15 +08:00
toast.error(error instanceof Error ? error.message : t("knowledge.loadRetrieveLogDetailFailed"))
2026-04-09 10:01:23 +08:00
} finally {
setLoading(false)
}
2026-05-25 12:06:15 +08:00
}, [retrieveLogId, t])
2026-04-09 10:01:23 +08:00
useEffect(() => {
if (open && retrieveLogId) {
void loadDetail()
}
}, [open, retrieveLogId, loadDetail])
const traceData = useMemo(() => safeParseJSON(detail?.log.traceData ?? ""), [detail?.log.traceData])
if (!open) {
return null
}
return (
<Drawer open={open} onOpenChange={onOpenChange} direction="right">
<DrawerContent className="max-w-3xl">
<DrawerHeader>
2026-05-25 12:06:15 +08:00
<DrawerTitle>{t("knowledge.retrieveLogDetail")}</DrawerTitle>
2026-04-09 10:01:23 +08:00
<DrawerDescription>
2026-05-25 12:06:15 +08:00
{detail?.log.question || (loading ? t("knowledge.loading") : t("knowledge.retrieveLogNotFound"))}
2026-04-09 10:01:23 +08:00
</DrawerDescription>
</DrawerHeader>
<ScrollArea className="h-[calc(100vh-6rem)] px-4 pb-6">
{!detail ? (
<div className="py-6 text-sm text-muted-foreground">
2026-05-25 12:06:15 +08:00
{loading ? t("knowledge.loadingDetail") : t("knowledge.emptyDetail")}
2026-04-09 10:01:23 +08:00
</div>
) : (
<div className="space-y-6 pb-6">
<section className="space-y-3">
2026-05-25 12:06:15 +08:00
<h3 className="text-sm font-semibold">{t("knowledge.requestInfo")}</h3>
2026-04-09 10:01:23 +08:00
<div className="grid gap-3 rounded-lg border p-4 md:grid-cols-2">
<div>
2026-05-25 12:06:15 +08:00
<div className="text-xs text-muted-foreground">{t("knowledge.title")}</div>
2026-04-09 10:01:23 +08:00
<div className="mt-1 text-sm">{detail.log.knowledgeBaseName || `#${detail.log.knowledgeBaseId}`}</div>
</div>
<div>
2026-05-25 12:06:15 +08:00
<div className="text-xs text-muted-foreground">{t("knowledge.createdAt")}</div>
2026-04-09 10:01:23 +08:00
<div className="mt-1 text-sm">{formatDateTime(detail.log.createdAt)}</div>
</div>
<div>
2026-05-25 12:06:15 +08:00
<div className="text-xs text-muted-foreground">{t("knowledge.channelScene")}</div>
<div className="mt-1 text-sm">
{getKnowledgeRetrieveChannelLabel(detail.log.channel, detail.log.channelName, t)} /{" "}
{getKnowledgeRetrieveSceneLabel(detail.log.scene, detail.log.sceneName, t)}
</div>
2026-04-09 10:01:23 +08:00
</div>
<div>
<div className="text-xs text-muted-foreground">Request ID</div>
<div className="mt-1 break-all font-mono text-xs">{detail.log.requestId || "-"}</div>
</div>
<div className="md:col-span-2">
2026-05-25 12:06:15 +08:00
<div className="text-xs text-muted-foreground">{t("knowledge.originalQuestion")}</div>
2026-04-09 10:01:23 +08:00
<div className="mt-1 text-sm leading-6 whitespace-pre-wrap">{detail.log.question || "-"}</div>
</div>
<div className="md:col-span-2">
2026-05-25 12:06:15 +08:00
<div className="text-xs text-muted-foreground">{t("knowledge.rewriteQuestion")}</div>
2026-04-09 10:01:23 +08:00
<div className="mt-1 text-sm leading-6 whitespace-pre-wrap">{detail.log.rewriteQuestion || "-"}</div>
</div>
<div className="md:col-span-2">
2026-05-25 12:06:15 +08:00
<div className="text-xs text-muted-foreground">{t("knowledge.answerContent")}</div>
2026-04-09 10:01:23 +08:00
<div className="mt-1 text-sm leading-6 whitespace-pre-wrap">{detail.log.answer || "-"}</div>
</div>
</div>
</section>
<section className="space-y-3">
2026-05-25 12:06:15 +08:00
<h3 className="text-sm font-semibold">{t("knowledge.retrieveStrategy")}</h3>
2026-04-09 10:01:23 +08:00
<div className="grid gap-3 rounded-lg border p-4 md:grid-cols-3">
2026-05-25 12:06:15 +08:00
<Metric
label="Chunk Provider"
value={detail.log.chunkProvider ? getKnowledgeChunkProviderLabel(detail.log.chunkProvider, t) : "-"}
mono
/>
2026-04-09 10:01:23 +08:00
<Metric label="Target Tokens" value={detail.log.chunkTargetTokens} />
<Metric label="Max Tokens" value={detail.log.chunkMaxTokens} />
<Metric label="Overlap Tokens" value={detail.log.chunkOverlapTokens} />
2026-05-25 12:06:15 +08:00
<Metric label="Rerank" value={detail.log.rerankEnabled ? t("knowledge.rerankEnabled") : t("knowledge.rerankDisabled")} />
2026-04-09 10:01:23 +08:00
<Metric label="Rerank Limit" value={detail.log.rerankLimit} />
</div>
</section>
<section className="space-y-3">
2026-05-25 12:06:15 +08:00
<h3 className="text-sm font-semibold">{t("knowledge.resultSummary")}</h3>
2026-04-09 10:01:23 +08:00
<div className="grid gap-3 rounded-lg border p-4 md:grid-cols-4">
2026-05-25 12:06:15 +08:00
<Metric
label={t("knowledge.answerStatus")}
value={getKnowledgeAnswerStatusLabel(detail.log.answerStatus, detail.log.answerStatusName, t)}
/>
<Metric label={t("knowledge.hitCount")} value={detail.log.hitCount} />
<Metric label={t("knowledge.citations")} value={detail.log.citationCount} />
<Metric label={t("knowledge.contextChunks")} value={detail.log.usedChunkCount} />
2026-04-09 10:01:23 +08:00
<Metric label="Top Score" value={detail.log.topScore.toFixed(4)} mono />
2026-05-25 12:06:15 +08:00
<Metric label={t("knowledge.retrieveMs")} value={`${detail.log.retrieveMs} ms`} />
<Metric label={t("knowledge.generateMs")} value={`${detail.log.generateMs} ms`} />
<Metric label={t("knowledge.totalMs")} value={`${detail.log.latencyMs} ms`} />
2026-04-09 10:01:23 +08:00
<Metric label="Prompt Tokens" value={detail.log.promptTokens} />
<Metric label="Completion Tokens" value={detail.log.completionTokens} />
2026-05-25 12:06:15 +08:00
<Metric label={t("knowledge.model")} value={detail.log.modelName || "-"} mono />
<Metric label={t("knowledge.sessionId")} value={detail.log.sessionId || "-"} mono />
2026-04-09 10:01:23 +08:00
</div>
</section>
<section className="space-y-3">
2026-05-25 12:06:15 +08:00
<h3 className="text-sm font-semibold">{t("knowledge.citationSources")}</h3>
<CitationList hits={detail.hits} t={t} />
2026-04-09 10:01:23 +08:00
</section>
<section className="space-y-3">
<div className="flex items-center justify-between">
2026-05-25 12:06:15 +08:00
<h3 className="text-sm font-semibold">{t("knowledge.hitDetails")}</h3>
<div className="text-xs text-muted-foreground">{t("knowledge.itemsCount", { count: detail.hits.length })}</div>
2026-04-09 10:01:23 +08:00
</div>
<div className="space-y-3">
{detail.hits.map((item) => (
<div key={item.id} className="rounded-lg border p-3">
<div className="flex flex-wrap items-center gap-2">
<Badge variant="outline">#{item.rankNo}</Badge>
2026-05-25 12:06:15 +08:00
<span className="font-medium">{getHitSourceLabel(item, t)}</span>
2026-04-09 10:01:23 +08:00
<Badge variant={item.usedInAnswer ? "default" : "secondary"}>
2026-05-25 12:06:15 +08:00
{item.usedInAnswer ? t("knowledge.usedInContext") : t("knowledge.notUsedInContext")}
2026-04-09 10:01:23 +08:00
</Badge>
2026-05-25 12:06:15 +08:00
{item.isCitation ? <Badge>{t("knowledge.citations")}</Badge> : null}
2026-04-09 10:01:23 +08:00
</div>
<div className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
2026-05-25 12:06:15 +08:00
<span>{t("knowledge.section", { section: item.sectionPath || item.title || "-" })}</span>
2026-04-09 10:01:23 +08:00
<span>Chunk #{item.chunkNo}</span>
<span>Provider{item.provider || "-"}</span>
<span>Score{item.score.toFixed(4)}</span>
<span>Rerank{item.rerankScore ? item.rerankScore.toFixed(4) : "-"}</span>
</div>
<Separator className="my-3" />
<div className="text-sm leading-6 whitespace-pre-wrap text-foreground/90">
{item.snippet || "-"}
</div>
</div>
))}
</div>
</section>
<section className="space-y-3">
<h3 className="text-sm font-semibold">TraceData</h3>
<div className="rounded-lg border bg-muted/20 p-4">
<pre className="overflow-x-auto text-xs leading-6 text-muted-foreground">
{traceData ? JSON.stringify(traceData, null, 2) : detail.log.traceData || "-"}
</pre>
</div>
</section>
</div>
)}
</ScrollArea>
</DrawerContent>
</Drawer>
)
}
2026-05-25 12:06:15 +08:00
function getHitSourceLabel(item: KnowledgeRetrieveHit, t: TFunction) {
2026-04-09 10:01:23 +08:00
if (item.faqQuestion) {
return item.faqQuestion
}
if (item.documentTitle) {
return item.documentTitle
}
if (item.faqId > 0) {
return `FAQ #${item.faqId}`
}
2026-05-25 12:06:15 +08:00
return `${t("knowledge.document")} #${item.documentId}`
2026-04-09 10:01:23 +08:00
}
function Metric({
label,
value,
mono = false,
}: {
label: string
value: string | number
mono?: boolean
}) {
return (
<div>
<div className="text-xs text-muted-foreground">{label}</div>
<div className={`mt-1 text-sm ${mono ? "font-mono" : ""}`}>{String(value || value === 0 ? value : "-")}</div>
</div>
)
}