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:
parent
4405979fa5
commit
56eb2f50cb
1 changed files with 349 additions and 3 deletions
|
|
@ -2,9 +2,12 @@
|
|||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { messages, currentInput, isProcessing, addMessage, currentSessionId, messageToDb, type Message } from '$lib/stores/app';
|
||||
import { marked, type Tokens } from 'marked';
|
||||
import { tick, onDestroy } from 'svelte';
|
||||
import { tick, onDestroy, onMount } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
// Input-Referenz für Focus-Shortcuts
|
||||
let inputTextarea: HTMLTextAreaElement;
|
||||
|
||||
// Custom Renderer für Code-Blöcke mit Wrapper
|
||||
const renderer = new marked.Renderer();
|
||||
renderer.code = function ({ text, lang }: Tokens.Code): string {
|
||||
|
|
@ -129,6 +132,31 @@
|
|||
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() {
|
||||
const text = $currentInput.trim();
|
||||
if (!text || $isProcessing) return;
|
||||
|
|
@ -159,6 +187,13 @@
|
|||
}
|
||||
|
||||
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) {
|
||||
event.preventDefault();
|
||||
sendMessage();
|
||||
|
|
@ -265,6 +300,74 @@
|
|||
}
|
||||
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>
|
||||
|
||||
<div class="chat-panel">
|
||||
|
|
@ -278,7 +381,7 @@
|
|||
<div class="empty-state">
|
||||
<div class="empty-icon">🤖</div>
|
||||
<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>
|
||||
{:else}
|
||||
{#each $messages as message, index}
|
||||
|
|
@ -300,6 +403,9 @@
|
|||
{#if message.role === 'assistant' && !$isProcessing && isLastAssistantMessage(index) && message.content}
|
||||
<button class="action-btn" onclick={() => regenerateResponse(index)} title="Antwort neu generieren">🔄</button>
|
||||
{/if}
|
||||
{#if message.content && message.role !== 'system'}
|
||||
<button class="action-btn" onclick={() => openRememberDialog(message)} title="Das merken">💡</button>
|
||||
{/if}
|
||||
<span class="message-time">
|
||||
{message.timestamp.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
|
||||
</span>
|
||||
|
|
@ -348,9 +454,10 @@
|
|||
|
||||
<div class="chat-input">
|
||||
<textarea
|
||||
bind:this={inputTextarea}
|
||||
bind:value={$currentInput}
|
||||
on:keydown={handleKeydown}
|
||||
placeholder="Nachricht eingeben..."
|
||||
placeholder="Nachricht eingeben... (Ctrl+K = Focus, Ctrl+Enter = Senden)"
|
||||
disabled={$isProcessing}
|
||||
rows="3"
|
||||
></textarea>
|
||||
|
|
@ -368,6 +475,83 @@
|
|||
</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>
|
||||
.chat-panel {
|
||||
display: flex;
|
||||
|
|
@ -772,4 +956,166 @@
|
|||
cursor: not-allowed;
|
||||
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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue