claude-desktop/src/lib/components/SettingsPanel.svelte
Eddy 433e2de2b6 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>
2026-04-14 09:32:26 +02:00

277 lines
5.8 KiB
Svelte

<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>