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)
|
// 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,16 +72,17 @@ 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
static SESSION_TOPIC: std::sync::LazyLock<Mutex<SessionTopic>> =
|
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",
|
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)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue