"use client" import { useEffect, useState } from "react" import { Star, Pencil, Trash2, Repeat, ExternalLink, Maximize2, Minimize2, Mic, Loader2, Copy } from "lucide-react" import { toast } from "sonner" import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { cn } from "@/lib/utils" import type { KnowledgeItem } from "@/lib/types" import { DIFFICULTY_META, MASTERY_META } from "@/lib/types" import { AIChatPanel } from "@/components/ai/ai-chat-panel" import { Markdown } from "@/components/ai/markdown" import { useAISettings } from "@/components/ai/ai-settings-provider" import { requestOpenAICompatible } from "@/lib/ai/client" import { validateForRequest } from "@/lib/ai/settings" import { buildInterviewPrompt } from "@/lib/ai/prompts" import { saveInterviewAnswer } from "@/lib/knowledge" function formatDate(value: string | null) { if (!value) return "—" return new Date(value).toLocaleDateString("zh-CN", { month: "short", day: "numeric" }) } export function DetailDialog({ item, onOpenChange, onEdit, onDelete, onReview, onToggleFav, onItemUpdated, }: { item: KnowledgeItem | null onOpenChange: (open: boolean) => void onEdit: (item: KnowledgeItem) => void onDelete: (id: string) => void onReview: (item: KnowledgeItem) => void onToggleFav: (item: KnowledgeItem) => void onItemUpdated?: (item: KnowledgeItem) => void }) { const { settings } = useAISettings() const [fullscreen, setFullscreen] = useState(false) const [interview, setInterview] = useState(null) const [interviewLoading, setInterviewLoading] = useState(false) // 切换知识点时加载该卡片已保存的面试回答,避免不同卡片间串味 useEffect(() => { setInterview(item?.interview_answer ?? null) setInterviewLoading(false) }, [item?.id, item?.interview_answer]) async function handleInterview(current: KnowledgeItem) { const err = validateForRequest(settings) if (err) return toast.error(err) setInterviewLoading(true) try { const reply = await requestOpenAICompatible({ baseUrl: settings.baseUrl, apiKey: settings.apiKey, model: settings.model, customPrompt: buildInterviewPrompt(current), messages: [], temperature: settings.temperature, maxTokens: settings.maxTokens, }) const answer = reply.trim() setInterview(answer) // 持久化到数据库,新回答覆盖旧回答 await saveInterviewAnswer(current.id, answer) onItemUpdated?.({ ...current, interview_answer: answer }) } catch (e) { toast.error((e as Error).message) } finally { setInterviewLoading(false) } } if (!item) return null const diff = DIFFICULTY_META[item.difficulty] const mastery = MASTERY_META[item.mastery] return (
{item.category} {mastery.label} {diff.label}
{item.title} {item.summary &&

{item.summary}

}
{item.content && (

详细内容

{item.content}

)} {item.code_snippet && (

代码片段

                {item.code_snippet}
              
)} {item.notes && (

个人笔记

{item.notes}

)} {item.tags.length > 0 && (
{item.tags.map((t) => ( #{t} ))}
)}

{item.review_count}

复习次数

{formatDate(item.last_reviewed_at)}

上次复习

{formatDate(item.next_review_at)}

下次复习

{item.source_url && ( 查看来源 )} {/* AI 面试回答版 */}

面试回答版

{interview && (
)}
{/* AI 追问聊天 */}
) }