"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import { CheckCircle2, Loader2, Save, XCircle } from "lucide-react"; import { useTranslation } from "react-i18next"; import { SettingRow, SettingSection, inputClass, nativeSelectClass, selectOptionClass, } from "@/components/settings/shared"; import { Toggle } from "@/components/settings/Toggle"; import { apiFetch, apiUrl } from "@/lib/api"; type MinerUMode = "local" | "cloud"; type MinerUModelVersion = "pipeline" | "vlm"; type MinerUDownloadSource = "huggingface" | "modelscope"; type MinerUDownloadType = "pipeline" | "vlm" | "all"; const DEFAULT_BASE_URL = "https://mineru.net"; const DEFAULT_HF_ENDPOINT = "https://huggingface.co"; const LANGUAGE_AUTO = "auto"; const TOKEN_MASK = "••••••••••••"; const MODEL_VERSIONS: MinerUModelVersion[] = ["pipeline", "vlm"]; const DOWNLOAD_SOURCES: MinerUDownloadSource[] = ["huggingface", "modelscope"]; const DOWNLOAD_SOURCE_LABELS: Record = { huggingface: "HuggingFace", modelscope: "ModelScope", }; const DOWNLOAD_TYPES: MinerUDownloadType[] = ["pipeline", "vlm", "all"]; type MinerUSettings = { mode: MinerUMode; api_base_url: string; local_cli_path: string; model_download_source: MinerUDownloadSource; model_download_endpoint: string; model_version: MinerUModelVersion; language: string; enable_formula: boolean; enable_table: boolean; is_ocr: boolean; allow_local_model_download: boolean; }; type DownloadStatus = { state: "running" | "done" | "failed" | "cancelled" | string; lines: string[]; message: string; }; type MinerUPayload = { settings: MinerUSettings & { version?: number }; api_token_set: boolean; local_cli?: { found: boolean; command: string; path: string; source?: "configured" | "path"; }; }; function normalizeDraft(payload: MinerUPayload): MinerUSettings { const s = payload.settings; return { mode: s.mode === "cloud" ? "cloud" : "local", api_base_url: s.api_base_url || "https://mineru.net", local_cli_path: s.local_cli_path || "", model_download_source: s.model_download_source === "modelscope" ? "modelscope" : "huggingface", model_download_endpoint: s.model_download_endpoint || "", model_version: s.model_version === "vlm" ? "vlm" : "pipeline", language: s.language || "auto", enable_formula: Boolean(s.enable_formula), enable_table: Boolean(s.enable_table), is_ocr: Boolean(s.is_ocr), allow_local_model_download: Boolean(s.allow_local_model_download), }; } export function MinerUEngineSettings() { const { t } = useTranslation(); const [payload, setPayload] = useState(null); const [draft, setDraft] = useState(null); // Token is write-only: blank field + "set/not set" hint. Only sent on save // when the user actually edits it (tokenTouched). const [tokenDraft, setTokenDraft] = useState(""); const [tokenTouched, setTokenTouched] = useState(false); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState<{ ok: boolean; message: string; } | null>(null); const [error, setError] = useState(null); const [message, setMessage] = useState(""); const [download, setDownload] = useState(null); const [downloadType, setDownloadType] = useState("pipeline"); const [startingDownload, setStartingDownload] = useState(false); const downloadCursor = useRef(0); useEffect(() => { let cancelled = false; async function load() { setLoading(true); setError(null); try { const response = await apiFetch(apiUrl("/api/v1/settings/mineru")); const data = (await response.json().catch(() => ({}))) as | MinerUPayload | { detail?: string }; if (!response.ok) { throw new Error( "detail" in data && data.detail ? data.detail : t("Failed to load MinerU settings."), ); } if (cancelled) return; const next = data as MinerUPayload; setPayload(next); setDraft(normalizeDraft(next)); setTokenDraft(""); setTokenTouched(false); } catch (err) { if (!cancelled) setError(err instanceof Error ? err.message : String(err)); } finally { if (!cancelled) setLoading(false); } } load(); return () => { cancelled = true; }; }, [t]); const dirty = useMemo(() => { if (!payload || !draft) return false; const current = normalizeDraft(payload); return ( tokenTouched || current.mode !== draft.mode || current.api_base_url !== draft.api_base_url || current.local_cli_path !== draft.local_cli_path || current.model_download_source !== draft.model_download_source || current.model_download_endpoint !== draft.model_download_endpoint || current.model_version !== draft.model_version || current.language !== draft.language || current.enable_formula !== draft.enable_formula || current.enable_table !== draft.enable_table || current.is_ocr !== draft.is_ocr || current.allow_local_model_download !== draft.allow_local_model_download ); }, [draft, payload, tokenTouched]); function patch(next: Partial) { setDraft((current) => (current ? { ...current, ...next } : current)); } async function save() { if (!draft) return; setSaving(true); setError(null); setMessage(""); setTestResult(null); try { const body: Record = { ...draft, api_token: tokenTouched ? tokenDraft : null, }; const response = await apiFetch(apiUrl("/api/v1/settings/mineru"), { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const data = (await response.json().catch(() => ({}))) as | MinerUPayload | { detail?: string }; if (!response.ok) { throw new Error( "detail" in data && data.detail ? data.detail : t("Failed to save MinerU settings."), ); } const next = data as MinerUPayload; setPayload(next); setDraft(normalizeDraft(next)); setTokenDraft(""); setTokenTouched(false); setMessage(t("MinerU settings saved.")); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setSaving(false); } } async function testConnection() { if (!draft) return; setTesting(true); setTestResult(null); setError(null); try { const body: Record = { ...draft, api_token: tokenTouched ? tokenDraft : null, }; const response = await apiFetch(apiUrl("/api/v1/settings/mineru/test"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const data = (await response.json().catch(() => ({}))) as { ok?: boolean; message?: string; detail?: string; }; if (!response.ok) { throw new Error(data.detail || t("Connection test failed.")); } setTestResult({ ok: Boolean(data.ok), message: data.message || (data.ok ? t("OK") : t("Connection test failed.")), }); } catch (err) { setTestResult({ ok: false, message: err instanceof Error ? err.message : String(err), }); } finally { setTesting(false); } } // Poll the download job while it runs; the cursor protocol fetches only // new log lines each tick. useEffect(() => { if (download?.state !== "running") return; const timer = setInterval(async () => { try { const response = await apiFetch( apiUrl( `/api/v1/settings/mineru/models/download/status?cursor=${downloadCursor.current}`, ), ); if (!response.ok) return; const data = (await response.json()) as { state?: string; lines?: string[]; next_cursor?: number; message?: string; }; downloadCursor.current = data.next_cursor ?? downloadCursor.current; setDownload((current) => current ? { state: data.state || current.state, lines: [...current.lines, ...(data.lines || [])].slice(-100), message: data.message || "", } : current, ); } catch { // transient network error — keep polling } }, 1000); return () => clearInterval(timer); }, [download?.state]); async function startDownload() { if (!draft) return; setStartingDownload(true); try { const response = await apiFetch( apiUrl("/api/v1/settings/mineru/models/download"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model_type: downloadType, source: draft.model_download_source, endpoint: draft.model_download_endpoint, local_cli_path: draft.local_cli_path, }), }, ); const data = (await response.json().catch(() => ({}))) as { ok?: boolean; message?: string; detail?: string; }; if (!response.ok || !data.ok) { setDownload({ state: "failed", lines: [], message: data.message || data.detail || t("Download failed."), }); return; } downloadCursor.current = 0; setDownload({ state: "running", lines: [], message: "" }); } finally { setStartingDownload(false); } } async function cancelDownload() { try { await apiFetch(apiUrl("/api/v1/settings/mineru/models/download/cancel"), { method: "POST", }); } catch { // status polling will surface the final state either way } } const tokenSet = payload?.api_token_set ?? false; const isCloud = draft?.mode === "cloud"; const localCli = payload?.local_cli; function renderTestControl(label: string) { return (
{testResult && ( {testResult.ok ? ( ) : ( )} {testResult.message} )}
); } if (loading) { return (
{t("Loading MinerU settings...")}
); } if (error && !draft) { return (
{error}
); } if (!draft) return null; return ( <> {error && (
{error}
)} {(["local", "cloud"] as MinerUMode[]).map((m) => ( ))} } /> {!isCloud && ( =20 GB of free disk (official guidance): ~2-4 GB install, ~1-2 GB models lazily downloaded on first parse, plus parsing cache.", )} > patch({ local_cli_path: e.target.value })} /> } /> )} {!isCloud && ( patch({ allow_local_model_download: v })} /> } /> patch({ model_download_source: e.target .value as MinerUDownloadSource, }) } > {DOWNLOAD_SOURCES.map((s) => ( ))} } /> {draft.model_download_source === "huggingface" && ( patch({ model_download_endpoint: e.target.value }) } /> } /> )} {download?.state === "running" ? ( ) : ( )} } /> {download && (
{download.state === "running" ? ( ) : download.state === "done" ? ( ) : ( )} {download.state === "running" ? t("Downloading models...") : download.message || download.state}
{download.lines.length > 0 && (
                  {download.lines.join("\n")}
                
)}
)}
)} {isCloud && ( patch({ api_base_url: e.target.value })} /> } /> { setTokenDraft(e.target.value); setTokenTouched(true); }} /> } /> )} patch({ model_version: e.target.value as MinerUModelVersion }) } > {MODEL_VERSIONS.map((v) => ( ))} } /> patch({ language: e.target.value })} /> } /> patch({ enable_formula: v })} /> } /> patch({ enable_table: v })} /> } /> patch({ is_ocr: v })} /> } />

{message || t( "MinerU settings are written to data/user/settings/document_parsing.json.", )}

); }