fix(kb): RTL-correct pane order + new-result button + 30-min auto-clear
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
This commit is contained in:
@@ -28,6 +28,8 @@
|
|||||||
<option value="circular">חוזרים</option>
|
<option value="circular">חוזרים</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="button" class="btn btn-primary" data-action="submit">חפש</button>
|
<button type="button" class="btn btn-primary" data-action="submit">חפש</button>
|
||||||
|
<button type="button" class="btn btn-default" data-action="clearResults"
|
||||||
|
title="התחל חיפוש חדש (מנקה את התוצאות הקודמות)">חיפוש חדש</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="small text-muted" style="margin-top:6px;">
|
<div class="small text-muted" style="margin-top:6px;">
|
||||||
חיפוש Hybrid: וקטורי + מילולי + rerank. מחזיר עד 8 קטעים רלוונטיים.
|
חיפוש Hybrid: וקטורי + מילולי + rerank. מחזיר עד 8 קטעים רלוונטיים.
|
||||||
@@ -42,6 +44,8 @@
|
|||||||
placeholder="שאלה בשפה חופשית…"
|
placeholder="שאלה בשפה חופשית…"
|
||||||
style="flex:1 1 320px;min-width:280px;" />
|
style="flex:1 1 320px;min-width:280px;" />
|
||||||
<button type="button" class="btn btn-primary" data-action="submit">שאל את שירה</button>
|
<button type="button" class="btn btn-primary" data-action="submit">שאל את שירה</button>
|
||||||
|
<button type="button" class="btn btn-default" data-action="clearResults"
|
||||||
|
title="התחל שאלה חדשה (מנקה את התשובה הקודמת)">שאלה חדשה</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="small text-muted" style="margin-top:6px;">
|
<div class="small text-muted" style="margin-top:6px;">
|
||||||
שירה תחפש בבסיס הידע ותחזיר תשובה מסוכמת עם ציטוטים.
|
שירה תחפש בבסיס הידע ותחזיר תשובה מסוכמת עם ציטוטים.
|
||||||
|
|||||||
@@ -14,12 +14,21 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
|||||||
const SS_ASK = 'kb-last-ask';
|
const SS_ASK = 'kb-last-ask';
|
||||||
const SS_SEARCH = 'kb-last-search';
|
const SS_SEARCH = 'kb-last-search';
|
||||||
const LS_SPLIT = 'kb-split-pct'; // long-lived UI preference (not session)
|
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) {
|
function _loadJson(storage, key, validator) {
|
||||||
try {
|
try {
|
||||||
const raw = storage.getItem(key);
|
const raw = storage.getItem(key);
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
const parsed = JSON.parse(raw);
|
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;
|
return validator(parsed) ? parsed : null;
|
||||||
} catch (e) { return 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);
|
const id = parseInt($(e.currentTarget).data('id'), 10);
|
||||||
if (id) this.openSource(id);
|
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 () {
|
setup: function () {
|
||||||
@@ -419,8 +464,8 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
|||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
$results.html(this._buildSplitShell({
|
$results.html(this._buildSplitShell({
|
||||||
leftHtml: `<div class="kb-search-list" style="height:100%;overflow-y:auto;padding-left:6px;">${listHtml}</div>`,
|
primaryHtml: '<div class="kb-search-list" style="height:100%;overflow-y:auto;padding-right:6px;">' + listHtml + '</div>',
|
||||||
rightHtml: '<div class="kb-preview-slot" style="height:100%;"></div>',
|
referenceHtml: '<div class="kb-preview-slot" style="height:100%;"></div>',
|
||||||
}));
|
}));
|
||||||
this._wireSplitter($results.find('.kb-split'));
|
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
|
// Returns the HTML for a flex-row split layout with a draggable
|
||||||
// 6px handle. The container's flex direction is forced LTR so the
|
// 6px handle. In our Hebrew/RTL CRM users expect primary content
|
||||||
// splitter math (clientX) doesn't get inverted by RTL — content
|
// (Shira's answer + sources, or the search hit list) on the visual
|
||||||
// inside each pane keeps its own direction.
|
// RIGHT — matching how Hebrew reads — and the supporting reference
|
||||||
_buildSplitShell: function ({leftHtml, rightHtml}) {
|
// (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();
|
const pct = this._loadSplitPct();
|
||||||
return (
|
return (
|
||||||
'<div class="kb-split" style="' +
|
'<div class="kb-split" style="' +
|
||||||
@@ -454,7 +508,7 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
|||||||
'gap:0;height:86vh;align-items:stretch;">' +
|
'gap:0;height:86vh;align-items:stretch;">' +
|
||||||
' <div class="kb-split-left" style="' +
|
' <div class="kb-split-left" style="' +
|
||||||
'flex:0 0 ' + pct + '%;min-width:200px;overflow:hidden;">' +
|
'flex:0 0 ' + pct + '%;min-width:200px;overflow:hidden;">' +
|
||||||
leftHtml +
|
referenceHtml +
|
||||||
' </div>' +
|
' </div>' +
|
||||||
' <div class="kb-split-handle" title="גרור כדי לשנות חלוקה" style="' +
|
' <div class="kb-split-handle" title="גרור כדי לשנות חלוקה" style="' +
|
||||||
'flex:0 0 6px;background:#dde1e7;cursor:col-resize;' +
|
'flex:0 0 6px;background:#dde1e7;cursor:col-resize;' +
|
||||||
@@ -462,7 +516,7 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
|||||||
'transition:background 0.15s;"></div>' +
|
'transition:background 0.15s;"></div>' +
|
||||||
' <div class="kb-split-right" style="' +
|
' <div class="kb-split-right" style="' +
|
||||||
'flex:1 1 auto;min-width:200px;overflow:hidden;">' +
|
'flex:1 1 auto;min-width:200px;overflow:hidden;">' +
|
||||||
rightHtml +
|
primaryHtml +
|
||||||
' </div>' +
|
' </div>' +
|
||||||
'</div>'
|
'</div>'
|
||||||
);
|
);
|
||||||
@@ -473,7 +527,10 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
|||||||
const v = parseFloat(localStorage.getItem(LS_SPLIT));
|
const v = parseFloat(localStorage.getItem(LS_SPLIT));
|
||||||
if (!isNaN(v) && v >= 15 && v <= 85) return v;
|
if (!isNaN(v) && v >= 15 && v <= 85) return v;
|
||||||
} catch (e) { /* ignore */ }
|
} 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) {
|
_saveSplitPct: function (pct) {
|
||||||
@@ -741,8 +798,8 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
|||||||
+ encodeURIComponent(first.source_id)
|
+ encodeURIComponent(first.source_id)
|
||||||
+ '#page=' + encodeURIComponent(firstPage);
|
+ '#page=' + encodeURIComponent(firstPage);
|
||||||
|
|
||||||
const leftHtml = `
|
const primaryHtml = `
|
||||||
<div style="height:100%;overflow-y:auto;padding-left:6px;">
|
<div style="height:100%;overflow-y:auto;padding-right:6px;">
|
||||||
<div class="kb-answer panel panel-info">
|
<div class="kb-answer panel panel-info">
|
||||||
<div class="panel-body" style="direction:rtl;text-align:right;">
|
<div class="panel-body" style="direction:rtl;text-align:right;">
|
||||||
${answerHtml}
|
${answerHtml}
|
||||||
@@ -759,13 +816,13 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
const rightHtml = `
|
const referenceHtml = `
|
||||||
<iframe class="kb-pdf-iframe"
|
<iframe class="kb-pdf-iframe"
|
||||||
src="${initialUrl}"
|
src="${initialUrl}"
|
||||||
style="width:100%;height:100%;border:1px solid #ddd;background:#fafafa;"
|
style="width:100%;height:100%;border:1px solid #ddd;background:#fafafa;"
|
||||||
title="מקור"></iframe>
|
title="מקור"></iframe>
|
||||||
`;
|
`;
|
||||||
$results.html(this._buildSplitShell({leftHtml, rightHtml}));
|
$results.html(this._buildSplitShell({primaryHtml, referenceHtml}));
|
||||||
this._wireSplitter($results.find('.kb-split'));
|
this._wireSplitter($results.find('.kb-split'));
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "KnowledgeBase",
|
"name": "KnowledgeBase",
|
||||||
"module": "KnowledgeBase",
|
"module": "KnowledgeBase",
|
||||||
"version": "0.2.0",
|
"version": "0.2.1",
|
||||||
"acceptableVersions": [
|
"acceptableVersions": [
|
||||||
">=8.0.0"
|
">=8.0.0"
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user