From e80ccb8bdf8b6728f5bfe01769a03fa4ea4ca760 Mon Sep 17 00:00:00 2001 From: Chaim Date: Sat, 25 Apr 2026 13:16:30 +0000 Subject: [PATCH] fix(kb): RTL-correct pane order + new-result button + 30-min auto-clear MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three issues from the v0.2.0 user test: 1. Sides flipped relative to expectation In our Hebrew/RTL CRM the user reads primary content (the answer or the hit list) on the visual right and the supporting reference (the PDF) on the visual left — that's how Hebrew layouts stage "main + sidebar". v0.1.10's _buildSplitShell forced direction:ltr on the splitter container (correct, for mouseX math) but laid the children in the order [primary, handle, reference], which under LTR put primary on the LEFT and PDF on the RIGHT. Fixed by renaming the helper params to {primaryHtml, referenceHtml} and ordering the children as [reference, handle, primary] — visual LEFT is now the PDF, visual RIGHT is the answer/list. Default split moved from 42% to 58% so the reference pane (now on the left) keeps the same absolute width the PDF column had under v0.1.9's bootstrap rows. 2. No way to start a new query afterRender replays _lastSearch / _lastAsk on every remount, which means the prior result hangs around forever. Added a "חיפוש חדש" / "שאלה חדשה" button next to the submit button. clearResults() cancels any in-flight request (closes the EventSource for ask, drops _activeSearch reference for search), removes the cached entry from sessionStorage, empties the results pane, and refocuses the input. 3. Stale results from another session Cached results now auto-expire after 30 minutes — _loadJson checks completedAt against STALE_AFTER_MS and silently drops anything older. So coming back tomorrow the KB tab opens clean instead of showing yesterday's answer to whatever you asked then. Refs Task Master #4, #6 --- .../knowledge-base/res/templates/kb/index.tpl | 4 + .../knowledge-base/src/views/kb/index.js | 83 ++++++++++++++++--- manifest.json | 2 +- 3 files changed, 75 insertions(+), 14 deletions(-) diff --git a/files/client/custom/modules/knowledge-base/res/templates/kb/index.tpl b/files/client/custom/modules/knowledge-base/res/templates/kb/index.tpl index 2c943e4..8a9cc08 100644 --- a/files/client/custom/modules/knowledge-base/res/templates/kb/index.tpl +++ b/files/client/custom/modules/knowledge-base/res/templates/kb/index.tpl @@ -28,6 +28,8 @@ +
חיפוש Hybrid: וקטורי + מילולי + rerank. מחזיר עד 8 קטעים רלוונטיים. @@ -42,6 +44,8 @@ placeholder="שאלה בשפה חופשית…" style="flex:1 1 320px;min-width:280px;" /> +
שירה תחפש בבסיס הידע ותחזיר תשובה מסוכמת עם ציטוטים. diff --git a/files/client/custom/modules/knowledge-base/src/views/kb/index.js b/files/client/custom/modules/knowledge-base/src/views/kb/index.js index 6117ea4..57a21b0 100644 --- a/files/client/custom/modules/knowledge-base/src/views/kb/index.js +++ b/files/client/custom/modules/knowledge-base/src/views/kb/index.js @@ -14,12 +14,21 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) { const SS_ASK = 'kb-last-ask'; const SS_SEARCH = 'kb-last-search'; const LS_SPLIT = 'kb-split-pct'; // long-lived UI preference (not session) + // Cached results auto-expire after this long. The user explicitly asked + // for a clear button, but auto-clearing also handles the case where they + // come back hours later and the prior result is no longer relevant. + const STALE_AFTER_MS = 30 * 60 * 1000; function _loadJson(storage, key, validator) { try { const raw = storage.getItem(key); if (!raw) return null; const parsed = JSON.parse(raw); + if (parsed && parsed.completedAt && + (Date.now() - parsed.completedAt) > STALE_AFTER_MS) { + storage.removeItem(key); + return null; + } return validator(parsed) ? parsed : null; } catch (e) { return null; } } @@ -78,6 +87,42 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) { const id = parseInt($(e.currentTarget).data('id'), 10); if (id) this.openSource(id); }, + 'click [data-action="clearResults"]': function (e) { + e.preventDefault(); + this.clearResults(); + }, + }, + + // Cancel any in-flight ask/search, drop the cached last-result for + // the current mode, empty the results pane, and refocus the input. + // Bound to the "חיפוש חדש" / "שאלה חדשה" button as well as the + // 30-min auto-expiry path in afterRender. + clearResults: function () { + if (this.mode === 'search') { + if (_activeSearch) { + // Promise will still resolve in the background, but the + // .then handler short-circuits when _activeSearch !== self. + _activeSearch = null; + } + _lastSearch = null; + _clear(sessionStorage, SS_SEARCH); + } else if (this.mode === 'ask') { + if (_activeAsk) { + try { if (_activeAsk.es) _activeAsk.es.close(); } + catch (e) { /* already closed */ } + _activeAsk = null; + } + _lastAsk = null; + _clear(sessionStorage, SS_ASK); + this.stopAskProgress(); + } + this.setLoading(false); + this.$el.find('.kb-results').empty(); + const $input = this.$el.find('input[data-name="query"]'); + if ($input.length) { + $input.val(''); + $input.trigger('focus'); + } }, setup: function () { @@ -419,8 +464,8 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) { }).join(''); $results.html(this._buildSplitShell({ - leftHtml: `
${listHtml}
`, - rightHtml: '
', + primaryHtml: '
' + listHtml + '
', + referenceHtml: '
', })); this._wireSplitter($results.find('.kb-split')); @@ -443,10 +488,19 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) { }, // Returns the HTML for a flex-row split layout with a draggable - // 6px handle. The container's flex direction is forced LTR so the - // splitter math (clientX) doesn't get inverted by RTL — content - // inside each pane keeps its own direction. - _buildSplitShell: function ({leftHtml, rightHtml}) { + // 6px handle. In our Hebrew/RTL CRM users expect primary content + // (Shira's answer + sources, or the search hit list) on the visual + // RIGHT — matching how Hebrew reads — and the supporting reference + // (PDF iframe or section context) on the visual LEFT. We force + // direction:ltr on the container so the splitter's mouseX math + // doesn't get inverted by RTL inheritance; inner panes carry their + // own RTL direction as needed. + // + // Children render in DOM order [reference, handle, primary], which + // under direction:ltr maps to visual [LEFT, divider, RIGHT]. + // pct is the LEFT (reference) pane's width %; default 58 matches + // the v0.1.9 col-md-7 PDF column. + _buildSplitShell: function ({primaryHtml, referenceHtml}) { const pct = this._loadSplitPct(); return ( '
' + '
' + - leftHtml + + referenceHtml + '
' + '
' + '
' + - rightHtml + + primaryHtml + '
' + '
' ); @@ -473,7 +527,10 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) { const v = parseFloat(localStorage.getItem(LS_SPLIT)); if (!isNaN(v) && v >= 15 && v <= 85) return v; } catch (e) { /* ignore */ } - return 42; + // Default = 58% reference (PDF) on left, ~42% primary on right. + // Matches the visual proportions of v0.1.9's col-md-7/col-md-5 + // when the CRM rendered Bootstrap rows in RTL order. + return 58; }, _saveSplitPct: function (pct) { @@ -741,8 +798,8 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) { + encodeURIComponent(first.source_id) + '#page=' + encodeURIComponent(firstPage); - const leftHtml = ` -
+ const primaryHtml = ` +
${answerHtml} @@ -759,13 +816,13 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
`; - const rightHtml = ` + const referenceHtml = ` `; - $results.html(this._buildSplitShell({leftHtml, rightHtml})); + $results.html(this._buildSplitShell({primaryHtml, referenceHtml})); this._wireSplitter($results.find('.kb-split')); const self = this; diff --git a/manifest.json b/manifest.json index a3e8202..41bdd64 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "KnowledgeBase", "module": "KnowledgeBase", - "version": "0.2.0", + "version": "0.2.1", "acceptableVersions": [ ">=8.0.0" ],