"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 { KnowledgeAnswerStatus, KnowledgeRetrieveChannel, KnowledgeRetrieveScene, } from "@/lib/generated/enums" import { getKnowledgeAnswerStatusLabel, getKnowledgeChunkProviderLabel, getKnowledgeRetrieveChannelLabel, getKnowledgeRetrieveSceneLabel, } from "@/lib/knowledge-i18n" import { formatDateTime } from "@/lib/utils" import { useI18n } from "@/i18n/provider" 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 } type TFunction = (key: string, values?: Record) => string function getChannelOptions(t: TFunction) { return [ { value: "all", label: t("knowledge.allChannels") }, { value: KnowledgeRetrieveChannel.IM, label: t("knowledge.channelIM") }, { value: KnowledgeRetrieveChannel.AgentAssist, label: t("knowledge.channelAgentAssist") }, { value: KnowledgeRetrieveChannel.API, label: t("knowledge.channelAPI") }, { value: KnowledgeRetrieveChannel.Debug, label: t("knowledge.channelDebug") }, ] } function getSceneOptions(t: TFunction) { return [ { value: "all", label: t("knowledge.allScenes") }, { value: KnowledgeRetrieveScene.FirstResponse, label: t("knowledge.sceneFirstResponse") }, { value: KnowledgeRetrieveScene.Assist, label: t("knowledge.sceneAssist") }, { value: KnowledgeRetrieveScene.QA, label: t("knowledge.sceneQA") }, ] } function getAnswerStatusOptions(t: TFunction) { return [ { value: "all", label: t("knowledge.allAnswerStatus") }, { value: String(KnowledgeAnswerStatus.Normal), label: t("knowledge.answerNormal") }, { value: String(KnowledgeAnswerStatus.NoAnswer), label: t("knowledge.answerNoAnswer") }, { value: String(KnowledgeAnswerStatus.Fallback), label: t("knowledge.answerFallback") }, { value: String(KnowledgeAnswerStatus.Blocked), label: t("knowledge.answerBlocked") }, ] } function getProviderOptions(t: TFunction) { return [ { value: "all", label: t("knowledge.allChunkProviders") }, { value: "fixed", label: t("knowledge.chunkFixed") }, { value: "structured", label: t("knowledge.chunkStructured") }, { value: "faq", label: t("knowledge.chunkFAQ") }, { value: "semantic", label: t("knowledge.chunkSemantic") }, ] } function getRerankOptions(t: TFunction) { return [ { value: "all", label: t("knowledge.allRerank") }, { value: "1", label: t("knowledge.rerankEnabled") }, { value: "0", label: t("knowledge.rerankDisabled") }, ] } function channelLabel(value: string, fallback: string, t: TFunction) { return getKnowledgeRetrieveChannelLabel(value, fallback, t) } function sceneLabel(value: string, fallback: string, t: TFunction) { return getKnowledgeRetrieveSceneLabel(value, fallback, t) } function answerStatusLabel(value: number, fallback: string, t: TFunction) { return getKnowledgeAnswerStatusLabel(value, fallback, t) } function providerLabel(value: string, t: TFunction) { return getKnowledgeChunkProviderLabel(value, t) } 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 t = useI18n() 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 channelOptions = useMemo(() => getChannelOptions(t), [t]) const sceneOptions = useMemo(() => getSceneOptions(t), [t]) const answerStatusOptions = useMemo(() => getAnswerStatusOptions(t), [t]) const providerOptions = useMemo(() => getProviderOptions(t), [t]) const rerankOptions = useMemo(() => getRerankOptions(t), [t]) 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 : t("knowledge.loadRetrieveLogsFailed")) } finally { setLoading(false) } }, [answerStatus, channel, chunkProvider, knowledgeBaseId, limit, page, question, rerankEnabled, scene, t]) useEffect(() => { void loadData() }, [loadData]) useEffect(() => { setPage(1) setSelectedLogId(null) setDetailOpen(false) }, [knowledgeBaseId]) const emptyStateText = useMemo(() => { if (!knowledgeBaseId) { return t("knowledge.selectBaseForLogs") } if (loading) { return t("knowledge.loadingRetrieveLogs") } return t("knowledge.emptyRetrieveLogs") }, [knowledgeBaseId, loading, t]) 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={t("knowledge.filterQuestion")} className="pl-9" />
{t("knowledge.time")} {t("knowledge.question")} {t("knowledge.answerStatus")} {t("knowledge.hitCount")} TopScore Provider Rerank {t("knowledge.citations")} {t("knowledge.duration")} {result.results.length === 0 ? ( {emptyStateText} ) : ( result.results.map((item) => ( handleOpenDetail(item.id)} > {formatDateTime(item.createdAt)}
{item.question || "-"}
{channelLabel(item.channel, item.channelName, t)} {sceneLabel(item.scene, item.sceneName, t)} {item.knowledgeBaseName ? {item.knowledgeBaseName} : null}
{answerStatusLabel(item.answerStatus, item.answerStatusName, t)} {item.hitCount} {item.topScore.toFixed(4)} {item.chunkProvider ? providerLabel(item.chunkProvider, t) : "-"} {item.rerankEnabled ? `${t("knowledge.yes")} (${item.rerankLimit})` : t("knowledge.no")} {item.citationCount} {item.latencyMs} ms
)) )}
{ setLimit(nextLimit) setPage(1) }} />
) }