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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user