Phase 5: Session-Verwaltung + permanente Konversationen
- session.rs: Neues Modul mit 7 Tauri-Commands (CRUD, Resume, aktive Session) - db.rs: Sessions-Tabelle + CRUD-Methoden (bleiben bis User sie löscht) - claude.rs: Session-ID und Token/Kosten automatisch in DB speichern - SessionList.svelte: Sidebar mit Session-Liste, Erstellen, Fortsetzen, Löschen - +page.svelte: 4-Panel Layout (Sessions | Chat | Aktivität | Agents) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
532c91c605
commit
f101661016
6 changed files with 716 additions and 14 deletions
|
|
@ -7,6 +7,8 @@ use std::process::{Command, Stdio};
|
|||
use std::sync::{Arc, Mutex};
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
|
||||
use crate::db;
|
||||
|
||||
/// Status eines Agents
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentStatus {
|
||||
|
|
@ -176,6 +178,30 @@ fn handle_bridge_message(app: &AppHandle, msg: BridgeMessage) {
|
|||
let _ = app.emit("claude-text", &payload);
|
||||
}
|
||||
"result" => {
|
||||
// Session-ID aus Result extrahieren und in DB speichern
|
||||
if let Some(sid) = payload.get("session_id").and_then(|v| v.as_str()) {
|
||||
if let Some(db_state) = app.try_state::<Arc<Mutex<db::Database>>>() {
|
||||
let db_lock = db_state.lock().unwrap();
|
||||
if let Ok(Some(active_id)) = db_lock.get_setting("active_session_id") {
|
||||
if !active_id.is_empty() {
|
||||
if let Ok(Some(mut session)) = db_lock.get_session(&active_id) {
|
||||
session.claude_session_id = Some(sid.to_string());
|
||||
session.message_count += 1;
|
||||
if let Some(cost) = payload.get("cost").and_then(|v| v.as_f64()) {
|
||||
session.cost_usd += cost;
|
||||
}
|
||||
if let Some(tin) = payload.get("tokens").and_then(|t| t.get("input")).and_then(|v| v.as_i64()) {
|
||||
session.token_input += tin;
|
||||
}
|
||||
if let Some(tout) = payload.get("tokens").and_then(|t| t.get("output")).and_then(|v| v.as_i64()) {
|
||||
session.token_output += tout;
|
||||
}
|
||||
let _ = db_lock.update_session(&session);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = app.emit("claude-result", &payload);
|
||||
}
|
||||
"all-stopped" => {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,23 @@ use crate::audit::{AuditAction, AuditCategory, AuditEntry, AuditStats};
|
|||
use crate::guard::{Permission, PermissionAction, PermissionType};
|
||||
use crate::memory::{ContextCategory, MemoryEntry, Pattern};
|
||||
|
||||
/// Eine Claude-Session
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Session {
|
||||
pub id: String,
|
||||
pub claude_session_id: Option<String>,
|
||||
pub title: String,
|
||||
pub working_dir: Option<String>,
|
||||
pub message_count: i64,
|
||||
pub token_input: i64,
|
||||
pub token_output: i64,
|
||||
pub cost_usd: f64,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub last_message: Option<String>,
|
||||
}
|
||||
|
||||
/// Datenbank-Wrapper
|
||||
pub struct Database {
|
||||
conn: Connection,
|
||||
|
|
@ -100,6 +117,24 @@ impl Database {
|
|||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Sessions (Claude-Konversationen)
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
claude_session_id TEXT,
|
||||
title TEXT NOT NULL,
|
||||
working_dir TEXT,
|
||||
message_count INTEGER DEFAULT 0,
|
||||
token_input INTEGER DEFAULT 0,
|
||||
token_output INTEGER DEFAULT 0,
|
||||
cost_usd REAL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
last_message TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_updated ON sessions(updated_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status);
|
||||
|
||||
-- Einstellungen (Key-Value)
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
|
|
@ -344,6 +379,119 @@ impl Database {
|
|||
Ok(patterns)
|
||||
}
|
||||
|
||||
// ============ Sessions ============
|
||||
|
||||
/// Erstellt eine neue Session
|
||||
pub fn create_session(&self, session: &Session) -> SqlResult<()> {
|
||||
self.conn.execute(
|
||||
"INSERT INTO sessions (id, claude_session_id, title, working_dir, message_count, token_input, token_output, cost_usd, status, created_at, updated_at, last_message)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
|
||||
params![
|
||||
session.id,
|
||||
session.claude_session_id,
|
||||
session.title,
|
||||
session.working_dir,
|
||||
session.message_count,
|
||||
session.token_input,
|
||||
session.token_output,
|
||||
session.cost_usd,
|
||||
session.status,
|
||||
session.created_at,
|
||||
session.updated_at,
|
||||
session.last_message,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Aktualisiert eine Session
|
||||
pub fn update_session(&self, session: &Session) -> SqlResult<()> {
|
||||
self.conn.execute(
|
||||
"UPDATE sessions SET claude_session_id = ?2, title = ?3, message_count = ?4,
|
||||
token_input = ?5, token_output = ?6, cost_usd = ?7, status = ?8,
|
||||
updated_at = ?9, last_message = ?10
|
||||
WHERE id = ?1",
|
||||
params![
|
||||
session.id,
|
||||
session.claude_session_id,
|
||||
session.title,
|
||||
session.message_count,
|
||||
session.token_input,
|
||||
session.token_output,
|
||||
session.cost_usd,
|
||||
session.status,
|
||||
chrono::Local::now().to_rfc3339(),
|
||||
session.last_message,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lädt alle Sessions (neueste zuerst)
|
||||
pub fn load_sessions(&self, limit: usize) -> SqlResult<Vec<Session>> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT id, claude_session_id, title, working_dir, message_count,
|
||||
token_input, token_output, cost_usd, status, created_at, updated_at, last_message
|
||||
FROM sessions ORDER BY updated_at DESC LIMIT ?1"
|
||||
)?;
|
||||
|
||||
let sessions = stmt.query_map(params![limit as i64], |row| {
|
||||
Ok(Session {
|
||||
id: row.get(0)?,
|
||||
claude_session_id: row.get(1)?,
|
||||
title: row.get(2)?,
|
||||
working_dir: row.get(3)?,
|
||||
message_count: row.get(4)?,
|
||||
token_input: row.get(5)?,
|
||||
token_output: row.get(6)?,
|
||||
cost_usd: row.get(7)?,
|
||||
status: row.get(8)?,
|
||||
created_at: row.get(9)?,
|
||||
updated_at: row.get(10)?,
|
||||
last_message: row.get(11)?,
|
||||
})
|
||||
})?.collect::<SqlResult<Vec<_>>>()?;
|
||||
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
/// Holt eine Session nach ID
|
||||
pub fn get_session(&self, id: &str) -> SqlResult<Option<Session>> {
|
||||
let result = self.conn.query_row(
|
||||
"SELECT id, claude_session_id, title, working_dir, message_count,
|
||||
token_input, token_output, cost_usd, status, created_at, updated_at, last_message
|
||||
FROM sessions WHERE id = ?1",
|
||||
params![id],
|
||||
|row| {
|
||||
Ok(Session {
|
||||
id: row.get(0)?,
|
||||
claude_session_id: row.get(1)?,
|
||||
title: row.get(2)?,
|
||||
working_dir: row.get(3)?,
|
||||
message_count: row.get(4)?,
|
||||
token_input: row.get(5)?,
|
||||
token_output: row.get(6)?,
|
||||
cost_usd: row.get(7)?,
|
||||
status: row.get(8)?,
|
||||
created_at: row.get(9)?,
|
||||
updated_at: row.get(10)?,
|
||||
last_message: row.get(11)?,
|
||||
})
|
||||
},
|
||||
);
|
||||
match result {
|
||||
Ok(s) => Ok(Some(s)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Löscht eine Session
|
||||
pub fn delete_session(&self, id: &str) -> SqlResult<()> {
|
||||
self.conn.execute("DELETE FROM sessions WHERE id = ?1", params![id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============ Settings ============
|
||||
|
||||
/// Speichert eine Einstellung
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ mod claude;
|
|||
mod db;
|
||||
mod guard;
|
||||
mod memory;
|
||||
mod session;
|
||||
|
||||
/// Initialisiert die App
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
|
|
@ -40,6 +41,15 @@ pub fn run() {
|
|||
// Datenbank
|
||||
db::init_database,
|
||||
db::get_db_stats,
|
||||
// Sessions
|
||||
session::create_session,
|
||||
session::update_session,
|
||||
session::list_sessions,
|
||||
session::get_session,
|
||||
session::delete_session,
|
||||
session::resume_session,
|
||||
session::get_active_session,
|
||||
session::set_claude_session_id,
|
||||
])
|
||||
.setup(|app| {
|
||||
let handle = app.handle().clone();
|
||||
|
|
|
|||
155
src-tauri/src/session.rs
Normal file
155
src-tauri/src/session.rs
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
// Claude Desktop — Session-Verwaltung
|
||||
// Sessions bleiben permanent gespeichert bis der User sie löscht
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
use crate::db::{self, Session};
|
||||
|
||||
// ============ Tauri Commands ============
|
||||
|
||||
/// Neue Session erstellen
|
||||
#[tauri::command]
|
||||
pub async fn create_session(
|
||||
app: AppHandle,
|
||||
title: String,
|
||||
working_dir: Option<String>,
|
||||
) -> Result<Session, String> {
|
||||
let session = Session {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
claude_session_id: None,
|
||||
title,
|
||||
working_dir,
|
||||
message_count: 0,
|
||||
token_input: 0,
|
||||
token_output: 0,
|
||||
cost_usd: 0.0,
|
||||
status: "active".to_string(),
|
||||
created_at: chrono::Local::now().to_rfc3339(),
|
||||
updated_at: chrono::Local::now().to_rfc3339(),
|
||||
last_message: None,
|
||||
};
|
||||
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db = state.lock().unwrap();
|
||||
db.create_session(&session).map_err(|e| e.to_string())?;
|
||||
|
||||
// Als aktive Session speichern
|
||||
db.set_setting("active_session_id", &session.id).map_err(|e| e.to_string())?;
|
||||
|
||||
println!("📝 Neue Session: {} ({})", session.title, session.id);
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
/// Session aktualisieren (nach Nachrichten, Token-Update, etc.)
|
||||
#[tauri::command]
|
||||
pub async fn update_session(
|
||||
app: AppHandle,
|
||||
session: Session,
|
||||
) -> Result<(), String> {
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db = state.lock().unwrap();
|
||||
db.update_session(&session).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Alle Sessions laden
|
||||
#[tauri::command]
|
||||
pub async fn list_sessions(
|
||||
app: AppHandle,
|
||||
limit: Option<usize>,
|
||||
) -> Result<Vec<Session>, String> {
|
||||
let limit = limit.unwrap_or(50);
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db = state.lock().unwrap();
|
||||
db.load_sessions(limit).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Session nach ID laden
|
||||
#[tauri::command]
|
||||
pub async fn get_session(
|
||||
app: AppHandle,
|
||||
id: String,
|
||||
) -> Result<Option<Session>, String> {
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db = state.lock().unwrap();
|
||||
db.get_session(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Session löschen
|
||||
#[tauri::command]
|
||||
pub async fn delete_session(
|
||||
app: AppHandle,
|
||||
id: String,
|
||||
) -> Result<(), String> {
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db = state.lock().unwrap();
|
||||
db.delete_session(&id).map_err(|e| e.to_string())?;
|
||||
|
||||
// Falls es die aktive Session war, Setting löschen
|
||||
if let Ok(Some(active_id)) = db.get_setting("active_session_id") {
|
||||
if active_id == id {
|
||||
let _ = db.set_setting("active_session_id", "");
|
||||
}
|
||||
}
|
||||
|
||||
println!("🗑️ Session gelöscht: {}", id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Session fortsetzen — setzt die aktive Session und gibt die claude_session_id zurück
|
||||
#[tauri::command]
|
||||
pub async fn resume_session(
|
||||
app: AppHandle,
|
||||
id: String,
|
||||
) -> Result<Session, String> {
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db = state.lock().unwrap();
|
||||
|
||||
let session = db.get_session(&id)
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| format!("Session {} nicht gefunden", id))?;
|
||||
|
||||
// Als aktive Session setzen
|
||||
db.set_setting("active_session_id", &session.id).map_err(|e| e.to_string())?;
|
||||
|
||||
println!("▶️ Session fortgesetzt: {} (claude: {:?})", session.title, session.claude_session_id);
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
/// Aktive Session holen (nach App-Start)
|
||||
#[tauri::command]
|
||||
pub async fn get_active_session(
|
||||
app: AppHandle,
|
||||
) -> Result<Option<Session>, String> {
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db = state.lock().unwrap();
|
||||
|
||||
if let Ok(Some(id)) = db.get_setting("active_session_id") {
|
||||
if !id.is_empty() {
|
||||
return db.get_session(&id).map_err(|e| e.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Claude Session-ID speichern (kommt von der Bridge nach erstem Request)
|
||||
#[tauri::command]
|
||||
pub async fn set_claude_session_id(
|
||||
app: AppHandle,
|
||||
session_id: String,
|
||||
claude_session_id: String,
|
||||
) -> Result<(), String> {
|
||||
let state = app.state::<Arc<Mutex<db::Database>>>();
|
||||
let db = state.lock().unwrap();
|
||||
|
||||
if let Ok(Some(mut session)) = db.get_session(&session_id) {
|
||||
session.claude_session_id = Some(claude_session_id.clone());
|
||||
db.update_session(&session).map_err(|e| e.to_string())?;
|
||||
println!("🔗 Claude Session-ID gesetzt: {} → {}", session_id, claude_session_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
344
src/lib/components/SessionList.svelte
Normal file
344
src/lib/components/SessionList.svelte
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { messages, clearAll, isProcessing } from '$lib/stores/app';
|
||||
|
||||
interface Session {
|
||||
id: string;
|
||||
claude_session_id: string | null;
|
||||
title: string;
|
||||
working_dir: string | null;
|
||||
message_count: number;
|
||||
token_input: number;
|
||||
token_output: number;
|
||||
cost_usd: number;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
last_message: string | null;
|
||||
}
|
||||
|
||||
let sessions: Session[] = [];
|
||||
let activeSessionId: string | null = null;
|
||||
let loading = true;
|
||||
let showNewForm = false;
|
||||
let newTitle = '';
|
||||
|
||||
async function loadSessions() {
|
||||
try {
|
||||
sessions = await invoke('list_sessions', { limit: 50 });
|
||||
const active: Session | null = await invoke('get_active_session');
|
||||
activeSessionId = active?.id || null;
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Laden der Sessions:', err);
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadSessions();
|
||||
});
|
||||
|
||||
async function createSession() {
|
||||
const title = newTitle.trim() || `Session ${new Date().toLocaleDateString('de-DE')}`;
|
||||
try {
|
||||
const session: Session = await invoke('create_session', {
|
||||
title,
|
||||
workingDir: null,
|
||||
});
|
||||
activeSessionId = session.id;
|
||||
clearAll();
|
||||
newTitle = '';
|
||||
showNewForm = false;
|
||||
await loadSessions();
|
||||
} catch (err) {
|
||||
console.error('Fehler:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function resumeSession(id: string) {
|
||||
if (id === activeSessionId) return;
|
||||
try {
|
||||
const session: Session = await invoke('resume_session', { id });
|
||||
activeSessionId = session.id;
|
||||
clearAll();
|
||||
// TODO: Nachrichten aus Session-Historie laden
|
||||
} catch (err) {
|
||||
console.error('Fehler:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSession(id: string) {
|
||||
try {
|
||||
await invoke('delete_session', { id });
|
||||
if (activeSessionId === id) {
|
||||
activeSessionId = null;
|
||||
clearAll();
|
||||
}
|
||||
await loadSessions();
|
||||
} catch (err) {
|
||||
console.error('Fehler:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
const date = new Date(dateStr);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
|
||||
if (diff < 60000) return 'gerade eben';
|
||||
if (diff < 3600000) return `vor ${Math.floor(diff / 60000)} Min`;
|
||||
if (diff < 86400000) return `vor ${Math.floor(diff / 3600000)} Std`;
|
||||
if (diff < 172800000) return 'gestern';
|
||||
return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
|
||||
}
|
||||
|
||||
function formatCost(usd: number): string {
|
||||
if (usd === 0) return '';
|
||||
return `$${usd.toFixed(3)}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="session-list">
|
||||
<div class="session-header">
|
||||
<h2>💬 Sessions</h2>
|
||||
<button class="btn-new" on:click={() => showNewForm = !showNewForm}>
|
||||
{showNewForm ? '✕' : '+ Neu'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if showNewForm}
|
||||
<div class="new-form">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newTitle}
|
||||
placeholder="Session-Titel (optional)"
|
||||
on:keydown={(e) => e.key === 'Enter' && createSession()}
|
||||
/>
|
||||
<button class="btn-create" on:click={createSession}>Erstellen</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
<div class="loading">Lade Sessions...</div>
|
||||
{:else if sessions.length === 0}
|
||||
<div class="empty">
|
||||
<p>Keine Sessions vorhanden.</p>
|
||||
<button class="btn-first" on:click={() => { showNewForm = true; }}>
|
||||
Erste Session starten
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="sessions">
|
||||
{#each sessions as session}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="session-item"
|
||||
class:active={session.id === activeSessionId}
|
||||
on:click={() => resumeSession(session.id)}
|
||||
>
|
||||
<div class="session-main">
|
||||
<span class="session-status">
|
||||
{#if session.id === activeSessionId}
|
||||
🟢
|
||||
{:else if session.claude_session_id}
|
||||
⏸️
|
||||
{:else}
|
||||
⚪
|
||||
{/if}
|
||||
</span>
|
||||
<div class="session-info">
|
||||
<span class="session-title">{session.title}</span>
|
||||
<span class="session-meta">
|
||||
{session.message_count} Nachrichten
|
||||
{#if session.cost_usd > 0}
|
||||
· {formatCost(session.cost_usd)}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-right">
|
||||
<span class="session-time">{formatDate(session.updated_at)}</span>
|
||||
<button
|
||||
class="btn-delete"
|
||||
on:click|stopPropagation={() => deleteSession(session.id)}
|
||||
title="Session löschen"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.session-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--border);
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.session-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(--border);
|
||||
}
|
||||
|
||||
.session-header h2 {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-new {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-new:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* Neues Session Formular */
|
||||
.new-form {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-sm);
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.new-form input {
|
||||
flex: 1;
|
||||
font-size: 0.75rem;
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
}
|
||||
|
||||
.btn-create {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
background: var(--success);
|
||||
color: white;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.loading, .empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
padding: var(--spacing-md);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-first {
|
||||
margin-top: var(--spacing-sm);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Session-Items */
|
||||
.sessions {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.session-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--bg-tertiary);
|
||||
transition: background 0.15s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.session-item:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.session-item.active {
|
||||
background: var(--bg-tertiary);
|
||||
border-left: 3px solid var(--accent);
|
||||
}
|
||||
|
||||
.session-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.session-status {
|
||||
font-size: 0.6rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.session-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.session-title {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.session-meta {
|
||||
font-size: 0.65rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.session-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.session-time {
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
font-size: 0.65rem;
|
||||
opacity: 0;
|
||||
padding: 2px;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.session-item:hover .btn-delete {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import SessionList from '$lib/components/SessionList.svelte';
|
||||
import ChatPanel from '$lib/components/ChatPanel.svelte';
|
||||
import ActivityPanel from '$lib/components/ActivityPanel.svelte';
|
||||
import AgentView from '$lib/components/AgentView.svelte';
|
||||
|
|
@ -25,12 +26,17 @@
|
|||
</script>
|
||||
|
||||
<div class="panels">
|
||||
<!-- Linkes Panel: Chat -->
|
||||
<!-- Session-Sidebar (links) -->
|
||||
<aside class="panel panel-sessions">
|
||||
<SessionList />
|
||||
</aside>
|
||||
|
||||
<!-- Chat (Mitte-Links) -->
|
||||
<section class="panel panel-chat">
|
||||
<ChatPanel />
|
||||
</section>
|
||||
|
||||
<!-- Mittleres Panel: Aktivität / Memory / Audit -->
|
||||
<!-- Aktivität / Memory / Audit (Mitte-Rechts) -->
|
||||
<section class="panel panel-activity">
|
||||
<div class="panel-tabs">
|
||||
{#each middleTabs as tab}
|
||||
|
|
@ -54,7 +60,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Rechtes Panel: Agents / Guard-Rails -->
|
||||
<!-- Agents / Guard-Rails (rechts) -->
|
||||
<section class="panel panel-details">
|
||||
<div class="panel-tabs">
|
||||
{#each rightTabs as tab}
|
||||
|
|
@ -80,10 +86,10 @@
|
|||
<style>
|
||||
.panels {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-template-columns: 220px 1fr 1fr 1fr;
|
||||
gap: 1px;
|
||||
height: 100%;
|
||||
background: var(--bg-tertiary);
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.panel {
|
||||
|
|
@ -93,28 +99,32 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-sessions {
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.panel-tabs {
|
||||
display: flex;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--bg-tertiary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
font-size: 0.8rem;
|
||||
padding: var(--spacing-sm) var(--spacing-sm);
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-tertiary);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
|
|
@ -128,21 +138,30 @@
|
|||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1200px) {
|
||||
@media (max-width: 1400px) {
|
||||
.panels {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: 200px 1fr 1fr;
|
||||
}
|
||||
.panel-details {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
@media (max-width: 1000px) {
|
||||
.panels {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: 180px 1fr;
|
||||
}
|
||||
.panel-activity {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.panels {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.panel-sessions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue