Modell-Auswahl in Settings implementiert

- Neues SettingsPanel mit Modell-Auswahl (Haiku/Sonnet/Opus)
- Modell wird in SQLite persistiert (claude_model Setting)
- Bridge unterstützt set-model und get-models Commands
- Modell kann zur Laufzeit gewechselt werden
- Preisanzeige pro Modell im Settings-Panel
- Aktuelles Modell wird beim App-Start geladen

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-14 09:32:26 +02:00
parent 9129163876
commit 433e2de2b6
7 changed files with 459 additions and 13 deletions

View file

@ -17,7 +17,14 @@ process.stdin.resume();
let activeAbort = null; let activeAbort = null;
let currentAgentId = null; let currentAgentId = null;
const MODEL = process.env.CLAUDE_MODEL || 'opus'; let currentModel = process.env.CLAUDE_MODEL || 'opus';
// Verfügbare Modelle
const AVAILABLE_MODELS = [
{ id: 'haiku', name: 'Claude Haiku', description: 'Schnell & günstig' },
{ id: 'sonnet', name: 'Claude Sonnet', description: 'Ausgewogen' },
{ id: 'opus', name: 'Claude Opus', description: 'Leistungsstark' },
];
// ============ Kommunikation mit Tauri ============ // ============ Kommunikation mit Tauri ============
@ -39,7 +46,10 @@ function sendError(id, error) {
// ============ Claude Agent SDK ============ // ============ Claude Agent SDK ============
async function sendMessage(message, requestId) { async function sendMessage(message, requestId, model = null) {
// Modell für diese Anfrage (Parameter > State > Default)
const useModel = model || currentModel;
currentAgentId = randomUUID(); currentAgentId = randomUUID();
activeAbort = new AbortController(); activeAbort = new AbortController();
@ -47,19 +57,20 @@ async function sendMessage(message, requestId) {
id: currentAgentId, id: currentAgentId,
type: 'Main', type: 'Main',
task: message.substring(0, 100), task: message.substring(0, 100),
model: useModel,
}); });
sendResponse(requestId, { agentId: currentAgentId, status: 'gestartet' }); sendResponse(requestId, { agentId: currentAgentId, status: 'gestartet', model: useModel });
const startTime = Date.now(); const startTime = Date.now();
let fullText = ''; let fullText = '';
let usedModel = MODEL; let usedModel = useModel;
try { try {
const conversation = query({ const conversation = query({
prompt: message, prompt: message,
options: { options: {
model: MODEL, model: useModel,
maxTurns: 25, maxTurns: 25,
abortController: activeAbort, abortController: activeAbort,
}, },
@ -140,7 +151,8 @@ function handleCommand(msg) {
sendError(msg.id, 'Keine Nachricht angegeben'); sendError(msg.id, 'Keine Nachricht angegeben');
return; return;
} }
sendMessage(msg.message, msg.id); // Modell kann pro Anfrage überschrieben werden
sendMessage(msg.message, msg.id, msg.model);
break; break;
case 'stop': case 'stop':
@ -150,10 +162,33 @@ function handleCommand(msg) {
sendResponse(msg.id, { status: 'gestoppt' }); sendResponse(msg.id, { status: 'gestoppt' });
break; break;
case 'set-model':
if (!msg.model) {
sendError(msg.id, 'Kein Modell angegeben');
return;
}
const validModels = AVAILABLE_MODELS.map(m => m.id);
if (!validModels.includes(msg.model)) {
sendError(msg.id, `Ungültiges Modell: ${msg.model}. Verfügbar: ${validModels.join(', ')}`);
return;
}
currentModel = msg.model;
sendResponse(msg.id, { model: currentModel, status: 'Modell geändert' });
sendEvent('model-changed', { model: currentModel });
break;
case 'get-models':
sendResponse(msg.id, {
current: currentModel,
available: AVAILABLE_MODELS,
});
break;
case 'status': case 'status':
sendResponse(msg.id, { sendResponse(msg.id, {
model: MODEL, model: currentModel,
isProcessing: !!currentAgentId, isProcessing: !!currentAgentId,
availableModels: AVAILABLE_MODELS,
}); });
break; break;
@ -186,4 +221,4 @@ process.on('SIGTERM', () => { clearInterval(keepAlive); process.exit(0); });
process.on('SIGINT', () => { clearInterval(keepAlive); process.exit(0); }); process.on('SIGINT', () => { clearInterval(keepAlive); process.exit(0); });
// Bereit // Bereit
sendEvent('ready', { version: '1.0.0', pid: process.pid, model: MODEL }); sendEvent('ready', { version: '1.1.0', pid: process.pid, model: currentModel, availableModels: AVAILABLE_MODELS });

View file

@ -264,11 +264,19 @@ fn send_to_bridge(app: &AppHandle, command: &str, message: &str) -> Result<Strin
state.request_counter += 1; state.request_counter += 1;
let request_id = format!("req-{}", state.request_counter); let request_id = format!("req-{}", state.request_counter);
let msg = serde_json::json!({ // Je nach Command unterschiedliche Payload-Struktur
"command": command, let msg = match command {
"id": request_id, "set-model" => serde_json::json!({
"message": message "command": command,
}); "id": request_id,
"model": message
}),
_ => serde_json::json!({
"command": command,
"id": request_id,
"message": message
}),
};
if let Some(stdin) = &mut state.bridge_stdin { if let Some(stdin) = &mut state.bridge_stdin {
writeln!(stdin, "{}", msg.to_string()).map_err(|e| e.to_string())?; writeln!(stdin, "{}", msg.to_string()).map_err(|e| e.to_string())?;
@ -323,3 +331,75 @@ pub async fn get_agent_status(app: AppHandle) -> Result<Vec<AgentStatus>, String
let state = state.lock().unwrap(); let state = state.lock().unwrap();
Ok(state.agents.clone()) Ok(state.agents.clone())
} }
/// Modell wechseln
#[tauri::command]
pub async fn set_model(app: AppHandle, model: String) -> Result<String, String> {
println!("🔄 Modell wechseln zu: {}", model);
// Modell in Settings speichern
if let Some(db_state) = app.try_state::<Arc<Mutex<crate::db::Database>>>() {
let db = db_state.lock().unwrap();
let _ = db.set_setting("claude_model", &model);
}
// Bridge starten falls nicht aktiv
let needs_start = {
let state = app.state::<Arc<Mutex<ClaudeState>>>();
let state_guard = state.lock().unwrap();
state_guard.bridge_stdin.is_none()
};
if needs_start {
start_bridge(&app)?;
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
}
// Modell an Bridge senden
send_to_bridge(&app, "set-model", &model)?;
Ok(model)
}
/// Verfügbare Modelle abrufen
#[tauri::command]
pub async fn get_available_models() -> Result<Vec<ModelInfo>, String> {
Ok(vec![
ModelInfo {
id: "haiku".to_string(),
name: "Claude Haiku".to_string(),
description: "Schnell & günstig".to_string(),
},
ModelInfo {
id: "sonnet".to_string(),
name: "Claude Sonnet".to_string(),
description: "Ausgewogen".to_string(),
},
ModelInfo {
id: "opus".to_string(),
name: "Claude Opus".to_string(),
description: "Leistungsstark".to_string(),
},
])
}
/// Aktuelles Modell aus Settings laden
#[tauri::command]
pub async fn get_current_model(app: AppHandle) -> Result<String, String> {
if let Some(db_state) = app.try_state::<Arc<Mutex<crate::db::Database>>>() {
let db = db_state.lock().unwrap();
if let Ok(Some(model)) = db.get_setting("claude_model") {
return Ok(model);
}
}
// Default
Ok("opus".to_string())
}
/// Modell-Info Struct
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ModelInfo {
pub id: String,
pub name: String,
pub description: String,
}

View file

@ -517,6 +517,15 @@ impl Database {
} }
} }
/// Lädt alle Einstellungen
pub fn get_all_settings(&self) -> SqlResult<Vec<(String, String)>> {
let mut stmt = self.conn.prepare("SELECT key, value FROM settings")?;
let settings = stmt.query_map([], |row| {
Ok((row.get(0)?, row.get(1)?))
})?.collect::<SqlResult<Vec<_>>>()?;
Ok(settings)
}
// ============ Statistiken ============ // ============ Statistiken ============
/// DB-Statistiken /// DB-Statistiken
@ -604,3 +613,27 @@ pub async fn get_db_stats(app: AppHandle) -> Result<DbStats, String> {
let db = state.lock().unwrap(); let db = state.lock().unwrap();
db.stats().map_err(|e| e.to_string()) db.stats().map_err(|e| e.to_string())
} }
/// Einstellung lesen
#[tauri::command]
pub async fn get_setting(app: AppHandle, key: String) -> Result<Option<String>, String> {
let state = app.state::<DbState>();
let db = state.lock().unwrap();
db.get_setting(&key).map_err(|e| e.to_string())
}
/// Einstellung speichern
#[tauri::command]
pub async fn set_setting(app: AppHandle, key: String, value: String) -> Result<(), String> {
let state = app.state::<DbState>();
let db = state.lock().unwrap();
db.set_setting(&key, &value).map_err(|e| e.to_string())
}
/// Alle Einstellungen laden
#[tauri::command]
pub async fn get_all_settings(app: AppHandle) -> Result<Vec<(String, String)>, String> {
let state = app.state::<DbState>();
let db = state.lock().unwrap();
db.get_all_settings().map_err(|e| e.to_string())
}

View file

@ -23,6 +23,9 @@ pub fn run() {
claude::send_message, claude::send_message,
claude::stop_all_agents, claude::stop_all_agents,
claude::get_agent_status, claude::get_agent_status,
claude::set_model,
claude::get_available_models,
claude::get_current_model,
// Gedächtnis-System // Gedächtnis-System
memory::load_memory, memory::load_memory,
memory::get_sticky_context, memory::get_sticky_context,
@ -41,6 +44,10 @@ pub fn run() {
// Datenbank // Datenbank
db::init_database, db::init_database,
db::get_db_stats, db::get_db_stats,
// Settings
db::get_setting,
db::set_setting,
db::get_all_settings,
// Sessions // Sessions
session::create_session, session::create_session,
session::update_session, session::update_session,

View file

@ -0,0 +1,277 @@
<script lang="ts">
import { onMount } from 'svelte';
import { invoke } from '@tauri-apps/api/core';
import { currentModel } from '$lib/stores/app';
interface ModelInfo {
id: string;
name: string;
description: string;
}
let availableModels: ModelInfo[] = [];
let selectedModel = '';
let loading = true;
let saving = false;
// Modell-Icons
const modelIcons: Record<string, string> = {
haiku: '🐦',
sonnet: '📝',
opus: '🎭',
};
// Preise pro 1M Token (ungefähre Werte)
const modelPrices: Record<string, { input: number; output: number }> = {
haiku: { input: 0.25, output: 1.25 },
sonnet: { input: 3, output: 15 },
opus: { input: 15, output: 75 },
};
async function loadSettings() {
try {
availableModels = await invoke('get_available_models');
const current: string = await invoke('get_current_model');
selectedModel = current;
$currentModel = current;
} catch (err) {
console.error('Fehler beim Laden:', err);
}
loading = false;
}
async function changeModel(modelId: string) {
if (modelId === selectedModel) return;
saving = true;
try {
await invoke('set_model', { model: modelId });
selectedModel = modelId;
$currentModel = modelId;
} catch (err) {
console.error('Fehler beim Speichern:', err);
}
saving = false;
}
onMount(() => {
loadSettings();
});
function formatPrice(price: number): string {
return `$${price.toFixed(2)}`;
}
</script>
<div class="settings-panel">
<div class="panel-header">
<h2>⚙️ Einstellungen</h2>
</div>
{#if loading}
<div class="loading">Lade Einstellungen...</div>
{:else}
<div class="settings-content">
<!-- Modell-Auswahl -->
<section class="settings-section">
<h3>🤖 Claude-Modell</h3>
<p class="section-hint">Wähle das Modell für deine Anfragen</p>
<div class="model-list">
{#each availableModels as model}
<button
class="model-card"
class:selected={selectedModel === model.id}
class:saving={saving && selectedModel !== model.id}
on:click={() => changeModel(model.id)}
disabled={saving}
>
<div class="model-header">
<span class="model-icon">{modelIcons[model.id] || '🤖'}</span>
<span class="model-name">{model.name}</span>
{#if selectedModel === model.id}
<span class="model-active">✓ Aktiv</span>
{/if}
</div>
<div class="model-description">{model.description}</div>
{#if modelPrices[model.id]}
<div class="model-pricing">
<span>Input: {formatPrice(modelPrices[model.id].input)}/1M</span>
<span>Output: {formatPrice(modelPrices[model.id].output)}/1M</span>
</div>
{/if}
</button>
{/each}
</div>
</section>
<!-- Weitere Einstellungen (Platzhalter) -->
<section class="settings-section">
<h3>🎨 Darstellung</h3>
<div class="setting-row">
<span class="setting-label">Theme</span>
<span class="setting-value">AWL Dark (fest)</span>
</div>
</section>
<section class="settings-section">
<h3>📁 Pfade</h3>
<div class="setting-row">
<span class="setting-label">Arbeitsverzeichnis</span>
<span class="setting-value placeholder">Wird aus Session geladen</span>
</div>
</section>
</div>
{/if}
</div>
<style>
.settings-panel {
display: flex;
flex-direction: column;
height: 100%;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--bg-secondary);
border-bottom: 1px solid var(--bg-tertiary);
}
.panel-header h2 {
font-size: 0.875rem;
font-weight: 600;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-secondary);
}
.settings-content {
flex: 1;
overflow-y: auto;
padding: var(--spacing-md);
}
.settings-section {
margin-bottom: var(--spacing-lg);
}
.settings-section h3 {
font-size: 0.875rem;
font-weight: 600;
margin-bottom: var(--spacing-xs);
}
.section-hint {
font-size: 0.75rem;
color: var(--text-secondary);
margin-bottom: var(--spacing-md);
}
/* Modell-Karten */
.model-list {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.model-card {
width: 100%;
text-align: left;
padding: var(--spacing-md);
background: var(--bg-secondary);
border: 2px solid var(--bg-tertiary);
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s ease;
}
.model-card:hover:not(:disabled) {
background: var(--bg-tertiary);
border-color: var(--text-secondary);
}
.model-card.selected {
border-color: var(--accent);
background: rgba(233, 69, 96, 0.1);
}
.model-card.saving {
opacity: 0.5;
cursor: wait;
}
.model-card:disabled {
cursor: not-allowed;
}
.model-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-xs);
}
.model-icon {
font-size: 1.2rem;
}
.model-name {
font-weight: 600;
flex: 1;
}
.model-active {
font-size: 0.7rem;
padding: 2px 6px;
background: var(--accent);
color: white;
border-radius: var(--radius-sm);
}
.model-description {
font-size: 0.8rem;
color: var(--text-secondary);
margin-bottom: var(--spacing-xs);
}
.model-pricing {
display: flex;
gap: var(--spacing-md);
font-size: 0.7rem;
color: var(--text-secondary);
font-family: var(--font-mono);
}
/* Allgemeine Einstellungs-Zeilen */
.setting-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-sm) 0;
border-bottom: 1px solid var(--bg-tertiary);
}
.setting-label {
font-size: 0.85rem;
}
.setting-value {
font-size: 0.85rem;
color: var(--text-secondary);
}
.setting-value.placeholder {
font-style: italic;
opacity: 0.6;
}
</style>

View file

@ -7,6 +7,16 @@
onMount(async () => { onMount(async () => {
await initEventListeners(); await initEventListeners();
// Aktuelles Modell aus Settings laden
try {
const model: string = await invoke('get_current_model');
if (model) {
$currentModel = model;
}
} catch (err) {
console.warn('Modell konnte nicht geladen werden:', err);
}
}); });
onDestroy(async () => { onDestroy(async () => {

View file

@ -7,6 +7,7 @@
import MemoryPanel from '$lib/components/MemoryPanel.svelte'; import MemoryPanel from '$lib/components/MemoryPanel.svelte';
import AuditLog from '$lib/components/AuditLog.svelte'; import AuditLog from '$lib/components/AuditLog.svelte';
import GuardRailsPanel from '$lib/components/GuardRailsPanel.svelte'; import GuardRailsPanel from '$lib/components/GuardRailsPanel.svelte';
import SettingsPanel from '$lib/components/SettingsPanel.svelte';
let activeMiddleTab = 'activity'; let activeMiddleTab = 'activity';
let activeRightTab = 'agents'; let activeRightTab = 'agents';
@ -20,6 +21,7 @@
const rightTabs = [ const rightTabs = [
{ id: 'agents', label: 'Agents', icon: '🤖' }, { id: 'agents', label: 'Agents', icon: '🤖' },
{ id: 'guards', label: 'Guard-Rails', icon: '🛡️' }, { id: 'guards', label: 'Guard-Rails', icon: '🛡️' },
{ id: 'settings', label: 'Settings', icon: '⚙️' },
]; ];
</script> </script>
@ -88,6 +90,8 @@
<AgentView /> <AgentView />
{:else if activeRightTab === 'guards'} {:else if activeRightTab === 'guards'}
<GuardRailsPanel /> <GuardRailsPanel />
{:else if activeRightTab === 'settings'}
<SettingsPanel />
{/if} {/if}
</div> </div>
</Pane> </Pane>