"use client" import { useEffect, useMemo, useState } from "react" import { Loader2, Sparkles, X, Database, AlertTriangle } from "lucide-react" import { toast } from "sonner" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Textarea } from "@/components/ui/textarea" import { Label } from "@/components/ui/label" import { cn } from "@/lib/utils" import { useAISettings } from "./ai-settings-provider" import { PromptEditor } from "./prompt-editor" import { AIResultPreview, type DraftRow } from "./ai-result-preview" import { requestOpenAICompatible } from "@/lib/ai/client" import { validateForRequest } from "@/lib/ai/settings" import { BATCH_IMPORT_PROMPT } from "@/lib/ai/prompts" import { safeParseAIJson } from "@/lib/ai/json" import type { KnowledgeItem, KnowledgeItemInput } from "@/lib/types" export function AIBatchImportDialog({ open, onOpenChange, onInserted, }: { open: boolean onOpenChange: (o: boolean) => void onInserted: (items: KnowledgeItem[]) => void }) { const { settings } = useAISettings() const [rawInput, setRawInput] = useState("") const [customPrompt, setCustomPrompt] = useState(BATCH_IMPORT_PROMPT) const [loading, setLoading] = useState(false) const [inserting, setInserting] = useState(false) const [error, setError] = useState(null) const [rawResponse, setRawResponse] = useState(null) const [rows, setRows] = useState([]) const [controller, setController] = useState(null) useEffect(() => { if (open) { setError(null) setRawResponse(null) } }, [open]) const selectedCount = useMemo(() => rows.filter((r) => r._selected).length, [rows]) async function handleGenerate() { const err = validateForRequest(settings) if (err) { toast.error(err) return } if (!rawInput.trim()) { toast.error("请先粘贴题目内容") return } setLoading(true) setError(null) setRawResponse(null) setRows([]) const ac = new AbortController() setController(ac) try { const content = await requestOpenAICompatible({ baseUrl: settings.baseUrl, apiKey: settings.apiKey, model: settings.model, systemPrompt: settings.systemPrompt, customPrompt, messages: [{ role: "user", content: rawInput }], temperature: settings.temperature, maxTokens: settings.maxTokens, signal: ac.signal, }) const parsed = safeParseAIJson(content) if (!parsed.ok) { setError(parsed.error) setRawResponse(parsed.raw) return } setRows( parsed.items.map((it, i) => ({ ...it, _id: `${Date.now()}-${i}`, _selected: true, })), ) toast.success(`已生成 ${parsed.items.length} 条`) } catch (e) { if ((e as Error).name === "AbortError") { toast.message("已取消生成") return } setError((e as Error).message) } finally { setLoading(false) setController(null) } } function handleCancel() { controller?.abort() } async function handleInsert() { const selected = rows.filter((r) => r._selected) if (selected.length === 0) { toast.error("请至少勾选一条") return } setInserting(true) try { const { createItems } = await import("@/lib/knowledge") const inputs: KnowledgeItemInput[] = selected.map((r) => ({ title: r.title, summary: r.summary, content: r.content, code_snippet: r.code_snippet, tags: r.tags, category: r.category, difficulty: r.difficulty, mastery: r.mastery, is_favorite: false, source_url: r.source_url, notes: r.notes, })) const created = await createItems(inputs) onInserted(created) toast.success(`成功插入 ${created.length} 条知识点`) // remove inserted rows from preview setRows((prev) => prev.filter((r) => !r._selected)) } catch (e) { toast.error(`插入失败:${(e as Error).message}`) console.log("[v0] batch insert error:", e) } finally { setInserting(false) } } return ( (loading ? null : onOpenChange(o))}> AI 批量整理 粘贴多个题目 / 面试题 / 知识点,AI 会整理成结构化数据。插入前可预览、编辑并勾选。
model {settings.model || "—"} | base {settings.baseUrl || "—"}