From 243ae2353f03e2e5ce9555bd57705e401448a280 Mon Sep 17 00:00:00 2001 From: Chaim Date: Fri, 24 Apr 2026 15:49:29 +0000 Subject: [PATCH] feat(kb): split-view PDF jump-to-page for both ask and search modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .env.example | 12 + .gitignore | 25 + .taskmaster/config.json | 44 ++ .taskmaster/state.json | 6 + .taskmaster/tasks/tasks.json | 35 ++ .taskmaster/templates/example_prd.txt | 47 ++ .taskmaster/templates/example_prd_rpg.txt | 511 ++++++++++++++++++ .../knowledge-base/src/views/kb/index.js | 252 ++++++++- manifest.json | 2 +- 9 files changed, 908 insertions(+), 26 deletions(-) create mode 100644 .env.example create mode 100644 .taskmaster/config.json create mode 100644 .taskmaster/state.json create mode 100644 .taskmaster/tasks/tasks.json create mode 100644 .taskmaster/templates/example_prd.txt create mode 100644 .taskmaster/templates/example_prd_rpg.txt diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..60bd23e --- /dev/null +++ b/.env.example @@ -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_... \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4c8b8eb..d6a0845 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/.taskmaster/config.json b/.taskmaster/config.json new file mode 100644 index 0000000..75c8952 --- /dev/null +++ b/.taskmaster/config.json @@ -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" + } +} \ No newline at end of file diff --git a/.taskmaster/state.json b/.taskmaster/state.json new file mode 100644 index 0000000..f3493fd --- /dev/null +++ b/.taskmaster/state.json @@ -0,0 +1,6 @@ +{ + "currentTag": "master", + "lastSwitched": "2026-04-24T15:47:33.139Z", + "branchTagMapping": {}, + "migrationNoticeShown": false +} \ No newline at end of file diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json new file mode 100644 index 0000000..05f9279 --- /dev/null +++ b/.taskmaster/tasks/tasks.json @@ -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" + } + } +} diff --git a/.taskmaster/templates/example_prd.txt b/.taskmaster/templates/example_prd.txt new file mode 100644 index 0000000..194114d --- /dev/null +++ b/.taskmaster/templates/example_prd.txt @@ -0,0 +1,47 @@ + +# 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] + + +# 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] + \ No newline at end of file diff --git a/.taskmaster/templates/example_prd_rpg.txt b/.taskmaster/templates/example_prd_rpg.txt new file mode 100644 index 0000000..5ad908f --- /dev/null +++ b/.taskmaster/templates/example_prd_rpg.txt @@ -0,0 +1,511 @@ + +# 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 `` block +- Look at `` 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. + + +--- + + + +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. + + +## 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"] + + + +--- + + + +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 + + +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 + + + +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.) + + + +## 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] +... + + + +--- + + + +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). + + +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) + + + +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.) + + + +## 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] + + + +--- + + + +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?" + + +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] + + + +- 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.) + + + +## 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...] + + + +--- + + + +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. + + +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 + + + +Phase 1: Build Everything + Tasks: + - API + - Database + - UI + - Tests + (Problem: No clear focus. Too broad. Dependencies not considered.) + + + +## 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...] + + + +--- + + + +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. + + +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 + + + +## 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] + + + +--- + + + +Describe technical architecture, data models, and key design decisions. + +Keep this section AFTER functional/structural decomposition - implementation details come after understanding structure. + + +## 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] + + + +--- + + + +Identify risks that could derail development and how to mitigate them. + +Categories: +- Technical risks (complexity, unknowns) +- Dependency risks (blocking issues) +- Scope risks (creep, underestimation) + + +## 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] + + + +--- + + +## References +[Papers, documentation, similar systems] + +## Glossary +[Domain-specific terms] + +## Open Questions +[Things to resolve during development] + + +--- + + +# How Task Master Uses This PRD + +When you run `task-master parse-prd .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 + diff --git a/files/client/custom/modules/knowledge-base/src/views/kb/index.js b/files/client/custom/modules/knowledge-base/src/views/kb/index.js index f5e15f0..613c8e2 100644 --- a/files/client/custom/modules/knowledge-base/src/views/kb/index.js +++ b/files/client/custom/modules/knowledge-base/src/views/kb/index.js @@ -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
; that would double-break. - const content = this.escape(h.content || ''); - const srcLink = h.source_url - ? `מקור` - : ''; - 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;'; + const pageLabel = (isPdfBacked(h) && h.page_number) + ? ` · עמ׳ ${h.page_number}` : ''; + const activeClass = i === 0 ? 'panel-primary' : 'panel-default'; return ` -
-
+
+
[${kind}${ident}] ${this.escape(h.title || '')} ${path ? ' — ' + this.escape(path) + '' : ''} - ${pub} + ${pub}${pageLabel}
-
${content}
- ${srcLink ? '' : ''} +
${this.escape(h.content || '')}
`; }).join(''); - $results.html(rows); + + $results.html(` +
+
+ ${listHtml} +
+
+
+
+
+ `); + + // 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); + }); }, - renderAskAnswer: function (text) { + 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(` +
+ מציג עמ׳ ${page} של המסמך +
+ + `); + 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;'; + const srcLink = h.source_url + ? `מקור ב-Wikisource` + : ''; + $slot.html(` +
+
${this.escape(h.title || '')}
+
+ ${this.escape(h.content || '')} +
+ ${srcLink ? '' : ''} +
+ `); + }, + + 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 = '
' + this.escape(text || '') + '
'; + if (!answerHtml) { + answerHtml = '
' + this.escape(text || '') + '
'; } - $results.html('
' + html + '
'); + + 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( + '
' + + '
' + + answerHtml + '
' + ); + 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 ``; + }).join(' '); + + // Extra jump links per page, for the active source. + const pageLinks = first.pages.map(p => + `` + ).join(' '); + + const initialUrl = '?entryPoint=KnowledgeBasePdf&sourceId=' + + encodeURIComponent(first.source_id) + + '#page=' + encodeURIComponent(firstPage); + + $results.html(` +
+
+
+
+ ${answerHtml} +
+
+
+
📎 מקורות
+
+
${pickerItems}
+ +
+
+
+
+ +
+
+ `); + + 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 => + `` + ).join(' '); + self.$el.find('.kb-page-links').html( + 'עמודים רלוונטיים: ' + 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') { diff --git a/manifest.json b/manifest.json index 9eed14c..2a65b37 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "KnowledgeBase", "module": "KnowledgeBase", - "version": "0.1.7", + "version": "0.1.8", "acceptableVersions": [ ">=8.0.0" ],