Compare commits
2 commits
9129163876
...
4ba14a53e1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ba14a53e1 | ||
|
|
433e2de2b6 |
11 changed files with 709 additions and 25 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!({
|
||||
// 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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,17 @@ pub struct Session {
|
|||
pub last_message: Option<String>,
|
||||
}
|
||||
|
||||
/// Eine Chat-Nachricht
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ChatMessage {
|
||||
pub id: String,
|
||||
pub session_id: String,
|
||||
pub role: String, // "user", "assistant", "system"
|
||||
pub content: String,
|
||||
pub model: Option<String>,
|
||||
pub timestamp: String,
|
||||
}
|
||||
|
||||
/// Datenbank-Wrapper
|
||||
pub struct Database {
|
||||
conn: Connection,
|
||||
|
|
@ -141,6 +152,18 @@ impl Database {
|
|||
value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Chat-Nachrichten
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id TEXT PRIMARY KEY,
|
||||
session_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
model TEXT,
|
||||
timestamp TEXT NOT NULL,
|
||||
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);
|
||||
",
|
||||
)?;
|
||||
Ok(())
|
||||
|
|
@ -488,10 +511,58 @@ impl Database {
|
|||
|
||||
/// Löscht eine Session
|
||||
pub fn delete_session(&self, id: &str) -> SqlResult<()> {
|
||||
// Erst Nachrichten löschen (wegen Foreign Key)
|
||||
self.conn.execute("DELETE FROM messages WHERE session_id = ?1", params![id])?;
|
||||
self.conn.execute("DELETE FROM sessions WHERE id = ?1", params![id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============ Messages ============
|
||||
|
||||
/// Speichert eine Nachricht
|
||||
pub fn save_message(&self, msg: &ChatMessage) -> SqlResult<()> {
|
||||
self.conn.execute(
|
||||
"INSERT OR REPLACE INTO messages (id, session_id, role, content, model, timestamp)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![
|
||||
msg.id,
|
||||
msg.session_id,
|
||||
msg.role,
|
||||
msg.content,
|
||||
msg.model,
|
||||
msg.timestamp,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lädt alle Nachrichten einer Session
|
||||
pub fn load_messages(&self, session_id: &str) -> SqlResult<Vec<ChatMessage>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, session_id, role, content, model, timestamp
|
||||
FROM messages WHERE session_id = ?1 ORDER BY timestamp ASC"
|
||||
)?;
|
||||
|
||||
let messages = stmt.query_map(params![session_id], |row| {
|
||||
Ok(ChatMessage {
|
||||
id: row.get(0)?,
|
||||
session_id: row.get(1)?,
|
||||
role: row.get(2)?,
|
||||
content: row.get(3)?,
|
||||
model: row.get(4)?,
|
||||
timestamp: row.get(5)?,
|
||||
})
|
||||
})?.collect::<SqlResult<Vec<_>>>()?;
|
||||
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
/// Löscht alle Nachrichten einer Session
|
||||
pub fn clear_messages(&self, session_id: &str) -> SqlResult<()> {
|
||||
self.conn.execute("DELETE FROM messages WHERE session_id = ?1", params![session_id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============ Settings ============
|
||||
|
||||
/// Speichert eine Einstellung
|
||||
|
|
@ -517,6 +588,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 +684,51 @@ 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())
|
||||
}
|
||||
|
||||
/// Nachricht speichern
|
||||
#[tauri::command]
|
||||
pub async fn save_message(app: AppHandle, message: ChatMessage) -> Result<(), String> {
|
||||
let state = app.state::<DbState>();
|
||||
let db = state.lock().unwrap();
|
||||
db.save_message(&message).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Nachrichten einer Session laden
|
||||
#[tauri::command]
|
||||
pub async fn load_messages(app: AppHandle, session_id: String) -> Result<Vec<ChatMessage>, String> {
|
||||
let state = app.state::<DbState>();
|
||||
let db = state.lock().unwrap();
|
||||
db.load_messages(&session_id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Alle Nachrichten einer Session löschen
|
||||
#[tauri::command]
|
||||
pub async fn clear_messages(app: AppHandle, session_id: String) -> Result<(), String> {
|
||||
let state = app.state::<DbState>();
|
||||
let db = state.lock().unwrap();
|
||||
db.clear_messages(&session_id).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,
|
||||
|
|
@ -50,6 +57,10 @@ pub fn run() {
|
|||
session::resume_session,
|
||||
session::get_active_session,
|
||||
session::set_claude_session_id,
|
||||
// Messages
|
||||
db::save_message,
|
||||
db::load_messages,
|
||||
db::clear_messages,
|
||||
])
|
||||
.setup(|app| {
|
||||
let handle = app.handle().clone();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { messages, currentInput, isProcessing, addMessage } from '$lib/stores/app';
|
||||
import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, type Message } from '$lib/stores/app';
|
||||
import { marked } from 'marked';
|
||||
import { tick } from 'svelte';
|
||||
import { tick, onDestroy } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
marked.setOptions({ breaks: true, gfm: true });
|
||||
|
||||
|
|
@ -25,11 +26,56 @@
|
|||
|
||||
$: if ($messages.length) scrollToBottom();
|
||||
|
||||
// Nachricht in DB speichern
|
||||
async function saveMessageToDb(msg: Message) {
|
||||
const sessionId = get(currentSessionId);
|
||||
if (!sessionId) return;
|
||||
|
||||
try {
|
||||
const dbMsg = messageToDb(msg, sessionId);
|
||||
await invoke('save_message', { message: dbMsg });
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Speichern der Nachricht:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Neue Nachrichten automatisch speichern
|
||||
let lastMessageCount = 0;
|
||||
const unsubscribe = messages.subscribe(async (msgs) => {
|
||||
if (msgs.length > lastMessageCount && lastMessageCount > 0) {
|
||||
// Neue Nachricht(en) hinzugefügt
|
||||
const newMessages = msgs.slice(lastMessageCount);
|
||||
for (const msg of newMessages) {
|
||||
// Nur speichern wenn Nachricht Content hat (nicht die leere Streaming-Nachricht)
|
||||
if (msg.content && msg.content.trim()) {
|
||||
await saveMessageToDb(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
lastMessageCount = msgs.length;
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
async function sendMessage() {
|
||||
const text = $currentInput.trim();
|
||||
if (!text || $isProcessing) return;
|
||||
|
||||
addMessage('user', text);
|
||||
// Nachricht hinzufügen (wird durch den Store-Subscriber gespeichert)
|
||||
const msgId = crypto.randomUUID();
|
||||
const msg: Message = {
|
||||
id: msgId,
|
||||
role: 'user',
|
||||
content: text,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
messages.update((msgs) => [...msgs, msg]);
|
||||
|
||||
// Sofort speichern (nicht auf Subscriber warten)
|
||||
await saveMessageToDb(msg);
|
||||
|
||||
$currentInput = '';
|
||||
$isProcessing = true;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { messages, clearAll, isProcessing } from '$lib/stores/app';
|
||||
import { messages, clearAll, isProcessing, currentSessionId, setMessagesFromDb, type DbMessage } from '$lib/stores/app';
|
||||
|
||||
interface Session {
|
||||
id: string;
|
||||
|
|
@ -29,12 +29,28 @@
|
|||
sessions = await invoke('list_sessions', { limit: 50 });
|
||||
const active: Session | null = await invoke('get_active_session');
|
||||
activeSessionId = active?.id || null;
|
||||
$currentSessionId = activeSessionId;
|
||||
|
||||
// Wenn aktive Session existiert, Nachrichten laden
|
||||
if (activeSessionId) {
|
||||
await loadSessionMessages(activeSessionId);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden der Sessions:', err);
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
|
||||
async function loadSessionMessages(sessionId: string) {
|
||||
try {
|
||||
const dbMessages: DbMessage[] = await invoke('load_messages', { sessionId });
|
||||
setMessagesFromDb(dbMessages);
|
||||
console.log(`📨 ${dbMessages.length} Nachrichten geladen`);
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden der Nachrichten:', err);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadSessions();
|
||||
});
|
||||
|
|
@ -47,6 +63,7 @@
|
|||
workingDir: null,
|
||||
});
|
||||
activeSessionId = session.id;
|
||||
$currentSessionId = session.id;
|
||||
clearAll();
|
||||
newTitle = '';
|
||||
showNewForm = false;
|
||||
|
|
@ -61,8 +78,10 @@
|
|||
try {
|
||||
const session: Session = await invoke('resume_session', { id });
|
||||
activeSessionId = session.id;
|
||||
$currentSessionId = session.id;
|
||||
clearAll();
|
||||
// TODO: Nachrichten aus Session-Historie laden
|
||||
// Nachrichten aus DB laden
|
||||
await loadSessionMessages(session.id);
|
||||
} catch (err) {
|
||||
console.error('Fehler:', err);
|
||||
}
|
||||
|
|
|
|||
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>
|
||||
|
|
@ -51,6 +51,7 @@ export const isProcessing = writable(false);
|
|||
export const currentInput = writable('');
|
||||
export const selectedAgentId = writable<string | null>(null);
|
||||
export const currentModel = writable('');
|
||||
export const currentSessionId = writable<string | null>(null);
|
||||
|
||||
// Session-Statistiken (kumuliert)
|
||||
export const sessionStats = writable({
|
||||
|
|
@ -156,3 +157,41 @@ export function clearAll() {
|
|||
messages.set([]);
|
||||
isProcessing.set(false);
|
||||
}
|
||||
|
||||
// DB-Nachricht Format (für Tauri)
|
||||
export interface DbMessage {
|
||||
id: string;
|
||||
session_id: string;
|
||||
role: string;
|
||||
content: string;
|
||||
model: string | null;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
// Konvertierung: Store → DB
|
||||
export function messageToDb(msg: Message, sessionId: string): DbMessage {
|
||||
return {
|
||||
id: msg.id,
|
||||
session_id: sessionId,
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
model: msg.model || null,
|
||||
timestamp: msg.timestamp.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// Konvertierung: DB → Store
|
||||
export function dbToMessage(db: DbMessage): Message {
|
||||
return {
|
||||
id: db.id,
|
||||
role: db.role as Message['role'],
|
||||
content: db.content,
|
||||
model: db.model || undefined,
|
||||
timestamp: new Date(db.timestamp),
|
||||
};
|
||||
}
|
||||
|
||||
// Nachrichten aus DB in Store laden
|
||||
export function setMessagesFromDb(dbMessages: DbMessage[]) {
|
||||
messages.set(dbMessages.map(dbToMessage));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
// Empfängt Events vom Tauri-Backend und aktualisiert die Stores
|
||||
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { get } from 'svelte/store';
|
||||
import {
|
||||
agents,
|
||||
toolCalls,
|
||||
|
|
@ -14,7 +16,10 @@ import {
|
|||
completeToolCall,
|
||||
clearAll,
|
||||
currentModel,
|
||||
sessionStats
|
||||
sessionStats,
|
||||
currentSessionId,
|
||||
messageToDb,
|
||||
type Message
|
||||
} from './app';
|
||||
|
||||
// Event-Typen vom Backend
|
||||
|
|
@ -53,6 +58,20 @@ let listeners: UnlistenFn[] = [];
|
|||
// Streaming: ID der aktuellen Live-Nachricht
|
||||
let streamingMessageId: string | null = null;
|
||||
|
||||
// Nachricht in DB speichern
|
||||
async function saveMessageToDb(msg: Message) {
|
||||
const sessionId = get(currentSessionId);
|
||||
if (!sessionId) return;
|
||||
|
||||
try {
|
||||
const dbMsg = messageToDb(msg, sessionId);
|
||||
await invoke('save_message', { message: dbMsg });
|
||||
console.log('💾 Nachricht gespeichert:', msg.role);
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Speichern der Nachricht:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Events initialisieren
|
||||
export async function initEventListeners(): Promise<void> {
|
||||
console.log('🎧 Initialisiere Event-Listener...');
|
||||
|
|
@ -161,15 +180,31 @@ export async function initEventListeners(): Promise<void> {
|
|||
|
||||
// Ergebnis (Kosten, Token, Modell)
|
||||
listeners.push(
|
||||
await listen<ResultEvent>('claude-result', (event) => {
|
||||
await listen<ResultEvent>('claude-result', async (event) => {
|
||||
const { cost, tokens, session_id, model } = event.payload;
|
||||
console.log('📊 Ergebnis:', { cost: cost ? `$${cost.toFixed(4)}` : '-', tokens, model });
|
||||
|
||||
// Modell an die Streaming-Nachricht anhängen
|
||||
if (model && streamingMessageId) {
|
||||
messages.update((msgs) =>
|
||||
msgs.map((m) => m.id === streamingMessageId ? { ...m, model } : m)
|
||||
);
|
||||
// Modell an die Streaming-Nachricht anhängen und speichern
|
||||
if (streamingMessageId) {
|
||||
let finalMessage: Message | null = null;
|
||||
|
||||
messages.update((msgs) => {
|
||||
return msgs.map((m) => {
|
||||
if (m.id === streamingMessageId) {
|
||||
finalMessage = { ...m, model: model || m.model };
|
||||
return finalMessage;
|
||||
}
|
||||
return m;
|
||||
});
|
||||
});
|
||||
|
||||
// Nachricht in DB speichern (nur wenn Content vorhanden)
|
||||
if (finalMessage && finalMessage.content && finalMessage.content.trim()) {
|
||||
await saveMessageToDb(finalMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (model) {
|
||||
currentModel.set(model);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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