Files
Nat20-Notes/frontend/src/pages/Settings.tsx
KansaiGaijin babc0f58ef multi-stage Dockerfile + player recap styles
Dockerfile: split into whisperx-base and runtime stages so app-only
changes skip the 10min pip install on rebuild. Example compose updated.

Player recap: replace hardcoded 'story so far' prompt with selectable
styles (story/diary/bullets/custom) via dropdown in Setup and Settings.

Settings: add NAT20_ONBOARDING_COMPLETED env var; fix merge logic so
env vars properly override defaults (not just empty DB values).
2026-07-10 16:25:52 +12:00

82 lines
4.0 KiB
TypeScript

import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { api } from "../api";
export default function Settings() {
const [settings, setSettings] = useState<any>(null);
const [saved, setSaved] = useState(false);
useEffect(() => {
api.getSettings().then(setSettings);
}, []);
if (!settings) return null;
const set = (k: string, v: any) => setSettings({ ...settings, [k]: v });
const save = async () => {
await api.updateSettings(settings);
setSaved(true);
setTimeout(() => setSaved(false), 2000);
};
return (
<div className="max-w-xl mx-auto py-16 px-4">
<Link to="/" className="text-sm text-ink/40 hover:text-brass">&larr; Back</Link>
<h1 className="font-display text-3xl mt-4 mb-8">Settings</h1>
<div className="card p-6 space-y-4 mb-6">
<h2 className="eyebrow">Transcription</h2>
<select className="input w-full" value={settings.whisper_model} onChange={(e) => set("whisper_model", e.target.value)}>
<option value="tiny">Tiny</option>
<option value="small">Small</option>
<option value="medium">Medium</option>
<option value="large-v3">Large v3</option>
</select>
<input className="input w-full" type="password" placeholder="HuggingFace token" value={settings.hf_token} onChange={(e) => set("hf_token", e.target.value)} />
</div>
<div className="card p-6 space-y-4 mb-6">
<h2 className="eyebrow">Summarization</h2>
<div className="flex gap-3">
<button className={settings.llm_mode === "ollama" ? "btn-primary" : "btn-secondary"} onClick={() => set("llm_mode", "ollama")}>Local (Ollama)</button>
<button className={settings.llm_mode === "api" ? "btn-primary" : "btn-secondary"} onClick={() => set("llm_mode", "api")}>Hosted API</button>
</div>
{settings.llm_mode === "ollama" ? (
<>
<input className="input w-full" value={settings.ollama_host} onChange={(e) => set("ollama_host", e.target.value)} />
<input className="input w-full" value={settings.ollama_model} onChange={(e) => set("ollama_model", e.target.value)} />
</>
) : (
<>
<input className="input w-full" value={settings.api_base_url} onChange={(e) => set("api_base_url", e.target.value)} />
<input className="input w-full" type="password" value={settings.api_key} onChange={(e) => set("api_key", e.target.value)} />
<input className="input w-full" value={settings.api_model} onChange={(e) => set("api_model", e.target.value)} />
</>
)}
</div>
<div className="card p-6 space-y-3 mb-6">
<h2 className="eyebrow">Campaign context</h2>
<textarea className="input w-full h-32" value={settings.world_context} onChange={(e) => set("world_context", e.target.value)} placeholder="Paste world context directly..." />
<input className="input w-full" value={settings.world_context_path} onChange={(e) => set("world_context_path", e.target.value)} placeholder="...or path to a file inside the container" />
</div>
<div className="card p-6 space-y-3 mb-6">
<h2 className="eyebrow">Player recap style</h2>
<select className="input w-full" value={settings.player_recap_style} onChange={(e) => set("player_recap_style", e.target.value)}>
<option value="story">Story Recap (previously on)</option>
<option value="diary">Dear Diary</option>
<option value="bullets">Bullet Points</option>
<option value="custom">Custom Prompt</option>
</select>
{settings.player_recap_style === "custom" && (
<textarea className="input w-full h-32" value={settings.player_recap_custom_prompt} onChange={(e) => set("player_recap_custom_prompt", e.target.value)} placeholder="Write your own final-combine prompt. Use {'{summaries}'} where the chunk summaries should be inserted." />
)}
</div>
<button className="btn-primary" onClick={save}>{saved ? "Saved" : "Save settings"}</button>
</div>
);
}