claude-desktop/src/lib/components/WorkingIndicator.svelte
Eddy fec8aea22c
All checks were successful
Build AppImage / build (push) Successful in 8m8s
feat: KB-Hints, Voice-Konversation, Chat-Darstellung, Cross-Session-Recall [appimage]
- Block A: KB-Hint-Pillen im Chat (💡) über Tool-Cards, Klick öffnet KB-Browser
- Block B: KB-Usage-Tracking (usage_count/last_used), Sortier-Boost für bewährte Einträge
- Block C: Cross-Session-Recall per SQLite-FTS5 (🕒 Pille "Schon mal beantwortet")
- Block D: Voice-Konversationsmodus (Langes Halten = Loop mit Barge-In-Unterbrechung)
- Block F: Select-Button im Audit-Log (appearance:none + SVG-Chevron, WebKitGTK-Fix)
- Block G: Chat-Darstellungseinstellungen (Schriftart, -größe, Zeilenhöhe, Code-Größe)
- WorkingIndicator: Deutsche Animationstexte beim Verarbeiten

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 16:54:58 +02:00

141 lines
3.3 KiB
Svelte

<script lang="ts">
// Animierter "Claude arbeitet"-Indikator unter der letzten Message.
// Rotiert ein zufaelliges deutsches Verb alle ~2.5s, daneben laeuft ein
// Braille-Spinner und ein Sekunden-Counter. Stil orientiert sich an
// Claude Code.
import { onMount, onDestroy } from 'svelte';
const VERBS = [
'Denke nach',
'Gruebele',
'Bruete',
'Sinniere',
'Tueftle',
'Werkle',
'Stoebere',
'Knirsche',
'Bastle',
'Mische',
'Brodelt',
'Kluegele',
'Wuehle',
'Krame',
'Spinne',
'Schmiede',
'Pruefe',
'Lese',
'Verdaue',
'Sortiere',
'Sammle',
'Suche',
'Forsche',
'Faedele ein',
'Knete',
'Falte die Stirn',
'Klopfe ab',
'Lege Hand an',
];
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
let verb = $state(pickVerb());
let spinnerIdx = $state(0);
let elapsed = $state(0);
let verbTimer: ReturnType<typeof setInterval> | null = null;
let spinTimer: ReturnType<typeof setInterval> | null = null;
let secondsTimer: ReturnType<typeof setInterval> | null = null;
let lastVerb = '';
function pickVerb(): string {
// Nicht zweimal hintereinander dasselbe Wort
let next = VERBS[Math.floor(Math.random() * VERBS.length)];
if (next === lastVerb && VERBS.length > 1) {
next = VERBS[(VERBS.indexOf(next) + 1) % VERBS.length];
}
lastVerb = next;
return next;
}
onMount(() => {
const start = Date.now();
verbTimer = setInterval(() => { verb = pickVerb(); }, 2500);
spinTimer = setInterval(() => {
spinnerIdx = (spinnerIdx + 1) % SPINNER_FRAMES.length;
}, 90);
secondsTimer = setInterval(() => {
elapsed = Math.floor((Date.now() - start) / 1000);
}, 1000);
});
onDestroy(() => {
if (verbTimer) clearInterval(verbTimer);
if (spinTimer) clearInterval(spinTimer);
if (secondsTimer) clearInterval(secondsTimer);
});
</script>
<div class="working" role="status" aria-live="polite">
<span class="spinner">{SPINNER_FRAMES[spinnerIdx]}</span>
<span class="verb">{verb}</span>
<span class="dots"><span>.</span><span>.</span><span>.</span></span>
{#if elapsed > 1}
<span class="elapsed">({elapsed}s)</span>
{/if}
</div>
<style>
.working {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px 14px 52px;
font-size: 13px;
color: var(--vscode-descriptionForeground, #888);
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
opacity: 0;
animation: fade-in 200ms ease-out forwards;
}
.spinner {
color: var(--accent, var(--vscode-button-background, #007acc));
font-size: 15px;
line-height: 1;
display: inline-block;
min-width: 14px;
text-align: center;
}
.verb {
color: var(--text-primary, var(--vscode-foreground, #ddd));
font-style: italic;
min-width: 100px;
transition: opacity 200ms ease;
}
.dots {
display: inline-flex;
gap: 2px;
color: var(--text-primary, var(--vscode-foreground, #ddd));
}
.dots span {
animation: dot-pulse 1.4s ease-in-out infinite;
}
.dots span:nth-child(2) { animation-delay: 200ms; }
.dots span:nth-child(3) { animation-delay: 400ms; }
.elapsed {
color: var(--text-muted, #888);
font-size: 11px;
margin-left: 4px;
}
@keyframes fade-in {
to { opacity: 1; }
}
@keyframes dot-pulse {
0%, 60%, 100% { opacity: 0.25; transform: translateY(0); }
30% { opacity: 1; transform: translateY(-2px); }
}
</style>