חיפוש 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) {