feat: Smart Hints v3 (TF-gewichtete Keywords) + Mic-Button Fix [appimage]
Some checks failed
Build AppImage / build (push) Failing after 2m30s
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:
parent
f80f37884c
commit
113efaacc6
2 changed files with 44 additions and 16 deletions
|
|
@ -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,13 +72,14 @@ 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)
|
||||
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")
|
||||
|
|
@ -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)
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue