From 113efaacc6e163ff2109d5de368b8c2d3464574c Mon Sep 17 00:00:00 2001 From: Eddy Date: Sat, 2 May 2026 22:27:10 +0200 Subject: [PATCH] feat: Smart Hints v3 (TF-gewichtete Keywords) + Mic-Button Fix [appimage] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - KB-Hints: Keywords werden über die Session akkumuliert mit Häufigkeitszähler. Top-8 gewichtete Keywords für die Suche — je mehr man zum Thema schreibt, desto präziser werden die Hints. Generische Einmal-Keywords fallen weg. - Mic-Button: micPressed-Guard verhindert dass pointerleave ohne vorherigen pointerdown die Aufnahme startet (Hover-Aktivierung Bug). Co-Authored-By: Claude Opus 4.6 --- src-tauri/src/knowledge.rs | 56 ++++++++++++++++++++--------- src/lib/components/ChatPanel.svelte | 4 +++ 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/src-tauri/src/knowledge.rs b/src-tauri/src/knowledge.rs index 0388191..6f69351 100644 --- a/src-tauri/src/knowledge.rs +++ b/src-tauri/src/knowledge.rs @@ -1,7 +1,7 @@ // Claude Desktop — Wissensbasis (claude-db) // Direkte MySQL-Anbindung zur zentralen Wissensdatenbank // Phase 2.0: MySQL Pool als Managed State + Themen-Erkennung -// Phase 3.1: Smart Hints v2 — Session-Context-Aware KB-Hints +// Phase 3.2: Smart Hints v3 — TF-gewichtete Session-Keywords für präzisere Hints use mysql_async::{Pool, prelude::*}; use serde::{Deserialize, Serialize}; @@ -72,16 +72,17 @@ impl KbCache { static KB_CACHE: std::sync::LazyLock> = std::sync::LazyLock::new(|| Mutex::new(KbCache::new(15))); -// ============ Smart Hints v2: Session Topic Tracking ============ -// Akkumuliert Keywords über die Session hinweg, verhindert Wiederholungen, -// und liefert nur alle 3 Nachrichten frische Hints. +// ============ Smart Hints v3: TF-gewichtete Session-Keywords ============ +// Keywords werden über die Session akkumuliert mit Häufigkeitszähler. +// Je öfter ein Keyword vorkommt, desto höher sein Gewicht in der Suche. +// Generische Einmal-Keywords fallen weg, das echte Thema dominiert. #[derive(Default)] struct SessionTopic { - keywords: Vec, // Alle Keywords der Session (gewichtet durch Reihenfolge) - shown_ids: Vec, // Bereits gezeigte KB-Einträge (nicht wiederholen!) - message_count: u32, // Anzahl Nachrichten seit Session-Start - last_project: Option, // Erkanntes Projekt (z.B. "claude-desktop", "dolibarr") + keyword_freq: HashMap, // Keyword → Häufigkeit über die Session + shown_ids: Vec, // Bereits gezeigte KB-Einträge (nicht wiederholen!) + message_count: u32, // Anzahl Nachrichten seit Session-Start + last_project: Option, // Erkanntes Projekt (z.B. "claude-desktop", "dolibarr") } static SESSION_TOPIC: std::sync::LazyLock> = @@ -481,29 +482,52 @@ pub async fn search_knowledge_internal(query: &str, limit: i32) -> Result 1 && topic.message_count % 2 != 0; - // Query aus aktuellen Keywords — direkt und thematisch - let query = if new_keywords.is_empty() { + // Query aus gewichteten Keywords bauen: + // Top-8 Keywords nach Häufigkeit, bei Gleichstand neuere bevorzugen + // (neue Keywords aus new_keywords bekommen +0.5 Bonus) + let mut scored: Vec<(String, f64)> = topic.keyword_freq.iter() + .map(|(kw, &freq)| { + let recency_bonus = if new_keywords.contains(kw) { 0.5 } else { 0.0 }; + (kw.clone(), freq as f64 + recency_bonus) + }) + .collect(); + scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); + + let top_keywords: Vec = scored.into_iter() + .take(8) + .map(|(kw, _)| kw) + .collect(); + + let query = if top_keywords.is_empty() { safe_truncate(query, 100).to_string() } else { - new_keywords.join(" ") + top_keywords.join(" ") }; + println!("📊 Session-Keywords ({} unique, msg #{}): {:?}", + topic.keyword_freq.len(), topic.message_count, + top_keywords.iter().take(5).cloned().collect::>()); + (query, skip) }; diff --git a/src/lib/components/ChatPanel.svelte b/src/lib/components/ChatPanel.svelte index c65a586..457bf3b 100644 --- a/src/lib/components/ChatPanel.svelte +++ b/src/lib/components/ChatPanel.svelte @@ -691,6 +691,7 @@ const LONG_PRESS_MS = 450; let micPressTimer: ReturnType | null = null; let micWasLongPress = false; + let micPressed = false; // Guard: nur true nach pointerdown function onMicPointerDown(_e: PointerEvent) { // Wenn Konversation laeuft → erster Klick beendet @@ -698,6 +699,7 @@ stopConversation(); return; } + micPressed = true; micWasLongPress = false; micPressTimer = setTimeout(() => { micWasLongPress = true; @@ -709,6 +711,8 @@ } function onMicPointerUp(_e: PointerEvent) { + if (!micPressed) return; // Kein pointerdown vorher → ignorieren (Hover-Leave) + micPressed = false; if (micPressTimer) { clearTimeout(micPressTimer); micPressTimer = null;