v0.2.0-llm: LLM integration, core utilities, and persistence

- Rust backend: LLM abstraction with OpenAI-compatible + Ollama API clients
- Tauri commands: generate, generate_stream, get/set LLM config
- LLM config: api_url, api_key, model, temperature, max_tokens, top_p
- Works with Ollama (localhost:11434), LM Studio, any OpenAI-compatible API
- Streaming infrastructure via Tauri Channels (LlmEvent)
- tauri-plugin-store, tauri-plugin-fs added
- Frontend: NPC Generator with race/class/alignment selection + AI generation
- Frontend: Dice Roller with notation parsing (2d6+3), presets, history
- Frontend: Session Logger with note-taking + AI summarization
- Frontend: Settings panel for LLM configuration (URL, key, model, temperature)
- App shell: left rail nav with active states, settings toggle
- All glassmorphism cards with gold glow hover states
- Builds clean: tsc, vite build, cargo check, tauri build
This commit is contained in:
itsamejms
2026-06-28 22:36:30 +01:00
parent f30a92ddeb
commit 9675e05b07
13 changed files with 1358 additions and 56 deletions
+139
View File
@@ -0,0 +1,139 @@
import { useState } from "react";
import { invoke } from "@tauri-apps/api/core";
interface LlmConfig {
api_url: string;
api_key: string;
model: string;
temperature: number;
max_tokens: number;
top_p: number;
}
export function SettingsPanel() {
const [config, setConfig] = useState<LlmConfig | null>(null);
const [loading, setLoading] = useState(false);
const [saved, setSaved] = useState(false);
async function loadConfig() {
try {
const c = await invoke<LlmConfig>("get_llm_config");
setConfig(c);
} catch (e) {
console.error("Failed to load config:", e);
}
}
async function saveConfig() {
if (!config) return;
setLoading(true);
try {
await invoke("set_llm_config", { config });
setSaved(true);
setTimeout(() => setSaved(false), 2000);
} catch (e) {
console.error("Failed to save config:", e);
}
setLoading(false);
}
if (!config) {
return (
<div className="flex flex-col items-center justify-center h-full gap-4">
<p className="text-[var(--color-text-secondary)] text-sm">
Configure your LLM connection
</p>
<button
onClick={loadConfig}
className="rounded-lg bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-4 py-2 text-sm font-semibold hover:bg-[var(--color-gold-muted)] transition-colors cursor-pointer"
>
Load Settings
</button>
</div>
);
}
return (
<div className="flex flex-col gap-4 text-sm">
<div className="flex flex-col gap-1">
<label className="text-[var(--color-text-secondary)] text-xs font-medium">
API URL
</label>
<input
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-2 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)]"
value={config.api_url}
onChange={(e) => setConfig({ ...config, api_url: e.target.value })}
placeholder="http://localhost:11434"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-[var(--color-text-secondary)] text-xs font-medium">
API Key (optional)
</label>
<input
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-2 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)]"
type="password"
value={config.api_key}
onChange={(e) => setConfig({ ...config, api_key: e.target.value })}
placeholder="sk-... (leave blank for local)"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-[var(--color-text-secondary)] text-xs font-medium">
Model
</label>
<input
className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] px-3 py-2 text-[var(--color-text-primary)] placeholder-[var(--color-text-dim)] focus:outline-none focus:border-[var(--color-gold-bright)]"
value={config.model}
onChange={(e) => setConfig({ ...config, model: e.target.value })}
placeholder="llama3.2"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="flex flex-col gap-1">
<label className="text-[var(--color-text-secondary)] text-xs font-medium">
Temperature: {config.temperature}
</label>
<input
type="range"
min="0"
max="2"
step="0.1"
value={config.temperature}
onChange={(e) =>
setConfig({ ...config, temperature: parseFloat(e.target.value) })
}
className="accent-[var(--color-gold-bright)]"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-[var(--color-text-secondary)] text-xs font-medium">
Max Tokens: {config.max_tokens}
</label>
<input
type="range"
min="64"
max="4096"
step="64"
value={config.max_tokens}
onChange={(e) =>
setConfig({ ...config, max_tokens: parseInt(e.target.value) })
}
className="accent-[var(--color-gold-bright)]"
/>
</div>
</div>
<button
onClick={saveConfig}
disabled={loading}
className="rounded-lg bg-[var(--color-gold-bright)] text-[var(--color-bg-deep)] px-4 py-2 text-sm font-semibold hover:bg-[var(--color-gold-muted)] transition-colors cursor-pointer disabled:opacity-50"
>
{saved ? "✓ Saved" : loading ? "Saving…" : "Save Settings"}
</button>
</div>
);
}