Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ac5823ffe5 | |||
| 50a60c13b3 | |||
| eb1b0fc7d4 |
@@ -5,7 +5,7 @@
|
||||
"id": 1,
|
||||
"title": "feat: search-mode split-view with PDF + page jump",
|
||||
"description": "Render /kb/search results as a two-column split view: left column is the stack of ranked hit cards (kind, title, section, page label), right column is an iframe viewing the selected hit's PDF scrolled to page_number. Clicking any hit swaps the iframe. Text-only sources (law from Wikisource) fall back to a chunk/text panel with a Wikisource link so the right column never 404s.",
|
||||
"status": "in-progress",
|
||||
"status": "done",
|
||||
"priority": "high",
|
||||
"details": "Before this change renderSearchResults produced plain vertical text cards — users got the chunk body and had to open the Wikisource link (or track down the PDF manually) to verify context. With 5 of 5 PDFs now carrying accurate page_number (shira-hermes e534709 + 1ca1cc0), the search UI can finally deep-link to the right page. Mirrors the ask-mode split view (v0.1.7 uncommitted).",
|
||||
"testStrategy": "Search 'תקנה 37' → first hit should be a regulation/circular → right pane loads the PDF at page ~N where the section appears. Click a lower-ranked law hit → right pane swaps to a Wikisource link view. Search 'הגדרות' → hits span multiple sources → each click swaps iframe source.",
|
||||
@@ -24,11 +24,59 @@
|
||||
"subtasks": [],
|
||||
"dependencies": [],
|
||||
"createdAt": "2026-04-24T15:47:00Z"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "feat(kb/search): LLM query expansion so ranked-by-title docs don't crowd everything else out (shira-hermes)",
|
||||
"description": "User reports searching 'מהי תקנה 37' returns only the תקנה 37 circular and the law — never ספר הליקויים, even though that's where the underlying medical content lives. Root cause is a single-shot retrieval where any document whose title contains the query terms wins by a huge margin. Fix: ask the LLM (via ai-gateway) for up to 3 alternative phrasings, retrieve candidates for the original + each variant in parallel, merge by chunk_id keeping best RRF score, then rerank the union against the ORIGINAL query. After fix: src 29 (ספר הליקויים) shows up with 2 hits in the same query.",
|
||||
"status": "done",
|
||||
"priority": "high",
|
||||
"details": "Lives in espocrm-extensions/shira-hermes commit 1441a41. /kb/search now has expand=true by default, agent path (search_insurance_kb tool) keeps expand=false because the agent already does multi-query via tool calls.",
|
||||
"testStrategy": "POST /kb/search {query:'מהי תקנה 37', top_k:8} should return hits from at least 2 distinct source_ids, including src 29 (ספר הליקויים) and src 26 (תקנה 37 חוזר).",
|
||||
"subtasks": [],
|
||||
"dependencies": [],
|
||||
"createdAt": "2026-04-25T10:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "feat(kb): drag-to-resize splitter (both ask + search)",
|
||||
"description": "Replace the fixed Bootstrap col-md-5/7 split with a flex layout containing a 6px drag handle. mousedown on the handle starts the drag; while dragging an invisible full-screen overlay covers any iframe so the PDF.js viewer doesn't swallow mouse events. The chosen ratio (15-85%) persists to localStorage as kb-split-pct so the layout sticks across sessions. Both modes share the same _buildSplitShell helper so the look stays consistent.",
|
||||
"status": "done",
|
||||
"priority": "normal",
|
||||
"details": "Default ratio = 42% left (matches the previous col-md-5 ≈ 41.66%). Applied to renderSearchResults, renderAskAnswer, and showSearchPreview (which now uses 100%-height containers via flex column).",
|
||||
"testStrategy": "Open ask mode, drag the divider; verify PDF iframe doesn't lose mouse focus mid-drag; reload — divider stays at last position. Repeat in search mode.",
|
||||
"subtasks": [],
|
||||
"dependencies": [],
|
||||
"createdAt": "2026-04-25T11:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title": "fix: search survives view switch (parity with ask in v0.1.9)",
|
||||
"description": "Search mode had the same view-bound promise problem as ask did before v0.1.9: navigating to another EspoCRM screen and back blanked the results. Hoisted {query, kind, promise} to a module-level _activeSearch with the same {query, kind, hits, selectedIdx} sessionStorage replay we already built for ask. Selected hit index also persists so the right pane comes back to the same source/page the user had open.",
|
||||
"status": "done",
|
||||
"priority": "high",
|
||||
"details": "Mirrors the v0.1.9 ask fix in _attachSearchHandlers + afterRender. Click handler updates _lastSearch.selectedIdx so a return-trip lands on the user's last-clicked hit, not always the top-ranked one.",
|
||||
"testStrategy": "Search 'מהי תקנה 37'; click a result other than the first; navigate to Contacts; navigate back — same hit selected, same PDF page on the right.",
|
||||
"subtasks": [],
|
||||
"dependencies": [],
|
||||
"createdAt": "2026-04-25T11:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "fix: ask survives view switch via module-level promise + sessionStorage",
|
||||
"description": "User reports asking Shira a question and switching to a different EspoCRM view loses the in-flight request — coming back shows a blank screen as if nothing was asked. Cause: the Espo.Ajax promise was view-bound, so handlers fired against a detached DOM after re-mount. Fix: hoist {question, promise, startedAt} to a module-level _activeAsk; afterRender re-attaches handlers and resumes the progress UI with the original startedAt; completed answers persist to sessionStorage and replay on remount or a hard reload.",
|
||||
"status": "done",
|
||||
"priority": "high",
|
||||
"details": "All client-side, files/client/custom/modules/knowledge-base/src/views/kb/index.js. State is intentionally session-scoped — kept in sessionStorage so the next browser session starts clean.",
|
||||
"testStrategy": "Ask a question; while the spinner shows, click Contacts (or any other navbar item); wait until the request would have finished; click back to בסיס ידע — the answer (or active spinner with correct elapsed-time) should be there.",
|
||||
"subtasks": [],
|
||||
"dependencies": [],
|
||||
"createdAt": "2026-04-25T10:00:00Z"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"created": "2026-04-24T15:47:00Z",
|
||||
"updated": "2026-04-24T15:47:00Z",
|
||||
"updated": "2026-04-25T10:30:00Z",
|
||||
"description": "KnowledgeBase extension tasks"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
|
||||
// Module-level state survives view unmount/remount within a single
|
||||
// SPA session so a user can fire an ask, navigate elsewhere, and come
|
||||
// back to the answer (or to the still-spinning request) without losing
|
||||
// it. sessionStorage backs the last completed answer so a hard reload
|
||||
// also recovers it. None of this leaks to the next browser session —
|
||||
// ask history is intentionally ephemeral.
|
||||
let _activeAsk = null; // {question, promise, startedAt} while in flight
|
||||
let _lastAsk = null; // {question, text, sources, completedAt} after success
|
||||
let _activeSearch = null; // {query, kind, promise} while in flight
|
||||
let _lastSearch = null; // {query, kind, hits, selectedIdx, completedAt}
|
||||
|
||||
const SS_ASK = 'kb-last-ask';
|
||||
const SS_SEARCH = 'kb-last-search';
|
||||
const LS_SPLIT = 'kb-split-pct'; // long-lived UI preference (not session)
|
||||
|
||||
function _loadJson(storage, key, validator) {
|
||||
try {
|
||||
const raw = storage.getItem(key);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
return validator(parsed) ? parsed : null;
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
function _saveJson(storage, key, value) {
|
||||
try { storage.setItem(key, JSON.stringify(value)); }
|
||||
catch (e) { /* quota or disabled, fine */ }
|
||||
}
|
||||
function _clear(storage, key) {
|
||||
try { storage.removeItem(key); } catch (e) { /* ignore */ }
|
||||
}
|
||||
if (!_lastAsk) {
|
||||
_lastAsk = _loadJson(sessionStorage, SS_ASK, x => x && typeof x.question === 'string');
|
||||
}
|
||||
if (!_lastSearch) {
|
||||
_lastSearch = _loadJson(sessionStorage, SS_SEARCH, x => x && typeof x.query === 'string' && Array.isArray(x.hits));
|
||||
}
|
||||
|
||||
return Dep.extend({
|
||||
|
||||
template: 'knowledge-base:kb/index',
|
||||
@@ -68,6 +105,38 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
} else if (this._sources) {
|
||||
this.renderSourcesList();
|
||||
}
|
||||
|
||||
// Recover prior state across view remounts (user navigated away and
|
||||
// back). Active in-flight wins; otherwise replay the last result
|
||||
// from sessionStorage so the work the user already did doesn't
|
||||
// disappear behind a tab switch.
|
||||
if (this.mode === 'ask') {
|
||||
if (_activeAsk) {
|
||||
const $input = this.$el.find('input[data-name="query"]');
|
||||
if ($input.length) $input.val(_activeAsk.question);
|
||||
this.setLoading(true);
|
||||
this.startAskProgress(_activeAsk.startedAt);
|
||||
this._attachAskHandlers(_activeAsk);
|
||||
} else if (_lastAsk) {
|
||||
const $input = this.$el.find('input[data-name="query"]');
|
||||
if ($input.length) $input.val(_lastAsk.question);
|
||||
this.renderAskAnswer(_lastAsk.text, _lastAsk.sources);
|
||||
}
|
||||
} else if (this.mode === 'search') {
|
||||
if (_activeSearch) {
|
||||
const $input = this.$el.find('input[data-name="query"]');
|
||||
if ($input.length) $input.val(_activeSearch.query);
|
||||
this.setLoading(true);
|
||||
this._attachSearchHandlers(_activeSearch);
|
||||
} else if (_lastSearch && _lastSearch.hits.length) {
|
||||
const $input = this.$el.find('input[data-name="query"]');
|
||||
if ($input.length) $input.val(_lastSearch.query);
|
||||
const $kind = this.$el.find('select[data-name="kind"]');
|
||||
if ($kind.length && _lastSearch.kind) $kind.val(_lastSearch.kind);
|
||||
this.renderSearchResults(_lastSearch.hits, _lastSearch.selectedIdx || 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Focus the input on every render so users can start typing right away.
|
||||
const $input = this.$el.find('input[data-name="query"]');
|
||||
if ($input.length) $input.trigger('focus');
|
||||
@@ -87,45 +156,109 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
},
|
||||
|
||||
runSearch: function (query, kind) {
|
||||
// A new search invalidates the prior cached one.
|
||||
_lastSearch = null;
|
||||
_clear(sessionStorage, SS_SEARCH);
|
||||
|
||||
this.setLoading(true);
|
||||
Espo.Ajax.postRequest('KnowledgeBase/action/search', {
|
||||
const promise = Espo.Ajax.postRequest('KnowledgeBase/action/search', {
|
||||
query: query,
|
||||
kind: kind,
|
||||
topK: 8,
|
||||
}).then(res => {
|
||||
this.setLoading(false);
|
||||
this.renderSearchResults(res.hits || []);
|
||||
});
|
||||
_activeSearch = {query: query, kind: kind, promise: promise};
|
||||
this._attachSearchHandlers(_activeSearch);
|
||||
},
|
||||
|
||||
_attachSearchHandlers: function (search) {
|
||||
const self = this;
|
||||
search.promise.then(res => {
|
||||
const hits = res.hits || [];
|
||||
const entry = {
|
||||
query: search.query,
|
||||
kind: search.kind,
|
||||
hits: hits,
|
||||
selectedIdx: 0,
|
||||
completedAt: Date.now(),
|
||||
};
|
||||
_lastSearch = entry;
|
||||
_saveJson(sessionStorage, SS_SEARCH, entry);
|
||||
if (_activeSearch === search) _activeSearch = null;
|
||||
if (self.$el && self.$el.length) {
|
||||
self.setLoading(false);
|
||||
if (self.mode === 'search') {
|
||||
self.renderSearchResults(hits);
|
||||
}
|
||||
}
|
||||
}).catch(err => {
|
||||
this.setLoading(false);
|
||||
this.showError(err);
|
||||
if (_activeSearch === search) _activeSearch = null;
|
||||
if (self.$el && self.$el.length) {
|
||||
self.setLoading(false);
|
||||
self.showError(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
runAsk: function (message) {
|
||||
// A new ask invalidates whatever last-result was on screen.
|
||||
_lastAsk = null;
|
||||
_clear(sessionStorage, SS_ASK);
|
||||
|
||||
this.setLoading(true);
|
||||
this.startAskProgress();
|
||||
// Ask goes through the full agent loop (search + rerank + LLM),
|
||||
// which routinely takes 30-90s. The default jQuery Ajax timeout
|
||||
// is too short; override it here so the browser doesn't give up
|
||||
// before shira-hermes responds.
|
||||
Espo.Ajax.postRequest('KnowledgeBase/action/ask', {
|
||||
const promise = Espo.Ajax.postRequest('KnowledgeBase/action/ask', {
|
||||
message: message,
|
||||
}, {timeout: 240000}).then(res => {
|
||||
this.stopAskProgress();
|
||||
this.setLoading(false);
|
||||
this.renderAskAnswer(res.text || '', res.sources || []);
|
||||
}, {timeout: 240000});
|
||||
_activeAsk = {question: message, promise: promise, startedAt: Date.now()};
|
||||
this.startAskProgress(_activeAsk.startedAt);
|
||||
this._attachAskHandlers(_activeAsk);
|
||||
},
|
||||
|
||||
// Wire .then/.catch on a (possibly module-level) ask promise to
|
||||
// whichever view is currently mounted. Called from runAsk for the
|
||||
// initial bind and from afterRender when the user navigates back to
|
||||
// the KB view while an earlier request is still in flight.
|
||||
_attachAskHandlers: function (ask) {
|
||||
const self = this;
|
||||
ask.promise.then(res => {
|
||||
const entry = {
|
||||
question: ask.question,
|
||||
text: res.text || '',
|
||||
sources: res.sources || [],
|
||||
completedAt: Date.now(),
|
||||
};
|
||||
_lastAsk = entry;
|
||||
_saveJson(sessionStorage, SS_ASK, entry);
|
||||
if (_activeAsk === ask) _activeAsk = null;
|
||||
if (self.$el && self.$el.length) {
|
||||
self.stopAskProgress();
|
||||
self.setLoading(false);
|
||||
if (self.mode === 'ask') {
|
||||
self.renderAskAnswer(entry.text, entry.sources);
|
||||
}
|
||||
}
|
||||
}).catch(err => {
|
||||
this.stopAskProgress();
|
||||
this.setLoading(false);
|
||||
this.showError(err);
|
||||
if (_activeAsk === ask) _activeAsk = null;
|
||||
if (self.$el && self.$el.length) {
|
||||
self.stopAskProgress();
|
||||
self.setLoading(false);
|
||||
self.showError(err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
startAskProgress: function () {
|
||||
startAskProgress: function (startedAt) {
|
||||
// Show a live elapsed-time indicator so users know the request
|
||||
// is still running rather than stuck. Typical answers arrive in
|
||||
// 30-90s; we warn after 2 minutes.
|
||||
// 30-90s; we warn after 2 minutes. `startedAt` is passed in so
|
||||
// a request that began before a view re-mount keeps showing the
|
||||
// accumulated elapsed time, not 0.
|
||||
if (this.mode !== 'ask') return;
|
||||
this.stopAskProgress();
|
||||
startedAt = startedAt || Date.now();
|
||||
const $results = this.$el.find('.kb-results');
|
||||
$results.html(
|
||||
'<div class="kb-ask-progress panel panel-info">' +
|
||||
@@ -144,8 +277,7 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
' </div>' +
|
||||
'</div>'
|
||||
);
|
||||
const startedAt = Date.now();
|
||||
this._askTimer = setInterval(() => {
|
||||
const tick = () => {
|
||||
const sec = Math.round((Date.now() - startedAt) / 1000);
|
||||
const $elapsed = this.$el.find('.kb-elapsed');
|
||||
if ($elapsed.length) $elapsed.text(sec);
|
||||
@@ -155,7 +287,9 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
'לוקח יותר זמן מהרגיל. עדיין ממתינים…</div>'
|
||||
);
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
tick();
|
||||
this._askTimer = setInterval(tick, 1000);
|
||||
},
|
||||
|
||||
stopAskProgress: function () {
|
||||
@@ -165,12 +299,13 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
}
|
||||
},
|
||||
|
||||
renderSearchResults: function (hits) {
|
||||
renderSearchResults: function (hits, selectedIdx) {
|
||||
const $results = this.$el.find('.kb-results');
|
||||
if (!hits.length) {
|
||||
$results.html('<div class="text-muted">לא נמצאו תוצאות.</div>');
|
||||
return;
|
||||
}
|
||||
const initialIdx = (typeof selectedIdx === 'number' && hits[selectedIdx]) ? selectedIdx : 0;
|
||||
const kindHe = {law: 'חוק', regulation: 'תקנה', circular: 'חוזר'};
|
||||
const self = this;
|
||||
|
||||
@@ -188,7 +323,7 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
const pub = h.published_at ? ' · פורסם ' + h.published_at : '';
|
||||
const pageLabel = (isPdfBacked(h) && h.page_number)
|
||||
? ` · <span class="label label-default">עמ׳ ${h.page_number}</span>` : '';
|
||||
const activeClass = i === 0 ? 'panel-primary' : 'panel-default';
|
||||
const activeClass = i === initialIdx ? 'panel-primary' : 'panel-default';
|
||||
return `
|
||||
<div class="kb-hit panel ${activeClass}" data-hit-idx="${i}"
|
||||
style="margin-bottom:8px;cursor:pointer;">
|
||||
@@ -201,20 +336,15 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
$results.html(`
|
||||
<div class="row" style="margin:0;">
|
||||
<div class="col-md-5" style="padding-right:0;max-height:86vh;overflow-y:auto;">
|
||||
${listHtml}
|
||||
</div>
|
||||
<div class="col-md-7 kb-search-preview" style="padding-left:0;">
|
||||
<div class="kb-preview-slot"></div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
$results.html(this._buildSplitShell({
|
||||
leftHtml: `<div class="kb-search-list" style="height:100%;overflow-y:auto;padding-left:6px;">${listHtml}</div>`,
|
||||
rightHtml: '<div class="kb-preview-slot" style="height:100%;"></div>',
|
||||
}));
|
||||
this._wireSplitter($results.find('.kb-split'));
|
||||
|
||||
// Cache hits on the view so click handlers can reach them.
|
||||
this._searchHits = hits;
|
||||
this.showSearchPreview(0);
|
||||
this.showSearchPreview(initialIdx);
|
||||
|
||||
this.$el.find('.kb-hit').off('click').on('click', function () {
|
||||
const idx = parseInt($(this).data('hit-idx'), 10);
|
||||
@@ -223,6 +353,90 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
.removeClass('panel-primary').addClass('panel-default');
|
||||
$(this).removeClass('panel-default').addClass('panel-primary');
|
||||
self.showSearchPreview(idx);
|
||||
if (_lastSearch && _lastSearch.hits === self._searchHits) {
|
||||
_lastSearch.selectedIdx = idx;
|
||||
_saveJson(sessionStorage, SS_SEARCH, _lastSearch);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 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}) {
|
||||
const pct = this._loadSplitPct();
|
||||
return (
|
||||
'<div class="kb-split" style="' +
|
||||
'display:flex;flex-direction:row;direction:ltr;' +
|
||||
'gap:0;height:86vh;align-items:stretch;">' +
|
||||
' <div class="kb-split-left" style="' +
|
||||
'flex:0 0 ' + pct + '%;min-width:200px;overflow:hidden;">' +
|
||||
leftHtml +
|
||||
' </div>' +
|
||||
' <div class="kb-split-handle" title="גרור כדי לשנות חלוקה" style="' +
|
||||
'flex:0 0 6px;background:#dde1e7;cursor:col-resize;' +
|
||||
'border-left:1px solid #cbd0d8;border-right:1px solid #cbd0d8;' +
|
||||
'transition:background 0.15s;"></div>' +
|
||||
' <div class="kb-split-right" style="' +
|
||||
'flex:1 1 auto;min-width:200px;overflow:hidden;">' +
|
||||
rightHtml +
|
||||
' </div>' +
|
||||
'</div>'
|
||||
);
|
||||
},
|
||||
|
||||
_loadSplitPct: function () {
|
||||
try {
|
||||
const v = parseFloat(localStorage.getItem(LS_SPLIT));
|
||||
if (!isNaN(v) && v >= 15 && v <= 85) return v;
|
||||
} catch (e) { /* ignore */ }
|
||||
return 42;
|
||||
},
|
||||
|
||||
_saveSplitPct: function (pct) {
|
||||
try { localStorage.setItem(LS_SPLIT, String(pct)); }
|
||||
catch (e) { /* ignore */ }
|
||||
},
|
||||
|
||||
// Wires mousedown/move/up on the handle to resize the left pane.
|
||||
// While dragging we cover any iframe with a transparent overlay so
|
||||
// mouse events don't disappear into PDF.js. On mouseup the chosen
|
||||
// ratio persists to localStorage.
|
||||
_wireSplitter: function ($split) {
|
||||
if (!$split.length) return;
|
||||
const $handle = $split.find('.kb-split-handle');
|
||||
const $left = $split.find('.kb-split-left');
|
||||
const self = this;
|
||||
$handle.on('mouseenter', () => $handle.css('background', '#b9c0cb'));
|
||||
$handle.on('mouseleave', () => $handle.css('background', '#dde1e7'));
|
||||
$handle.on('mousedown', function (e) {
|
||||
e.preventDefault();
|
||||
const rect = $split[0].getBoundingClientRect();
|
||||
const $overlay = $('<div class="kb-drag-overlay">').css({
|
||||
position: 'fixed',
|
||||
inset: '0',
|
||||
cursor: 'col-resize',
|
||||
zIndex: 9999,
|
||||
background: 'transparent',
|
||||
});
|
||||
$('body').append($overlay).css('user-select', 'none');
|
||||
const onMove = function (ev) {
|
||||
const x = ev.clientX - rect.left;
|
||||
let pct = (x / rect.width) * 100;
|
||||
pct = Math.max(15, Math.min(85, pct));
|
||||
$left.css('flex', '0 0 ' + pct + '%');
|
||||
self._dragPct = pct;
|
||||
};
|
||||
const onUp = function () {
|
||||
$(document).off('mousemove', onMove).off('mouseup', onUp);
|
||||
$overlay.remove();
|
||||
$('body').css('user-select', '');
|
||||
if (typeof self._dragPct === 'number') {
|
||||
self._saveSplitPct(self._dragPct);
|
||||
}
|
||||
};
|
||||
$(document).on('mousemove', onMove).on('mouseup', onUp);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -239,13 +453,14 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
+ encodeURIComponent(h.source_id)
|
||||
+ '#page=' + encodeURIComponent(page);
|
||||
$slot.html(`
|
||||
<div style="direction:rtl;text-align:right;margin-bottom:6px;">
|
||||
<span class="text-muted small">מציג עמ׳ ${page} של המסמך</span>
|
||||
<div style="display:flex;flex-direction:column;height:100%;">
|
||||
<div style="direction:rtl;text-align:right;margin-bottom:6px;flex:0 0 auto;">
|
||||
<span class="text-muted small">מציג עמ׳ ${page} של המסמך</span>
|
||||
</div>
|
||||
<iframe class="kb-pdf-iframe" src="${url}"
|
||||
style="flex:1 1 auto;width:100%;border:1px solid #ddd;background:#fafafa;"
|
||||
title="מקור"></iframe>
|
||||
</div>
|
||||
<iframe class="kb-pdf-iframe"
|
||||
src="${url}"
|
||||
style="width:100%;height:82vh;border:1px solid #ddd;background:#fafafa;"
|
||||
title="מקור"></iframe>
|
||||
`);
|
||||
return;
|
||||
}
|
||||
@@ -259,12 +474,12 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
? `<a href="${this.escape(h.source_url)}" target="_blank" rel="noopener">מקור ב-Wikisource</a>`
|
||||
: '';
|
||||
$slot.html(`
|
||||
<div class="panel panel-default" style="direction:rtl;text-align:right;">
|
||||
<div class="panel-heading">${this.escape(h.title || '')}</div>
|
||||
<div class="panel-body" style="${bodyStyle};max-height:80vh;overflow-y:auto;">
|
||||
<div class="panel panel-default" style="direction:rtl;text-align:right;height:100%;display:flex;flex-direction:column;">
|
||||
<div class="panel-heading" style="flex:0 0 auto;">${this.escape(h.title || '')}</div>
|
||||
<div class="panel-body" style="${bodyStyle};flex:1 1 auto;overflow-y:auto;">
|
||||
${this.escape(h.content || '')}
|
||||
</div>
|
||||
${srcLink ? '<div class="panel-footer small">' + srcLink + '</div>' : ''}
|
||||
${srcLink ? '<div class="panel-footer small" style="flex:0 0 auto;">' + srcLink + '</div>' : ''}
|
||||
</div>
|
||||
`);
|
||||
},
|
||||
@@ -353,32 +568,32 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
+ encodeURIComponent(first.source_id)
|
||||
+ '#page=' + encodeURIComponent(firstPage);
|
||||
|
||||
$results.html(`
|
||||
<div class="row" style="margin:0;">
|
||||
<div class="col-md-6" style="padding-right:0;">
|
||||
<div class="kb-answer panel panel-info">
|
||||
<div class="panel-body" style="direction:rtl;text-align:right;max-height:80vh;overflow-y:auto;">
|
||||
${answerHtml}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default" style="direction:rtl;text-align:right;">
|
||||
<div class="panel-heading"><strong>📎 מקורות</strong></div>
|
||||
<div class="panel-body">
|
||||
<div style="margin-bottom:6px;">${pickerItems}</div>
|
||||
<div class="kb-page-links" style="font-size:0.9em;">
|
||||
<span class="text-muted">עמודים רלוונטיים:</span> ${pageLinks}
|
||||
</div>
|
||||
</div>
|
||||
const leftHtml = `
|
||||
<div style="height:100%;overflow-y:auto;padding-left:6px;">
|
||||
<div class="kb-answer panel panel-info">
|
||||
<div class="panel-body" style="direction:rtl;text-align:right;">
|
||||
${answerHtml}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6" style="padding-left:0;">
|
||||
<iframe class="kb-pdf-iframe"
|
||||
src="${initialUrl}"
|
||||
style="width:100%;height:86vh;border:1px solid #ddd;background:#fafafa;"
|
||||
title="מקור"></iframe>
|
||||
<div class="panel panel-default" style="direction:rtl;text-align:right;">
|
||||
<div class="panel-heading"><strong>📎 מקורות</strong></div>
|
||||
<div class="panel-body">
|
||||
<div style="margin-bottom:6px;">${pickerItems}</div>
|
||||
<div class="kb-page-links" style="font-size:0.9em;">
|
||||
<span class="text-muted">עמודים רלוונטיים:</span> ${pageLinks}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
`;
|
||||
const rightHtml = `
|
||||
<iframe class="kb-pdf-iframe"
|
||||
src="${initialUrl}"
|
||||
style="width:100%;height:100%;border:1px solid #ddd;background:#fafafa;"
|
||||
title="מקור"></iframe>
|
||||
`;
|
||||
$results.html(this._buildSplitShell({leftHtml, rightHtml}));
|
||||
this._wireSplitter($results.find('.kb-split'));
|
||||
|
||||
const self = this;
|
||||
this.$el.find('.kb-pdf-pick').off('click').on('click', function () {
|
||||
|
||||
+2
-2
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "KnowledgeBase",
|
||||
"module": "KnowledgeBase",
|
||||
"version": "0.1.8",
|
||||
"version": "0.1.10",
|
||||
"acceptableVersions": [
|
||||
">=8.0.0"
|
||||
],
|
||||
"php": [
|
||||
">=8.1"
|
||||
],
|
||||
"releaseDate": "2026-04-24",
|
||||
"releaseDate": "2026-04-25",
|
||||
"author": "klear",
|
||||
"description": "Knowledge Base — Israeli National Insurance law, regulations, and circulars. Hybrid search + ask-shira, powered by shira-hermes KB."
|
||||
}
|
||||
Reference in New Issue
Block a user