/**
* Svelte 5 reactive store for the LLM orchestrator.
*
* Lives at module-scope as a singleton because there is exactly one
* orchestrator + settings per page session. Settings are persisted to
* localStorage for now (Phase 1) — Phase 2 will move them into the
* encrypted IndexedDB settings table once that exists.
*
* Usage in a Svelte 5 component:
*
* import { llmOrchestrator, llmSettingsState, useTaskAvailability } from '@mana/shared-llm';
* import { extractDateTask } from '$lib/llm-tasks/extract-date';
*
* const available = useTaskAvailability(extractDateTask);
* // ... reactively true/false based on settings + backend readiness
*
* {#if available.current}
*
* {/if}
*/
import { BrowserBackend } from './backends/browser';
import { CloudBackend } from './backends/cloud';
import { ManaServerBackend } from './backends/mana-server';
import { LlmOrchestrator } from './orchestrator';
import type { LlmTask } from './task';
import { DEFAULT_LLM_SETTINGS, type LlmSettings } from './types';
const STORAGE_KEY = 'mana.llm.settings.v1';
/** Load persisted settings, falling back to defaults on first run or
* any parse error. localStorage is fine for Phase 1 — small payload,
* not encrypted-sensitive (the user's tier preference is hardly
* secret), and trivial to migrate to IndexedDB later. */
function loadSettings(): LlmSettings {
if (typeof localStorage === 'undefined') return { ...DEFAULT_LLM_SETTINGS };
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return { ...DEFAULT_LLM_SETTINGS };
const parsed = JSON.parse(raw) as Partial;
return { ...DEFAULT_LLM_SETTINGS, ...parsed };
} catch {
return { ...DEFAULT_LLM_SETTINGS };
}
}
function persistSettings(settings: LlmSettings): void {
if (typeof localStorage === 'undefined') return;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
} catch {
// Quota exceeded or storage disabled — non-fatal, settings just
// won't persist across sessions.
}
}
// ─── Reactive state ──────────────────────────────────────────────
const initialSettings = loadSettings();
let _settings = $state(initialSettings);
// Backends are constructed once per page session. They're stateless
// (or hold their own internal state in the case of BrowserBackend
// pointing at @mana/local-llm's singleton), so a fresh instance per
// orchestrator is fine.
const backends = [new BrowserBackend(), new ManaServerBackend(), new CloudBackend()];
export const llmOrchestrator = new LlmOrchestrator({
settings: initialSettings,
backends,
});
/** Reactive accessor for the current settings. UI components read
* via `llmSettingsState.current` to get a $state-tracked snapshot. */
export const llmSettingsState = {
get current(): LlmSettings {
return _settings;
},
};
/** Update settings (or part of them). Persists to localStorage and
* pushes the new value into the orchestrator. */
export function updateLlmSettings(patch: Partial): void {
_settings = { ..._settings, ...patch };
persistSettings(_settings);
llmOrchestrator.updateSettings(_settings);
}
/**
* Svelte 5 reactive hook: returns `{ current: boolean }` indicating
* whether the given task can run with the user's current settings.
* Reactive against `llmSettingsState` so the UI re-renders when the
* user toggles a tier in the settings page.
*
* Use this to gate feature buttons — show them as enabled when the
* task is runnable, disabled (with a tooltip) when not.
*/
export function useTaskAvailability(
task: LlmTask
): { readonly current: boolean } {
return {
get current() {
// Reading _settings here registers the reactive dependency
void _settings;
return llmOrchestrator.canRun(task);
},
};
}