2 Commits

Author SHA1 Message Date
chaim 2fe10727eb fix(kb): search preview shows section-in-context for text sources
User reported the search card "looks broken" when the selected hit is the
law (Wikisource source 5) — the right preview pane just duplicates the
chunk text already visible in the left list card. For PDF sources the
right pane shows the PDF page, but for text-only sources it had nothing
useful to add and fell back to a copy.

Fix:
- /kb/search hits gained chunk_index (shira-hermes b127caf), and
  /kb/source/{id}/chunks gained an `around=<chunk_index>&ctx=<k>` window.
- Espo proxy plumbing: getActionChunks reads `around`/`ctx` query
  params and forwards them; KnowledgeBaseService::sourceChunks accepts
  optional $around + $ctx and appends them to the upstream URL.
- showSearchPreview for non-PDF hits now fetches the matched chunk plus
  2 sections before and 2 after, renders them as stacked panels: the
  matched section gets a yellow #fff3cd highlight, neighbors render as
  muted context blocks. The panel header carries a prominent
  "פתח ב-Wikisource" button so users can jump to the public source page.
  After render the matched section auto-scrolls into view.
- Race protection via _previewToken: a click on a different hit while a
  fetch is in flight cancels the stale render so the user always sees
  the section that matches the currently-selected card.

PDF path unchanged.

Refs Task Master #7
2026-04-25 12:28:50 +00:00
chaim ac5823ffe5 feat(kb): drag-to-resize splitter + search survives view switch
Two requests on top of v0.1.9:

1. Drag-to-resize splitter
   The previous fixed Bootstrap col-md-5/col-md-7 split locked the
   user into 41.66% answer / 58.33% PDF. Replaced with a flex-row
   layout containing a 6px drag handle. mousedown spawns a fixed
   transparent overlay so the PDF.js iframe doesn't swallow mouse
   events while dragging; mouseup persists the chosen ratio (clamped
   15-85%) to localStorage as kb-split-pct, so the next page-load and
   the next session both come back to the same layout. Both the
   ask split view and the search split view share _buildSplitShell +
   _wireSplitter — single source of truth for the geometry.

2. Search survives a view switch
   Search had the same view-bound promise problem ask had before
   v0.1.9. Hoisted the in-flight request to a module-level
   _activeSearch and the most recent {query, kind, hits, selectedIdx}
   to _lastSearch + sessionStorage. afterRender restores the input,
   the kind filter, the hit list (with the same hit highlighted), and
   the right-pane preview. Click handler updates selectedIdx so a
   round-trip lands on the user's last-clicked hit, not always on the
   top-ranked one.

The persistence helpers grew a tiny refactor: _loadJson/_saveJson/_clear
replace the ask-only SS_KEY shim and now back both ask + search keys.

Refs Task Master #5, #6
2026-04-25 10:46:39 +00:00
5 changed files with 349 additions and 82 deletions
+36
View File
@@ -37,6 +37,42 @@
"dependencies": [],
"createdAt": "2026-04-25T10:00:00Z"
},
{
"id": 7,
"title": "fix: search preview duplicates left card for text sources",
"description": "User report: searching the law (Wikisource source 5) shows the same chunk content in both the left list-card and the right preview pane. The preview adds zero value when the matched source has no PDF — both panes are visually identical (modulo the left's 14em truncation), which reads as a bug. Fixed by fetching the matched chunk plus K=2 surrounding sections from /kb/source/{id}/chunks?around=N&ctx=2 and rendering them stacked: matched section is highlighted with a yellow header (#fff3cd), neighbors render as muted context blocks above and below. Adds a prominent 'פתח ב-Wikisource' button in the panel header, scrolls the matched section into view on render. Race-safe via _previewToken so a quick second click doesn't render stale results.",
"status": "done",
"priority": "high",
"details": "shira-hermes b127caf added chunk_index to /kb/search hits + an `around`/`ctx` query param to /source/{id}/chunks. Espo controller passes them through. Client uses the new fields in showSearchPreview when the hit is a text source.",
"testStrategy": "Search 'מהי תקנה 36'; click the law-kind hit. Right pane: yellow-highlighted ס' 36 in the middle, ס' 35 above (muted), ס' 37 below (muted), Wikisource button in header. Click the תקנה 37 חוזר hit: right pane swaps to PDF iframe at the cited page (PDF path unchanged).",
"subtasks": [],
"dependencies": [],
"createdAt": "2026-04-25T12:30: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",
@@ -6,28 +6,36 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
// 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} while a request is in flight
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_KEY = 'kb-last-ask';
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 _loadLastFromSession() {
function _loadJson(storage, key, validator) {
try {
const raw = sessionStorage.getItem(SS_KEY);
const raw = storage.getItem(key);
if (!raw) return null;
const parsed = JSON.parse(raw);
if (parsed && typeof parsed.question === 'string') return parsed;
} catch (e) { /* corrupted, ignore */ }
return null;
return validator(parsed) ? parsed : null;
} catch (e) { return null; }
}
function _saveLastToSession(entry) {
try { sessionStorage.setItem(SS_KEY, JSON.stringify(entry)); }
function _saveJson(storage, key, value) {
try { storage.setItem(key, JSON.stringify(value)); }
catch (e) { /* quota or disabled, fine */ }
}
function _clearLastInSession() {
try { sessionStorage.removeItem(SS_KEY); } catch (e) { /* ignore */ }
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));
}
if (!_lastAsk) _lastAsk = _loadLastFromSession();
return Dep.extend({
@@ -98,10 +106,10 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
this.renderSourcesList();
}
// Recover ask state across view remounts (user navigated away and
// back). _activeAsk winsthere's still a request in flight, so
// re-show the progress panel and re-bind handlers. Otherwise
// _lastAsk replays the most recent answer in this browser session.
// 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"]');
@@ -114,6 +122,19 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
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.
@@ -135,24 +156,53 @@ 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;
_clearLastInSession();
_clear(sessionStorage, SS_ASK);
this.setLoading(true);
// Ask goes through the full agent loop (search + rerank + LLM),
@@ -181,7 +231,7 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
completedAt: Date.now(),
};
_lastAsk = entry;
_saveLastToSession(entry);
_saveJson(sessionStorage, SS_ASK, entry);
if (_activeAsk === ask) _activeAsk = null;
if (self.$el && self.$el.length) {
self.stopAskProgress();
@@ -249,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;
@@ -272,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;">
@@ -285,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);
@@ -307,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);
});
},
@@ -323,34 +453,126 @@ 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;">
<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="width:100%;height:82vh;border:1px solid #ddd;background:#fafafa;"
<iframe class="kb-pdf-iframe" src="${url}"
style="flex:1 1 auto;width:100%;border:1px solid #ddd;background:#fafafa;"
title="מקור"></iframe>
</div>
`);
return;
}
// Text source (law from Wikisource) — show the full chunk content
// and a link to the public source page.
// Text source (law from Wikisource): fetch the matched chunk +
// surrounding sections so the preview pane shows context, not a
// copy of what's already in the left card. While loading, show a
// placeholder; on failure fall back to the chunk-only view.
this._renderSectionContextPlaceholder(h);
const token = this._previewToken;
const sourceId = h.source_id;
const around = (typeof h.chunk_index === 'number') ? h.chunk_index : null;
if (sourceId == null || around == null) {
this._renderSectionContextSingle(h);
return;
}
const self = this;
Espo.Ajax.getRequest('KnowledgeBase/action/chunks', {
sourceId: sourceId,
around: around,
ctx: 2,
}).then(res => {
// Bail if user has clicked a different hit while we were waiting.
if (self._previewToken !== token) return;
self._renderSectionContext(h, res && res.chunks ? res.chunks : []);
}).catch(err => {
console.warn('KB: section-window fetch failed', err);
if (self._previewToken === token) self._renderSectionContextSingle(h);
});
},
_renderSectionContextPlaceholder: function (h) {
this._previewToken = (this._previewToken || 0) + 1;
const $slot = this.$el.find('.kb-preview-slot');
const title = this.escape(h.title || '');
$slot.html(
'<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;">' + title +
' <span class="text-muted small" style="margin-right:8px;">— טוען הקשר…</span>' +
' </div>' +
' <div class="panel-body" style="flex:1 1 auto;overflow-y:auto;">' +
' <i class="fas fa-circle-notch fa-spin"></i>' +
' </div>' +
'</div>'
);
},
_renderSectionContextSingle: function (h) {
// Fallback when we can't fetch the window — render just the
// matched chunk, which is at least the full section for the
// statute chunker (1 section = 1 chunk).
this._renderSectionContext(h, [{
chunk_index: h.chunk_index,
section_ref: h.section_ref || '',
heading_path: h.heading_path || '',
content: h.content || '',
}]);
},
_renderSectionContext: function (h, chunks) {
const $slot = this.$el.find('.kb-preview-slot');
const bodyStyle =
'direction:rtl;unicode-bidi:plaintext;white-space:pre-wrap;' +
'font-family:"Segoe UI",Arial,sans-serif;line-height:1.7;text-align:right;';
const srcLink = h.source_url
? `<a href="${this.escape(h.source_url)}" target="_blank" rel="noopener">מקור ב-Wikisource</a>`
? '<a href="' + this.escape(h.source_url) + '" target="_blank" rel="noopener" ' +
'class="btn btn-xs btn-default" style="margin-right:6px;">' +
'<i class="fas fa-external-link-alt"></i> פתח ב-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;">
${this.escape(h.content || '')}
</div>
${srcLink ? '<div class="panel-footer small">' + srcLink + '</div>' : ''}
</div>
`);
// The matched chunk is highlighted; siblings render as muted
// context blocks above and below so the user can read into the
// section without leaving the CRM.
const sectionsHtml = chunks.map(c => {
const isMatch = c.chunk_index === h.chunk_index;
const ref = c.section_ref || c.heading_path || '';
const headerStyle = isMatch
? 'background:#fff3cd;border-bottom:1px solid #ffe69c;color:#664d03;font-weight:600;'
: 'background:#f5f5f7;border-bottom:1px solid #e7e7ea;color:#6c757d;';
return (
'<div style="margin-bottom:12px;border:1px solid ' +
(isMatch ? '#ffe69c' : '#e7e7ea') + ';border-radius:4px;overflow:hidden;">' +
' <div style="' + headerStyle + 'padding:6px 10px;direction:rtl;text-align:right;font-size:0.9em;">' +
(isMatch ? 'התאמה — ' : '') + this.escape(ref) +
' </div>' +
' <div style="' + bodyStyle + ';padding:10px;">' +
this.escape(c.content || '') +
' </div>' +
'</div>'
);
}).join('');
$slot.html(
'<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;display:flex;justify-content:space-between;align-items:center;">' +
' <div><strong>' + this.escape(h.title || '') + '</strong>' +
' <span class="text-muted small" style="margin-right:6px;">— הסעיף בהקשרו</span>' +
' </div>' +
srcLink +
' </div>' +
' <div class="panel-body" style="flex:1 1 auto;overflow-y:auto;background:#fafbfc;">' +
sectionsHtml +
' </div>' +
'</div>'
);
// Scroll the matched chunk into view inside the preview pane.
const $body = $slot.find('.panel-body');
const $match = $body.find('div[style*="#fff3cd"]').closest('div[style*="border-radius:4px"]');
if ($match.length && $body.length) {
const offset = $match.position().top - 20;
$body.scrollTop(Math.max(0, offset));
}
},
renderAskAnswer: function (text, sources) {
@@ -437,11 +659,10 @@ 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;">
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;max-height:80vh;overflow-y:auto;">
<div class="panel-body" style="direction:rtl;text-align:right;">
${answerHtml}
</div>
</div>
@@ -455,14 +676,15 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
</div>
</div>
</div>
<div class="col-md-6" style="padding-left:0;">
`;
const rightHtml = `
<iframe class="kb-pdf-iframe"
src="${initialUrl}"
style="width:100%;height:86vh;border:1px solid #ddd;background:#fafafa;"
style="width:100%;height:100%;border:1px solid #ddd;background:#fafafa;"
title="מקור"></iframe>
</div>
</div>
`);
`;
$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 () {
@@ -73,7 +73,12 @@ class KnowledgeBase
throw new BadRequest('sourceId (numeric) is required.');
}
return $this->getService()->sourceChunks((int) $sourceId);
$around = $request->getQueryParam('around');
$ctx = $request->getQueryParam('ctx');
$aroundInt = ($around !== null && is_numeric($around)) ? (int) $around : null;
$ctxInt = ($ctx !== null && is_numeric($ctx)) ? (int) $ctx : 2;
return $this->getService()->sourceChunks((int) $sourceId, $aroundInt, $ctxInt);
}
public function postActionAsk(Request $request, Response $response): array
@@ -48,9 +48,13 @@ class KnowledgeBaseService
return $this->get('/kb/sources');
}
public function sourceChunks(int $sourceId): array
public function sourceChunks(int $sourceId, ?int $around = null, int $ctx = 2): array
{
return $this->get('/kb/source/' . $sourceId . '/chunks');
$path = '/kb/source/' . $sourceId . '/chunks';
if ($around !== null) {
$path .= '?around=' . $around . '&ctx=' . $ctx;
}
return $this->get($path);
}
public function ask(string $message, ?string $conversationId, array $history): array
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "KnowledgeBase",
"module": "KnowledgeBase",
"version": "0.1.9",
"version": "0.1.11",
"acceptableVersions": [
">=8.0.0"
],