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)
// 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<Mutex<KbCache>> =
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<String>, // Alle Keywords der Session (gewichtet durch Reihenfolge)
shown_ids: Vec<i64>, // Bereits gezeigte KB-Einträge (nicht wiederholen!)
message_count: u32, // Anzahl Nachrichten seit Session-Start
last_project: Option<String>, // Erkanntes Projekt (z.B. "claude-desktop", "dolibarr")
keyword_freq: HashMap<String, u32>, // Keyword → Häufigkeit über die Session
shown_ids: Vec<i64>, // Bereits gezeigte KB-Einträge (nicht wiederholen!)
message_count: u32, // Anzahl Nachrichten seit Session-Start
last_project: Option<String>, // Erkanntes Projekt (z.B. "claude-desktop", "dolibarr")
}
static SESSION_TOPIC: std::sync::LazyLock<Mutex<SessionTopic>> =
@ -481,29 +482,52 @@ pub async fn search_knowledge_internal(query: &str, limit: i32) -> Result<String
println!("🔀 Projekt-Wechsel erkannt: {:?}{} — Reset Hints",
topic.last_project, proj);
topic.shown_ids.clear();
topic.keyword_freq.clear();
topic.message_count = 0;
topic.last_project = Some(proj.clone());
}
}
// 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();
// Keywords akkumulieren — Häufigkeit zählen.
// Je öfter ein Keyword in der Session vorkommt, desto relevanter
// ist es für die Suche. Generische Einmal-Keywords fallen weg,
// 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;
// Alle 2 Nachrichten frische Hints liefern (statt alle 3)
// Alle 2 Nachrichten frische Hints liefern
// Erste Nachricht (count=1) bekommt immer Hints
let skip = topic.message_count > 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<String> = 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::<Vec<_>>());
(query, skip)
};

View file

@ -691,6 +691,7 @@
const LONG_PRESS_MS = 450;
let micPressTimer: ReturnType<typeof setTimeout> | 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;