From 1f91b902da2f725a8a01ff18a1521d9a7cc99ae3 Mon Sep 17 00:00:00 2001 From: ekko <152005280+EKKOLearnAI@users.noreply.github.com> Date: Thu, 23 Apr 2026 11:18:56 +0800 Subject: [PATCH] fix: handle special char search 500 & polish live badge (#144) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(search): handle numeric query FTS errors regardless of table existence Remove the `no such table: messages_fts` condition so numeric queries fall back to LIKE search on any FTS failure (malformed MATCH, missing table, etc.). Co-Authored-By: Claude Opus 4.6 * fix(search): handle special char queries, polish live badge UI - Add hasUnsafeChars() to catch FTS5-breaking queries (¥, @, #, etc.) and fall back to LIKE search, preventing 500 errors - Polish session live badge: smaller size, remove border/shadow, add pulsing dot indicator for a cleaner look - Remove spinner drop-shadow glow effect Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .../src/components/hermes/chat/ChatPanel.vue | 31 ++++++++++++------- .../hermes/chat/SessionListItem.vue | 5 ++- packages/server/src/db/hermes/sessions-db.ts | 6 +++- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/packages/client/src/components/hermes/chat/ChatPanel.vue b/packages/client/src/components/hermes/chat/ChatPanel.vue index d7b4bbf..0c8386b 100644 --- a/packages/client/src/components/hermes/chat/ChatPanel.vue +++ b/packages/client/src/components/hermes/chat/ChatPanel.vue @@ -623,26 +623,35 @@ async function handleRenameConfirm() { :deep(.session-item-active-spinner) { animation: session-spin 1.1s linear infinite; - filter: drop-shadow(0 0 6px rgba(var(--accent-primary-rgb), 0.35)); } :deep(.session-item-live-badge) { display: inline-flex; align-items: center; - justify-content: center; + gap: 4px; flex-shrink: 0; - padding: 0 8px; - min-height: 20px; + padding: 1px 7px; border-radius: 999px; - font-size: 11px; - line-height: 20px; - font-weight: 700; - letter-spacing: 0.05em; + font-size: 10px; + line-height: 16px; + font-weight: 600; + letter-spacing: 0.04em; text-transform: uppercase; color: $accent-primary; - background: rgba(var(--accent-primary-rgb), 0.18); - border: 1px solid rgba(var(--accent-primary-rgb), 0.34); - box-shadow: 0 0 0 1px rgba(var(--accent-primary-rgb), 0.06), 0 0 10px rgba(var(--accent-primary-rgb), 0.14); + background: rgba(var(--accent-primary-rgb), 0.10); +} + +:deep(.live-dot) { + width: 6px; + height: 6px; + border-radius: 50%; + background: $accent-primary; + animation: live-pulse 2s ease-in-out infinite; +} + +@keyframes live-pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.4; transform: scale(0.7); } } :deep(.session-item-pin) { diff --git a/packages/client/src/components/hermes/chat/SessionListItem.vue b/packages/client/src/components/hermes/chat/SessionListItem.vue index 4fbf0c1..ab8515e 100644 --- a/packages/client/src/components/hermes/chat/SessionListItem.vue +++ b/packages/client/src/components/hermes/chat/SessionListItem.vue @@ -45,7 +45,10 @@ const { t } = useI18n() {{ session.title }} - {{ t('chat.liveMode') }} + + + {{ t('chat.liveMode') }} + {{ session.model }} diff --git a/packages/server/src/db/hermes/sessions-db.ts b/packages/server/src/db/hermes/sessions-db.ts index dba73e7..203e564 100644 --- a/packages/server/src/db/hermes/sessions-db.ts +++ b/packages/server/src/db/hermes/sessions-db.ts @@ -163,6 +163,10 @@ function isNumericQuery(text: string): boolean { return /^\d+(?:\s+\d+)*$/.test(text.trim()) } +function hasUnsafeChars(text: string): boolean { + return /[^\w\s\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(text) +} + function runLikeContentSearch( db: { prepare: (sql: string) => { all: (...params: any[]) => Record[] } }, source: string | undefined, @@ -358,7 +362,7 @@ export async function searchSessionSummaries( return [...merged.values()].slice(0, limit) } - if (message.includes('no such table: messages_fts') && isNumericQuery(trimmed)) { + if (isNumericQuery(trimmed) || hasUnsafeChars(trimmed)) { const likeRows = runLikeContentSearch(db, source, trimmed) const merged = new Map() for (const row of titleRows) {