fix: Chat-Chronologie + Auto-Scroll + KB-Hints — 8 Bugs behoben [appimage]
Some checks failed
Build AppImage / build (push) Has been cancelled
Some checks failed
Build AppImage / build (push) Has been cancelled
- Messages werden nach Timestamp sortiert + Race-Condition-sicherer Merge beim DB-Load - ChatPanel.scrollToBottom() auf falschem Container entfernt, Scroll nur noch in MessageList - userScrolledUp wird bei Session-Wechsel resettet (blockierte Auto-Scroll) - Instant-Scroll Guard schneller freigegeben (1 statt 2 rAFs bei Streaming) - KB-Hints: Session-Reset bei Chat-Wechsel, Relevance statt Priority im Ranking - KB-Keywords nicht mehr akkumuliert (verhindert generische Dauertreffer) - Cache-TTL 60s→15s, Hints alle 2 statt 3 Nachrichten Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8d52505d14
commit
09a9513983
4 changed files with 57 additions and 44 deletions
|
|
@ -68,9 +68,9 @@ impl KbCache {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Globaler Cache — 60 Sekunden TTL
|
// Globaler Cache — 15 Sekunden TTL (kurz genug damit thematische Wechsel ankommen)
|
||||||
static KB_CACHE: std::sync::LazyLock<Mutex<KbCache>> =
|
static KB_CACHE: std::sync::LazyLock<Mutex<KbCache>> =
|
||||||
std::sync::LazyLock::new(|| Mutex::new(KbCache::new(60)));
|
std::sync::LazyLock::new(|| Mutex::new(KbCache::new(15)));
|
||||||
|
|
||||||
// ============ Smart Hints v2: Session Topic Tracking ============
|
// ============ Smart Hints v2: Session Topic Tracking ============
|
||||||
// Akkumuliert Keywords über die Session hinweg, verhindert Wiederholungen,
|
// Akkumuliert Keywords über die Session hinweg, verhindert Wiederholungen,
|
||||||
|
|
@ -314,29 +314,19 @@ pub async fn search_knowledge_internal(query: &str, limit: i32) -> Result<String
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keywords zur Session hinzufügen (dedupliziert)
|
// Keywords der aktuellen Nachricht merken (fuer shown_ids Tracking)
|
||||||
for kw in &new_keywords {
|
// NICHT akkumulieren — sonst wird die Query generisch und liefert immer
|
||||||
if !topic.keywords.contains(kw) {
|
// die gleichen "Favoriten"-Eintraege statt thematisch passende Hints.
|
||||||
topic.keywords.push(kw.clone());
|
topic.keywords = new_keywords.clone();
|
||||||
}
|
|
||||||
}
|
|
||||||
// Max 20 Keywords behalten (neueste bevorzugt)
|
|
||||||
if topic.keywords.len() > 20 {
|
|
||||||
let start = topic.keywords.len() - 20;
|
|
||||||
topic.keywords = topic.keywords[start..].to_vec();
|
|
||||||
}
|
|
||||||
|
|
||||||
topic.message_count += 1;
|
topic.message_count += 1;
|
||||||
|
|
||||||
// Nur alle 3 Nachrichten frische Hints liefern (nicht jedes Mal!)
|
// Alle 2 Nachrichten frische Hints liefern (statt alle 3)
|
||||||
// Erste Nachricht (count=1) bekommt immer Hints
|
// Erste Nachricht (count=1) bekommt immer Hints
|
||||||
let skip = topic.message_count > 1 && topic.message_count % 3 != 1;
|
let skip = topic.message_count > 1 && topic.message_count % 2 != 0;
|
||||||
|
|
||||||
// Session-Query bauen: letzte 6 Keywords für breiteren Kontext
|
// Query aus aktuellen Keywords — direkt und thematisch
|
||||||
let query = if topic.keywords.len() > 3 {
|
let query = if new_keywords.is_empty() {
|
||||||
let start = topic.keywords.len().saturating_sub(6);
|
|
||||||
topic.keywords[start..].join(" ")
|
|
||||||
} else if new_keywords.is_empty() {
|
|
||||||
safe_truncate(query, 100).to_string()
|
safe_truncate(query, 100).to_string()
|
||||||
} else {
|
} else {
|
||||||
new_keywords.join(" ")
|
new_keywords.join(" ")
|
||||||
|
|
@ -399,12 +389,13 @@ async fn search_knowledge_filtered(search_query: &str, limit: usize, project: &O
|
||||||
WHERE status = 'active'
|
WHERE status = 'active'
|
||||||
AND MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE)
|
AND MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE)
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE WHEN tags LIKE ? THEN 1 ELSE 0 END DESC,
|
CASE WHEN tags LIKE ? THEN 10 ELSE 0 END +
|
||||||
priority DESC,
|
(MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE) * 100) +
|
||||||
usage_count DESC,
|
(priority * 5) +
|
||||||
relevance DESC
|
LEAST(usage_count, 50)
|
||||||
|
DESC
|
||||||
LIMIT ?"#,
|
LIMIT ?"#,
|
||||||
(search_query, search_query, &proj_pattern, fetch_limit),
|
(search_query, search_query, &proj_pattern, search_query, fetch_limit),
|
||||||
).await.map_err(|e| e.to_string())?
|
).await.map_err(|e| e.to_string())?
|
||||||
} else {
|
} else {
|
||||||
conn.exec(
|
conn.exec(
|
||||||
|
|
@ -416,9 +407,13 @@ async fn search_knowledge_filtered(search_query: &str, limit: usize, project: &O
|
||||||
FROM knowledge
|
FROM knowledge
|
||||||
WHERE status = 'active'
|
WHERE status = 'active'
|
||||||
AND MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE)
|
AND MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE)
|
||||||
ORDER BY priority DESC, usage_count DESC, relevance DESC
|
ORDER BY
|
||||||
|
(MATCH(title, content, tags) AGAINST(? IN NATURAL LANGUAGE MODE) * 100) +
|
||||||
|
(priority * 5) +
|
||||||
|
LEAST(usage_count, 50)
|
||||||
|
DESC
|
||||||
LIMIT ?"#,
|
LIMIT ?"#,
|
||||||
(search_query, search_query, fetch_limit),
|
(search_query, search_query, search_query, fetch_limit),
|
||||||
).await.map_err(|e| e.to_string())?
|
).await.map_err(|e| e.to_string())?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -388,22 +388,17 @@
|
||||||
let silenceStartTime: number | null = null;
|
let silenceStartTime: number | null = null;
|
||||||
let vadEnabled = $state(true); // VAD ein/aus
|
let vadEnabled = $state(true); // VAD ein/aus
|
||||||
|
|
||||||
async function scrollToBottom() {
|
// Scroll wird komplett von MessageList verwaltet (hat den echten scroll-Container).
|
||||||
await tick();
|
// ChatPanel.scrollToBottom() war auf messagesContainer (Wrapper ohne overflow) und
|
||||||
if (messagesContainer) {
|
// tat nichts — entfernt um Konflikte mit MessageList zu vermeiden.
|
||||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$effect(() => {
|
// Bei Session-Wechsel: Compacting-Flag + KB-Hints zurücksetzen
|
||||||
if ($messages.length) scrollToBottom();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bei Session-Wechsel: Compacting-Flag zurücksetzen
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if ($currentSessionId) {
|
if ($currentSessionId) {
|
||||||
compactingWarningShown = false;
|
compactingWarningShown = false;
|
||||||
showCompactingDialog = false;
|
showCompactingDialog = false;
|
||||||
|
// KB-Hints Session-Topic zurücksetzen damit neue Hints kommen
|
||||||
|
invoke('reset_kb_session').catch(() => {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1199,6 +1194,7 @@
|
||||||
|
|
||||||
<div class="chat-messages-wrap" bind:this={messagesContainer}>
|
<div class="chat-messages-wrap" bind:this={messagesContainer}>
|
||||||
<MessageList
|
<MessageList
|
||||||
|
sessionId={$currentSessionId}
|
||||||
{streamingMessageId}
|
{streamingMessageId}
|
||||||
onEdit={handleEditById}
|
onEdit={handleEditById}
|
||||||
onRegenerate={handleRegenerateById}
|
onRegenerate={handleRegenerateById}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
sessionId?: string | null;
|
||||||
streamingMessageId?: string | null;
|
streamingMessageId?: string | null;
|
||||||
onEdit?: (id: string) => void;
|
onEdit?: (id: string) => void;
|
||||||
onRegenerate?: (id: string) => void;
|
onRegenerate?: (id: string) => void;
|
||||||
|
|
@ -21,6 +22,7 @@
|
||||||
onRewind?: (id: string) => void;
|
onRewind?: (id: string) => void;
|
||||||
}
|
}
|
||||||
let {
|
let {
|
||||||
|
sessionId = null,
|
||||||
streamingMessageId = null,
|
streamingMessageId = null,
|
||||||
onEdit,
|
onEdit,
|
||||||
onRegenerate,
|
onRegenerate,
|
||||||
|
|
@ -82,10 +84,10 @@
|
||||||
autoScrollTimer = setTimeout(releaseAutoScroll, 700);
|
autoScrollTimer = setTimeout(releaseAutoScroll, 700);
|
||||||
} else {
|
} else {
|
||||||
container.scrollTop = container.scrollHeight;
|
container.scrollTop = container.scrollHeight;
|
||||||
// Instant-Scroll: ein rAF reicht damit das onscroll-Event durch ist
|
// Instant-Scroll: Guard sofort nach einem rAF loesen.
|
||||||
requestAnimationFrame(() => {
|
// Zwei rAFs waren zu langsam bei schnellem Streaming — der naechste
|
||||||
|
// Token kam bevor der Guard aufgehoben war.
|
||||||
requestAnimationFrame(releaseAutoScroll);
|
requestAnimationFrame(releaseAutoScroll);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,6 +132,16 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Session-Wechsel: Scroll-State zuruecksetzen damit neue Session
|
||||||
|
// immer am Ende angezeigt wird (nicht vom alten userScrolledUp blockiert)
|
||||||
|
$effect(() => {
|
||||||
|
if (sessionId) {
|
||||||
|
userScrolledUp = false;
|
||||||
|
lastRole = null;
|
||||||
|
requestAnimationFrame(() => scrollToBottom(true));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// ResizeObserver fuer den Container: feuert wenn sich die Hoehe
|
// ResizeObserver fuer den Container: feuert wenn sich die Hoehe
|
||||||
// aendert (Tool-Card aufklappen, Diff-Erweitern, Code-Block rendern).
|
// aendert (Tool-Card aufklappen, Diff-Erweitern, Code-Block rendern).
|
||||||
|
|
|
||||||
|
|
@ -435,9 +435,19 @@ export function dbToMessage(db: DbMessage): Message {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nachrichten aus DB in Store laden
|
// Nachrichten aus DB in Store laden — mit Sortierung und Merge-Schutz
|
||||||
export function setMessagesFromDb(dbMessages: DbMessage[]) {
|
export function setMessagesFromDb(dbMessages: DbMessage[]) {
|
||||||
messages.set(dbMessages.map(dbToMessage));
|
const mapped = dbMessages.map(dbToMessage);
|
||||||
|
mapped.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
||||||
|
messages.update(existing => {
|
||||||
|
// Wenn waehrend des Ladens schon neue Messages reinkamen (Streaming): mergen
|
||||||
|
const dbIds = new Set(mapped.map(m => m.id));
|
||||||
|
const newOnly = existing.filter(m => !dbIds.has(m.id));
|
||||||
|
if (newOnly.length === 0) return mapped;
|
||||||
|
return [...mapped, ...newOnly].sort((a, b) =>
|
||||||
|
a.timestamp.getTime() - b.timestamp.getTime()
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ System-Monitor ============
|
// ============ System-Monitor ============
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue