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>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
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);
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user