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:
parent
9129163876
commit
433e2de2b6
7 changed files with 459 additions and 13 deletions
|
|
@ -17,7 +17,14 @@ process.stdin.resume();
|
|||
|
||||
let activeAbort = 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 ============
|
||||
|
||||
|
|
@ -39,7 +46,10 @@ function sendError(id, error) {
|
|||
|
||||
// ============ 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();
|
||||
activeAbort = new AbortController();
|
||||
|
||||
|
|
@ -47,19 +57,20 @@ async function sendMessage(message, requestId) {
|
|||
id: currentAgentId,
|
||||
type: 'Main',
|
||||
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();
|
||||
let fullText = '';
|
||||
let usedModel = MODEL;
|
||||
let usedModel = useModel;
|
||||
|
||||
try {
|
||||
const conversation = query({
|
||||
prompt: message,
|
||||
options: {
|
||||
model: MODEL,
|
||||
model: useModel,
|
||||
maxTurns: 25,
|
||||
abortController: activeAbort,
|
||||
},
|
||||
|
|
@ -140,7 +151,8 @@ function handleCommand(msg) {
|
|||
sendError(msg.id, 'Keine Nachricht angegeben');
|
||||
return;
|
||||
}
|
||||
sendMessage(msg.message, msg.id);
|
||||
// Modell kann pro Anfrage überschrieben werden
|
||||
sendMessage(msg.message, msg.id, msg.model);
|
||||
break;
|
||||
|
||||
case 'stop':
|
||||
|
|
@ -150,10 +162,33 @@ function handleCommand(msg) {
|
|||
sendResponse(msg.id, { status: 'gestoppt' });
|
||||
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':
|
||||
sendResponse(msg.id, {
|
||||
model: MODEL,
|
||||
model: currentModel,
|
||||
isProcessing: !!currentAgentId,
|
||||
availableModels: AVAILABLE_MODELS,
|
||||
});
|
||||
break;
|
||||
|
||||
|
|
@ -186,4 +221,4 @@ process.on('SIGTERM', () => { clearInterval(keepAlive); process.exit(0); });
|
|||
process.on('SIGINT', () => { clearInterval(keepAlive); process.exit(0); });
|
||||
|
||||
// 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 });
|
||||
|
|
|
|||
|
|
@ -264,11 +264,19 @@ fn send_to_bridge(app: &AppHandle, command: &str, message: &str) -> Result<Strin
|
|||
state.request_counter += 1;
|
||||
let request_id = format!("req-{}", state.request_counter);
|
||||
|
||||
let msg = serde_json::json!({
|
||||
"command": command,
|
||||
"id": request_id,
|
||||
"message": message
|
||||
});
|
||||
// Je nach Command unterschiedliche Payload-Struktur
|
||||
let msg = match command {
|
||||
"set-model" => serde_json::json!({
|
||||
"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 {
|
||||
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();
|
||||
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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ============
|
||||
|
||||
/// DB-Statistiken
|
||||
|
|
@ -604,3 +613,27 @@ pub async fn get_db_stats(app: AppHandle) -> Result<DbStats, String> {
|
|||
let db = state.lock().unwrap();
|
||||
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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ pub fn run() {
|
|||
claude::send_message,
|
||||
claude::stop_all_agents,
|
||||
claude::get_agent_status,
|
||||
claude::set_model,
|
||||
claude::get_available_models,
|
||||
claude::get_current_model,
|
||||
// Gedächtnis-System
|
||||
memory::load_memory,
|
||||
memory::get_sticky_context,
|
||||
|
|
@ -41,6 +44,10 @@ pub fn run() {
|
|||
// Datenbank
|
||||
db::init_database,
|
||||
db::get_db_stats,
|
||||
// Settings
|
||||
db::get_setting,
|
||||
db::set_setting,
|
||||
db::get_all_settings,
|
||||
// Sessions
|
||||
session::create_session,
|
||||
session::update_session,
|
||||
|
|
|
|||
277
src/lib/components/SettingsPanel.svelte
Normal file
277
src/lib/components/SettingsPanel.svelte
Normal 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>
|
||||
|
|
@ -7,6 +7,16 @@
|
|||
|
||||
onMount(async () => {
|
||||
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 () => {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import MemoryPanel from '$lib/components/MemoryPanel.svelte';
|
||||
import AuditLog from '$lib/components/AuditLog.svelte';
|
||||
import GuardRailsPanel from '$lib/components/GuardRailsPanel.svelte';
|
||||
import SettingsPanel from '$lib/components/SettingsPanel.svelte';
|
||||
|
||||
let activeMiddleTab = 'activity';
|
||||
let activeRightTab = 'agents';
|
||||
|
|
@ -20,6 +21,7 @@
|
|||
const rightTabs = [
|
||||
{ id: 'agents', label: 'Agents', icon: '🤖' },
|
||||
{ id: 'guards', label: 'Guard-Rails', icon: '🛡️' },
|
||||
{ id: 'settings', label: 'Settings', icon: '⚙️' },
|
||||
];
|
||||
</script>
|
||||
|
||||
|
|
@ -88,6 +90,8 @@
|
|||
<AgentView />
|
||||
{:else if activeRightTab === 'guards'}
|
||||
<GuardRailsPanel />
|
||||
{:else if activeRightTab === 'settings'}
|
||||
<SettingsPanel />
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
|
|
|
|||
Loading…
Reference in a new issue