feat: Smart Hints v3 (TF-gewichtete Keywords) + Mic-Button Fix [appimage]
Some checks failed
Build AppImage / build (push) Failing after 2m30s

- 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 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-05-02 22:27:10 +02:00
parent f80f37884c
commit 113efaacc6
2 changed files with 44 additions and 16 deletions

View file

@ -1,7 +1,7 @@
// Claude Desktop — Wissensbasis (claude-db) // Claude Desktop — Wissensbasis (claude-db)
// Direkte MySQL-Anbindung zur zentralen Wissensdatenbank // Direkte MySQL-Anbindung zur zentralen Wissensdatenbank
// Phase 2.0: MySQL Pool als Managed State + Themen-Erkennung // 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 mysql_async::{Pool, prelude::*};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -72,13 +72,14 @@ impl KbCache {
static KB_CACHE: std::sync::LazyLock<Mutex<KbCache>> = static KB_CACHE: std::sync::LazyLock<Mutex<KbCache>> =
std::sync::LazyLock::new(|| Mutex::new(KbCache::new(15))); std::sync::LazyLock::new(|| Mutex::new(KbCache::new(15)));
// ============ Smart Hints v2: Session Topic Tracking ============ // ============ Smart Hints v3: TF-gewichtete Session-Keywords ============
// Akkumuliert Keywords über die Session hinweg, verhindert Wiederholungen, // Keywords werden über die Session akkumuliert mit Häufigkeitszähler.
// und liefert nur alle 3 Nachrichten frische Hints. // Je öfter ein Keyword vorkommt, desto höher sein Gewicht in der Suche.
// Generische Einmal-Keywords fallen weg, das echte Thema dominiert.
#[derive(Default)] #[derive(Default)]
struct SessionTopic { struct SessionTopic {
keywords: Vec<String>, // Alle Keywords der Session (gewichtet durch Reihenfolge) keyword_freq: HashMap<String, u32>, // Keyword → Häufigkeit über die Session
shown_ids: Vec<i64>, // Bereits gezeigte KB-Einträge (nicht wiederholen!) shown_ids: Vec<i64>, // Bereits gezeigte KB-Einträge (nicht wiederholen!)
message_count: u32, // Anzahl Nachrichten seit Session-Start message_count: u32, // Anzahl Nachrichten seit Session-Start
last_project: Option<String>, // Erkanntes Projekt (z.B. "claude-desktop", "dolibarr") last_project: Option<String>, // Erkanntes Projekt (z.B. "claude-desktop", "dolibarr")
@ -481,29 +482,52 @@ pub async fn search_knowledge_internal(query: &str, limit: i32) -> Result<String
println!("🔀 Projekt-Wechsel erkannt: {:?}{} — Reset Hints", println!("🔀 Projekt-Wechsel erkannt: {:?}{} — Reset Hints",
topic.last_project, proj); topic.last_project, proj);
topic.shown_ids.clear(); topic.shown_ids.clear();
topic.keyword_freq.clear();
topic.message_count = 0; topic.message_count = 0;
topic.last_project = Some(proj.clone()); topic.last_project = Some(proj.clone());
} }
} }
// Keywords der aktuellen Nachricht merken (fuer shown_ids Tracking) // Keywords akkumulieren — Häufigkeit zählen.
// NICHT akkumulieren — sonst wird die Query generisch und liefert immer // Je öfter ein Keyword in der Session vorkommt, desto relevanter
// die gleichen "Favoriten"-Eintraege statt thematisch passende Hints. // ist es für die Suche. Generische Einmal-Keywords fallen weg,
topic.keywords = new_keywords.clone(); // das echte Thema kristallisiert sich heraus.
for kw in &new_keywords {
*topic.keyword_freq.entry(kw.clone()).or_insert(0) += 1;
}
topic.message_count += 1; topic.message_count += 1;
// Alle 2 Nachrichten frische Hints liefern (statt alle 3) // Alle 2 Nachrichten frische Hints liefern
// Erste Nachricht (count=1) bekommt immer Hints // Erste Nachricht (count=1) bekommt immer Hints
let skip = topic.message_count > 1 && topic.message_count % 2 != 0; let skip = topic.message_count > 1 && topic.message_count % 2 != 0;
// Query aus aktuellen Keywords — direkt und thematisch // Query aus gewichteten Keywords bauen:
let query = if new_keywords.is_empty() { // 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<String> = scored.into_iter()
.take(8)
.map(|(kw, _)| kw)
.collect();
let query = if top_keywords.is_empty() {
safe_truncate(query, 100).to_string() safe_truncate(query, 100).to_string()
} else { } 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::<Vec<_>>());
(query, skip) (query, skip)
}; };

View file

@ -691,6 +691,7 @@
const LONG_PRESS_MS = 450; const LONG_PRESS_MS = 450;
let micPressTimer: ReturnType<typeof setTimeout> | null = null; let micPressTimer: ReturnType<typeof setTimeout> | null = null;
let micWasLongPress = false; let micWasLongPress = false;
let micPressed = false; // Guard: nur true nach pointerdown
function onMicPointerDown(_e: PointerEvent) { function onMicPointerDown(_e: PointerEvent) {
// Wenn Konversation laeuft → erster Klick beendet // Wenn Konversation laeuft → erster Klick beendet
@ -698,6 +699,7 @@
stopConversation(); stopConversation();
return; return;
} }
micPressed = true;
micWasLongPress = false; micWasLongPress = false;
micPressTimer = setTimeout(() => { micPressTimer = setTimeout(() => {
micWasLongPress = true; micWasLongPress = true;
@ -709,6 +711,8 @@
} }
function onMicPointerUp(_e: PointerEvent) { function onMicPointerUp(_e: PointerEvent) {
if (!micPressed) return; // Kein pointerdown vorher → ignorieren (Hover-Leave)
micPressed = false;
if (micPressTimer) { if (micPressTimer) {
clearTimeout(micPressTimer); clearTimeout(micPressTimer);
micPressTimer = null; micPressTimer = null;