Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2fe10727eb |
@@ -37,6 +37,18 @@
|
|||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"createdAt": "2026-04-25T10:00:00Z"
|
"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,
|
"id": 5,
|
||||||
"title": "feat(kb): drag-to-resize splitter (both ask + search)",
|
"title": "feat(kb): drag-to-resize splitter (both ask + search)",
|
||||||
|
|||||||
@@ -465,23 +465,114 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text source (law from Wikisource) — show the full chunk content
|
// Text source (law from Wikisource): fetch the matched chunk +
|
||||||
// and a link to the public source page.
|
// 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 =
|
const bodyStyle =
|
||||||
'direction:rtl;unicode-bidi:plaintext;white-space:pre-wrap;' +
|
'direction:rtl;unicode-bidi:plaintext;white-space:pre-wrap;' +
|
||||||
'font-family:"Segoe UI",Arial,sans-serif;line-height:1.7;text-align:right;';
|
'font-family:"Segoe UI",Arial,sans-serif;line-height:1.7;text-align:right;';
|
||||||
const srcLink = h.source_url
|
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(`
|
// The matched chunk is highlighted; siblings render as muted
|
||||||
<div class="panel panel-default" style="direction:rtl;text-align:right;height:100%;display:flex;flex-direction:column;">
|
// context blocks above and below so the user can read into the
|
||||||
<div class="panel-heading" style="flex:0 0 auto;">${this.escape(h.title || '')}</div>
|
// section without leaving the CRM.
|
||||||
<div class="panel-body" style="${bodyStyle};flex:1 1 auto;overflow-y:auto;">
|
const sectionsHtml = chunks.map(c => {
|
||||||
${this.escape(h.content || '')}
|
const isMatch = c.chunk_index === h.chunk_index;
|
||||||
</div>
|
const ref = c.section_ref || c.heading_path || '';
|
||||||
${srcLink ? '<div class="panel-footer small" style="flex:0 0 auto;">' + srcLink + '</div>' : ''}
|
const headerStyle = isMatch
|
||||||
</div>
|
? '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) {
|
renderAskAnswer: function (text, sources) {
|
||||||
|
|||||||
@@ -73,7 +73,12 @@ class KnowledgeBase
|
|||||||
throw new BadRequest('sourceId (numeric) is required.');
|
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
|
public function postActionAsk(Request $request, Response $response): array
|
||||||
|
|||||||
@@ -48,9 +48,13 @@ class KnowledgeBaseService
|
|||||||
return $this->get('/kb/sources');
|
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
|
public function ask(string $message, ?string $conversationId, array $history): array
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "KnowledgeBase",
|
"name": "KnowledgeBase",
|
||||||
"module": "KnowledgeBase",
|
"module": "KnowledgeBase",
|
||||||
"version": "0.1.10",
|
"version": "0.1.11",
|
||||||
"acceptableVersions": [
|
"acceptableVersions": [
|
||||||
">=8.0.0"
|
">=8.0.0"
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user