545 lines
20 KiB
TypeScript
545 lines
20 KiB
TypeScript
import { create } from "zustand";
|
|
import {
|
|
applyColorOverrides,
|
|
applyDensity,
|
|
applyFontSettings,
|
|
applyTheme,
|
|
THEME_TOKENS,
|
|
type ThemeId,
|
|
} from "@pubscript/ui";
|
|
import { discoverLocalProviders, type ProviderKind, type ThinkingEffort } from "@pubscript/ai";
|
|
import { createUlid } from "@pubscript/core";
|
|
|
|
export type Density = "compact" | "comfortable";
|
|
|
|
/** Body typeface identifier for the writing column. */
|
|
export type FontFamily =
|
|
| "serif"
|
|
| "eb-garamond"
|
|
| "cormorant"
|
|
| "crimson"
|
|
| "libre-baskerville"
|
|
| "baskerville"
|
|
| "palatino"
|
|
| "book-antiqua"
|
|
| "source-serif"
|
|
| "merriweather"
|
|
| "lora"
|
|
| "literata"
|
|
| "spectral"
|
|
| "pt-serif"
|
|
| "ibm-plex-serif"
|
|
| "times"
|
|
| "cambria"
|
|
| "playfair"
|
|
| "cinzel"
|
|
| "sans"
|
|
| "inter"
|
|
| "ibm-plex-sans"
|
|
| "open-sans"
|
|
| "nunito"
|
|
| "space-grotesk"
|
|
| "gill-sans"
|
|
| "optima"
|
|
| "trebuchet"
|
|
| "verdana"
|
|
| "mono"
|
|
| "jetbrains-mono"
|
|
| "fira-code"
|
|
| "ibm-plex-mono"
|
|
| "courier-new";
|
|
|
|
export interface FontOption {
|
|
value: FontFamily;
|
|
label: string;
|
|
category: "Serif" | "Sans-Serif" | "Monospace" | "Display";
|
|
css: string;
|
|
}
|
|
|
|
export const FONT_FAMILIES: FontOption[] = [
|
|
// Classic & Literary Serifs
|
|
{ value: "serif", label: "Georgia", category: "Serif", css: 'Georgia, "Times New Roman", serif' },
|
|
{ value: "eb-garamond", label: "EB Garamond", category: "Serif", css: '"EB Garamond", Garamond, Georgia, serif' },
|
|
{ value: "cormorant", label: "Cormorant Garamond", category: "Serif", css: '"Cormorant Garamond", Garamond, Georgia, serif' },
|
|
{ value: "crimson", label: "Crimson Text", category: "Serif", css: '"Crimson Text", Georgia, serif' },
|
|
{ value: "libre-baskerville", label: "Libre Baskerville", category: "Serif", css: '"Libre Baskerville", Baskerville, Georgia, serif' },
|
|
{ value: "baskerville", label: "Baskerville", category: "Serif", css: 'Baskerville, "Baskerville Old Face", "Hoefler Text", Garamond, serif' },
|
|
{ value: "palatino", label: "Palatino Linotype", category: "Serif", css: '"Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif' },
|
|
{ value: "book-antiqua", label: "Book Antiqua", category: "Serif", css: '"Book Antiqua", Palatino, Georgia, serif' },
|
|
{ value: "source-serif", label: "Source Serif 4", category: "Serif", css: '"Source Serif 4", Charter, Georgia, serif' },
|
|
{ value: "literata", label: "Literata", category: "Serif", css: '"Literata", Georgia, serif' },
|
|
{ value: "lora", label: "Lora", category: "Serif", css: '"Lora", Georgia, serif' },
|
|
{ value: "merriweather", label: "Merriweather", category: "Serif", css: '"Merriweather", Georgia, serif' },
|
|
{ value: "spectral", label: "Spectral", category: "Serif", css: '"Spectral", Georgia, serif' },
|
|
{ value: "pt-serif", label: "PT Serif", category: "Serif", css: '"PT Serif", Georgia, serif' },
|
|
{ value: "ibm-plex-serif", label: "IBM Plex Serif", category: "Serif", css: '"IBM Plex Serif", Georgia, serif' },
|
|
{ value: "cambria", label: "Cambria", category: "Serif", css: 'Cambria, Georgia, serif' },
|
|
{ value: "times", label: "Times New Roman", category: "Serif", css: '"Times New Roman", Times, serif' },
|
|
|
|
// Display Serifs
|
|
{ value: "playfair", label: "Playfair Display", category: "Display", css: '"Playfair Display", Georgia, serif' },
|
|
{ value: "cinzel", label: "Cinzel", category: "Display", css: '"Cinzel", Georgia, serif' },
|
|
|
|
// Sans-Serif
|
|
{ value: "sans", label: "System Sans", category: "Sans-Serif", css: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif' },
|
|
{ value: "inter", label: "Inter", category: "Sans-Serif", css: '"Inter", -apple-system, BlinkMacSystemFont, sans-serif' },
|
|
{ value: "ibm-plex-sans", label: "IBM Plex Sans", category: "Sans-Serif", css: '"IBM Plex Sans", -apple-system, sans-serif' },
|
|
{ value: "open-sans", label: "Open Sans", category: "Sans-Serif", css: '"Open Sans", -apple-system, sans-serif' },
|
|
{ value: "nunito", label: "Nunito", category: "Sans-Serif", css: '"Nunito", -apple-system, sans-serif' },
|
|
{ value: "space-grotesk", label: "Space Grotesk", category: "Sans-Serif", css: '"Space Grotesk", -apple-system, sans-serif' },
|
|
{ value: "gill-sans", label: "Gill Sans", category: "Sans-Serif", css: '"Gill Sans", "Gill Sans MT", Calibri, sans-serif' },
|
|
{ value: "optima", label: "Optima", category: "Sans-Serif", css: 'Optima, Candara, "Noto Sans", sans-serif' },
|
|
{ value: "trebuchet", label: "Trebuchet MS", category: "Sans-Serif", css: '"Trebuchet MS", Helvetica, sans-serif' },
|
|
{ value: "verdana", label: "Verdana", category: "Sans-Serif", css: 'Verdana, Geneva, sans-serif' },
|
|
|
|
// Monospace & Manuscript
|
|
{ value: "courier-new", label: "Courier New (Manuscript)", category: "Monospace", css: '"Courier New", Courier, monospace' },
|
|
{ value: "jetbrains-mono", label: "JetBrains Mono", category: "Monospace", css: '"JetBrains Mono", monospace' },
|
|
{ value: "fira-code", label: "Fira Code", category: "Monospace", css: '"Fira Code", monospace' },
|
|
{ value: "ibm-plex-mono", label: "IBM Plex Mono", category: "Monospace", css: '"IBM Plex Mono", monospace' },
|
|
{ value: "mono", label: "System Monospace", category: "Monospace", css: '"SFMono-Regular", ui-monospace, "Cascadia Code", Consolas, monospace' },
|
|
];
|
|
|
|
/** Resolve a stored font-family enum to its CSS font-family string. */
|
|
export function fontFamilyCss(value: FontFamily): string {
|
|
const found = FONT_FAMILIES.find((f) => f.value === value);
|
|
return found?.css ?? FONT_FAMILIES[0]!.css;
|
|
}
|
|
|
|
export type GrammarProvider = "local" | "languagetool";
|
|
|
|
export interface AIProviderSetting {
|
|
id: string;
|
|
name: string;
|
|
kind: ProviderKind;
|
|
baseUrl: string;
|
|
apiKey: string;
|
|
defaultModel: string;
|
|
models: string[];
|
|
}
|
|
|
|
export interface AIAgentConfig {
|
|
id: string;
|
|
name: string;
|
|
role: "editor" | "reader" | "publisher" | "co-author" | "custom";
|
|
temperature: number;
|
|
contextWindow: number;
|
|
maxTokens: number;
|
|
thinkingEnabled: boolean;
|
|
thinkingEffort: ThinkingEffort;
|
|
systemPrompt: string;
|
|
}
|
|
|
|
export const DEFAULT_AI_PROVIDERS: AIProviderSetting[] = [
|
|
{
|
|
id: "ollama-local",
|
|
name: "Ollama (Local)",
|
|
kind: "openai-compatible",
|
|
baseUrl: "http://localhost:11434/v1",
|
|
apiKey: "",
|
|
defaultModel: "llama3.2",
|
|
models: ["llama3.2", "qwen2.5:7b", "mistral", "deepseek-r1:8b", "gemma2:9b"],
|
|
},
|
|
{
|
|
id: "llamacpp-local",
|
|
name: "llama.cpp / LM Studio (Local)",
|
|
kind: "openai-compatible",
|
|
baseUrl: "http://localhost:1234/v1",
|
|
apiKey: "",
|
|
defaultModel: "local-model",
|
|
models: ["local-model", "qwen2.5-coder", "llama-3.1-8b-instruct"],
|
|
},
|
|
{
|
|
id: "openai",
|
|
name: "OpenAI",
|
|
kind: "openai-compatible",
|
|
baseUrl: "https://api.openai.com/v1",
|
|
apiKey: "",
|
|
defaultModel: "gpt-4o",
|
|
models: ["gpt-4o", "gpt-4o-mini", "o3-mini"],
|
|
},
|
|
{
|
|
id: "anthropic",
|
|
name: "Anthropic Claude",
|
|
kind: "anthropic",
|
|
baseUrl: "https://api.anthropic.com/v1",
|
|
apiKey: "",
|
|
defaultModel: "claude-3-7-sonnet-latest",
|
|
models: ["claude-3-7-sonnet-latest", "claude-3-5-haiku-latest"],
|
|
},
|
|
{
|
|
id: "google",
|
|
name: "Google Gemini",
|
|
kind: "google",
|
|
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
apiKey: "",
|
|
defaultModel: "gemini-2.5-pro",
|
|
models: ["gemini-2.5-pro", "gemini-2.5-flash"],
|
|
},
|
|
{
|
|
id: "openrouter",
|
|
name: "OpenRouter",
|
|
kind: "openrouter",
|
|
baseUrl: "https://openrouter.ai/api/v1",
|
|
apiKey: "",
|
|
defaultModel: "anthropic/claude-3.7-sonnet",
|
|
models: [
|
|
"anthropic/claude-3.7-sonnet",
|
|
"openai/gpt-4o",
|
|
"google/gemini-2.5-pro",
|
|
"deepseek/deepseek-r1",
|
|
],
|
|
},
|
|
];
|
|
|
|
export const LINE_SUGGESTION_PROMPT_RULES =
|
|
"IMPORTANT GRANULAR TEXT-ANCHORED PROTOCOL: Anchor every suggested edit to the EXACT verbatim phrase, clause, or single sentence from the manuscript—never rely on line numbers and never replace an entire multi-sentence paragraph at once. Quote the exact target phrase in double quotes (e.g. \"left the mainland before dawn\") in your discussion so the author can click it to highlight that exact phrase in the editor. At the end of your reply, emit 3 to 6 granular suggestion blocks targeting specific phrases, clauses, or sentences in this exact format so the author can step through and click Accept, Deny, or Copy to Note without affecting surrounding sentences:\n<<<SUGGESTION\nTITLE: <short granular edit label, e.g. Sharpen verb phrase / Cut filter word>\nORIGINAL: <exact verbatim phrase, clause, or single sentence copied character-for-character from the chapter>\nREPLACEMENT: <revised text that replaces ONLY the ORIGINAL span>\nSUMMARY: <1-sentence editorial rationale>\nSUGGESTION>>>";
|
|
|
|
export const DEFAULT_AI_AGENTS: AIAgentConfig[] = [
|
|
{
|
|
id: "editor",
|
|
name: "Editor",
|
|
role: "editor",
|
|
temperature: 0.3,
|
|
contextWindow: 32768,
|
|
maxTokens: 2048,
|
|
thinkingEnabled: false,
|
|
thinkingEffort: "medium",
|
|
systemPrompt: `You are the PubScript Line Editor. Tighten prose at the phrase and clause level, eliminate filter words, sharpen verbs and dialogue tags, improve sentence rhythm, and verify consistency against character and world sheets while preserving the author's voice.\n\n${LINE_SUGGESTION_PROMPT_RULES}`,
|
|
},
|
|
{
|
|
id: "reader",
|
|
name: "Reader",
|
|
role: "reader",
|
|
temperature: 0.6,
|
|
contextWindow: 32768,
|
|
maxTokens: 2048,
|
|
thinkingEnabled: false,
|
|
thinkingEffort: "medium",
|
|
systemPrompt: `You are an honest, attentive first-time reader. Report what felt vivid, where your attention drifted, what was confusing, what was telegraphed too early, and what emotional beats paid off by quoting exact phrases from the chapter.\n\n${LINE_SUGGESTION_PROMPT_RULES}`,
|
|
},
|
|
{
|
|
id: "publisher",
|
|
name: "Publisher",
|
|
role: "publisher",
|
|
temperature: 0.5,
|
|
contextWindow: 32768,
|
|
maxTokens: 2048,
|
|
thinkingEnabled: false,
|
|
thinkingEffort: "medium",
|
|
systemPrompt: `You are a literary Publisher and developmental strategist. Evaluate commercial hook, genre pacing, comparable titles (comps), blurb/pitch angles, audience expectations, and structural stakes across acts, quoting exact phrases and sentences.\n\n${LINE_SUGGESTION_PROMPT_RULES}`,
|
|
},
|
|
{
|
|
id: "co-author",
|
|
name: "Co-Author",
|
|
role: "co-author",
|
|
temperature: 0.8,
|
|
contextWindow: 65536,
|
|
maxTokens: 4096,
|
|
thinkingEnabled: true,
|
|
thinkingEffort: "medium",
|
|
systemPrompt: `You are a collaborative Co-Author with full knowledge of the manuscript's characters, world-building, and plot beats. Brainstorm plot turns, propose surgical phrase and sentence revisions in the author's established voice, and expand world lore as granular adoptable suggestions.\n\n${LINE_SUGGESTION_PROMPT_RULES}`,
|
|
},
|
|
];
|
|
|
|
export interface AppSettings {
|
|
theme: ThemeId;
|
|
accentColor: string;
|
|
textColor: string;
|
|
density: Density;
|
|
fontFamily: FontFamily;
|
|
fontSize: number;
|
|
leading: number;
|
|
measure: number;
|
|
showLineNumbers: boolean;
|
|
wordCountGoal: number;
|
|
autoSave: boolean;
|
|
focusMode: boolean;
|
|
projectDirectory: string;
|
|
// Navigation & AI Sidebars
|
|
navCollapsed: boolean;
|
|
aiSidebarOpen: boolean;
|
|
aiSidebarWidth: number;
|
|
// Spell & grammar (Chunk #7)
|
|
enableSpellcheck: boolean;
|
|
enableGrammarCheck: boolean;
|
|
spellcheckLanguage: string;
|
|
grammarProvider: GrammarProvider;
|
|
grammarApiKey: string;
|
|
// AI Providers & Agents
|
|
aiProviders: AIProviderSetting[];
|
|
activeProviderId: string;
|
|
activeModel: string;
|
|
aiAgents: AIAgentConfig[];
|
|
activeAgentId: string;
|
|
// Local / Self-Hosted Server & Startup Settings
|
|
serverEnabled: boolean;
|
|
webPort: number;
|
|
serverPort: number;
|
|
serverStartOnBoot: boolean;
|
|
}
|
|
|
|
export const DEFAULT_SETTINGS: AppSettings = {
|
|
theme: "ink",
|
|
accentColor: "#7c8bf5",
|
|
textColor: "",
|
|
density: "comfortable",
|
|
fontFamily: "serif",
|
|
fontSize: 16,
|
|
leading: 1.7,
|
|
measure: 720,
|
|
showLineNumbers: false,
|
|
wordCountGoal: 80_000,
|
|
autoSave: true,
|
|
focusMode: false,
|
|
projectDirectory: "~/Documents/PubScript",
|
|
navCollapsed: false,
|
|
aiSidebarOpen: false,
|
|
aiSidebarWidth: 370,
|
|
// Spell & grammar (Chunk #7)
|
|
enableSpellcheck: true,
|
|
enableGrammarCheck: false,
|
|
spellcheckLanguage: "en_US",
|
|
grammarProvider: "local",
|
|
grammarApiKey: "",
|
|
// AI Providers & Agents
|
|
aiProviders: DEFAULT_AI_PROVIDERS,
|
|
activeProviderId: DEFAULT_AI_PROVIDERS[0]!.id,
|
|
activeModel: DEFAULT_AI_PROVIDERS[0]!.defaultModel,
|
|
aiAgents: DEFAULT_AI_AGENTS,
|
|
activeAgentId: DEFAULT_AI_AGENTS[0]!.id,
|
|
// Local / Self-Hosted Server & Startup Settings
|
|
serverEnabled: true,
|
|
webPort: 5173,
|
|
serverPort: 8081,
|
|
serverStartOnBoot: false,
|
|
};
|
|
|
|
const STORAGE_KEY = "pubscript.settings.v1";
|
|
|
|
function loadSettings(): AppSettings {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
if (!raw) return { ...DEFAULT_SETTINGS };
|
|
const parsed = JSON.parse(raw) as Partial<AppSettings>;
|
|
const rawAgents =
|
|
Array.isArray(parsed.aiAgents) && parsed.aiAgents.length > 0
|
|
? parsed.aiAgents
|
|
: DEFAULT_AI_AGENTS;
|
|
// Upgrade any stored built-in agent whose prompt predates the granular text-anchored protocol.
|
|
const aiAgents = rawAgents.map((agent) => {
|
|
const def = DEFAULT_AI_AGENTS.find((d) => d.id === agent.id);
|
|
if (def && !agent.systemPrompt.includes("GRANULAR TEXT-ANCHORED PROTOCOL")) {
|
|
return { ...agent, systemPrompt: def.systemPrompt };
|
|
}
|
|
return agent;
|
|
});
|
|
return {
|
|
...DEFAULT_SETTINGS,
|
|
...parsed,
|
|
aiProviders:
|
|
Array.isArray(parsed.aiProviders) && parsed.aiProviders.length > 0
|
|
? parsed.aiProviders
|
|
: DEFAULT_AI_PROVIDERS,
|
|
aiAgents,
|
|
};
|
|
} catch {
|
|
return { ...DEFAULT_SETTINGS };
|
|
}
|
|
}
|
|
|
|
export interface SettingsStore extends AppSettings {
|
|
set: (patch: Partial<AppSettings>) => void;
|
|
setTheme: (theme: ThemeId) => void;
|
|
updateProvider: (id: string, patch: Partial<AIProviderSetting>) => void;
|
|
addProvider: (provider: Omit<AIProviderSetting, "id">) => AIProviderSetting;
|
|
deleteProvider: (id: string) => void;
|
|
updateAgent: (id: string, patch: Partial<AIAgentConfig>) => void;
|
|
addAgent: (agent: Omit<AIAgentConfig, "id">) => AIAgentConfig;
|
|
deleteAgent: (id: string) => void;
|
|
probeProviderModels: (providerId?: string) => Promise<string[]>;
|
|
reset: () => void;
|
|
}
|
|
|
|
export const useSettings = create<SettingsStore>((set, get) => {
|
|
const initial = loadSettings();
|
|
applyTheme(initial.theme);
|
|
applyColorOverrides({
|
|
accentColor: initial.accentColor,
|
|
textColor: initial.textColor,
|
|
});
|
|
applyDensity(initial.density);
|
|
applyFontSettings({
|
|
size: initial.fontSize,
|
|
leading: initial.leading,
|
|
measure: initial.measure,
|
|
family: fontFamilyCss(initial.fontFamily),
|
|
});
|
|
|
|
return {
|
|
...initial,
|
|
set: (patch) => {
|
|
const next = { ...get(), ...patch };
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
if (patch.theme != null) {
|
|
applyTheme(next.theme);
|
|
}
|
|
if (patch.theme != null || patch.accentColor != null || patch.textColor != null) {
|
|
applyColorOverrides({
|
|
accentColor: next.accentColor,
|
|
textColor: next.textColor,
|
|
});
|
|
}
|
|
if (patch.density != null) {
|
|
applyDensity(next.density);
|
|
}
|
|
if (
|
|
patch.fontSize != null ||
|
|
patch.leading != null ||
|
|
patch.measure != null ||
|
|
patch.fontFamily != null
|
|
) {
|
|
applyFontSettings({
|
|
size: next.fontSize,
|
|
leading: next.leading,
|
|
measure: next.measure,
|
|
family: fontFamilyCss(next.fontFamily),
|
|
});
|
|
}
|
|
set(next);
|
|
},
|
|
setTheme: (theme) => {
|
|
applyTheme(theme);
|
|
const themeDefaultAccent = THEME_TOKENS[theme]?.css["--ps-accent"] ?? "#7c8bf5";
|
|
get().set({ theme, accentColor: themeDefaultAccent, textColor: "" });
|
|
},
|
|
updateProvider: (id, patch) => {
|
|
const nextProviders = get().aiProviders.map((p) =>
|
|
p.id === id ? { ...p, ...patch } : p,
|
|
);
|
|
get().set({ aiProviders: nextProviders });
|
|
},
|
|
addProvider: (input) => {
|
|
const created: AIProviderSetting = {
|
|
...input,
|
|
id: `prov-${createUlid().slice(-8).toLowerCase()}`,
|
|
};
|
|
const nextProviders = [...get().aiProviders, created];
|
|
get().set({
|
|
aiProviders: nextProviders,
|
|
activeProviderId: created.id,
|
|
activeModel: created.defaultModel,
|
|
});
|
|
return created;
|
|
},
|
|
deleteProvider: (id) => {
|
|
const remaining = get().aiProviders.filter((p) => p.id !== id);
|
|
if (remaining.length === 0) return;
|
|
const nextActive =
|
|
get().activeProviderId === id ? remaining[0]! : remaining.find((p) => p.id === get().activeProviderId) ?? remaining[0]!;
|
|
get().set({
|
|
aiProviders: remaining,
|
|
activeProviderId: nextActive.id,
|
|
activeModel: nextActive.defaultModel,
|
|
});
|
|
},
|
|
updateAgent: (id, patch) => {
|
|
const nextAgents = get().aiAgents.map((a) =>
|
|
a.id === id ? { ...a, ...patch } : a,
|
|
);
|
|
get().set({ aiAgents: nextAgents });
|
|
},
|
|
addAgent: (input) => {
|
|
const created: AIAgentConfig = {
|
|
...input,
|
|
id: `agent-${createUlid().slice(-8).toLowerCase()}`,
|
|
};
|
|
const nextAgents = [...get().aiAgents, created];
|
|
get().set({ aiAgents: nextAgents, activeAgentId: created.id });
|
|
return created;
|
|
},
|
|
deleteAgent: (id) => {
|
|
const remaining = get().aiAgents.filter((a) => a.id !== id);
|
|
if (remaining.length === 0) return;
|
|
const nextActiveId =
|
|
get().activeAgentId === id ? remaining[0]!.id : get().activeAgentId;
|
|
get().set({ aiAgents: remaining, activeAgentId: nextActiveId });
|
|
},
|
|
probeProviderModels: async (providerId) => {
|
|
const state = get();
|
|
const targetId = providerId ?? state.activeProviderId;
|
|
const provider = state.aiProviders.find((p) => p.id === targetId);
|
|
if (!provider) return [];
|
|
|
|
const foundModels = new Set<string>(provider.models);
|
|
try {
|
|
const base = provider.baseUrl.replace(/\/+$/, "");
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), 1800);
|
|
try {
|
|
const headers: Record<string, string> = {};
|
|
if (provider.apiKey) {
|
|
headers.Authorization = `Bearer ${provider.apiKey}`;
|
|
}
|
|
const res = await fetch(`${base}/models`, {
|
|
headers,
|
|
signal: controller.signal,
|
|
});
|
|
if (res.ok) {
|
|
const json = (await res.json()) as { data?: Array<{ id?: string }> };
|
|
for (const item of json.data ?? []) {
|
|
if (typeof item.id === "string" && item.id.trim()) {
|
|
foundModels.add(item.id.trim());
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
} catch {
|
|
/* endpoint unreachable */
|
|
}
|
|
|
|
try {
|
|
const localDetected = await discoverLocalProviders({ timeoutMs: 800 });
|
|
for (const det of localDetected) {
|
|
for (const m of det.models) {
|
|
if (m.id) foundModels.add(m.id);
|
|
}
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
|
|
const list = [...foundModels];
|
|
if (list.length > 0) {
|
|
get().updateProvider(targetId, {
|
|
models: list,
|
|
defaultModel: provider.defaultModel || list[0]!,
|
|
});
|
|
}
|
|
return list;
|
|
},
|
|
reset: () => {
|
|
try {
|
|
localStorage.removeItem(STORAGE_KEY);
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
applyTheme(DEFAULT_SETTINGS.theme);
|
|
applyDensity(DEFAULT_SETTINGS.density);
|
|
applyFontSettings({
|
|
size: DEFAULT_SETTINGS.fontSize,
|
|
leading: DEFAULT_SETTINGS.leading,
|
|
measure: DEFAULT_SETTINGS.measure,
|
|
family: fontFamilyCss(DEFAULT_SETTINGS.fontFamily),
|
|
});
|
|
set({ ...DEFAULT_SETTINGS });
|
|
},
|
|
};
|
|
});
|