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

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

AI-Viewer 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 4100

Key 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]

Document discovery flow

  1. Primary: pdai/index.json — Canonical document list
  2. Fallback: pdai-documents.json — Cached copy at project root
  3. 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

Document mode layout

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

Agent mode layout

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

Page load sequence

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

Agent output loading

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: German

Default 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 4100

Production 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"]