388 lines · 12.1 KB · TypeScript
RawDownload
"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<EngineBackend, "vllm" | "sglang" | "mlx">; 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<string | null>(null);  const [loadWarning, setLoadWarning] = useState<string | null>(null);  const [settings, setSettings] = useState<StudioSettings | null>(null);  const [modelsDir, setModelsDir] = useState("");  const [diagnostics, setDiagnostics] = useState<StudioDiagnostics | null>(null);  const [presets, setPresets] = useState<StarterPreset[]>([]);  const [selectedPreset, setSelectedPreset] = useState<StarterPreset | null>(    initialProgress.selectedPreset,  );  const [remoteApiKey, setRemoteApiKey] = useState("");  const [connectingRemote, setConnectingRemote] = useState(false);  const [remoteError, setRemoteError] = useState<string | null>(null);  const [runtimeTargets, setRuntimeTargets] = useState<RuntimeTarget[]>([]);  const [runtimeJobs, setRuntimeJobs] = useState<EngineJob[]>([]);  const [maxVram, setMaxVram] = useState(0);  const [selectedModel, setSelectedModel] = useState(initialProgress.selectedModel);  const [manualModelId, setManualModelIdState] = useState(initialProgress.manualModelId);  const [manualGgufOptions, setManualGgufOptions] = useState<GgufFileOption[]>([]);  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<string | null>(null);  const [createdRecipeId, setCreatedRecipeIdState] = useState<string | null>(    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,  };}