- Neue messages-Tabelle in SQLite für Chat-Nachrichten - save_message, load_messages, clear_messages Tauri-Commands - User-Nachrichten werden beim Senden sofort gespeichert - Assistant-Nachrichten werden nach Abschluss gespeichert - Beim Session-Wechsel werden Nachrichten aus DB geladen - currentSessionId Store für Session-Tracking Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
363 lines
8 KiB
Svelte
363 lines
8 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
import { messages, clearAll, isProcessing, currentSessionId, setMessagesFromDb, type DbMessage } 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;
|
|
$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();
|
|
});
|
|
|
|
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;
|
|
$currentSessionId = 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;
|
|
$currentSessionId = session.id;
|
|
clearAll();
|
|
// Nachrichten aus DB laden
|
|
await loadSessionMessages(session.id);
|
|
} 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>
|