diff --git a/src-tauri/src/knowledge.rs b/src-tauri/src/knowledge.rs index beeba5a..1ac9380 100644 --- a/src-tauri/src/knowledge.rs +++ b/src-tauri/src/knowledge.rs @@ -68,9 +68,9 @@ impl KbCache { } } -// Globaler Cache — 60 Sekunden TTL +// Globaler Cache — 15 Sekunden TTL (kurz genug damit thematische Wechsel ankommen) static KB_CACHE: std::sync::LazyLock> = - std::sync::LazyLock::new(|| Mutex::new(KbCache::new(60))); + std::sync::LazyLock::new(|| Mutex::new(KbCache::new(15))); // ============ Smart Hints v2: Session Topic Tracking ============ // Akkumuliert Keywords über die Session hinweg, verhindert Wiederholungen, @@ -314,29 +314,19 @@ pub async fn search_knowledge_internal(query: &str, limit: i32) -> Result 20 { - let start = topic.keywords.len() - 20; - topic.keywords = topic.keywords[start..].to_vec(); - } + // Keywords der aktuellen Nachricht merken (fuer shown_ids Tracking) + // NICHT akkumulieren — sonst wird die Query generisch und liefert immer + // die gleichen "Favoriten"-Eintraege statt thematisch passende Hints. + topic.keywords = new_keywords.clone(); topic.message_count += 1; - // Nur alle 3 Nachrichten frische Hints liefern (nicht jedes Mal!) + // Alle 2 Nachrichten frische Hints liefern (statt alle 3) // Erste Nachricht (count=1) bekommt immer Hints - let skip = topic.message_count > 1 && topic.message_count % 3 != 1; + let skip = topic.message_count > 1 && topic.message_count % 2 != 0; - // Session-Query bauen: letzte 6 Keywords für breiteren Kontext - let query = if topic.keywords.len() > 3 { - let start = topic.keywords.len().saturating_sub(6); - topic.keywords[start..].join(" ") - } else if new_keywords.is_empty() { + // Query aus aktuellen Keywords — direkt und thematisch + let query = if new_keywords.is_empty() { safe_truncate(query, 100).to_string() } else { new_keywords.join(" ") @@ -399,12 +389,13 @@ async fn search_knowledge_filtered(search_query: &str, limit: usize, project: &O WHERE status = 'active' AND MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE) ORDER BY - CASE WHEN tags LIKE ? THEN 1 ELSE 0 END DESC, - priority DESC, - usage_count DESC, - relevance DESC + CASE WHEN tags LIKE ? THEN 10 ELSE 0 END + + (MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE) * 100) + + (priority * 5) + + LEAST(usage_count, 50) + DESC LIMIT ?"#, - (search_query, search_query, &proj_pattern, fetch_limit), + (search_query, search_query, &proj_pattern, search_query, fetch_limit), ).await.map_err(|e| e.to_string())? } else { conn.exec( @@ -416,9 +407,13 @@ async fn search_knowledge_filtered(search_query: &str, limit: usize, project: &O FROM knowledge WHERE status = 'active' AND MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE) - ORDER BY priority DESC, usage_count DESC, relevance DESC + ORDER BY + (MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE) * 100) + + (priority * 5) + + LEAST(usage_count, 50) + DESC LIMIT ?"#, - (search_query, search_query, fetch_limit), + (search_query, search_query, search_query, fetch_limit), ).await.map_err(|e| e.to_string())? }; diff --git a/src/lib/components/ChatPanel.svelte b/src/lib/components/ChatPanel.svelte index 88788c4..c65a586 100644 --- a/src/lib/components/ChatPanel.svelte +++ b/src/lib/components/ChatPanel.svelte @@ -388,22 +388,17 @@ let silenceStartTime: number | null = null; let vadEnabled = $state(true); // VAD ein/aus - async function scrollToBottom() { - await tick(); - if (messagesContainer) { - messagesContainer.scrollTop = messagesContainer.scrollHeight; - } - } + // Scroll wird komplett von MessageList verwaltet (hat den echten scroll-Container). + // ChatPanel.scrollToBottom() war auf messagesContainer (Wrapper ohne overflow) und + // tat nichts — entfernt um Konflikte mit MessageList zu vermeiden. - $effect(() => { - if ($messages.length) scrollToBottom(); - }); - - // Bei Session-Wechsel: Compacting-Flag zurücksetzen + // Bei Session-Wechsel: Compacting-Flag + KB-Hints zurücksetzen $effect(() => { if ($currentSessionId) { compactingWarningShown = false; showCompactingDialog = false; + // KB-Hints Session-Topic zurücksetzen damit neue Hints kommen + invoke('reset_kb_session').catch(() => {}); } }); @@ -1199,6 +1194,7 @@
void; onRegenerate?: (id: string) => void; @@ -21,6 +22,7 @@ onRewind?: (id: string) => void; } let { + sessionId = null, streamingMessageId = null, onEdit, onRegenerate, @@ -82,10 +84,10 @@ autoScrollTimer = setTimeout(releaseAutoScroll, 700); } else { container.scrollTop = container.scrollHeight; - // Instant-Scroll: ein rAF reicht damit das onscroll-Event durch ist - requestAnimationFrame(() => { - requestAnimationFrame(releaseAutoScroll); - }); + // Instant-Scroll: Guard sofort nach einem rAF loesen. + // Zwei rAFs waren zu langsam bei schnellem Streaming — der naechste + // Token kam bevor der Guard aufgehoben war. + requestAnimationFrame(releaseAutoScroll); } } @@ -130,6 +132,16 @@ } }); + // Session-Wechsel: Scroll-State zuruecksetzen damit neue Session + // immer am Ende angezeigt wird (nicht vom alten userScrolledUp blockiert) + $effect(() => { + if (sessionId) { + userScrolledUp = false; + lastRole = null; + requestAnimationFrame(() => scrollToBottom(true)); + } + }); + onMount(() => { // ResizeObserver fuer den Container: feuert wenn sich die Hoehe // aendert (Tool-Card aufklappen, Diff-Erweitern, Code-Block rendern). diff --git a/src/lib/stores/app.ts b/src/lib/stores/app.ts index 805f24e..54961d3 100644 --- a/src/lib/stores/app.ts +++ b/src/lib/stores/app.ts @@ -435,9 +435,19 @@ export function dbToMessage(db: DbMessage): Message { }; } -// Nachrichten aus DB in Store laden +// Nachrichten aus DB in Store laden — mit Sortierung und Merge-Schutz export function setMessagesFromDb(dbMessages: DbMessage[]) { - messages.set(dbMessages.map(dbToMessage)); + const mapped = dbMessages.map(dbToMessage); + mapped.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); + messages.update(existing => { + // Wenn waehrend des Ladens schon neue Messages reinkamen (Streaming): mergen + const dbIds = new Set(mapped.map(m => m.id)); + const newOnly = existing.filter(m => !dbIds.has(m.id)); + if (newOnly.length === 0) return mapped; + return [...mapped, ...newOnly].sort((a, b) => + a.timestamp.getTime() - b.timestamp.getTime() + ); + }); } // ============ System-Monitor ============