Keyboard Shortcuts + "Das merken" Button

- Ctrl+K: Focus auf Chat-Input
- Ctrl+Shift+K: Input leeren + Focus
- Ctrl+Enter: Nachricht senden (auch mehrzeilig)
- "Das merken" Button (💡) bei jeder Nachricht
- Modal-Dialog zum Speichern in Wissensbasis
- Kategorie, Priorität, Tags auswählbar

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-14 13:59:23 +02:00
parent 4405979fa5
commit 56eb2f50cb

View file

@ -2,9 +2,12 @@
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, type Message } from '$lib/stores/app'; import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, type Message } from '$lib/stores/app';
import { marked, type Tokens } from 'marked'; import { marked, type Tokens } from 'marked';
import { tick, onDestroy } from 'svelte'; import { tick, onDestroy, onMount } from 'svelte';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
// Input-Referenz für Focus-Shortcuts
let inputTextarea: HTMLTextAreaElement;
// Custom Renderer für Code-Blöcke mit Wrapper // Custom Renderer für Code-Blöcke mit Wrapper
const renderer = new marked.Renderer(); const renderer = new marked.Renderer();
renderer.code = function ({ text, lang }: Tokens.Code): string { renderer.code = function ({ text, lang }: Tokens.Code): string {
@ -129,6 +132,31 @@
unsubscribe(); unsubscribe();
}); });
// Globale Keyboard Shortcuts
function handleGlobalKeydown(event: KeyboardEvent) {
// Ctrl+K: Focus auf Input
if (event.ctrlKey && event.key === 'k') {
event.preventDefault();
inputTextarea?.focus();
return;
}
// Ctrl+Shift+K: Input leeren und Focus
if (event.ctrlKey && event.shiftKey && event.key === 'K') {
event.preventDefault();
$currentInput = '';
inputTextarea?.focus();
return;
}
}
onMount(() => {
window.addEventListener('keydown', handleGlobalKeydown);
return () => {
window.removeEventListener('keydown', handleGlobalKeydown);
};
});
async function sendMessage() { async function sendMessage() {
const text = $currentInput.trim(); const text = $currentInput.trim();
if (!text || $isProcessing) return; if (!text || $isProcessing) return;
@ -159,6 +187,13 @@
} }
function handleKeydown(event: KeyboardEvent) { function handleKeydown(event: KeyboardEvent) {
// Ctrl+Enter: Immer senden (auch bei mehrzeiligem Text)
if (event.key === 'Enter' && event.ctrlKey) {
event.preventDefault();
sendMessage();
return;
}
// Enter (ohne Shift): Senden
if (event.key === 'Enter' && !event.shiftKey) { if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault(); event.preventDefault();
sendMessage(); sendMessage();
@ -265,6 +300,74 @@
} }
return false; return false;
} }
// "Das merken" - Speichern in Wissensbasis
let rememberDialogOpen = $state(false);
let rememberContent = $state('');
let rememberEntry = $state({
category: 'pattern',
title: '',
tags: '',
priority: 2,
});
let rememberSaving = $state(false);
let rememberError = $state('');
const knowledgeCategories = [
{ id: 'dolibarr', label: 'Dolibarr' },
{ id: 'fints', label: 'FinTS/Banking' },
{ id: 'pattern', label: 'Pattern/Code' },
{ id: 'projekt', label: 'Projekt' },
{ id: 'setup', label: 'Setup/Config' },
{ id: 'ids', label: 'IDs/Referenzen' },
];
function openRememberDialog(message: Message) {
rememberContent = message.content;
rememberEntry = {
category: 'pattern',
title: '',
tags: '',
priority: 2,
};
rememberError = '';
rememberDialogOpen = true;
}
function closeRememberDialog() {
rememberDialogOpen = false;
rememberContent = '';
}
async function saveToKnowledge() {
if (!rememberEntry.title.trim() || !rememberContent.trim()) {
rememberError = 'Titel und Inhalt sind erforderlich';
return;
}
rememberSaving = true;
rememberError = '';
try {
await invoke('save_knowledge', {
entry: {
category: rememberEntry.category,
title: rememberEntry.title.trim(),
content: rememberContent.trim(),
tags: rememberEntry.tags.trim() || null,
priority: rememberEntry.priority,
status: 'active',
source: 'chat',
related_ids: null,
}
});
closeRememberDialog();
} catch (err) {
rememberError = `Fehler: ${err}`;
} finally {
rememberSaving = false;
}
}
</script> </script>
<div class="chat-panel"> <div class="chat-panel">
@ -278,7 +381,7 @@
<div class="empty-state"> <div class="empty-state">
<div class="empty-icon">🤖</div> <div class="empty-icon">🤖</div>
<p>Starte eine Konversation mit Claude.</p> <p>Starte eine Konversation mit Claude.</p>
<p class="hint">Enter = Senden, Shift+Enter = Neue Zeile, Escape = Stopp</p> <p class="hint">Enter/Ctrl+Enter = Senden, Shift+Enter = Neue Zeile, Ctrl+K = Focus, Escape = Stopp</p>
</div> </div>
{:else} {:else}
{#each $messages as message, index} {#each $messages as message, index}
@ -300,6 +403,9 @@
{#if message.role === 'assistant' && !$isProcessing && isLastAssistantMessage(index) && message.content} {#if message.role === 'assistant' && !$isProcessing && isLastAssistantMessage(index) && message.content}
<button class="action-btn" onclick={() => regenerateResponse(index)} title="Antwort neu generieren">🔄</button> <button class="action-btn" onclick={() => regenerateResponse(index)} title="Antwort neu generieren">🔄</button>
{/if} {/if}
{#if message.content && message.role !== 'system'}
<button class="action-btn" onclick={() => openRememberDialog(message)} title="Das merken">💡</button>
{/if}
<span class="message-time"> <span class="message-time">
{message.timestamp.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })} {message.timestamp.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
</span> </span>
@ -348,9 +454,10 @@
<div class="chat-input"> <div class="chat-input">
<textarea <textarea
bind:this={inputTextarea}
bind:value={$currentInput} bind:value={$currentInput}
on:keydown={handleKeydown} on:keydown={handleKeydown}
placeholder="Nachricht eingeben..." placeholder="Nachricht eingeben... (Ctrl+K = Focus, Ctrl+Enter = Senden)"
disabled={$isProcessing} disabled={$isProcessing}
rows="3" rows="3"
></textarea> ></textarea>
@ -368,6 +475,83 @@
</div> </div>
</div> </div>
<!-- "Das merken" Dialog -->
{#if rememberDialogOpen}
<div class="modal-backdrop" onclick={closeRememberDialog}>
<div class="modal-dialog" onclick={(e) => e.stopPropagation()}>
<div class="modal-header">
<h3>💡 Das merken</h3>
<button class="close-btn" onclick={closeRememberDialog}>✕</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="rem-title">Titel *</label>
<input
type="text"
id="rem-title"
bind:value={rememberEntry.title}
placeholder="Kurze Beschreibung der Erkenntnis"
/>
</div>
<div class="form-row">
<div class="form-group">
<label for="rem-category">Kategorie</label>
<select id="rem-category" bind:value={rememberEntry.category}>
{#each knowledgeCategories as cat}
<option value={cat.id}>{cat.label}</option>
{/each}
</select>
</div>
<div class="form-group">
<label for="rem-priority">Priorität</label>
<select id="rem-priority" bind:value={rememberEntry.priority}>
<option value={1}>1 - Kritisch</option>
<option value={2}>2 - Wichtig</option>
<option value={3}>3 - Normal</option>
<option value={4}>4 - Niedrig</option>
</select>
</div>
</div>
<div class="form-group">
<label for="rem-tags">Tags (kommagetrennt)</label>
<input
type="text"
id="rem-tags"
bind:value={rememberEntry.tags}
placeholder="z.B. rust, tauri, async"
/>
</div>
<div class="form-group">
<label for="rem-content">Inhalt *</label>
<textarea
id="rem-content"
bind:value={rememberContent}
rows="8"
placeholder="Der zu merkende Inhalt..."
></textarea>
</div>
{#if rememberError}
<div class="form-error">{rememberError}</div>
{/if}
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick={closeRememberDialog}>Abbrechen</button>
<button
class="btn-primary"
onclick={saveToKnowledge}
disabled={rememberSaving || !rememberEntry.title.trim()}
>
{rememberSaving ? 'Speichern...' : 'In Wissensbasis speichern'}
</button>
</div>
</div>
</div>
{/if}
<style> <style>
.chat-panel { .chat-panel {
display: flex; display: flex;
@ -772,4 +956,166 @@
cursor: not-allowed; cursor: not-allowed;
transform: none; transform: none;
} }
/* Modal Styles */
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-dialog {
background: var(--bg-primary);
border-radius: var(--radius-md);
width: 90%;
max-width: 500px;
max-height: 85vh;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-md);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
}
.modal-header h3 {
margin: 0;
font-size: 0.9rem;
}
.close-btn {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
color: var(--text-secondary);
font-size: 1rem;
}
.close-btn:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.modal-body {
padding: var(--spacing-md);
overflow-y: auto;
flex: 1;
}
.form-group {
margin-bottom: var(--spacing-sm);
}
.form-group label {
display: block;
font-size: 0.75rem;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: 0.25rem;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 0.5rem;
font-size: 0.85rem;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-primary);
}
.form-group textarea {
resize: vertical;
font-family: var(--font-mono);
font-size: 0.75rem;
line-height: 1.5;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--accent);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-sm);
}
.form-error {
padding: 0.5rem;
background: rgba(239, 68, 68, 0.1);
border: 1px solid var(--error);
border-radius: var(--radius-sm);
color: var(--error);
font-size: 0.75rem;
margin-top: var(--spacing-sm);
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-sm);
padding: var(--spacing-md);
background: var(--bg-secondary);
border-top: 1px solid var(--border);
}
.btn-secondary {
padding: 0.5rem 1rem;
font-size: 0.8rem;
background: transparent;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-secondary);
cursor: pointer;
}
.btn-secondary:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.btn-primary {
padding: 0.5rem 1rem;
font-size: 0.8rem;
background: var(--accent);
border: 1px solid var(--accent);
border-radius: var(--radius-sm);
color: white;
cursor: pointer;
}
.btn-primary:hover:not(:disabled) {
background: var(--accent-hover);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style> </style>