fix: Drawer bleibt offen + Auto-Scroll MutationObserver + KB-Hints Relevanz [appimage]
Some checks failed
Build AppImage / build (push) Has been cancelled

- Drawer: Backdrop entfernt, Panel bleibt fest offen bis X/Esc/Toggle
- Auto-Scroll: MutationObserver statt ResizeObserver (feuert bei jeder
  DOM-Aenderung), $effect scrollt nach tick() statt sofort
- KB-Hints: 60+ generische Stoppwoerter ergaenzt (bleibt, offen, rechts...),
  Relevanz-Schwelle 1.5 filtert zu generische Treffer raus

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-05-02 22:22:10 +02:00
parent c2f2cae2ef
commit f80f37884c
3 changed files with 50 additions and 50 deletions

View file

@ -193,6 +193,17 @@ const STOP_WORDS: &[&str] = &[
"schau", "guck", "check", "prüf", "teste", "versuch", "schau", "guck", "check", "prüf", "teste", "versuch",
"können", "müssen", "sollen", "wollen", "dürfen", "können", "müssen", "sollen", "wollen", "dürfen",
"noch", "schon", "gerade", "gleich", "erstmal", "nochmal", "noch", "schon", "gerade", "gleich", "erstmal", "nochmal",
// Generische Verben/Adjektive die keine guten Suchbegriffe sind
"bleibt", "bleiben", "blieb", "kommt", "kommen", "kam", "macht", "machen",
"steht", "stehen", "stand", "liegt", "liegen", "lag", "nimmt", "nehmen",
"sieht", "sehen", "sah", "findet", "finden", "fand", "brauche", "braucht",
"offen", "öffnet", "öffnen", "geöffnet", "geschlossen", "schließen", "schließt",
"rechts", "links", "oben", "unten", "vorne", "hinten",
"neue", "neuer", "neues", "neuem", "neuen", "alte", "alter", "altes",
"ganze", "ganzen", "ganzer", "ganzes", "selbe", "selben", "selber", "selbes",
"gleiche", "gleichen", "gleicher", "gleiches", "andere", "anderen", "anderer",
"kaputt", "richtig", "fertig", "bereit", "leer",
"wenn", "weil", "damit", "dass", "sobald",
]; ];
// ============ Konzept-Erkennung (wie Google/Facebook Textanalyse) ============ // ============ Konzept-Erkennung (wie Google/Facebook Textanalyse) ============
@ -585,10 +596,23 @@ async fn search_knowledge_filtered(search_query: &str, limit: usize, project: &O
return Ok(String::new()); return Ok(String::new());
} }
// Relevanz-Schwelle: Ergebnisse mit zu niedrigem Score rausfiltern.
// MySQL FULLTEXT NATURAL LANGUAGE MODE gibt Scores > 0 zurück,
// aber generische Einzel-Wort-Treffer haben oft Score < 2.
let min_relevance = 1.5;
let relevant: Vec<_> = results.into_iter()
.filter(|(_, _, _, _, _, _, relevance)| *relevance >= min_relevance)
.collect();
if relevant.is_empty() {
println!("🔍 Keine Hints über Relevanz-Schwelle ({:.1})", min_relevance);
return Ok(String::new());
}
// Bereits gezeigte IDs filtern // Bereits gezeigte IDs filtern
let filtered: Vec<_> = { let filtered: Vec<_> = {
let topic = SESSION_TOPIC.lock().unwrap(); let topic = SESSION_TOPIC.lock().unwrap();
results.into_iter() relevant.into_iter()
.filter(|(id, _, _, _, _, _, _)| !topic.shown_ids.contains(id)) .filter(|(id, _, _, _, _, _, _)| !topic.shown_ids.contains(id))
.take(limit) .take(limit)
.collect() .collect()

View file

@ -4,14 +4,14 @@
// Auto-Scroll nicht mehr zum Ende. Stattdessen erscheint ein // Auto-Scroll nicht mehr zum Ende. Stattdessen erscheint ein
// Back-to-Bottom-Button. // Back-to-Bottom-Button.
// //
// Phase 9.1: ResizeObserver am Container — feuert auch bei // Phase 9.2: MutationObserver statt ResizeObserver — feuert bei jeder
// Tool-Card-Slide-In, Diff-Aufklappen, Markdown-Code-Blocks. Tracker // DOM-Aenderung (neue Messages, WorkingIndicator, Tool-Cards, Markdown).
// liest jetzt zusaetzlich die Anzahl Tool-Calls. // $effect-Tracker scrollt nach tick() damit DOM schon gerendert ist.
import { messages, isProcessing, type Message as ChatMessage } from '$lib/stores'; import { messages, isProcessing, type Message as ChatMessage } from '$lib/stores';
import MessageItem from './Message.svelte'; import MessageItem from './Message.svelte';
import WorkingIndicator from './WorkingIndicator.svelte'; import WorkingIndicator from './WorkingIndicator.svelte';
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy, tick } from 'svelte';
interface Props { interface Props {
sessionId?: string | null; sessionId?: string | null;
@ -34,7 +34,7 @@
let userScrolledUp = $state(false); let userScrolledUp = $state(false);
let autoScrolling = false; // Guard: ignoriert onscroll waehrend wir selbst scrollen let autoScrolling = false; // Guard: ignoriert onscroll waehrend wir selbst scrollen
let autoScrollTimer: ReturnType<typeof setTimeout> | null = null; let autoScrollTimer: ReturnType<typeof setTimeout> | null = null;
let resizeObs: ResizeObserver | null = null; let mutationObs: MutationObserver | null = null;
// Working-Indicator: zeigen wenn Claude verarbeitet, aber noch keine // Working-Indicator: zeigen wenn Claude verarbeitet, aber noch keine
// Assistant-Tokens da sind (vor erstem Stream + waehrend Tool-Calls). // Assistant-Tokens da sind (vor erstem Stream + waehrend Tool-Calls).
@ -96,9 +96,9 @@
scrollToBottom(true); scrollToBottom(true);
} }
// Reactive-Tracker: deckt jetzt auch Tool-Calls ab. Sobald sich die // Reactive-Tracker: deckt Messages, Tool-Calls, Parts und Processing ab.
// Anzahl Tool-Calls in der letzten Message aendert (Slide-In, Status // Nach tick() scrollen, damit DOM-Aenderungen (WorkingIndicator, neue
// running→done), wird Auto-Scroll getriggert. // Messages) schon gerendert sind bevor scrollHeight gelesen wird.
$effect(() => { $effect(() => {
const last = $messages[$messages.length - 1]; const last = $messages[$messages.length - 1];
const _trackers = [ const _trackers = [
@ -106,7 +106,6 @@
$isProcessing, $isProcessing,
last?.content?.length ?? 0, last?.content?.length ?? 0,
last?.parts?.length ?? 0, last?.parts?.length ?? 0,
// Letzten Part tracken — bei Streaming waechst dessen content
(last?.parts && last.parts.length > 0) (last?.parts && last.parts.length > 0)
? last.parts[last.parts.length - 1]?.content?.length ?? 0 ? last.parts[last.parts.length - 1]?.content?.length ?? 0
: 0, : 0,
@ -114,7 +113,7 @@
last?.toolCalls?.map((t) => t.status).join(',') ?? '', last?.toolCalls?.map((t) => t.status).join(',') ?? '',
]; ];
void _trackers; void _trackers;
scrollToBottom(); tick().then(() => scrollToBottom());
}); });
// Wenn der User eine neue Message schreibt und die Antwort ankommt, soll // Wenn der User eine neue Message schreibt und die Antwort ankommt, soll
@ -146,35 +145,27 @@
}); });
onMount(() => { onMount(() => {
// ResizeObserver fuer den Container: feuert wenn sich die Hoehe // MutationObserver: feuert bei JEDER DOM-Aenderung im Container —
// aendert (Tool-Card aufklappen, Diff-Erweitern, Code-Block rendern). // neue Messages, WorkingIndicator ein/aus, Tool-Card-Expansion,
// Ohne das wuerde der Stream "abgehaengt" weil $effect nur bei // Markdown-Rendering, Diff-Aufklappen. Robuster als ResizeObserver
// Content-Length-Aenderung greift. // (Container-Groesse ist bei overflow:auto konstant, nur scrollHeight
if (container && typeof ResizeObserver !== 'undefined') { // waechst — ResizeObserver feuert da nicht).
resizeObs = new ResizeObserver(() => { if (container) {
mutationObs = new MutationObserver(() => {
if (!userScrolledUp) scrollToBottom(); if (!userScrolledUp) scrollToBottom();
}); });
// Den letzten Child beobachten, nicht den Container selbst — mutationObs.observe(container, {
// Container-Groesse ist konstant, sein Inhalt waechst. childList: true,
const inner = container.firstElementChild; subtree: true,
if (inner) { characterData: true,
// Alle direkten Kinder beobachten ist zu teuer. Ein Wrapper });
// reicht — der Container hat als Direct-Child die Message-
// Liste, und sein scrollHeight aendert sich passend.
// Wir beobachten den Container selbst — ResizeObserver
// feuert auch wenn der Inhalt waechst (clientHeight stays,
// scrollHeight grows → checkScroll triggert).
}
// Robusteste Variante: Container observed, plus Mutation-Observer
// fuer Content-Aenderungen.
resizeObs.observe(container);
} }
}); });
onDestroy(() => { onDestroy(() => {
if (resizeObs) { if (mutationObs) {
resizeObs.disconnect(); mutationObs.disconnect();
resizeObs = null; mutationObs = null;
} }
releaseAutoScroll(); releaseAutoScroll();
}); });

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
// Rechts-eingeschobenes Panel fuer Werkzeug-Tabs. // Rechts-eingeschobenes Panel fuer Werkzeug-Tabs.
// Esc schliesst, Klick auf Backdrop schliesst. // Bleibt fest offen bis explizit geschlossen (X-Button, Esc, erneuter Sidebar-Klick).
// //
// Nutzung: // Nutzung:
// <Drawer open={openDrawer === 'memory'} onClose={() => openDrawer = null}> // <Drawer open={openDrawer === 'memory'} onClose={() => openDrawer = null}>
@ -33,13 +33,6 @@
</script> </script>
{#if open} {#if open}
<div
class="backdrop"
role="presentation"
onclick={() => onClose?.()}
onkeydown={() => {}}
aria-hidden="true"
></div>
<aside class="drawer" style="width: {width}px" role="dialog" aria-label={title ?? 'Drawer'}> <aside class="drawer" style="width: {width}px" role="dialog" aria-label={title ?? 'Drawer'}>
{#if title} {#if title}
<header class="head"> <header class="head">
@ -54,14 +47,6 @@
{/if} {/if}
<style> <style>
.backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 80;
animation: fade-in var(--dur-fast) var(--ease);
}
.drawer { .drawer {
position: fixed; position: fixed;
top: 0; top: 0;