Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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,35 @@
|
||||
"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": 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,34 @@
|
||||
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} while a request is in flight
|
||||
let _lastAsk = null; // {question, text, sources, completedAt} after success
|
||||
|
||||
const SS_KEY = 'kb-last-ask';
|
||||
|
||||
function _loadLastFromSession() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(SS_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed.question === 'string') return parsed;
|
||||
} catch (e) { /* corrupted, ignore */ }
|
||||
return null;
|
||||
}
|
||||
function _saveLastToSession(entry) {
|
||||
try { sessionStorage.setItem(SS_KEY, JSON.stringify(entry)); }
|
||||
catch (e) { /* quota or disabled, fine */ }
|
||||
}
|
||||
function _clearLastInSession() {
|
||||
try { sessionStorage.removeItem(SS_KEY); } catch (e) { /* ignore */ }
|
||||
}
|
||||
if (!_lastAsk) _lastAsk = _loadLastFromSession();
|
||||
|
||||
return Dep.extend({
|
||||
|
||||
template: 'knowledge-base:kb/index',
|
||||
@@ -68,6 +97,25 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
} else if (this._sources) {
|
||||
this.renderSourcesList();
|
||||
}
|
||||
|
||||
// Recover ask state across view remounts (user navigated away and
|
||||
// back). _activeAsk wins — there'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.
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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');
|
||||
@@ -102,30 +150,65 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
},
|
||||
|
||||
runAsk: function (message) {
|
||||
// A new ask invalidates whatever last-result was on screen.
|
||||
_lastAsk = null;
|
||||
_clearLastInSession();
|
||||
|
||||
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;
|
||||
_saveLastToSession(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 +227,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 +237,9 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
|
||||
'לוקח יותר זמן מהרגיל. עדיין ממתינים…</div>'
|
||||
);
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
tick();
|
||||
this._askTimer = setInterval(tick, 1000);
|
||||
},
|
||||
|
||||
stopAskProgress: function () {
|
||||
|
||||
+2
-2
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "KnowledgeBase",
|
||||
"module": "KnowledgeBase",
|
||||
"version": "0.1.8",
|
||||
"version": "0.1.9",
|
||||
"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