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" ],