feat(kb): split-view PDF jump-to-page for both ask and search modes

Search and ask modes now render results as a two-column layout: a left
column with ranked hit cards (search) or Shira's answer + source picker
(ask), and a right column with an iframe showing the source PDF scrolled
to page_number. Clicking a hit (search) or source pill (ask) swaps the
iframe's src, so users can verify a quote against the original PDF
without leaving the KB tab.

- search: renderSearchResults lays out results as panel cards on the
  left (with kind + title + section + "עמ׳ N" label); the top hit is
  pre-selected and its PDF loads on the right. Clicking any card
  re-highlights it and swaps the preview. Law-kind hits (Wikisource
  text) gracefully fall back to a chunk-text panel with a Wikisource
  link so the right pane never 404s on a text source.
- ask: renderAskAnswer dedups /kb/ask's sources[] by source_id,
  collects all cited pages per source, and renders a picker row plus
  per-source page-jump buttons. First source's first page loads on
  initial render; buttons swap the iframe without re-running the query.

Depends on shira-hermes commits e534709 + 1ca1cc0 (chunker page_number
propagation + null-byte strip) — without them, every PDF collapses to
page 1 in the DB and the jump links are cosmetic.

Refs Task Master #1
This commit is contained in:
2026-04-24 15:49:29 +00:00
parent 34ad5cd023
commit 243ae2353f
9 changed files with 908 additions and 26 deletions
+12
View File
@@ -0,0 +1,12 @@
# API Keys (Required to enable respective provider)
ANTHROPIC_API_KEY="your_anthropic_api_key_here" # Required: Format: sk-ant-api03-...
PERPLEXITY_API_KEY="your_perplexity_api_key_here" # Optional: Format: pplx-...
OPENAI_API_KEY="your_openai_api_key_here" # Optional, for OpenAI models. Format: sk-proj-...
GOOGLE_API_KEY="your_google_api_key_here" # Optional, for Google Gemini models.
MISTRAL_API_KEY="your_mistral_key_here" # Optional, for Mistral AI models.
XAI_API_KEY="YOUR_XAI_KEY_HERE" # Optional, for xAI AI models.
GROQ_API_KEY="YOUR_GROQ_KEY_HERE" # Optional, for Groq models.
OPENROUTER_API_KEY="YOUR_OPENROUTER_KEY_HERE" # Optional, for OpenRouter models.
AZURE_OPENAI_API_KEY="your_azure_key_here" # Optional, for Azure OpenAI models (requires endpoint in .taskmaster/config.json).
OLLAMA_API_KEY="your_ollama_api_key_here" # Optional: For remote Ollama servers that require authentication.
GITHUB_API_KEY="your_github_api_key_here" # Optional: For GitHub import/export features. Format: ghp_... or github_pat_...
+25
View File
@@ -1,2 +1,27 @@
*.zip
.DS_Store
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
dev-debug.log
# Dependency directories
node_modules/
# Environment variables
.env
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# OS specific
# Task files
# tasks.json
# tasks/
+44
View File
@@ -0,0 +1,44 @@
{
"models": {
"main": {
"provider": "anthropic",
"modelId": "claude-sonnet-4-20250514",
"maxTokens": 64000,
"temperature": 0.2
},
"research": {
"provider": "perplexity",
"modelId": "sonar",
"maxTokens": 8700,
"temperature": 0.1
},
"fallback": {
"provider": "anthropic",
"modelId": "claude-3-7-sonnet-20250219",
"maxTokens": 120000,
"temperature": 0.2
}
},
"global": {
"logLevel": "info",
"debug": false,
"defaultNumTasks": 10,
"defaultSubtasks": 5,
"defaultPriority": "medium",
"projectName": "Task Master",
"ollamaBaseURL": "http://localhost:11434/api",
"bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com",
"responseLanguage": "English",
"enableCodebaseAnalysis": true,
"enableProxy": false,
"anonymousTelemetry": true,
"userId": "1234567890"
},
"claudeCode": {},
"codexCli": {},
"grokCli": {
"timeout": 120000,
"workingDirectory": null,
"defaultModel": "grok-4-latest"
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"currentTag": "master",
"lastSwitched": "2026-04-24T15:47:33.139Z",
"branchTagMapping": {},
"migrationNoticeShown": false
}
+35
View File
@@ -0,0 +1,35 @@
{
"master": {
"tasks": [
{
"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",
"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.",
"subtasks": [],
"dependencies": [],
"createdAt": "2026-04-24T15:47:00Z"
},
{
"id": 2,
"title": "fix(kb/chunker): page_number propagation through split + packing (shira-hermes)",
"description": "Applied in shira-hermes commits e534709 + 1ca1cc0: (1) _split_oversized now re-derives page_number per piece from markers embedded in parent content + a virtual (offset 0, parent.page_number) anchor, (2) chunk_circular tracks absolute paragraph offsets in the original text so each packed sub-chunk reports its real starting page, (3) _as_dicts strips stray \\x00 bytes left behind when _split_oversized slices through a marker sentinel. Reference data: ספר הליקויים now 211 chunks × 210 pages (1-413) vs 209 × 1 before. Not work for this repo, but the search split-view in task #1 depends on it.",
"status": "done",
"priority": "high",
"details": "Lives in espocrm-extensions/shira-hermes. Logged here for traceability because task #1 can't demonstrate correct page jumps without it.",
"testStrategy": "Query DB: SELECT source_id, MIN/MAX(page_number), COUNT(DISTINCT page_number) FROM kb_chunk GROUP BY source_id — every PDF source should span multiple distinct pages.",
"subtasks": [],
"dependencies": [],
"createdAt": "2026-04-24T15:47:00Z"
}
],
"metadata": {
"created": "2026-04-24T15:47:00Z",
"updated": "2026-04-24T15:47:00Z",
"description": "KnowledgeBase extension tasks"
}
}
}
+47
View File
@@ -0,0 +1,47 @@
<context>
# Overview
[Provide a high-level overview of your product here. Explain what problem it solves, who it's for, and why it's valuable.]
# Core Features
[List and describe the main features of your product. For each feature, include:
- What it does
- Why it's important
- How it works at a high level]
# User Experience
[Describe the user journey and experience. Include:
- User personas
- Key user flows
- UI/UX considerations]
</context>
<PRD>
# Technical Architecture
[Outline the technical implementation details:
- System components
- Data models
- APIs and integrations
- Infrastructure requirements]
# Development Roadmap
[Break down the development process into phases:
- MVP requirements
- Future enhancements
- Do not think about timelines whatsoever -- all that matters is scope and detailing exactly what needs to be build in each phase so it can later be cut up into tasks]
# Logical Dependency Chain
[Define the logical order of development:
- Which features need to be built first (foundation)
- Getting as quickly as possible to something usable/visible front end that works
- Properly pacing and scoping each feature so it is atomic but can also be built upon and improved as development approaches]
# Risks and Mitigations
[Identify potential risks and how they'll be addressed:
- Technical challenges
- Figuring out the MVP that we can build upon
- Resource constraints]
# Appendix
[Include any additional information:
- Research findings
- Technical specifications]
</PRD>
+511
View File
@@ -0,0 +1,511 @@
<rpg-method>
# Repository Planning Graph (RPG) Method - PRD Template
This template teaches you (AI or human) how to create structured, dependency-aware PRDs using the RPG methodology from Microsoft Research. The key insight: separate WHAT (functional) from HOW (structural), then connect them with explicit dependencies.
## Core Principles
1. **Dual-Semantics**: Think functional (capabilities) AND structural (code organization) separately, then map them
2. **Explicit Dependencies**: Never assume - always state what depends on what
3. **Topological Order**: Build foundation first, then layers on top
4. **Progressive Refinement**: Start broad, refine iteratively
## How to Use This Template
- Follow the instructions in each `<instruction>` block
- Look at `<example>` blocks to see good vs bad patterns
- Fill in the content sections with your project details
- The AI reading this will learn the RPG method by following along
- Task Master will parse the resulting PRD into dependency-aware tasks
## Recommended Tools for Creating PRDs
When using this template to **create** a PRD (not parse it), use **code-context-aware AI assistants** for best results:
**Why?** The AI needs to understand your existing codebase to make good architectural decisions about modules, dependencies, and integration points.
**Recommended tools:**
- **Claude Code** (claude-code CLI) - Best for structured reasoning and large contexts
- **Cursor/Windsurf** - IDE integration with full codebase context
- **Gemini CLI** (gemini-cli) - Massive context window for large codebases
- **Codex/Grok CLI** - Strong code generation with context awareness
**Note:** Once your PRD is created, `task-master parse-prd` works with any configured AI model - it just needs to read the PRD text itself, not your codebase.
</rpg-method>
---
<overview>
<instruction>
Start with the problem, not the solution. Be specific about:
- What pain point exists?
- Who experiences it?
- Why existing solutions don't work?
- What success looks like (measurable outcomes)?
Keep this section focused - don't jump into implementation details yet.
</instruction>
## Problem Statement
[Describe the core problem. Be concrete about user pain points.]
## Target Users
[Define personas, their workflows, and what they're trying to achieve.]
## Success Metrics
[Quantifiable outcomes. Examples: "80% task completion via autopilot", "< 5% manual intervention rate"]
</overview>
---
<functional-decomposition>
<instruction>
Now think about CAPABILITIES (what the system DOES), not code structure yet.
Step 1: Identify high-level capability domains
- Think: "What major things does this system do?"
- Examples: Data Management, Core Processing, Presentation Layer
Step 2: For each capability, enumerate specific features
- Use explore-exploit strategy:
* Exploit: What features are REQUIRED for core value?
* Explore: What features make this domain COMPLETE?
Step 3: For each feature, define:
- Description: What it does in one sentence
- Inputs: What data/context it needs
- Outputs: What it produces/returns
- Behavior: Key logic or transformations
<example type="good">
Capability: Data Validation
Feature: Schema validation
- Description: Validate JSON payloads against defined schemas
- Inputs: JSON object, schema definition
- Outputs: Validation result (pass/fail) + error details
- Behavior: Iterate fields, check types, enforce constraints
Feature: Business rule validation
- Description: Apply domain-specific validation rules
- Inputs: Validated data object, rule set
- Outputs: Boolean + list of violated rules
- Behavior: Execute rules sequentially, short-circuit on failure
</example>
<example type="bad">
Capability: validation.js
(Problem: This is a FILE, not a CAPABILITY. Mixing structure into functional thinking.)
Capability: Validation
Feature: Make sure data is good
(Problem: Too vague. No inputs/outputs. Not actionable.)
</example>
</instruction>
## Capability Tree
### Capability: [Name]
[Brief description of what this capability domain covers]
#### Feature: [Name]
- **Description**: [One sentence]
- **Inputs**: [What it needs]
- **Outputs**: [What it produces]
- **Behavior**: [Key logic]
#### Feature: [Name]
- **Description**:
- **Inputs**:
- **Outputs**:
- **Behavior**:
### Capability: [Name]
...
</functional-decomposition>
---
<structural-decomposition>
<instruction>
NOW think about code organization. Map capabilities to actual file/folder structure.
Rules:
1. Each capability maps to a module (folder or file)
2. Features within a capability map to functions/classes
3. Use clear module boundaries - each module has ONE responsibility
4. Define what each module exports (public interface)
The goal: Create a clear mapping between "what it does" (functional) and "where it lives" (structural).
<example type="good">
Capability: Data Validation
→ Maps to: src/validation/
├── schema-validator.js (Schema validation feature)
├── rule-validator.js (Business rule validation feature)
└── index.js (Public exports)
Exports:
- validateSchema(data, schema)
- validateRules(data, rules)
</example>
<example type="bad">
Capability: Data Validation
→ Maps to: src/utils.js
(Problem: "utils" is not a clear module boundary. Where do I find validation logic?)
Capability: Data Validation
→ Maps to: src/validation/everything.js
(Problem: One giant file. Features should map to separate files for maintainability.)
</example>
</instruction>
## Repository Structure
```
project-root/
├── src/
│ ├── [module-name]/ # Maps to: [Capability Name]
│ │ ├── [file].js # Maps to: [Feature Name]
│ │ └── index.js # Public exports
│ └── [module-name]/
├── tests/
└── docs/
```
## Module Definitions
### Module: [Name]
- **Maps to capability**: [Capability from functional decomposition]
- **Responsibility**: [Single clear purpose]
- **File structure**:
```
module-name/
├── feature1.js
├── feature2.js
└── index.js
```
- **Exports**:
- `functionName()` - [what it does]
- `ClassName` - [what it does]
</structural-decomposition>
---
<dependency-graph>
<instruction>
This is THE CRITICAL SECTION for Task Master parsing.
Define explicit dependencies between modules. This creates the topological order for task execution.
Rules:
1. List modules in dependency order (foundation first)
2. For each module, state what it depends on
3. Foundation modules should have NO dependencies
4. Every non-foundation module should depend on at least one other module
5. Think: "What must EXIST before I can build this module?"
<example type="good">
Foundation Layer (no dependencies):
- error-handling: No dependencies
- config-manager: No dependencies
- base-types: No dependencies
Data Layer:
- schema-validator: Depends on [base-types, error-handling]
- data-ingestion: Depends on [schema-validator, config-manager]
Core Layer:
- algorithm-engine: Depends on [base-types, error-handling]
- pipeline-orchestrator: Depends on [algorithm-engine, data-ingestion]
</example>
<example type="bad">
- validation: Depends on API
- API: Depends on validation
(Problem: Circular dependency. This will cause build/runtime issues.)
- user-auth: Depends on everything
(Problem: Too many dependencies. Should be more focused.)
</example>
</instruction>
## Dependency Chain
### Foundation Layer (Phase 0)
No dependencies - these are built first.
- **[Module Name]**: [What it provides]
- **[Module Name]**: [What it provides]
### [Layer Name] (Phase 1)
- **[Module Name]**: Depends on [[module-from-phase-0], [module-from-phase-0]]
- **[Module Name]**: Depends on [[module-from-phase-0]]
### [Layer Name] (Phase 2)
- **[Module Name]**: Depends on [[module-from-phase-1], [module-from-foundation]]
[Continue building up layers...]
</dependency-graph>
---
<implementation-roadmap>
<instruction>
Turn the dependency graph into concrete development phases.
Each phase should:
1. Have clear entry criteria (what must exist before starting)
2. Contain tasks that can be parallelized (no inter-dependencies within phase)
3. Have clear exit criteria (how do we know phase is complete?)
4. Build toward something USABLE (not just infrastructure)
Phase ordering follows topological sort of dependency graph.
<example type="good">
Phase 0: Foundation
Entry: Clean repository
Tasks:
- Implement error handling utilities
- Create base type definitions
- Setup configuration system
Exit: Other modules can import foundation without errors
Phase 1: Data Layer
Entry: Phase 0 complete
Tasks:
- Implement schema validator (uses: base types, error handling)
- Build data ingestion pipeline (uses: validator, config)
Exit: End-to-end data flow from input to validated output
</example>
<example type="bad">
Phase 1: Build Everything
Tasks:
- API
- Database
- UI
- Tests
(Problem: No clear focus. Too broad. Dependencies not considered.)
</example>
</instruction>
## Development Phases
### Phase 0: [Foundation Name]
**Goal**: [What foundational capability this establishes]
**Entry Criteria**: [What must be true before starting]
**Tasks**:
- [ ] [Task name] (depends on: [none or list])
- Acceptance criteria: [How we know it's done]
- Test strategy: [What tests prove it works]
- [ ] [Task name] (depends on: [none or list])
**Exit Criteria**: [Observable outcome that proves phase complete]
**Delivers**: [What can users/developers do after this phase?]
---
### Phase 1: [Layer Name]
**Goal**:
**Entry Criteria**: Phase 0 complete
**Tasks**:
- [ ] [Task name] (depends on: [[tasks-from-phase-0]])
- [ ] [Task name] (depends on: [[tasks-from-phase-0]])
**Exit Criteria**:
**Delivers**:
---
[Continue with more phases...]
</implementation-roadmap>
---
<test-strategy>
<instruction>
Define how testing will be integrated throughout development (TDD approach).
Specify:
1. Test pyramid ratios (unit vs integration vs e2e)
2. Coverage requirements
3. Critical test scenarios
4. Test generation guidelines for Surgical Test Generator
This section guides the AI when generating tests during the RED phase of TDD.
<example type="good">
Critical Test Scenarios for Data Validation module:
- Happy path: Valid data passes all checks
- Edge cases: Empty strings, null values, boundary numbers
- Error cases: Invalid types, missing required fields
- Integration: Validator works with ingestion pipeline
</example>
</instruction>
## Test Pyramid
```
/\
/E2E\ ← [X]% (End-to-end, slow, comprehensive)
/------\
/Integration\ ← [Y]% (Module interactions)
/------------\
/ Unit Tests \ ← [Z]% (Fast, isolated, deterministic)
/----------------\
```
## Coverage Requirements
- Line coverage: [X]% minimum
- Branch coverage: [X]% minimum
- Function coverage: [X]% minimum
- Statement coverage: [X]% minimum
## Critical Test Scenarios
### [Module/Feature Name]
**Happy path**:
- [Scenario description]
- Expected: [What should happen]
**Edge cases**:
- [Scenario description]
- Expected: [What should happen]
**Error cases**:
- [Scenario description]
- Expected: [How system handles failure]
**Integration points**:
- [What interactions to test]
- Expected: [End-to-end behavior]
## Test Generation Guidelines
[Specific instructions for Surgical Test Generator about what to focus on, what patterns to follow, project-specific test conventions]
</test-strategy>
---
<architecture>
<instruction>
Describe technical architecture, data models, and key design decisions.
Keep this section AFTER functional/structural decomposition - implementation details come after understanding structure.
</instruction>
## System Components
[Major architectural pieces and their responsibilities]
## Data Models
[Core data structures, schemas, database design]
## Technology Stack
[Languages, frameworks, key libraries]
**Decision: [Technology/Pattern]**
- **Rationale**: [Why chosen]
- **Trade-offs**: [What we're giving up]
- **Alternatives considered**: [What else we looked at]
</architecture>
---
<risks>
<instruction>
Identify risks that could derail development and how to mitigate them.
Categories:
- Technical risks (complexity, unknowns)
- Dependency risks (blocking issues)
- Scope risks (creep, underestimation)
</instruction>
## Technical Risks
**Risk**: [Description]
- **Impact**: [High/Medium/Low - effect on project]
- **Likelihood**: [High/Medium/Low]
- **Mitigation**: [How to address]
- **Fallback**: [Plan B if mitigation fails]
## Dependency Risks
[External dependencies, blocking issues]
## Scope Risks
[Scope creep, underestimation, unclear requirements]
</risks>
---
<appendix>
## References
[Papers, documentation, similar systems]
## Glossary
[Domain-specific terms]
## Open Questions
[Things to resolve during development]
</appendix>
---
<task-master-integration>
# How Task Master Uses This PRD
When you run `task-master parse-prd <file>.txt`, the parser:
1. **Extracts capabilities** → Main tasks
- Each `### Capability:` becomes a top-level task
2. **Extracts features** → Subtasks
- Each `#### Feature:` becomes a subtask under its capability
3. **Parses dependencies** → Task dependencies
- `Depends on: [X, Y]` sets task.dependencies = ["X", "Y"]
4. **Orders by phases** → Task priorities
- Phase 0 tasks = highest priority
- Phase N tasks = lower priority, properly sequenced
5. **Uses test strategy** → Test generation context
- Feeds test scenarios to Surgical Test Generator during implementation
**Result**: A dependency-aware task graph that can be executed in topological order.
## Why RPG Structure Matters
Traditional flat PRDs lead to:
- ❌ Unclear task dependencies
- ❌ Arbitrary task ordering
- ❌ Circular dependencies discovered late
- ❌ Poorly scoped tasks
RPG-structured PRDs provide:
- ✅ Explicit dependency chains
- ✅ Topological execution order
- ✅ Clear module boundaries
- ✅ Validated task graph before implementation
## Tips for Best Results
1. **Spend time on dependency graph** - This is the most valuable section for Task Master
2. **Keep features atomic** - Each feature should be independently testable
3. **Progressive refinement** - Start broad, use `task-master expand` to break down complex tasks
4. **Use research mode** - `task-master parse-prd --research` leverages AI for better task generation
</task-master-integration>
@@ -113,7 +113,7 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
}, {timeout: 240000}).then(res => {
this.stopAskProgress();
this.setLoading(false);
this.renderAskAnswer(res.text || '');
this.renderAskAnswer(res.text || '', res.sources || []);
}).catch(err => {
this.stopAskProgress();
this.setLoading(false);
@@ -172,50 +172,252 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
return;
}
const kindHe = {law: 'חוק', regulation: 'תקנה', circular: 'חוזר'};
const rows = hits.map(h => {
const self = this;
const isPdfBacked = h => h && (h.kind === 'regulation' || h.kind === 'circular');
// Left column: stacked hit cards. Click → preview on the right.
const bodyStyle =
'direction:rtl;unicode-bidi:plaintext;white-space:pre-wrap;' +
'font-family:"Segoe UI",Arial,sans-serif;line-height:1.6;text-align:right;' +
'max-height:14em;overflow:hidden;';
const listHtml = hits.map((h, i) => {
const kind = kindHe[h.kind] || h.kind;
const ident = h.identifier ? ' ' + this.escape(h.identifier) : '';
const path = h.heading_path || h.section_ref || '';
const pub = h.published_at ? ' · פורסם ' + h.published_at : '';
// Content is rendered inside a white-space:pre-wrap block —
// no need to convert \n to <br>; that would double-break.
const content = this.escape(h.content || '');
const srcLink = h.source_url
? `<a href="${this.escape(h.source_url)}" target="_blank" rel="noopener">מקור</a>`
: '';
const pageLabel = (isPdfBacked(h) && h.page_number)
? ` · <span class="label label-default">עמ׳ ${h.page_number}</span>` : '';
const activeClass = i === 0 ? 'panel-primary' : 'panel-default';
return `
<div class="kb-hit panel ${activeClass}" data-hit-idx="${i}"
style="margin-bottom:8px;cursor:pointer;">
<div class="panel-heading" style="direction:rtl;text-align:right;padding:6px 10px;">
<strong>[${kind}${ident}]</strong> ${this.escape(h.title || '')}
${path ? ' — <span class="text-muted">' + this.escape(path) + '</span>' : ''}
<span class="text-muted small">${pub}</span>${pageLabel}
</div>
<div class="panel-body" style="${bodyStyle}">${this.escape(h.content || '')}</div>
</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>
`);
// Cache hits on the view so click handlers can reach them.
this._searchHits = hits;
this.showSearchPreview(0);
this.$el.find('.kb-hit').off('click').on('click', function () {
const idx = parseInt($(this).data('hit-idx'), 10);
if (Number.isNaN(idx)) return;
self.$el.find('.kb-hit')
.removeClass('panel-primary').addClass('panel-default');
$(this).removeClass('panel-default').addClass('panel-primary');
self.showSearchPreview(idx);
});
},
showSearchPreview: function (idx) {
const hits = this._searchHits || [];
const h = hits[idx];
const $slot = this.$el.find('.kb-preview-slot');
if (!h) { $slot.empty(); return; }
const isPdf = h.kind === 'regulation' || h.kind === 'circular';
if (isPdf && h.source_id) {
const page = h.page_number || 1;
const url = '?entryPoint=KnowledgeBasePdf&sourceId='
+ encodeURIComponent(h.source_id)
+ '#page=' + encodeURIComponent(page);
$slot.html(`
<div style="direction:rtl;text-align:right;margin-bottom:6px;">
<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;"
title="מקור"></iframe>
`);
return;
}
// Text source (law from Wikisource) — show the full chunk content
// and a link to the public source page.
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;';
return `
<div class="kb-hit panel panel-default" style="margin-bottom:10px;">
<div class="panel-heading" style="direction:rtl;text-align:right;">
<strong>[${kind}${ident}]</strong> ${this.escape(h.title || '')}
${path ? ' — <span class="text-muted">' + this.escape(path) + '</span>' : ''}
<span class="text-muted small">${pub}</span>
const srcLink = h.source_url
? `<a href="${this.escape(h.source_url)}" target="_blank" rel="noopener">מקור ב-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>
<div class="panel-body" style="${bodyStyle}">${content}</div>
${srcLink ? '<div class="panel-footer small">' + srcLink + '</div>' : ''}
</div>`;
}).join('');
$results.html(rows);
</div>
`);
},
renderAskAnswer: function (text) {
renderAskAnswer: function (text, sources) {
const $results = this.$el.find('.kb-results');
sources = sources || [];
// Shira replies in markdown; render through EspoCRM's markdown
// helper when available, otherwise fall back to escaped text.
let html;
let answerHtml;
try {
const helper = this.getHelper && this.getHelper();
if (helper && typeof helper.transformMarkdownText === 'function') {
html = helper.transformMarkdownText(text || '');
if (html && html.toString) html = html.toString();
answerHtml = helper.transformMarkdownText(text || '');
if (answerHtml && answerHtml.toString) answerHtml = answerHtml.toString();
}
} catch (e) { /* fall through */ }
if (!html) {
html = '<div style="white-space:pre-wrap;">' + this.escape(text || '') + '</div>';
if (!answerHtml) {
answerHtml = '<div style="white-space:pre-wrap;">' + this.escape(text || '') + '</div>';
}
$results.html('<div class="kb-answer panel panel-info"><div class="panel-body">' + html + '</div></div>');
const kindHe = {law: 'חוק', regulation: 'תקנה', circular: 'חוזר'};
// Dedup sources by source_id (first occurrence = most relevant),
// but remember every page that came up per source so users can
// jump directly to the right page.
const perSource = {};
sources.forEach(s => {
if (!s.source_id) return;
const key = s.source_id;
if (!perSource[key]) {
perSource[key] = {
source_id: s.source_id,
title: s.title || '',
kind: s.kind || '',
identifier: s.identifier || '',
pages: [],
};
}
if (s.page_number && !perSource[key].pages.includes(s.page_number)) {
perSource[key].pages.push(s.page_number);
}
});
const sourceList = Object.values(perSource);
if (!sourceList.length) {
// No PDFs to show — render just the answer.
$results.html(
'<div class="kb-answer panel panel-info">' +
'<div class="panel-body" style="direction:rtl;text-align:right;">' +
answerHtml + '</div></div>'
);
return;
}
// Pick the initial PDF + page: the first source (highest-ranked)
// and its first-cited page.
const first = sourceList[0];
const firstPage = first.pages[0] || 1;
// Source picker: radio-like buttons that swap the iframe's src.
const pickerItems = sourceList.map((s, i) => {
const pagesLabel = s.pages.length
? ' · עמ׳ ' + s.pages.join(', ')
: '';
const k = kindHe[s.kind] || s.kind;
const active = i === 0 ? 'btn-primary' : 'btn-default';
return `<button type="button"
class="btn btn-xs ${active} kb-pdf-pick"
data-source-id="${s.source_id}"
data-page="${s.pages[0] || 1}"
style="margin:2px;">
${this.escape(k)}${this.escape(s.title)}${pagesLabel}
</button>`;
}).join(' ');
// Extra jump links per page, for the active source.
const pageLinks = first.pages.map(p =>
`<button type="button" class="btn btn-xs btn-default kb-page-jump"
data-source-id="${first.source_id}" data-page="${p}"
style="margin:2px;">עמ׳ ${p}</button>`
).join(' ');
const initialUrl = '?entryPoint=KnowledgeBasePdf&sourceId='
+ encodeURIComponent(first.source_id)
+ '#page=' + encodeURIComponent(firstPage);
$results.html(`
<div class="row" style="margin:0;">
<div class="col-md-6" style="padding-right:0;">
<div class="kb-answer panel panel-info">
<div class="panel-body" style="direction:rtl;text-align:right;max-height:80vh;overflow-y:auto;">
${answerHtml}
</div>
</div>
<div class="panel panel-default" style="direction:rtl;text-align:right;">
<div class="panel-heading"><strong>📎 מקורות</strong></div>
<div class="panel-body">
<div style="margin-bottom:6px;">${pickerItems}</div>
<div class="kb-page-links" style="font-size:0.9em;">
<span class="text-muted">עמודים רלוונטיים:</span> ${pageLinks}
</div>
</div>
</div>
</div>
<div class="col-md-6" style="padding-left:0;">
<iframe class="kb-pdf-iframe"
src="${initialUrl}"
style="width:100%;height:86vh;border:1px solid #ddd;background:#fafafa;"
title="מקור"></iframe>
</div>
</div>
`);
const self = this;
this.$el.find('.kb-pdf-pick').off('click').on('click', function () {
const id = $(this).data('source-id');
const page = $(this).data('page');
self.loadPdfInIframe(id, page);
self.$el.find('.kb-pdf-pick').removeClass('btn-primary').addClass('btn-default');
$(this).removeClass('btn-default').addClass('btn-primary');
// Refresh page links for the newly selected source.
const src = sourceList.find(x => x.source_id === id);
if (src && src.pages.length) {
const links = src.pages.map(p =>
`<button type="button" class="btn btn-xs btn-default kb-page-jump"
data-source-id="${src.source_id}" data-page="${p}"
style="margin:2px;">עמ׳ ${p}</button>`
).join(' ');
self.$el.find('.kb-page-links').html(
'<span class="text-muted">עמודים רלוונטיים:</span> ' + links
);
self.bindPageJumps();
}
});
this.bindPageJumps();
},
bindPageJumps: function () {
const self = this;
this.$el.find('.kb-page-jump').off('click').on('click', function () {
const id = $(this).data('source-id');
const page = $(this).data('page');
self.loadPdfInIframe(id, page);
});
},
loadPdfInIframe: function (sourceId, page) {
const url = '?entryPoint=KnowledgeBasePdf&sourceId='
+ encodeURIComponent(sourceId)
+ '#page=' + encodeURIComponent(page || 1);
this.$el.find('.kb-pdf-iframe').attr('src', url);
},
renderSourcesList: function () {
@@ -310,7 +512,7 @@ define('modules/knowledge-base/views/kb/index', ['view'], function (Dep) {
setLoading: function (loading) {
const $btn = this.$el.find('[data-action="submit"]');
if (loading) {
$btn.prop('disabled', true).text(this.translate('Loading', 'labels'));
$btn.prop('disabled', true).text('טוען…');
// Only overwrite results with "מחפש…" for search. For ask,
// startAskProgress() renders a richer progress panel.
if (this.mode !== 'ask') {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "KnowledgeBase",
"module": "KnowledgeBase",
"version": "0.1.7",
"version": "0.1.8",
"acceptableVersions": [
">=8.0.0"
],