flowchart TB
subgraph Client["Web Browser"]
VIEWER[viewer.html]
JS[JavaScript Engine]
UI[Interactive UI]
end
subgraph Server["HTTP Server"]
PYTHON[python -m http.server]
STATIC[Static File Serving]
end
subgraph Storage["PDAI Documents"]
PAGES["pages/page_NNN/"]
SEGS["segments/"]
CONFIG["manifest.json"]
end
VIEWER --> JS
JS --> UI
JS --> STATIC
PYTHON --> STATIC
STATIC --> PAGES
STATIC --> SEGS
STATIC --> CONFIG
Viewer Architecture
Static HTML viewer design and data flow
Overview
AI-Viewer is a static HTML/JavaScript application that runs in any web browser. It serves PDAI documents through a simple HTTP server, with no backend framework required.
System Architecture
Core Components
Static HTTP Server
The viewer works with any HTTP server. Python’s built-in server is recommended:
# Start from project root
cd /path/to/2026project
python -m http.server 4100Key requirements:
Serves static files from project root
No special configuration needed
Works with nginx, Apache, or any HTTP server
Viewer Application (viewer.html)
Single-page application with all logic embedded:
<!DOCTYPE html>
<html>
<head>
<title>AI Scholar</title>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/..."></script>
<style>/* All styles inline */</style>
</head>
<body>
<nav><!-- Navigation bar --></nav>
<main><!-- Content panels --></main>
<script>
class DocumentViewer {
// All viewer logic
}
</script>
</body>
</html>Key features:
Self-contained HTML file
No build process required
MathJax loaded from CDN
All styles and scripts inline
Document Discovery
The viewer discovers documents through multiple sources:
flowchart LR
VIEWER[viewer.html]
INDEX[pdai/index.json]
CACHE[pdai-documents.json]
FALLBACK[Known documents]
VIEWER --> INDEX
INDEX -->|success| CATALOG
INDEX -->|fail| CACHE
CACHE -->|fail| FALLBACK
CATALOG[Document Catalog]
- Primary:
pdai/index.json— Canonical document list - Fallback:
pdai-documents.json— Cached copy at project root - Last resort: Hardcoded list of known documents
Page-Centric Structure
All path resolution uses the page-centric structure:
pdai/DocumentName/
└── pages/
└── page_NNN/
├── page_NNN.png # Image
├── page_NNN.xml # OCR XML
├── page_NNN.html # HTML conversion
├── page_NNN_translation_english.xml # Agent output
└── page_NNN_fig_1.png # Extracted figure
Path Helper Functions
class DocumentViewer {
getPagePath(pageIdx, ext) {
const p = String(pageIdx).padStart(3, '0');
return `${this.apiBase}/pages/page_${p}/page_${p}.${ext}`;
}
getAgentPath(pageIdx, agentName) {
const p = String(pageIdx).padStart(3, '0');
const agent = agentName.replace(/-/g, '_');
return `${this.apiBase}/pages/page_${p}/page_${p}_${agent}.xml`;
}
getFigurePath(pageIdx, figNum) {
const p = String(pageIdx).padStart(3, '0');
return `${this.apiBase}/pages/page_${p}/page_${p}_fig_${figNum}.png`;
}
}Viewing Modes
Document Mode
Dual-pane layout for reading:
flowchart LR
subgraph Left["Left Pane"]
IMG[Page Image]
end
subgraph Right["Right Pane"]
TEXT[OCR Text / Translation]
end
Left --> Right
Content loading:
- Image:
pages/page_NNN/page_NNN.png - Original:
pages/page_NNN/page_NNN.html - Translation:
pages/page_NNN/page_NNN_translation_{lang}.xml
Agent Mode
Three-pane layout for agent work:
flowchart LR
subgraph Left["Source Input"]
SRC[Image or XML]
end
subgraph Middle["Agent Output"]
OUT[Translation / Analysis]
end
subgraph Right["Agent Control"]
NAV[Navigator]
CFG[Configuration]
ENGINE[AI Engine]
end
Left --> Middle --> Right
Data Flow
Page Load Sequence
sequenceDiagram
participant Browser
participant Server
participant PDAI
Browser->>Server: GET /ai-viewer/viewer.html
Server-->>Browser: viewer.html
Browser->>Server: GET /pdai/E57/pages/page_000/page_000.png
Server->>PDAI: Read image
PDAI-->>Server: Image bytes
Server-->>Browser: Image response
Browser->>Server: GET /pdai/E57/pages/page_000/page_000.html
Server->>PDAI: Read HTML
PDAI-->>Server: HTML content
Server-->>Browser: HTML response
Note over Browser: Render page with MathJax
Agent Output Loading
sequenceDiagram
participant Browser
participant Server
participant PDAI
Note over Browser: User selects "English Translation"
Browser->>Server: GET /pdai/E57/pages/page_000/page_000_translation_english.xml
alt File exists
Server->>PDAI: Read XML
PDAI-->>Server: Translation XML
Server-->>Browser: XML response
Note over Browser: Display translation
else File not found
Server-->>Browser: 404 Not Found
Note over Browser: Show "Not yet generated" placeholder
end
Configuration
Document Configuration
Each document can have an ai-orchestrator.yml:
version: "1.0"
document: E57
model: gemini-3-flash-preview
agents:
translation-english:
display_name: English Translation
model: gemini-3-flash-preview
instruction: translation.j2
variables:
target_lang: en
target_lang_full: English
translation-german:
display_name: German Translation
model: gemini-3-flash-preview
instruction: translation.j2
variables:
target_lang: de
target_lang_full: GermanDefault Configuration
When no orchestrator config exists, the viewer uses defaults:
const defaultConfig = {
agents: {
'translation-english': {
display_name: 'English Translation',
model: 'gemini-3-flash-preview',
instruction: 'translation.j2'
},
'translation-german': {
display_name: 'German Translation',
model: 'gemini-3-flash-preview',
instruction: 'translation.j2'
}
}
};Keyboard Handling
document.addEventListener('keydown', (e) => {
switch(e.key) {
case 'ArrowLeft':
this.prevPage();
break;
case 'ArrowRight':
this.nextPage();
break;
case 'ArrowUp':
this.zoomIn();
break;
case 'ArrowDown':
this.zoomOut();
break;
case 'd':
case 'D':
this.setMode('document');
break;
case 'a':
case 'A':
this.setMode('agent');
break;
case 'r':
case 'R':
this.refreshAgentResult();
break;
case 'f':
case 'F':
this.toggleFullscreen();
break;
case 'h':
case 'H':
this.toggleNavbar();
break;
}
});Performance Considerations
Browser Caching
Static files are cached by the browser:
| Content Type | Cache Strategy |
|---|---|
| Page images | Long cache (immutable) |
| HTML/XML content | Cache with revalidation |
| Viewer assets | Long cache |
Lazy Loading
Content is loaded on demand:
async loadPage(pageIdx) {
// Load image
const img = new Image();
img.src = this.getPagePath(pageIdx, 'png');
// Load text
const response = await fetch(this.getPagePath(pageIdx, 'html'));
const html = await response.text();
// Render
this.displayImage(img);
this.displayText(html);
}Page Count Detection
Binary search for efficient page count detection:
async detectPageCount() {
let low = 0, high = 1000;
while (low < high) {
const mid = Math.floor((low + high + 1) / 2);
const path = this.getPagePath(mid, 'png');
try {
const res = await fetch(path, { method: 'HEAD' });
if (res.ok) {
low = mid;
} else {
high = mid - 1;
}
} catch {
high = mid - 1;
}
}
return low + 1; // Convert 0-indexed to count
}Extension Points
Custom Themes
Add CSS variables for theming:
:root {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--text-primary: #eee;
--accent: #e94560;
}Custom Agents
Add agents to the default configuration:
defaultConfig.agents['custom-analysis'] = {
display_name: 'Custom Analysis',
model: 'gemini-3-flash-preview',
instruction: 'custom-analysis.j2'
};MathJax Configuration
Configure MathJax for specific needs:
window.MathJax = {
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']],
displayMath: [['$$', '$$'], ['\\[', '\\]']]
},
options: {
skipHtmlTags: ['script', 'noscript', 'style', 'textarea']
}
};Deployment
Development
# Simple Python server
cd /path/to/2026project
python -m http.server 4100Production with nginx
server {
listen 80;
server_name docs.example.com;
root /path/to/2026project;
location / {
try_files $uri $uri/ =404;
}
location /ai-viewer/ {
alias /path/to/2026project/ai-viewer/;
}
location /pdai/ {
alias /path/to/2026project/pdai/;
}
}
Docker
FROM python:3.12-slim
WORKDIR /app
COPY . .
EXPOSE 4100
CMD ["python", "-m", "http.server", "4100"]