"use client" import { useCallback, useEffect, useMemo, useState } from "react" import { RefreshCwIcon, SearchIcon } from "lucide-react" import { toast } from "sonner" import { fetchKnowledgeRetrieveLogs, type KnowledgeRetrieveLog, type PageResult, } from "@/lib/api/admin" import { KnowledgeAnswerStatusLabels, KnowledgeChunkProviderLabels, KnowledgeRetrieveChannelLabels, KnowledgeRetrieveSceneLabels, } from "@/lib/generated/enums" import { getEnumOptions } from "@/lib/enums" import { formatDateTime } from "@/lib/utils" import { ListPagination } from "@/components/list-pagination" import { OptionCombobox } from "@/components/option-combobox" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { RetrieveLogDetailDrawer } from "./retrieve-log-detail" type RetrieveLogListProps = { knowledgeBaseId: number | null } const channelOptions = [ { value: "all", label: "全部渠道" }, ...getEnumOptions(KnowledgeRetrieveChannelLabels).map((item) => ({ value: String(item.value), label: item.label, })), ] const sceneOptions = [ { value: "all", label: "全部场景" }, ...getEnumOptions(KnowledgeRetrieveSceneLabels).map((item) => ({ value: String(item.value), label: item.label, })), ] const answerStatusOptions = [ { value: "all", label: "全部回答状态" }, ...getEnumOptions(KnowledgeAnswerStatusLabels).map((item) => ({ value: String(item.value), label: item.label, })), ] const providerOptions = [ { value: "all", label: "全部切分策略" }, ...getEnumOptions(KnowledgeChunkProviderLabels).map((item) => ({ value: String(item.value), label: item.label, })), ] const rerankOptions = [ { value: "all", label: "全部 Rerank" }, { value: "1", label: "已启用 Rerank" }, { value: "0", label: "未启用 Rerank" }, ] function getAnswerStatusVariant(status: number): "default" | "secondary" | "outline" | "destructive" { switch (status) { case 1: return "default" case 2: return "secondary" case 3: return "outline" case 4: return "destructive" default: return "outline" } } export function RetrieveLogList({ knowledgeBaseId, }: RetrieveLogListProps) { const [questionInput, setQuestionInput] = useState("") const [question, setQuestion] = useState("") const [channel, setChannel] = useState("all") const [scene, setScene] = useState("all") const [answerStatus, setAnswerStatus] = useState("all") const [chunkProvider, setChunkProvider] = useState("all") const [rerankEnabled, setRerankEnabled] = useState("all") const [page, setPage] = useState(1) const [limit, setLimit] = useState(20) const [loading, setLoading] = useState(false) const [detailOpen, setDetailOpen] = useState(false) const [selectedLogId, setSelectedLogId] = useState(null) const [result, setResult] = useState>({ results: [], page: { page: 1, limit: 20, total: 0 }, }) const loadData = useCallback(async () => { if (!knowledgeBaseId) { setResult({ results: [], page: { page: 1, limit: 20, total: 0 } }) setLoading(false) return } setLoading(true) try { const data = await fetchKnowledgeRetrieveLogs({ knowledgeBaseId, question: question.trim() || undefined, channel: channel === "all" ? undefined : channel, scene: scene === "all" ? undefined : scene, answerStatus: answerStatus === "all" ? undefined : Number(answerStatus), chunkProvider: chunkProvider === "all" ? undefined : chunkProvider, rerankEnabled: rerankEnabled === "all" ? undefined : Number(rerankEnabled), page, limit, }) setResult(data) } catch (error) { toast.error(error instanceof Error ? error.message : "加载检索日志失败") } finally { setLoading(false) } }, [answerStatus, channel, chunkProvider, knowledgeBaseId, limit, page, question, rerankEnabled, scene]) useEffect(() => { void loadData() }, [loadData]) useEffect(() => { setPage(1) setSelectedLogId(null) setDetailOpen(false) }, [knowledgeBaseId]) const emptyStateText = useMemo(() => { if (!knowledgeBaseId) { return "请选择一个知识库查看检索日志" } if (loading) { return "正在加载检索日志..." } return "当前知识库还没有检索日志" }, [knowledgeBaseId, loading]) function applyFilters() { setQuestion(questionInput) setPage(1) } function handleQuestionKeyDown(event: React.KeyboardEvent) { if (event.key !== "Enter") { return } event.preventDefault() applyFilters() } function handleOpenDetail(logId: number) { setSelectedLogId(logId) setDetailOpen(true) } if (!knowledgeBaseId) { return (
{emptyStateText}
) } return ( <>
setQuestionInput(event.target.value)} onKeyDown={handleQuestionKeyDown} placeholder="按问题关键字筛选" className="pl-9" />
时间 问题 回答状态 命中数 TopScore Provider Rerank 引用 耗时 {result.results.length === 0 ? ( {emptyStateText} ) : ( result.results.map((item) => ( handleOpenDetail(item.id)} > {formatDateTime(item.createdAt)}
{item.question || "-"}
{item.channelName} {item.sceneName} {item.knowledgeBaseName ? {item.knowledgeBaseName} : null}
{item.answerStatusName} {item.hitCount} {item.topScore.toFixed(4)} {item.chunkProvider || "-"} {item.rerankEnabled ? `是 (${item.rerankLimit})` : "否"} {item.citationCount} {item.latencyMs} ms
)) )}
{ setLimit(nextLimit) setPage(1) }} />
) }