This repository has been archived on 2026-07-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
KnowledgeBase/files/client/custom/modules/knowledge-base/src/views/dashlets/kb-search.js
T
chaim 8829a2e93a feat: KnowledgeBase extension v0.1.6 — inline PDF viewer in browse tab
KnowledgeBase extension for EspoCRM: UI on top of the shira-hermes KB of
Israeli National Insurance law, regulations, and circulars.

Three modes:
- חיפוש — Hybrid (pgvector + tsvector) + Voyage rerank-2.5, returns ranked
  chunks with heading_path and citations.
- שאל את שירה — Full agent loop; Shira picks up search_insurance_kb as
  needed and returns a summary with citations.
- עיון — Browse all active sources. Click a source:
  - PDF source (ספר הליקויים, ספר המבחנים, circulars): renders the
    original PDF inline via an iframe proxied through the
    KnowledgeBasePdf EntryPoint, so the layout/columns/tables are
    preserved and Ctrl+F works natively.
  - TXT source (חוק הביטוח הלאומי scraped from Wikisource): falls back
    to the hierarchical chunk list with RTL styling.

Architecture:
- Controller: KnowledgeBase.php — thin proxy to shira-hermes /kb/*.
- Service: KnowledgeBaseService.php — shared curl plumbing; derives the
  shira-hermes base URL from the SmartAssistant integration record so
  there is no second admin config.
- EntryPoint: KnowledgeBasePdf.php — streams the PDF inline, wraps the
  body in a php://temp stream for setBody, applies a locked-down CSP.
- JS: views/kb/index.js branches on source.original_path; modes wired
  through the SmartAssistant fa_IR i18n convention.

Auth model:
- Browser → EspoCRM: session cookie / X-Api-Key (EspoCRM's existing auth).
- EspoCRM → shira-hermes: X-Api-Key from the SmartAssistant integration
  (never exposed to the browser).
- Portal users are blocked at both the Controller and the EntryPoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:57:23 +00:00

70 lines
3.1 KiB
JavaScript

define('modules/knowledge-base/views/dashlets/kb-search', ['views/dashlets/abstract/base'], function (Dep) {
return Dep.extend({
name: 'KbSearch',
templateContent: `
<div dir="rtl" style="padding:8px;">
<div style="display:flex;gap:6px;">
<input type="text" class="form-control input-sm" data-name="query"
placeholder="חפש בבסיס הידע…" style="flex:1;" />
<button type="button" class="btn btn-primary btn-sm" data-action="submit">חפש</button>
</div>
<div class="kb-dashlet-results" style="margin-top:8px;max-height:300px;overflow-y:auto;"></div>
<div style="margin-top:6px;">
<a href="#KnowledgeBase" class="small">פתח בבסיס הידע המלא →</a>
</div>
</div>
`,
events: {
'click [data-action="submit"]': function () {
this.runSearch();
},
'keypress input[data-name="query"]': function (e) {
if (e.which === 13) this.runSearch();
},
},
runSearch: function () {
const query = (this.$el.find('input[data-name="query"]').val() || '').trim();
if (!query) return;
const $r = this.$el.find('.kb-dashlet-results');
$r.html('<div class="text-muted small">מחפש…</div>');
Espo.Ajax.postRequest('KnowledgeBase/action/search', {
query: query,
kind: 'any',
topK: 5,
}).then(res => this.renderHits(res.hits || []))
.catch(err => {
$r.html('<div class="alert alert-danger small">' + ((err && err.statusText) || 'Error') + '</div>');
});
},
renderHits: function (hits) {
const $r = this.$el.find('.kb-dashlet-results');
if (!hits.length) {
$r.html('<div class="text-muted small">לא נמצאו תוצאות.</div>');
return;
}
const esc = s => String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const kindHe = {law: 'חוק', regulation: 'תקנה', circular: 'חוזר'};
const rows = hits.map(h => {
const kind = kindHe[h.kind] || h.kind;
const ident = h.identifier ? ' ' + esc(h.identifier) : '';
const ref = h.section_ref || h.heading_path || '';
const preview = esc((h.content || '').slice(0, 200)) + ((h.content || '').length > 200 ? '…' : '');
return `<div style="margin-bottom:8px;padding:6px;border-bottom:1px solid #eee;">
<div class="small"><strong>[${kind}${ident}]</strong> ${esc(h.title || '')}
${ref ? '<span class="text-muted"> — ' + esc(ref) + '</span>' : ''}</div>
<div class="small text-muted" style="white-space:pre-wrap;margin-top:3px;">${preview}</div>
</div>`;
}).join('');
$r.html(rows);
},
});
});