"use client"; import { Effect } from "effect"; import { useCallback, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import type { EngineBackend, EngineJob, RuntimeTarget, StarterPreset, StudioDiagnostics, StudioSettings, } from "@/lib/types"; import { useDownloads } from "@/hooks/use-downloads"; import { useMountSubscription } from "@/hooks/use-mount-subscription"; import { loadSecondarySetupDataEffect, loadSetupDataEffect, refreshRuntimeStateEffect, type SetupLoadSetters, } from "./setup-load"; import { beginDownloadEffect, configureAndLaunchEffect, connectRemotePresetEffect, markSetupComplete, runRuntimeJobEffect, saveSettingsEffect, } from "./setup-actions"; import { useSetupBenchmark } from "./use-setup-benchmark"; import { selectSetupDownload } from "./setup-downloads"; import { ggufFileOptions, manualDownloadPreset, type GgufFileOption } from "./setup-model-files"; import { loadSetupProgress, updateSetupProgress } from "./setup-progress"; import type { HuggingFaceModelCardPayload } from "@/lib/huggingface"; type ManagedSetupBackend = Extract; export function useSetup() { const router = useRouter(); const [initialProgress] = useState(loadSetupProgress); const [step, setStepState] = useState(initialProgress.step); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [loadWarning, setLoadWarning] = useState(null); const [settings, setSettings] = useState(null); const [modelsDir, setModelsDir] = useState(""); const [diagnostics, setDiagnostics] = useState(null); const [presets, setPresets] = useState([]); const [selectedPreset, setSelectedPreset] = useState( initialProgress.selectedPreset, ); const [remoteApiKey, setRemoteApiKey] = useState(""); const [connectingRemote, setConnectingRemote] = useState(false); const [remoteError, setRemoteError] = useState(null); const [runtimeTargets, setRuntimeTargets] = useState([]); const [runtimeJobs, setRuntimeJobs] = useState([]); const [maxVram, setMaxVram] = useState(0); const [selectedModel, setSelectedModel] = useState(initialProgress.selectedModel); const [manualModelId, setManualModelIdState] = useState(initialProgress.manualModelId); const [manualGgufOptions, setManualGgufOptions] = useState([]); const [manualGgufFile, setManualGgufFile] = useState(""); const [resolvingManualModel, setResolvingManualModel] = useState(false); const [savingSettings, setSavingSettings] = useState(false); const [upgrading, setUpgrading] = useState(false); const [hardwareConfirmed, setHardwareConfirmedState] = useState( initialProgress.hardwareConfirmed, ); const [configuringRecipe, setConfiguringRecipe] = useState(false); const [launchError, setLaunchError] = useState(null); const [createdRecipeId, setCreatedRecipeIdState] = useState( initialProgress.createdRecipeId, ); const setStep = useCallback((value: number) => { setStepState(value); updateSetupProgress({ step: value }); }, []); const setManualModelId = useCallback((value: string) => { setManualModelIdState(value); setManualGgufOptions([]); setManualGgufFile(""); updateSetupProgress({ manualModelId: value }); }, []); const setHardwareConfirmed = useCallback((value: boolean) => { setHardwareConfirmedState(value); updateSetupProgress({ hardwareConfirmed: value }); }, []); const setCreatedRecipeId = useCallback((value: string | null) => { setCreatedRecipeIdState(value); updateSetupProgress({ createdRecipeId: value }); }, []); const { benchmarking, benchmarkResult, benchmarkError, runSetupBenchmark, resetBenchmark } = useSetupBenchmark(); const [lifecycle] = useState(() => ({ abort: new AbortController() })); useMountSubscription(() => { lifecycle.abort = new AbortController(); return () => lifecycle.abort.abort(); }, [lifecycle]); const downloadsState = useDownloads(2000); const activeDownload = useMemo( () => selectSetupDownload(downloadsState.downloads, selectedModel, selectedPreset), [downloadsState.downloads, selectedModel, selectedPreset], ); const refreshRuntimeState = useCallback(() => { return Effect.runPromise(refreshRuntimeStateEffect({ setRuntimeTargets, setRuntimeJobs })); }, []); const loadSecondarySetupData = useCallback( (initialWarnings: string[], loadSetters: SetupLoadSetters) => { return Effect.runPromise(loadSecondarySetupDataEffect(initialWarnings, loadSetters)); }, [], ); const loadSetupData = useCallback(() => { const loadSetters: SetupLoadSetters = { setLoading, setError, setLoadWarning, setSettings, setModelsDir, setDiagnostics, setMaxVram, setRuntimeTargets, setRuntimeJobs, setPresets, }; return Effect.runPromise( loadSetupDataEffect(loadSetters, (warnings) => loadSecondarySetupData(warnings, loadSetters)), ); }, [loadSecondarySetupData]); useMountSubscription(() => { void loadSetupData(); }, [loadSetupData]); const saveSettings = useCallback(() => { if (!modelsDir.trim()) { setError("Models directory is required."); return Promise.resolve(); } setSavingSettings(true); return Effect.runPromise( saveSettingsEffect(modelsDir, { setSettings, setModelsDir, setHardwareConfirmed, setStep, setError, setSavingSettings, }), ); }, [modelsDir]); const runRuntimeJob = useCallback( (payload: { backend: EngineBackend; targetId?: string; type: "install" | "update" }) => { setUpgrading(true); setError(null); return Effect.runPromise( runRuntimeJobEffect(payload, { setError, setRuntimeJobs, setDiagnostics, setUpgrading, refreshRuntimeState, }), { signal: lifecycle.abort.signal }, ).catch(() => undefined); }, [lifecycle, refreshRuntimeState], ); const installRuntime = useCallback( (backend: ManagedSetupBackend) => runRuntimeJob({ backend, type: "install" }), [runRuntimeJob], ); const updateRuntimeTarget = useCallback( (target: RuntimeTarget) => runRuntimeJob({ backend: target.backend, targetId: target.id, type: target.installed ? "update" : "install", }), [runRuntimeJob], ); const beginDownload = useCallback( (modelId: string, preset?: StarterPreset, allowPatterns?: string[]) => { if (!modelId) return Promise.resolve(); setSelectedModel(modelId); setSelectedPreset(preset ?? null); updateSetupProgress({ selectedModel: modelId, selectedPreset: preset ?? null, createdRecipeId: null, }); setLaunchError(null); setCreatedRecipeId(null); resetBenchmark(); const completedDownload = selectSetupDownload( downloadsState.downloads.filter((download) => download.status === "completed"), modelId, preset ?? null, ); if (completedDownload) { setStep(3); return Promise.resolve(); } return Effect.runPromise( beginDownloadEffect(modelId, preset, allowPatterns, { startDownload: downloadsState.startDownload, setStep, setError, }), ); }, [downloadsState, resetBenchmark, setCreatedRecipeId, setStep], ); const beginVariantDownload = useCallback( (modelId: string, allowPatterns?: string[]) => beginDownload(modelId, undefined, allowPatterns), [beginDownload], ); const beginPresetSetup = useCallback( (preset: StarterPreset) => { if (preset.kind === "download" && preset.model_id) { return beginDownload(preset.model_id, preset); } return Promise.resolve(); }, [beginDownload], ); const connectRemotePreset = useCallback( (preset: StarterPreset) => { const remote = preset.remote; if (preset.kind !== "remote" || !remote) return Promise.resolve(); const apiKey = remoteApiKey.trim(); if (!apiKey) { setRemoteError("An API key is required to connect."); return Promise.resolve(); } setConnectingRemote(true); setRemoteError(null); return Effect.runPromise( connectRemotePresetEffect(preset, remote, apiKey, { setRemoteError, setConnectingRemote, openAgentChat: () => router.push("/agent?new=1"), }), ); }, [remoteApiKey, router], ); const submitManualModel = useCallback(async () => { const trimmed = manualModelId.trim(); if (!trimmed || resolvingManualModel) return; setResolvingManualModel(true); setError(null); try { const signal = AbortSignal.any([lifecycle.abort.signal, AbortSignal.timeout(12_000)]); const response = await fetch( `/api/huggingface/model-card?modelId=${encodeURIComponent(trimmed)}`, { cache: "no-store", signal }, ); const payload = (await response.json()) as HuggingFaceModelCardPayload & { error?: string }; if (!response.ok) throw new Error(payload.error || "Failed to inspect the model repository"); const options = ggufFileOptions(payload); setManualGgufOptions(options); const selected = options.find((option) => option.value === manualGgufFile) ?? (options.length === 1 ? options[0] : undefined); if (options.length > 1 && !selected) return; await beginDownload(trimmed, manualDownloadPreset(trimmed, selected)); } catch (cause) { if (!(cause instanceof DOMException && cause.name === "AbortError")) { setError(cause instanceof Error ? cause.message : "Failed to inspect the model repository"); } } finally { setResolvingManualModel(false); } }, [beginDownload, lifecycle, manualGgufFile, manualModelId, resolvingManualModel]); const continueFromHardware = useCallback(() => { if (!hardwareConfirmed) return; setStep(2); }, [hardwareConfirmed]); const configureAndLaunch = useCallback(() => { if (!activeDownload || activeDownload.status !== "completed") { return Promise.resolve(); } setConfiguringRecipe(true); setLaunchError(null); resetBenchmark(); return Effect.runPromise( configureAndLaunchEffect( { activeDownload, selectedPreset, createdRecipeId }, { setRuntimeJobs, setCreatedRecipeId, setStep, setLaunchError, setConfiguringRecipe }, ), ); }, [activeDownload, createdRecipeId, resetBenchmark, selectedPreset, setRuntimeJobs]); const openChat = useCallback(() => { markSetupComplete(); router.push("/agent?new=1"); }, [router]); const openDashboard = useCallback(() => { markSetupComplete(); router.push("/"); }, [router]); const skipSetup = useCallback(() => { markSetupComplete(); router.push("/"); }, [router]); return { step, setStep, loading, error, loadWarning, settings, modelsDir, setModelsDir, diagnostics, presets, selectedPreset, beginPresetSetup, remoteApiKey, setRemoteApiKey, connectingRemote, remoteError, connectRemotePreset, runtimeTargets, runtimeJobs, maxVram, selectedModel, manualModelId, setManualModelId, manualGgufOptions, manualGgufFile, setManualGgufFile, resolvingManualModel, savingSettings, upgrading, hardwareConfirmed, setHardwareConfirmed, downloads: downloadsState.downloads, activeDownload, pauseDownload: downloadsState.pauseDownload, resumeDownload: downloadsState.resumeDownload, cancelDownload: downloadsState.cancelDownload, saveSettings, installRuntime, updateRuntimeTarget, beginDownload, beginVariantDownload, submitManualModel, continueFromHardware, configuringRecipe, launchError, createdRecipeId, configureAndLaunch, benchmarking, benchmarkResult, benchmarkError, runSetupBenchmark, openChat, openDashboard, skipSetup, }; }