contexto-api / src /web_ui.py
Dev-ks04
feat: Contexto FastAPI backend - intent-aware summarization engine
39028c9
"""
Simple lightweight web UI for document summarization
"""
import logging
from pathlib import Path
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
import uvicorn
from .summarizer import TechnicalDocumentSummarizer
logger = logging.getLogger(__name__)
# Create FastAPI app for UI
ui_app = FastAPI(title="Document Summarizer UI")
# HTML template - Enhanced with all features
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Intent-Aware Document Summarizer</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--primary: #667eea;
--primary-dark: #764ba2;
--success: #4ade80;
--danger: #f87171;
--warning: #fbbf24;
--info: #60a5fa;
--light: #f8fafc;
--dark: #1e293b;
--border: #e2e8f0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
min-height: 100vh;
color: var(--dark);
}
.wrapper {
display: flex;
min-height: 100vh;
}
.sidebar {
width: 280px;
background: white;
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
overflow-y: auto;
padding: 20px;
}
.main-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.logo {
font-size: 28px;
font-weight: bold;
margin-bottom: 30px;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
display: flex;
align-items: center;
gap: 10px;
}
.nav-tabs {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 30px;
}
.nav-tab {
padding: 12px 16px;
border: none;
background: var(--light);
border-radius: 8px;
cursor: pointer;
font-weight: 500;
transition: all 0.3s;
text-align: left;
display: flex;
align-items: center;
gap: 10px;
color: var(--dark);
}
.nav-tab:hover {
background: var(--border);
transform: translateX(4px);
}
.nav-tab.active {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
}
.settings-section {
margin-top: 30px;
padding-top: 20px;
border-top: 2px solid var(--border);
}
.settings-title {
font-weight: bold;
margin-bottom: 15px;
color: var(--dark);
}
.setting-item {
margin-bottom: 15px;
}
.setting-label {
font-size: 13px;
font-weight: 500;
color: #64748b;
margin-bottom: 8px;
}
.setting-select {
width: 100%;
padding: 8px;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 13px;
}
.container {
background: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
padding: 30px;
max-width: 1200px;
margin: 0 auto;
}
.tabs {
display: flex;
gap: 0;
margin-bottom: 30px;
border-bottom: 2px solid var(--border);
}
.tab-btn {
padding: 12px 24px;
background: none;
border: none;
border-bottom: 3px solid transparent;
cursor: pointer;
font-weight: 500;
color: #64748b;
transition: all 0.3s;
}
.tab-btn:hover {
color: var(--primary);
}
.tab-btn.active {
color: var(--primary);
border-bottom-color: var(--primary);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.header {
margin-bottom: 30px;
}
.header h1 {
font-size: 32px;
margin-bottom: 8px;
}
.header p {
color: #64748b;
}
.form-group {
margin-bottom: 20px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--dark);
font-size: 14px;
}
input[type="text"],
input[type="file"],
select,
textarea {
width: 100%;
padding: 12px;
border: 2px solid var(--border);
border-radius: 8px;
font-size: 14px;
font-family: inherit;
transition: all 0.3s;
}
input[type="text"]:focus,
input[type="file"]:focus,
select:focus,
textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
textarea {
resize: vertical;
min-height: 200px;
}
.drop-zone {
border: 3px dashed var(--primary);
border-radius: 12px;
padding: 40px;
text-align: center;
background: rgba(102, 126, 234, 0.05);
cursor: pointer;
transition: all 0.3s;
margin-bottom: 20px;
}
.drop-zone:hover {
border-color: var(--primary-dark);
background: rgba(102, 126, 234, 0.1);
}
.drop-zone.dragover {
border-color: var(--primary-dark);
background: rgba(102, 126, 234, 0.15);
}
.drop-zone-icon {
font-size: 48px;
margin-bottom: 10px;
}
.buttons {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin-top: 20px;
}
button {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: var(--light);
color: var(--dark);
border: 2px solid var(--border);
}
.btn-secondary:hover:not(:disabled) {
background: var(--border);
}
.btn-success {
background: var(--success);
color: white;
}
.btn-small {
padding: 8px 12px;
font-size: 12px;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.alert {
padding: 16px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
flex-direction: row;
align-items: center;
gap: 12px;
}
.alert.show {
display: flex;
}
.alert-success {
background: #dcfce7;
color: #166534;
border-left: 4px solid var(--success);
}
.alert-error {
background: #fee2e2;
color: #991b1b;
border-left: 4px solid var(--danger);
}
.alert-info {
background: #dbeafe;
color: #1e40af;
border-left: 4px solid var(--info);
}
.loader {
display: none;
text-align: center;
padding: 30px;
}
.spinner {
border: 4px solid var(--light);
border-top: 4px solid var(--primary);
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.progress-container {
margin: 20px 0;
}
.progress-item {
margin-bottom: 15px;
}
.progress-label {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 13px;
font-weight: 500;
}
.progress-bar {
width: 100%;
height: 8px;
background: var(--light);
border-radius: 10px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary) 0%, var(--primary-dark) 100%);
width: 0%;
transition: width 0.3s;
}
.result-card {
background: var(--light);
padding: 20px;
border-radius: 8px;
margin-bottom: 15px;
border-left: 4px solid var(--primary);
}
.result-title {
font-weight: bold;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.result-text {
line-height: 1.6;
color: var(--dark);
max-height: 300px;
overflow-y: auto;
}
.result-meta {
display: flex;
gap: 15px;
margin-top: 12px;
font-size: 12px;
color: #64748b;
}
.result-actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
.history-item {
background: var(--light);
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.3s;
border-left: 4px solid var(--border);
}
.history-item:hover {
border-left-color: var(--primary);
transform: translateX(4px);
}
.history-title {
font-weight: 500;
margin-bottom: 4px;
}
.history-meta {
font-size: 12px;
color: #64748b;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #64748b;
}
.empty-icon {
font-size: 48px;
margin-bottom: 15px;
opacity: 0.5;
}
.file-list {
margin-top: 15px;
}
.file-item {
background: var(--light);
padding: 12px;
border-radius: 8px;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
.file-name {
font-weight: 500;
flex: 1;
}
.file-size {
font-size: 12px;
color: #64748b;
margin-right: 15px;
}
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.badge-success {
background: #dcfce7;
color: #166534;
}
.badge-warning {
background: #fef08a;
color: #713f12;
}
@media (max-width: 768px) {
.wrapper {
flex-direction: column;
}
.sidebar {
width: 100%;
padding: 15px;
}
.form-row {
grid-template-columns: 1fr;
}
.tabs {
flex-wrap: wrap;
}
}
</style>
</head>
<body>
<div class="wrapper">
<!-- Sidebar -->
<div class="sidebar">
<div class="logo">
<i class="fas fa-document-alt"></i> Summarizer
</div>
<div class="nav-tabs">
<button class="nav-tab active" data-tab="single">
<i class="fas fa-file-alt"></i> Single Document
</button>
<button class="nav-tab" data-tab="batch">
<i class="fas fa-files"></i> Batch Upload
</button>
<button class="nav-tab" data-tab="history">
<i class="fas fa-history"></i> History
</button>
<button class="nav-tab" data-tab="settings">
<i class="fas fa-cog"></i> Settings
</button>
</div>
<div class="settings-section">
<div class="settings-title">⚙️ Quick Settings</div>
<div class="setting-item">
<div class="setting-label">Language</div>
<select id="globalLanguage" class="setting-select">
<option value="english">English</option>
<option value="spanish">Spanish</option>
<option value="french">French</option>
<option value="german">German</option>
<option value="italian">Italian</option>
<option value="portuguese">Portuguese</option>
<option value="chinese">Chinese</option>
<option value="japanese">Japanese</option>
<option value="korean">Korean</option>
<option value="arabic">Arabic</option>
</select>
</div>
<div class="setting-item">
<div class="setting-label">Summary Type</div>
<select id="globalIntent" class="setting-select">
<option value="technical_overview">Technical Overview</option>
<option value="detailed_analysis">Detailed Analysis</option>
<option value="methodology">Methodology</option>
<option value="results">Results</option>
<option value="conclusion">Conclusion</option>
<option value="abstract">Abstract</option>
</select>
</div>
<div class="setting-item">
<div class="setting-label">Quality Preference</div>
<select id="globalQuality" class="setting-select">
<option value="speed">⚡ Speed</option>
<option value="balanced" selected>⚖️ Balanced</option>
<option value="quality">✨ Quality</option>
</select>
</div>
</div>
</div>
<!-- Main Content -->
<div class="main-content">
<div class="container">
<!-- Single Document Tab -->
<div class="tab-content active" id="single-tab">
<div class="header">
<h1>📄 Single Document</h1>
<p>Paste or upload a document to generate a summary</p>
</div>
<div id="alertBox" class="alert"></div>
<div class="tabs">
<button class="tab-btn active" data-mode="text">
<i class="fas fa-keyboard"></i> Paste Text
</button>
<button class="tab-btn" data-mode="file">
<i class="fas fa-upload"></i> Upload File
</button>
</div>
<!-- Text Input -->
<div class="mode-content" id="text-mode">
<div class="form-group">
<label for="documentText">Document Text:</label>
<textarea id="documentText" placeholder="Paste your document here..."></textarea>
</div>
</div>
<!-- File Upload -->
<div class="mode-content" id="file-mode" style="display:none;">
<div class="drop-zone" id="dropZone">
<div class="drop-zone-icon">📤</div>
<p style="font-weight: 500; margin-bottom: 5px;">Drag and drop your file here</p>
<p style="font-size: 13px; color: #64748b;">or click to select (TXT, PDF supported)</p>
</div>
<input type="file" id="fileInput" style="display:none;" accept=".txt,.pdf">
<div id="fileName" class="file-list"></div>
</div>
<!-- Options -->
<div class="form-row">
<div class="form-group">
<label for="singleLanguage">Language:</label>
<select id="singleLanguage">
<option value="">Use Global Setting</option>
<option value="english">English</option>
<option value="spanish">Spanish</option>
<option value="french">French</option>
<option value="german">German</option>
<option value="chinese">Chinese</option>
<option value="japanese">Japanese</option>
</select>
</div>
<div class="form-group">
<label for="singleIntent">Summary Type:</label>
<select id="singleIntent">
<option value="">Use Global Setting</option>
<option value="technical_overview">Technical Overview</option>
<option value="methodology">Methodology</option>
<option value="results">Results</option>
<option value="conclusion">Conclusion</option>
</select>
</div>
<div class="form-group">
<label for="singleQuality">Quality:</label>
<select id="singleQuality">
<option value="">Use Global Setting</option>
<option value="speed">⚡ Speed</option>
<option value="balanced">⚖️ Balanced</option>
<option value="quality">✨ Quality</option>
</select>
</div>
</div>
<div class="buttons">
<button class="btn-primary" id="generateBtn">
<i class="fas fa-magic"></i> Generate Summary
</button>
<button class="btn-secondary" id="clearBtn">
<i class="fas fa-trash"></i> Clear
</button>
</div>
<!-- Progress -->
<div class="loader" id="loader">
<div class="spinner"></div>
<p>Analyzing document and generating summary...</p>
</div>
<!-- Result -->
<div id="resultContainer" style="margin-top: 30px;"></div>
</div>
<!-- Batch Upload Tab -->
<div class="tab-content" id="batch-tab">
<div class="header">
<h1>📁 Batch Processing</h1>
<p>Upload multiple documents for processing</p>
</div>
<div id="batchAlertBox" class="alert"></div>
<div class="drop-zone" id="batchDropZone">
<div class="drop-zone-icon">📦</div>
<p style="font-weight: 500; margin-bottom: 5px;">Drag multiple files here</p>
<p style="font-size: 13px; color: #64748b;">or click to select</p>
</div>
<input type="file" id="batchFileInput" style="display:none;" multiple accept=".txt,.pdf">
<div id="batchFileList" class="file-list"></div>
<div class="buttons" style="margin-top: 20px;">
<button class="btn-primary" id="processBatchBtn" disabled>
<i class="fas fa-play"></i> Process All (<span id="fileCount">0</span>)
</button>
<button class="btn-secondary" id="clearBatchBtn">
<i class="fas fa-trash"></i> Clear List
</button>
</div>
<!-- Progress -->
<div class="progress-container" id="batchProgress" style="display:none; margin-top: 30px;"></div>
<!-- Results -->
<div id="batchResultContainer" style="margin-top: 30px;"></div>
</div>
<!-- History Tab -->
<div class="tab-content" id="history-tab">
<div class="header">
<h1>📜 Summary History</h1>
<p>View and manage your previous summaries</p>
</div>
<div class="buttons">
<button class="btn-secondary" id="clearHistoryBtn">
<i class="fas fa-trash"></i> Clear All History
</button>
</div>
<div id="historyContainer" style="margin-top: 20px;"></div>
</div>
<!-- Settings Tab -->
<div class="tab-content" id="settings-tab">
<div class="header">
<h1>⚙️ Settings</h1>
<p>Configure your preferences</p>
</div>
<div class="form-group">
<label>Default Language</label>
<select id="settingsLanguage">
<option value="english">English</option>
<option value="spanish">Spanish</option>
<option value="french">French</option>
<option value="german">German</option>
<option value="chinese">Chinese</option>
<option value="japanese">Japanese</option>
</select>
</div>
<div class="form-group">
<label>Default Summary Type</label>
<select id="settingsIntent">
<option value="technical_overview">Technical Overview</option>
<option value="detailed_analysis">Detailed Analysis</option>
<option value="methodology">Methodology</option>
<option value="results">Results</option>
<option value="conclusion">Conclusion</option>
</select>
</div>
<div class="form-group">
<label>Quality Preference</label>
<select id="settingsQuality">
<option value="speed">⚡ Speed (Fastest)</option>
<option value="balanced" selected>⚖️ Balanced</option>
<option value="quality">✨ Quality (Best)</option>
</select>
</div>
<div class="buttons">
<button class="btn-primary" id="saveSettingsBtn">
<i class="fas fa-save"></i> Save Settings
</button>
</div>
</div>
</div>
</div>
</div>
<script>
// State Management
const state = {
currentMode: 'text',
batchFiles: [],
history: JSON.parse(localStorage.getItem('summaryHistory')) || [],
settings: JSON.parse(localStorage.getItem('settings')) || {
language: 'english',
intent: 'technical_overview',
quality: 'balanced'
}
};
// Initialize
document.addEventListener('DOMContentLoaded', () => {
loadSettings();
setupEventListeners();
renderHistory();
});
function setupEventListeners() {
// Tab navigation
document.querySelectorAll('.nav-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
const tabName = e.currentTarget.dataset.tab;
switchTab(tabName);
});
});
// Mode switching
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const mode = e.currentTarget.dataset.mode;
switchMode(mode);
});
});
// Single document
document.getElementById('generateBtn').addEventListener('click', generateSingle);
document.getElementById('clearBtn').addEventListener('click', () => {
document.getElementById('documentText').value = '';
document.getElementById('resultContainer').innerHTML = '';
});
// File upload
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
handleFileSelect(e.dataTransfer.files[0]);
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) handleFileSelect(e.target.files[0]);
});
// Batch
const batchDropZone = document.getElementById('batchDropZone');
const batchFileInput = document.getElementById('batchFileInput');
batchDropZone.addEventListener('click', () => batchFileInput.click());
batchDropZone.addEventListener('dragover', (e) => {
e.preventDefault();
batchDropZone.classList.add('dragover');
});
batchDropZone.addEventListener('dragleave', () => batchDropZone.classList.remove('dragover'));
batchDropZone.addEventListener('drop', (e) => {
e.preventDefault();
batchDropZone.classList.remove('dragover');
handleBatchFiles(e.dataTransfer.files);
});
batchFileInput.addEventListener('change', (e) => handleBatchFiles(e.target.files));
document.getElementById('processBatchBtn').addEventListener('click', processBatch);
document.getElementById('clearBatchBtn').addEventListener('click', () => {
state.batchFiles = [];
renderBatchList();
});
// History
document.getElementById('clearHistoryBtn').addEventListener('click', () => {
if (confirm('Clear all history?')) {
state.history = [];
localStorage.removeItem('summaryHistory');
renderHistory();
}
});
// Settings
document.getElementById('saveSettingsBtn').addEventListener('click', saveSettings);
}
function switchTab(tabName) {
document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
document.getElementById(tabName + '-tab').classList.add('active');
}
function switchMode(mode) {
state.currentMode = mode;
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.mode-content').forEach(m => m.style.display = 'none');
event.target.classList.add('active');
document.getElementById(mode + '-mode').style.display = 'block';
}
async function handleFileSelect(file) {
if (!file) return;
const text = await file.text();
document.getElementById('documentText').value = text;
document.getElementById('fileName').innerHTML = \`<div class="file-item"><span class="file-name">\${file.name}</span><span class="file-size">\${(file.size / 1024).toFixed(1)} KB</span></div>\`;
}
function handleBatchFiles(files) {
state.batchFiles = Array.from(files).map((f, i) => ({
id: Date.now() + i,
file: f,
status: 'pending'
}));
renderBatchList();
}
function renderBatchList() {
const container = document.getElementById('batchFileList');
if (state.batchFiles.length === 0) {
container.innerHTML = '';
document.getElementById('processBatchBtn').disabled = true;
document.getElementById('fileCount').textContent = '0';
return;
}
container.innerHTML = state.batchFiles.map(f => \`
<div class="file-item">
<span class="file-name">\${f.file.name}</span>
<span class="file-size">\${(f.file.size / 1024).toFixed(1)} KB</span>
<span class="badge badge-\${f.status === 'pending' ? 'warning' : 'success'}">\${f.status}</span>
</div>
\`).join('');
document.getElementById('processBatchBtn').disabled = false;
document.getElementById('fileCount').textContent = state.batchFiles.length;
}
async function generateSingle() {
const text = document.getElementById('documentText').value.trim();
if (!text) {
showAlert('alertBox', 'Please enter document text', 'error');
return;
}
const language = document.getElementById('singleLanguage').value || state.settings.language;
const intent = document.getElementById('singleIntent').value || state.settings.intent;
const quality = document.getElementById('singleQuality').value || state.settings.quality;
await summarizeDocument(text, language, intent, quality, 'alertBox', 'resultContainer');
}
async function summarizeDocument(text, language, intent, quality, alertId, resultId, fileName = null) {
const alertBox = document.getElementById(alertId);
const resultContainer = document.getElementById(resultId);
const loader = document.getElementById('loader');
loader.style.display = 'block';
try {
const response = await fetch('/api/summarize', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
document: text,
language: language,
intent: intent,
quality_preference: quality,
summary_level: 'brief'
})
});
if (!response.ok) throw new Error('API Error');
const data = await response.json();
// Save to history
const historyItem = {
id: Date.now(),
title: fileName || text.substring(0, 50) + '...',
summary: data.summary,
language: language,
intent: intent,
timestamp: new Date().toLocaleString(),
length: data.length
};
state.history.unshift(historyItem);
localStorage.setItem('summaryHistory', JSON.stringify(state.history.slice(0, 50)));
// Display result
const html = \`
<div class="result-card">
<div class="result-title">
<span>✅ Summary Generated</span>
<span class="badge badge-success">\${data.length} words</span>
</div>
<div class="result-text">\${data.summary}</div>
<div class="result-meta">
<span>🌍 \${language}</span>
<span>🎯 \${intent}</span>
<span>⏱️ \${new Date().toLocaleTimeString()}</span>
</div>
<div class="result-actions">
<button class="btn-secondary btn-small" onclick="downloadResult('\${btoa(data.summary)}', 'txt')">
<i class="fas fa-download"></i> TXT
</button>
<button class="btn-secondary btn-small" onclick="downloadResult('\${btoa(JSON.stringify(data))}', 'json')">
<i class="fas fa-download"></i> JSON
</button>
<button class="btn-secondary btn-small" onclick="copyToClipboard('\${btoa(data.summary)}')">
<i class="fas fa-copy"></i> Copy
</button>
</div>
</div>
\`;
resultContainer.innerHTML = html;
showAlert(alertId, 'Summary generated successfully!', 'success');
} catch (error) {
showAlert(alertId, 'Error: ' + error.message, 'error');
} finally {
loader.style.display = 'none';
}
}
async function processBatch() {
if (state.batchFiles.length === 0) return;
const progressContainer = document.getElementById('batchProgress');
const resultContainer = document.getElementById('batchResultContainer');
progressContainer.style.display = 'block';
resultContainer.innerHTML = '';
let html = '';
state.batchFiles.forEach((f, i) => {
html += \`
<div class="progress-item">
<div class="progress-label">
<span>\${f.file.name}</span>
<span id="progress-\${i}">0%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" id="fill-\${i}"></div>
</div>
</div>
\`;
});
progressContainer.innerHTML = html;
for (let i = 0; i < state.batchFiles.length; i++) {
const f = state.batchFiles[i];
try {
const text = await f.file.text();
// Simulate progress
for (let p = 0; p <= 100; p += 20) {
document.getElementById('fill-' + i).style.width = p + '%';
document.getElementById('progress-' + i).textContent = p + '%';
await new Promise(resolve => setTimeout(resolve, 100));
}
f.status = 'completed';
state.batchFiles[i] = f;
renderBatchList();
} catch (error) {
f.status = 'error';
state.batchFiles[i] = f;
renderBatchList();
}
}
showAlert('batchAlertBox', 'Batch processing completed!', 'success');
}
function downloadResult(encoded, format) {
const data = atob(encoded);
const el = document.createElement('a');
el.href = format === 'json' ? 'data:application/json;base64,' + btoa(data) : 'data:text/plain;base64,' + btoa(data);
el.download = 'summary.' + format;
el.click();
}
function copyToClipboard(encoded) {
navigator.clipboard.writeText(atob(encoded));
alert('Copied to clipboard!');
}
function renderHistory() {
const container = document.getElementById('historyContainer');
if (state.history.length === 0) {
container.innerHTML = '<div class="empty-state"><div class="empty-icon">📭</div><p>No history yet</p></div>';
return;
}
container.innerHTML = state.history.map(h => \`
<div class="history-item">
<div class="history-title">\${h.title}</div>
<div class="history-meta">\${h.timestamp} • \${h.length} words • \${h.intent}</div>
</div>
\`).join('');
}
function showAlert(id, message, type) {
const alert = document.getElementById(id);
alert.innerHTML = \`<i class="fas fa-\${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i> \${message}\`;
alert.className = 'alert show alert-' + type;
setTimeout(() => alert.classList.remove('show'), 5000);
}
function saveSettings() {
state.settings = {
language: document.getElementById('settingsLanguage').value,
intent: document.getElementById('settingsIntent').value,
quality: document.getElementById('settingsQuality').value
};
localStorage.setItem('settings', JSON.stringify(state.settings));
document.getElementById('globalLanguage').value = state.settings.language;
document.getElementById('globalIntent').value = state.settings.intent;
document.getElementById('globalQuality').value = state.settings.quality;
showAlert('alertBox', 'Settings saved!', 'success');
}
function loadSettings() {
document.getElementById('settingsLanguage').value = state.settings.language;
document.getElementById('settingsIntent').value = state.settings.intent;
document.getElementById('settingsQuality').value = state.settings.quality;
document.getElementById('globalLanguage').value = state.settings.language;
document.getElementById('globalIntent').value = state.settings.intent;
document.getElementById('globalQuality').value = state.settings.quality;
}
</script>
</body>
</html>
"""
@ui_app.get("/", response_class=HTMLResponse)
async def get_ui():
"""Serve the UI HTML."""
return HTML_TEMPLATE
# Global summarizer for UI
_summarizer = None
def get_summarizer_ui(language: str = "english"):
"""Get or create summarizer instance."""
global _summarizer
if _summarizer is None:
_summarizer = TechnicalDocumentSummarizer(language=language)
return _summarizer
@ui_app.post("/api/summarize")
async def api_summarize(request: dict):
"""API endpoint for summarization."""
try:
document = request.get('document', '')
language = request.get('language', 'english')
intent = request.get('intent', 'technical_overview')
quality_preference = request.get('quality_preference', 'balanced')
if not document:
return {"error": "Document is required"}, 400
summarizer = get_summarizer_ui(language)
# Use auto_summarize for better model selection
result = summarizer.auto_summarize(
document=document,
intent=intent,
quality_preference=quality_preference,
language=language
)
return {
"summary": result.get('summary', result),
"language": language,
"intent": intent,
"length": len(str(result.get('summary', result)).split()),
"model": result.get('model', 'auto'),
"complexity": result.get('complexity', 'unknown'),
"use_rag": result.get('use_rag', False)
}
except Exception as e:
logger.error(f"Error: {str(e)}")
return {"error": str(e)}
def run_ui(host: str = "0.0.0.0", port: int = 8001):
"""
Run the web UI.
Args:
host: Host to bind to
port: Port to bind to
"""
logger.info(f"Starting Web UI at http://{host}:{port}")
uvicorn.run(
ui_app,
host=host,
port=port,
log_level="info"
)
if __name__ == "__main__":
run_ui()