fix: Drawer bleibt offen + Auto-Scroll MutationObserver + KB-Hints Relevanz [appimage]
Some checks failed
Build AppImage / build (push) Has been cancelled
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:
parent
c2f2cae2ef
commit
f80f37884c
3 changed files with 50 additions and 50 deletions
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue