Varun2007's picture
initial clean deployment commit with compilers
8c3e275
Raw
History Blame Contribute Delete
57.6 kB
// SPDX-FileCopyrightText: 2026 Team Centurions
// SPDX-License-Identifier: AGPL-3.0-or-later
// API Service for PageParse Frontend
// Implements dual mode: Live backend communication + Offline browser-local fallback with full simulations.
export interface Task {
id: number;
page_id: number;
source_id?: number;
type?: string;
task: string;
content?: string;
due_date: string | null;
priority: 'high' | 'medium' | 'low' | null;
category: string | null;
speaker?: string | null;
timestamp?: string | null;
status: 'todo' | 'done' | null;
ocr_confidence: number;
confidence?: number;
user_edited?: number;
filename?: string;
}
export interface PageRecord {
id: number;
filename: string;
source_type?: 'image' | 'audio' | 'video' | 'document';
source_path?: string;
captured_date: string;
title?: string;
summary?: string;
raw_ocr_text: string;
raw_text?: string;
created_at: string;
image_path?: string;
cleaned_image_path?: string;
}
export interface TelemetryData {
cpu_percent: number;
ram_used_mb: number;
ram_total_mb: number;
active_tasks: number;
}
export interface ProcessingJob {
id: string;
filename: string;
status: 'queued' | 'preprocessing' | 'ocr' | 'extracting' | 'saved' | 'failed';
progress: number;
error?: string;
tasksExtracted?: Task[];
}
export const API_BASE_URL = window.location.origin.includes('5173') ? 'http://127.0.0.1:8000' : window.location.origin;
// Check if airgap mode is saved in localStorage
export const getAirgapMode = (): boolean => {
return localStorage.getItem('pageparse_airgap') === 'true';
};
export const setAirgapMode = (val: boolean) => {
localStorage.setItem('pageparse_airgap', val ? 'true' : 'false');
};
export const INDIAN_LANGUAGES = [
'English',
'Assamese',
'Bengali',
'Bodo',
'Dogri',
'Gujarati',
'Hindi',
'Kannada',
'Kashmiri',
'Konkani',
'Maithili',
'Malayalam',
'Manipuri',
'Marathi',
'Nepali',
'Odia',
'Punjabi',
'Sanskrit',
'Santali',
'Sindhi',
'Tamil',
'Telugu',
'Urdu'
];
const MOCK_TRANSLATIONS: Record<string, Record<string, string>> = {
'Hindi': {
'javascript': `1.2 जावास्क्रिप्ट का उद्देश्य:
- जावास्क्रिप्ट का प्राथमिक उद्देश्य वेब पेजों पर क्लाइंट-साइड स्क्रिप्टिंग को सक्षम करना है। इसका मतलब है कि यह डेवलपर्स को सीधे उपयोगकर्ता के वेब ब्राउज़र के भीतर वेब पेजों की सामग्री और व्यवहार में हेरफेर करने की अनुमति देता है।
- जावास्क्रिप्ट के साथ, हम गतिशील रूप से वेब पेजों, तत्वों को अपडेट कर सकते हैं, फॉर्म इनपुट को मान्य कर सकते हैं, और उपयोगकर्ता इंटरैक्शन जैसे कि क्लिक, माउस की गति और कीबोर्ड इनपुट का जवाब दे सकते हैं।
- इसके अतिरिक्त, जावास्क्रिप्ट बिना पेज रीलोड के डेटा प्राप्त करने के लिए AJAX (अतुल्यकालिक जावास्क्रिप्ट और XML) के माध्यम से वेब सर्वर के साथ बातचीत कर सकता है।
सर्वाधिकार सुरक्षित CodeWithCurious.com
1.3 विकास वातावरण स्थापित करना:
जावास्क्रिप्ट में कोडिंग शुरू करने के लिए, हमें एक विकास वातावरण स्थापित करने की आवश्यकता है।
यहाँ बुनियादी कदम हैं:`,
'voice_memo': `वॉयस मेमो प्रतिलेखन:
सेंचुरियन परियोजना प्रस्ताव की समीक्षा करें। उच्च प्राथमिकता वाली वस्तुओं पर विवरण की जांच करने की आवश्यकता है। कार्रवाई का विषय: मध्यम प्राथमिकता के साथ 2026-06-28 तक बैकएंड कोड संरचना की समीक्षा करें। हमें आज टीम कार्यक्षेत्र के लिए शाम 6 बजे से पहले दूध और अंडे खरीदने चाहिए। साथ ही उच्च प्राथमिकता के साथ 2026-06-29 को होने वाले डेमो के लिए स्लाइड तैयार करने की आवश्यकता है।`,
'meeting': `बैठक प्रतिलेख:
वक्ता 1: सभी का स्वागत है। हमें सेंचुरियन परियोजना प्रस्ताव प्रस्तुत करना होगा। आइए इसे उच्च प्राथमिकता दें।
वक्ता 2: सहमत हूँ। इसके अलावा, मैं 2026-06-28 तक बैकएंड कोड संरचना की समीक्षा करूँगा।
वक्ता 1: बिल्कुल सही। सुनिश्चित करें कि हम पेजपार्स (PageParse) के लिए रीडमी (README) फ़ाइल भी तैयार कर लें।
=== ऑन-स्क्रीन टेक्स्ट (OCR) ===
पेजपार्स रिलीज समयरेखा:
- प्रस्ताव प्रस्तुत करें (उच्च प्राथमिकता)
- रीडमी का मसौदा तैयार करें (कम प्राथमिकता)`,
'form': `स्कैन किया हुआ फ़ॉर्म:
शीर्षक: प्रोजेक्ट सेंचुरियन
कार्य: पेजपार्स के लिए रीडमी फ़ाइल का मसौदा तैयार करें। प्राथमिकता: कम देय: 2026-06-30 स्थिति: पूर्ण
कार्य: सेंचुरियन परियोजना प्रस्ताव प्रस्तुत करें। प्राथमिकता: उच्च देय: 2026-06-25 स्थिति: पूर्ण`,
'sheet': `स्प्रेडशीट डेटा तालिका:
कार्य | प्राथमिकता | देय तिथि | स्थिति
सेंचुरियन परियोजना प्रस्ताव प्रस्तुत करें | उच्च | 2026-06-25 | पूर्ण
शाम 6 बजे से पहले दूध और अंडे खरीदें | कम | | कार्य सूची
बैकएंड कोड संरचना की समीक्षा करें | मध्यम | 2026-06-28 | कार्य सूची
डेमो के लिए स्लाइड तैयार करें | उच्च | | कार्य सूची
पेजपार्स के लिए रीडमी फ़ाइल का मसौदा तैयार करें | कम | | पूर्ण`
},
'Telugu': {
'javascript': `1.2 జావాస్క్రిప్ట్ యొక్క ఉద్దేశ్యం:
- జావాస్క్రిప్ట్ యొక్క ప్రాథమిక ఉద్దేశ్యం వెబ్ పేజీలలో క్లయింట్-సైడ్ స్క్రిప్టింగ్‌ను ప్రారంభించడం. అంటే వినియోగదారు వెబ్ బ్రౌజర్‌లో నేరుగా వెబ్ పేజీల కంటెంట్ మరియు ప్రవర్తనను మార్చడానికి ఇది డెవలపర్‌లను అనుమతిస్తుంది.
- జావాస్క్రిప్ట్‌తో, మనం వెబ్ పేజీలను, అంశాలను డైనమిక్‌గా అప్‌డేట్ చేయవచ్చు, ఫారమ్ ఇన్‌పుట్‌లను ధృవీకరించవచ్చు మరియు క్లిక్‌లు, మౌస్ కదలికలు మరియు కీబోర్డ్ ఇన్‌పుట్ వంటి వినియోగదారు పరస్పర చర్యలకు ప్రతిస్పందించవచ్చు.
- అదనంగా, పేజీ రీలోడ్ అవసరం లేకుండా డేటాను పొందడానికి AJAX ద్వారా జావాస్క్రిప్ట్ వెబ్ సర్వర్‌లతో పరస్పర చర్య చేయగలదు.
కాపీరైట్ CodeWithCurious.com ద్వారా
1.3 డెవలప్‌మెంట్ ఎన్విరాన్‌మెంట్‌ను సెటప్ చేయడం:
జావాస్క్రిప్ట్‌లో కోడింగ్ ప్రారంభించడానికి, మనకు డెవలప్‌మెంట్ ఎన్విరాన్‌మెంట్ సెటప్ అవసరం.
ఇక్కడ ప్రాథమిక దశలు ఉన్నాయి:`,
'voice_memo': `వాయిస్ మెమో ట్రాన్స్క్రిప్షన్:
సెంచూరియన్స్ ప్రాజెక్ట్ ప్రతిపాదనను సమీక్షించండి. అధిక ప్రాధాన్యత కలిగిన అంశాలపై వివరాలను తనిఖీ చేయాలి. కార్యాచరణ అంశం: మధ్యస్థ ప్రాధాన్యతతో 2026-06-28 నాటికి బ్యాకెండ్ కోడ్ నిర్మాణాన్ని సమీక్షించండి. ఈరోజు టీమ్ వర్క్‌స్పేస్ కోసం సాయంత్రం 6 గంటల కంటే ముందే పాలు మరియు గుడ్లు కొనాలి. అలాగే అధిక ప్రాధాన్యత కలిగిన 2026-06-29 నాటికి డెమో కోసం స్లైడ్‌లను సిద్ధం చేయాలి.`,
'meeting': `సమావేశం ట్రాన్స్క్రిప్ట్:
స్పీకర్ 1: అందరికీ స్వాగతం. మేము సెంచూరియన్స్ ప్రాజెక్ట్ ప్రతిపాదనను సమర్పించాలి. దానికి అధిక ప్రాధాన్యత ఇద్దాం.
స్పీకర్ 2: అంగీకరిస్తున్నాను. అలాగే, నేను 2026-06-28 నాటికి బ్యాకెండ్ కోడ్ నిర్మాణాన్ని సమీక్షిస్తాను.
స్పీకర్ 1: పర్ఫెక్ట్. పేజ్ పార్స్ (PageParse) కోసం రీడ్‌మీ (README) ఫైల్‌ను కూడా సిద్ధం చేస్తున్నామని నిర్ధారించుకోండి.
=== స్క్రీన్‌పై వచనం (OCR) ===
పేజ్ పార్స్ విడుదల కాలక్రమం:
- ప్రతిపాదనను సమర్పించండి (అధిక ప్రాధాన్యత)
- రీడ్‌మీ ముసాయిదాను సిద్ధం చేయండి (తక్కువ ప్రాధాన్యత)`,
'form': `స్కాన్ చేసిన ఫారమ్:
శీర్షిక: ప్రాజెక్ట్ సెంచూరియన్స్
టాస్క్: పేజ్ పార్స్ కోసం రీడ్‌మీ ఫైల్‌ను సిద్ధం చేయండి. ప్రాధాన్యత: తక్కువ గడువు: 2026-06-30 స్థితి: పూర్తయింది
టాస్క్: సెంచూరియన్స్ ప్రాజెక్ట్ ప్రతిపాదనను సమర్పించండి. ప్రాధాన్యత: ఎక్కువ గడువు: 2026-06-25 స్థితి: పూర్తయింది`,
'sheet': `స్ప్రెడ్‌షీట్ డేటా పట్టిక:
టాస్క్ | ప్రాధాన్యత | గడువు తేదీ | స్థితి
సెంచూరియన్స్ ప్రాజెక్ట్ ప్రతిపాదనను సమర్పించండి | ఎక్కువ | 2026-06-25 | పూర్తయింది
సాయంత్రం 6 గంటల లోపు పాలు & గుడ్లు కొనండి | తక్కువ | | చేయవలసినవి
బ్యాకెండ్ కోడ్ నిర్మాణాన్ని సమీక్షించండి | మధ్యస్థం | 2026-06-28 | చేయవలసినవి
డెమో కోసం స్లైడ్‌లను సిద్ధం చేయండి | ఎక్కువ | | చేయవలసినవి
పేజ్ పార్స్ కోసం రీడ్‌మీ ఫైల్‌ను సిద్ధం చేయండి | తక్కువ | | పూర్తయింది`
},
'Tamil': {
'javascript': `1.2 ஜாவாஸ்கிரிப்ட்டின் நோக்கம்:
- ஜாவாஸ்கிரிப்ட்டின் முதன்மை நோக்கம் வலைப்பக்கங்களில் கிளையண்ட்-சைட் ஸ்கிரிப்டிங்கை இயக்குவதாகும். இதன் பொருள் பயனர் வலை உலாவியிலேயே வலைப்பக்கங்களின் உள்ளடக்கம் மற்றும் நடத்தையை கையாள டெவலப்பர்களை இது அனுமதிக்கிறது.
- ஜாவாஸ்கிரிப்ட் மூலம், வலைப்பக்கங்கள், கூறுகளை மாறும் வகையில் புதுப்பிக்கலாம், படிவ உள்ளீடுகளை சரிபார்க்கலாம் மற்றும் கிளிக்குகள், சுட்டி நகர்வுகள் மற்றும் விசைப்பலகை உள்ளீடு போன்ற பயனர் தொடர்புகளுக்கு பதிலளிக்கலாம்.
- கூடுதலாக, பக்கத்தை மீண்டும் ஏற்ற வேண்டிய அவசியமின்றி தரவை மீட்டெடுக்க அஜாக்ஸ் (AJAX) மூலம் ஜாவாஸ்கிரிப்ட் வலை சேவையகங்களுடன் தொடர்பு கொள்ளலாம்.
பதிப்புரிமை CodeWithCurious.com
1.3 மேம்பாட்டு சூழலை அமைத்தல்:
ஜாவாஸ்கிரிப்டில் குறியீட்டைத் தொடங்க, நமக்கு ஒரு மேம்பாட்டு சூழல் அமைக்கப்பட வேண்டும்.
இதோ அடிப்படை படிகள்:`,
'voice_memo': `குரல் குறிப்பு டிரான்ஸ்கிரிப்ஷன்:
செஞ்சுரியன்ஸ் திட்ட முன்மொழிவை மதிப்பாய்வு செய்யவும். அதிக முன்னுரிமை கொண்ட பொருட்களின் விவரங்களை சரிபார்க்க வேண்டும். நடவடிக்கை உருப்படி: நடுத்தர முன்னுரிமையுடன் 2026-06-28க்குள் பின்தள குறியீடு கட்டமைப்பை மதிப்பாய்வு செய்யவும். இன்று குழு பணியிடத்திற்காக மாலை 6 மணிக்கு முன் பால் மற்றும் முட்டை வாங்க வேண்டும். மேலும் அதிக முன்னுரிமையுடன் 2026-06-29 அன்று நடைபெறவிருக்கும் டெமோவிற்கான ஸ்லைடுகளைத் தயாரிக்க வேண்டும்.`,
'meeting': `கூட்ட டிரான்ஸ்கிரிப்ட்:
பேச்சாளர் 1: அனைவரையும் வரவேற்கிறோம். செஞ்சுரியன்ஸ் திட்ட முன்மொழிவை சமர்ப்பிக்க வேண்டும். அதை உயர் முன்னுரிமையாக வைப்போம்.
பேச்சாளர் 2: ஒப்புக்கொள்கிறேன். மேலும், பின்தள குறியீடு கட்டமைப்பை 2026-06-28க்குள் மதிப்பாய்வு செய்கிறேன்.
பேச்சாளர் 1: அருமை. PageParse-க்கான README கோப்பையும் நாங்கள் வரைவு செய்கிறோம் என்பதை உறுதிப்படுத்திக் கொள்ளுங்கள்.
=== திரையில் உள்ள உரை (OCR) ===
PageParse வெளியீட்டு காலவரிசை:
- முன்மொழிவை சமர்ப்பிக்கவும் (உயர் முன்னுரிமை)
- README வரைவு (குறைந்த முன்னுரிமை)`,
'form': `ஸ்கேன் செய்யப்பட்ட படிவம்:
தலைப்பு: திட்டம் செஞ்சுரியன்ஸ்
பணி: PageParse-க்கான README கோப்பை வரைவு செய்யவும். முன்னுரிமை: குறைந்த காலக்கெடு: 2026-06-30 நிலை: முடிந்தது
பணி: செஞ்சுரியன்ஸ் திட்ட முன்மொழிவை சமர்ப்பிக்கவும். முன்னுரிமை: உயர் காலக்கெடு: 2026-06-25 நிலை: முடிந்தது`,
'sheet': `விரிதாள் தரவு அட்டவணை:
பணி | முன்னுரிமை | காலக்கெடு | நிலை
செஞ்சுரியன்ஸ் திட்ட முன்மொழிவை சமர்ப்பிக்கவும் | உயர் | 2026-06-25 | முடிந்தது
மாலை 6 மணிக்கு முன் பால் & முட்டை வாங்கவும் | குறைந்த | | செய்ய வேண்டியவை
பின்தள குறியீடு கட்டமைப்பை மதிப்பாய்வு செய்யவும் | நடுத்தர | 2026-06-28 | செய்ய வேண்டியவை
டெமோவிற்கான ஸ்லைடுகளை தயார் செய்யவும் | உயர் | | செய்ய வேண்டியவை
PageParse-க்கான README கோப்பை வரைவு செய்யவும் | குறைந்த | | முடிந்தது`
}
};
const MOCK_TASK_TRANSLATIONS: Record<string, Record<string, string>> = {
'Hindi': {
"Submit centurions project proposal": "सेंचुरियन परियोजना प्रस्ताव प्रस्तुत करें",
"Review centurions project proposal": "सेंचुरियन परियोजना प्रस्ताव की समीक्षा करें",
"Buy milk & eggs before 6pm": "शाम 6 बजे से पहले दूध और अंडे खरीदें",
"Buy milk and eggs before 6pm": "शाम 6 बजे से पहले दूध और अंडे खरीदें",
"Review backend code structure": "बैकएंड कोड संरचना की समीक्षा करें",
"Prepare slides for demo": "डेमो के लिए स्लाइड तैयार करें",
"Draft README file for PageParse": "पेजपार्स के लिए रीडमी फ़ाइल का मसौदा तैयार करें",
"Read about JavaScript's primary purpose in web pages": "वेब पेजों में जावास्क्रिप्ट के प्राथमिक उद्देश्य के बारे में पढ़ें",
"Learn how to dynamically update web elements and validate forms": "वेब तत्वों को गतिशील रूप से अपडेट करना और फ़ॉर्म सत्यापित करना सीखें",
"Understand server interaction via AJAX without reloading": "बिना रीलोड किए AJAX के माध्यम से सर्वर इंटरैक्शन को समझें",
"Set up the JavaScript development environment": "जावास्क्रिप्ट विकास वातावरण स्थापित करें",
"Milk (2 cartons)": "दूध (2 कार्टन)",
"Organic spinach (low confidence)": "जैविक पालक (कम आत्मविश्वास)",
"Chicken breast": "चिकन ब्रेस्ट"
},
'Telugu': {
"Submit centurions project proposal": "సెంచూరియన్స్ ప్రాజెక్ట్ ప్రతిపాదనను సమర్పించండి",
"Review centurions project proposal": "సెంచూరియన్స్ ప్రాజెక్ట్ ప్రతిపాదనను సమీక్షించండి",
"Buy milk & eggs before 6pm": "సాయంత్రం 6 గంటల లోపు పాలు & గుడ్లు కొనండి",
"Buy milk and eggs before 6pm": "సాయంత్రం 6 గంటల లోపు పాలు & గుడ్లు కొనండి",
"Review backend code structure": "బ్యాకెండ్ కోడ్ నిర్మాణాన్ని సమీక్షించండి",
"Prepare slides for demo": "డెమో కోసం స్లైడ్‌లను సిద్ధం చేయండి",
"Draft README file for PageParse": "పేజ్ పార్స్ కోసం రీడ్‌మీ ఫైల్‌ను సిద్ధం చేయండి",
"Read about JavaScript's primary purpose in web pages": "వెబ్ పేజీలలో జావాస్క్రిప్ట్ యొక్క ప్రాథమిక ఉద్దేశ్యం గురించి చదవండి",
"Learn how to dynamically update web elements and validate forms": "వెబ్ ఎలిమెంట్స్ డైనమిక్ అప్‌డేట్ మరియు ఫారమ్ ధృవీకరణ నేర్చుకోండి",
"Understand server interaction via AJAX without reloading": "రీలోడ్ చేయకుండా AJAX ద్వారా సర్వర్ పరస్పర చర్యను అర్థం చేసుకోండి",
"Set up the JavaScript development environment": "జావాస్క్రిప్ట్ డెవలప్మెంట్ ఎన్విరాన్మెంట్లు సెటప్ చేయండి",
"Milk (2 cartons)": "పాలు (2 కార్టన్లు)",
"Organic spinach (low confidence)": "సేంద్రీయ పాలకూర (తక్కువ విశ్వసనీయత)",
"Chicken breast": "చికెన్ బ్రెస్ట్"
},
'Tamil': {
"Submit centurions project proposal": "செஞ்சுரியன்ஸ் திட்ட முன்மொழிவை சமர்ப்பிக்கவும்",
"Review centurions project proposal": "செஞ்சுரியன்ஸ் திட்ட முன்மொழிவை மதிப்பாய்வு செய்யவும்",
"Buy milk & eggs before 6pm": "மாலை 6 மணிக்கு முன் பால் & முட்டை வாங்கவும்",
"Buy milk and eggs before 6pm": "மாலை 6 மணிக்கு முன் பால் & முட்டை வாங்கவும்",
"Review backend code structure": "பின்தள குறியீடு கட்டமைப்பை மதிப்பாய்வு செய்யவும்",
"Prepare slides for demo": "டெமோவிற்கான ஸ்லைடுகளை தயார் செய்யவும்",
"Draft README file for PageParse": "PageParse-க்கான README கோப்பை வரைவு செய்யவும்",
"Read about JavaScript's primary purpose in web pages": "வலைப்பக்கங்களில் ஜாவாஸ்கிரிப்ட்டின் முதன்மை நோக்கம் பற்றி படிக்கவும்",
"Learn how to dynamically update web elements and validate forms": "வலை கூறுகளை மாறும் வகையில் புதுப்பிக்கவும் மற்றும் படிவங்களை சரிபார்க்கவும் கற்றுக்கொள்ளுங்கள்",
"Understand server interaction via AJAX without reloading": "மீண்டும் ஏற்றாமல் அஜாக்ஸ் மூலம் சேவையக தொடர்புகளை புரிந்து கொள்ளுங்கள்",
"Set up the JavaScript development environment": "ஜாவாஸ்கிரிப்ட் மேம்பாட்டு சூழலை அமைக்கவும்",
"Milk (2 cartons)": "பால் (2 அட்டைப்பெட்டிகள்)",
"Organic spinach (low confidence)": "ஆர்கானிக் கீரை (குறைந்த நம்பிக்கை)",
"Chicken breast": "கோழி நெஞ்சுக்கறி"
}
};
const detectMockType = (text: string): string | null => {
const lower = text.toLowerCase();
if (lower.includes('purpose of javascript') || lower.includes('scripting on web pages')) {
return 'javascript';
}
if (lower.includes('voice memo transcription') || lower.includes('review centurions project proposal')) {
return 'voice_memo';
}
if (lower.includes('speaker 1: welcome everyone') || lower.includes('meeting transcript')) {
return 'meeting';
}
if (lower.includes('scanned form') && lower.includes('title: project centurions')) {
return 'form';
}
if (lower.includes('spreadsheet data table') || lower.includes('buy milk & eggs before 6pm')) {
return 'sheet';
}
return null;
};
const mockTranslate = (text: string, targetLanguage: string): string => {
if (targetLanguage === 'English') {
const lower = text.toLowerCase();
if (lower.includes('जावास्क्रिप्ट का उद्देश्य') || lower.includes('జావాస్క్రిప్ట్ యొక్క ఉద్దేశ్యం') || lower.includes('ஜாவாஸ்கிரிப்ட்டின் நோக்கம்')) {
return `1.2 Purpose of JavaScript:\n- JavaScript's primary purpose is to enable client-side scripting on web pages. This means it allows developers to manipulate the content and behavior of web pages directly within the user's web browser.\n- With JavaScript, we can dynamically update web pages, elements, validate form inputs, and respond to user interactions like clicks, mouse movements, and keyboard input.\n- Additionally, JavaScript can interact with web servers through AJAX (Asynchronous JavaScript and XML) to fetch data without requiring a page reload.\nCopyrighted by CodeWithCurious.com\n\n1.3 Setting up the development environment:\nTo start coding in JavaScript, we need a development environment set up.\nHere are the basic steps:`;
}
if (lower.includes('वॉयस मेमो प्रतिलेखन') || lower.includes('వాయిస్ మెమో') || lower.includes('குரல் குறிப்பு')) {
return `Voice Memo Transcription:\nReview centurions project proposal. Need to check details on priority high items. Action item: Review backend code structure by 2026-06-28 with medium priority. We should buy milk and eggs before 6pm for the team workspace today. Also need to prepare slides for demo due 2026-06-29 with high priority.`;
}
return text;
}
const type = detectMockType(text);
if (type && MOCK_TRANSLATIONS[targetLanguage]?.[type]) {
return MOCK_TRANSLATIONS[targetLanguage][type];
}
return `[Offline Translation to ${targetLanguage}]: ${text}`;
};
const translateTaskText = (taskText: string, targetLanguage: string): string => {
if (targetLanguage === 'English') return taskText;
if (MOCK_TASK_TRANSLATIONS[targetLanguage]?.[taskText]) {
return MOCK_TASK_TRANSLATIONS[targetLanguage][taskText];
}
for (const [eng, trans] of Object.entries(MOCK_TASK_TRANSLATIONS[targetLanguage] || {})) {
if (taskText.toLowerCase().includes(eng.toLowerCase())) {
return trans;
}
}
return `[${targetLanguage}] ${taskText}`;
};
// Initial Mock Seed Data
const DEFAULT_PAGES: PageRecord[] = [
{
id: 101,
filename: 'todo_page_01.jpg',
captured_date: '2026-06-25',
raw_ocr_text: 'TODO LIST 2026-06-25\n[x] Submit centurions project proposal (high)\n[ ] Buy milk & eggs before 6pm\n[ ] Review backend code structure (medium) due: 2026-06-28\n[ ] Prepare slides for demo (high)\n[x] Draft README file for PageParse',
created_at: '2026-06-25T14:32:00.000Z'
},
{
id: 102,
filename: 'grocery_notes.png',
captured_date: '2026-06-27',
raw_ocr_text: 'Groceries 27 Jun\n- Milk (2 cartons)\n- Organic spinach (low confidence OCR: sprnach?)\n- Wheat bread\n- Chicken breast (high priority)',
created_at: '2026-06-27T09:15:00.000Z'
}
];
const DEFAULT_TASKS: Task[] = [
{
id: 1,
page_id: 101,
task: 'Submit centurions project proposal',
due_date: '2026-06-25',
priority: 'high',
category: 'Work',
status: 'done',
ocr_confidence: 0.98
},
{
id: 2,
page_id: 101,
task: 'Buy milk & eggs before 6pm',
due_date: '2026-06-25',
priority: 'medium',
category: 'Personal',
status: 'todo',
ocr_confidence: 0.95
},
{
id: 3,
page_id: 101,
task: 'Review backend code structure',
due_date: '2026-06-28',
priority: 'medium',
category: 'Work',
status: 'todo',
ocr_confidence: 0.91
},
{
id: 4,
page_id: 101,
task: 'Prepare slides for demo',
due_date: '2026-06-29',
priority: 'high',
category: 'Work',
status: 'todo',
ocr_confidence: 0.89
},
{
id: 5,
page_id: 101,
task: 'Draft README file for PageParse',
due_date: null,
priority: 'low',
category: 'Documentation',
status: 'done',
ocr_confidence: 0.99
},
{
id: 6,
page_id: 102,
task: 'Milk (2 cartons)',
due_date: null,
priority: 'low',
category: 'Groceries',
status: 'todo',
ocr_confidence: 0.94
},
{
id: 7,
page_id: 102,
task: 'Organic spinach (low confidence)',
due_date: null,
priority: 'low',
category: 'Groceries',
status: 'todo',
ocr_confidence: 0.42 // Low confidence flag!
},
{
id: 8,
page_id: 102,
task: 'Chicken breast',
due_date: '2026-06-27',
priority: 'high',
category: 'Groceries',
status: 'todo',
ocr_confidence: 0.97
}
];
// Initialize LocalStorage Data if not present
if (!localStorage.getItem('pp_pages')) {
localStorage.setItem('pp_pages', JSON.stringify(DEFAULT_PAGES));
}
if (!localStorage.getItem('pp_tasks')) {
localStorage.setItem('pp_tasks', JSON.stringify(DEFAULT_TASKS));
}
// Keep track of active mock job list in memory (resets on reload, similar to real running queue)
let mockActiveJobs: ProcessingJob[] = [];
let jobListeners: ((jobs: ProcessingJob[]) => void)[] = [];
// Helper to notify listeners of job updates
const notifyJobListeners = () => {
jobListeners.forEach(l => l([...mockActiveJobs]));
};
export const subscribeToJobs = (listener: (jobs: ProcessingJob[]) => void) => {
jobListeners.push(listener);
listener([...mockActiveJobs]);
return () => {
jobListeners = jobListeners.filter(l => l !== listener);
};
};
let _serverKnownOnline: boolean | null = null;
// Main API Service Object
export const PageParseAPI = {
// Test backend connectivity
async checkServerConnection(): Promise<boolean> {
if (_serverKnownOnline) return true;
if (getAirgapMode()) return false;
if (!window.location.origin.includes('5173')) return true;
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 1500);
const res = await fetch(`${API_BASE_URL}/telemetry`, { signal: controller.signal });
clearTimeout(timeoutId);
_serverKnownOnline = res.status === 200;
return _serverKnownOnline;
} catch {
return false;
}
},
// Ingestion / Processing
async uploadPage(
file: File,
language: string = 'English',
schema_type: string = 'todo',
onProgressUpdate?: (job: ProcessingJob) => void
): Promise<ProcessingJob> {
const isAirgapped = getAirgapMode();
const serverOnline = !isAirgapped && (await this.checkServerConnection());
// Read image data URL for local display fallback
let fileDataUrl: string | undefined = undefined;
if (file.type.startsWith('image/')) {
fileDataUrl = await new Promise<string>((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.readAsDataURL(file);
});
}
if (serverOnline) {
// Live Mode
const formData = new FormData();
formData.append('file', file);
try {
if (onProgressUpdate) {
onProgressUpdate({
id: 'uploading',
filename: file.name,
status: 'preprocessing',
progress: 30
});
}
const response = await fetch(`${API_BASE_URL}/upload?language=${encodeURIComponent(language)}&schema_type=${encodeURIComponent(schema_type)}`, {
method: 'POST',
body: formData,
});
if (!response.ok) throw new Error('Ingestion failed on server');
const data = await response.json();
const pageId = data.source_id || data.page_id;
const result = data.result;
const records = result.records || result.tasks || [];
const rawText = data.raw_text || '';
// Persist to localStorage for the detail view
const newPage: PageRecord = {
id: pageId,
filename: file.name,
captured_date: '',
raw_ocr_text: rawText,
raw_text: rawText,
created_at: new Date().toISOString(),
image_path: data.image_path,
cleaned_image_path: data.cleaned_image_path,
};
const newTasks = records.map((t: any, idx: number) => ({
id: pageId * 1000 + idx,
page_id: pageId,
source_id: pageId,
type: t.type || 'task',
task: t.content || t.task || '',
content: t.content || t.task || '',
due_date: t.due_date,
priority: t.priority,
category: t.category,
speaker: t.speaker,
timestamp: t.timestamp,
status: t.status,
ocr_confidence: t.confidence || t.ocr_confidence || 0.9,
confidence: t.confidence || t.ocr_confidence || 0.9,
}));
const pages = JSON.parse(localStorage.getItem('pp_pages') || '[]');
localStorage.setItem('pp_pages', JSON.stringify([newPage, ...pages]));
const tasks = JSON.parse(localStorage.getItem('pp_tasks') || '[]');
localStorage.setItem('pp_tasks', JSON.stringify([...newTasks, ...tasks]));
const completedJob: ProcessingJob = {
id: pageId.toString(),
filename: file.name,
status: 'saved',
progress: 100,
tasksExtracted: newTasks,
};
if (onProgressUpdate) onProgressUpdate(completedJob);
mockActiveJobs = [completedJob, ...mockActiveJobs];
notifyJobListeners();
return completedJob;
} catch (err: any) {
console.error('Server error, falling back to local simulation:', err);
}
}
// Local Simulation Fallback (Offline First proof-of-concept)
const jobId = Math.random().toString(36).substring(2, 9);
const newJob: ProcessingJob = {
id: jobId,
filename: file.name,
status: 'queued',
progress: 10,
};
mockActiveJobs = [newJob, ...mockActiveJobs];
notifyJobListeners();
if (onProgressUpdate) onProgressUpdate(newJob);
// Simulate pipeline
const runSimulation = async () => {
const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
// 1. Queued -> Preprocessing
await delay(1200);
newJob.status = 'preprocessing';
newJob.progress = 30;
notifyJobListeners();
if (onProgressUpdate) onProgressUpdate({ ...newJob });
// 2. Preprocessing -> OCR
await delay(1500);
newJob.status = 'ocr';
newJob.progress = 60;
notifyJobListeners();
if (onProgressUpdate) onProgressUpdate({ ...newJob });
// 3. OCR -> Extracting
await delay(2000);
newJob.status = 'extracting';
newJob.progress = 85;
notifyJobListeners();
if (onProgressUpdate) onProgressUpdate({ ...newJob });
// 4. Extracting -> Saved
await delay(1500);
// Generate mock tasks based on filename/time
const pageId = Date.now();
let rawText = `[CONNECTION NOTICE]
The browser could not connect to the PageParse backend server (http://127.0.0.1:8000) and fell back to local offline simulation mode.
To get the real text from your image:
1. Make sure "Air-gap mode" in the top-right navbar is turned OFF.
2. Open and use the application through this URL: http://127.0.0.1:8000/ (this avoids browser cross-origin/CORS security blocks).
3. Ensure the terminal command is running: .venv\\Scripts\\python -m pageparse.cli serve
Simulated Result for ${file.name}:
- Task 1: Review data from ${file.name}
- Task 2: Real text extraction requires a live server connection.`;
const wordCount = file.name.length;
const confidence1 = Math.min(0.99, 0.85 + (wordCount % 15) / 100);
const confidence2 = Math.min(0.99, 0.70 + (wordCount % 23) / 100);
const confidence3 = 0.42; // Simulated low confidence field
let mockExtractedTasks: Task[] = [
{
id: pageId + 1,
page_id: pageId,
task: `Action item: Verify data from ${file.name}`,
due_date: new Date(Date.now() + 86400000 * 2).toISOString().split('T')[0], // 2 days later
priority: 'high',
category: 'Ingested',
status: 'todo',
ocr_confidence: confidence1,
},
{
id: pageId + 2,
page_id: pageId,
task: `Process contents of uploaded sheet`,
due_date: null,
priority: 'medium',
category: 'Automation',
status: 'todo',
ocr_confidence: confidence2,
},
{
id: pageId + 3,
page_id: pageId,
task: `Check for errors in ${file.name.substring(0, 8) || 'image'}`,
due_date: new Date().toISOString().split('T')[0],
priority: 'low',
category: 'System',
status: 'done',
ocr_confidence: confidence3, // low confidence
}
];
if (language !== 'English') {
rawText = mockTranslate(rawText, language);
mockExtractedTasks = mockExtractedTasks.map(t => ({
...t,
task: translateTaskText(t.task, language),
content: translateTaskText(t.content || t.task, language)
}));
}
const newPage: PageRecord = {
id: pageId,
filename: file.name,
captured_date: new Date().toISOString().split('T')[0],
raw_ocr_text: rawText,
created_at: new Date().toISOString(),
image_path: fileDataUrl,
cleaned_image_path: fileDataUrl,
};
// Save to localStorage
const pages = JSON.parse(localStorage.getItem('pp_pages') || '[]');
const tasks = JSON.parse(localStorage.getItem('pp_tasks') || '[]');
localStorage.setItem('pp_pages', JSON.stringify([newPage, ...pages]));
localStorage.setItem('pp_tasks', JSON.stringify([...mockExtractedTasks, ...tasks]));
newJob.status = 'saved';
newJob.progress = 100;
newJob.tasksExtracted = mockExtractedTasks;
notifyJobListeners();
if (onProgressUpdate) onProgressUpdate({ ...newJob });
// Remove from active jobs after 5 seconds
setTimeout(() => {
mockActiveJobs = mockActiveJobs.filter(j => j.id !== jobId);
notifyJobListeners();
}, 5000);
};
runSimulation();
return newJob;
},
// Read Tasks
async getTasks(): Promise<Task[]> {
const isAirgapped = getAirgapMode();
const serverOnline = !isAirgapped && (await this.checkServerConnection());
if (serverOnline) {
try {
const response = await fetch(`${API_BASE_URL}/records`);
if (response.ok) {
const data = await response.json();
return data.map((r: any) => ({
id: r.id,
page_id: r.source_id,
source_id: r.source_id,
type: r.type,
task: r.content,
content: r.content,
due_date: r.due_date,
priority: r.priority,
category: r.category,
speaker: r.speaker,
timestamp: r.timestamp,
status: r.status,
ocr_confidence: r.confidence || 0.9,
confidence: r.confidence || 0.9,
user_edited: r.user_edited,
filename: r.filename,
}));
}
} catch (err) {
console.error('Server error getting tasks, fallback to local:', err);
}
}
return JSON.parse(localStorage.getItem('pp_tasks') || '[]');
},
// Edit Task
async updateTask(taskId: number, updates: Partial<Task>): Promise<Task> {
const isAirgapped = getAirgapMode();
const serverOnline = !isAirgapped && (await this.checkServerConnection());
if (serverOnline) {
try {
const backendUpdates: any = {};
if (updates.task !== undefined) backendUpdates.content = updates.task;
if (updates.status !== undefined) backendUpdates.status = updates.status;
if (updates.priority !== undefined) backendUpdates.priority = updates.priority;
if (updates.due_date !== undefined) backendUpdates.due_date = updates.due_date;
if (updates.category !== undefined) backendUpdates.category = updates.category;
if (updates.type !== undefined) backendUpdates.type = updates.type;
if (updates.speaker !== undefined) backendUpdates.speaker = updates.speaker;
if (updates.timestamp !== undefined) backendUpdates.timestamp = updates.timestamp;
if (updates.ocr_confidence !== undefined) backendUpdates.confidence = updates.ocr_confidence;
if (updates.user_edited !== undefined) backendUpdates.user_edited = updates.user_edited;
const response = await fetch(`${API_BASE_URL}/records/${taskId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(backendUpdates),
});
if (response.ok) {
const r = await response.json();
if (r.success) {
return {
id: taskId,
...updates,
} as Task;
}
}
} catch (err) {
console.error('Server error updating task, fallback to local:', err);
}
}
// Local update
const tasks: Task[] = JSON.parse(localStorage.getItem('pp_tasks') || '[]');
const index = tasks.findIndex(t => t.id === taskId);
if (index === -1) throw new Error('Task not found');
const updatedTask = { ...tasks[index], ...updates };
tasks[index] = updatedTask;
localStorage.setItem('pp_tasks', JSON.stringify(tasks));
return updatedTask;
},
// Delete Task
async deleteTask(taskId: number): Promise<boolean> {
const isAirgapped = getAirgapMode();
const serverOnline = !isAirgapped && (await this.checkServerConnection());
if (serverOnline) {
try {
const response = await fetch(`${API_BASE_URL}/records/${taskId}`, {
method: 'DELETE'
});
if (response.ok) return true;
} catch (err) {
console.error('Server error deleting task, fallback to local:', err);
}
}
// Local delete
const tasks: Task[] = JSON.parse(localStorage.getItem('pp_tasks') || '[]');
const filteredTasks = tasks.filter(t => t.id !== taskId);
localStorage.setItem('pp_tasks', JSON.stringify(filteredTasks));
return true;
},
// Search Tasks
async searchTasks(query: string): Promise<Task[]> {
const isAirgapped = getAirgapMode();
const serverOnline = !isAirgapped && (await this.checkServerConnection());
if (serverOnline) {
try {
const response = await fetch(`${API_BASE_URL}/search?q=${encodeURIComponent(query)}`);
if (response.ok) return await response.json();
} catch (err) {
console.error('Server error searching tasks, fallback to local:', err);
}
}
// Local basic search
const tasks: Task[] = JSON.parse(localStorage.getItem('pp_tasks') || '[]');
if (!query.trim()) return tasks;
const lowerQuery = query.toLowerCase();
return tasks.filter(
t =>
t.task.toLowerCase().includes(lowerQuery) ||
(t.category && t.category.toLowerCase().includes(lowerQuery))
);
},
// Read Pages History
async getPages(): Promise<PageRecord[]> {
const isAirgapped = getAirgapMode();
const serverOnline = !isAirgapped && (await this.checkServerConnection());
if (serverOnline) {
try {
const response = await fetch(`${API_BASE_URL}/sources`);
if (response.ok) {
const data = await response.json();
return data.map((s: any) => ({
id: s.id,
filename: s.filename,
source_type: s.source_type || 'image',
source_path: s.source_path,
captured_date: s.captured_date,
title: s.title,
summary: s.summary,
raw_ocr_text: s.raw_text,
raw_text: s.raw_text,
created_at: s.created_at,
image_path: s.image_path,
cleaned_image_path: s.cleaned_image_path,
}));
}
} catch (err) {
console.error('Server error getting pages, fallback to local:', err);
}
}
return JSON.parse(localStorage.getItem('pp_pages') || '[]');
},
// Summarize Page
async summarizePage(pageId: number): Promise<string> {
const isAirgapped = getAirgapMode();
const serverOnline = !isAirgapped && (await this.checkServerConnection());
if (serverOnline) {
try {
const response = await fetch(`${API_BASE_URL}/sources/${pageId}/summarize`, {
method: 'POST',
});
if (response.ok) {
const data = await response.json();
return data.summary;
}
} catch (err) {
console.error('Server error summarizing page, fallback to local:', err);
}
}
// Local simulation fallback
const pages: PageRecord[] = JSON.parse(localStorage.getItem('pp_pages') || '[]');
const index = pages.findIndex(p => p.id === pageId);
if (index === -1) throw new Error('Page not found');
const simulatedSummary = `This page contains handwritten notes detailing various to-dos and action items. The content lists multiple tasks, including reviewing codebase structures, preparing documents, and other general reminders, with priorities and due dates associated where applicable.`;
pages[index].summary = simulatedSummary;
localStorage.setItem('pp_pages', JSON.stringify(pages));
return simulatedSummary;
},
// Dynamic Translation Service
async translateText(text: string, targetLanguage: string): Promise<string> {
const isAirgapped = getAirgapMode();
const serverOnline = !isAirgapped && (await this.checkServerConnection());
if (serverOnline) {
try {
const response = await fetch(`${API_BASE_URL}/translate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, target_lang: targetLanguage })
});
if (response.ok) {
const data = await response.json();
return data.translated_text;
}
} catch (err) {
console.error('Server translation failed, falling back to client-side:', err);
}
}
if (!isAirgapped && navigator.onLine) {
try {
const langCodes: Record<string, string> = {
'English': 'en', 'Assamese': 'as', 'Bengali': 'bn', 'Bodo': 'brx', 'Dogri': 'doi',
'Gujarati': 'gu', 'Hindi': 'hi', 'Kannada': 'kn', 'Kashmiri': 'ks', 'Konkani': 'kok',
'Maithili': 'mai', 'Malayalam': 'ml', 'Manipuri': 'mni', 'Marathi': 'mr', 'Nepali': 'ne',
'Odia': 'or', 'Punjabi': 'pa', 'Sanskrit': 'sa', 'Santali': 'sat', 'Sindhi': 'sd',
'Tamil': 'ta', 'Telugu': 'te', 'Urdu': 'ur'
};
const code = langCodes[targetLanguage] || 'en';
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${code}&dt=t&q=${encodeURIComponent(text)}`;
const res = await fetch(url);
if (res.ok) {
const data = await res.json();
return data[0].map((seg: any) => seg[0]).join('');
}
} catch (err) {
console.error('Browser Google Translate failed:', err);
}
}
return mockTranslate(text, targetLanguage);
},
// Get live CPU / RAM statistics (Simulated or Real)
async getTelemetry(isProcessing: boolean): Promise<TelemetryData> {
const isAirgapped = getAirgapMode();
const serverOnline = !isAirgapped && (await this.checkServerConnection());
if (serverOnline) {
try {
const response = await fetch(`${API_BASE_URL}/telemetry`);
if (response.ok) return await response.json();
} catch (err) {
// Fallback to simulation
}
}
// Local telemetry generator
const baseCpu = isProcessing ? 78 : 4;
const cpuNoise = Math.random() * 12 - 6; // +/- 6%
const cpu = Math.max(1, Math.min(100, parseFloat((baseCpu + cpuNoise).toFixed(1))));
const baseRam = isProcessing ? 1180 : 240; // 1.1GB vs 240MB
const ramNoise = Math.random() * 40 - 20; // +/- 20MB
const ram = Math.max(100, Math.min(4096, Math.round(baseRam + ramNoise)));
return {
cpu_percent: cpu,
ram_used_mb: ram,
ram_total_mb: 8192, // mock 8GB total
active_tasks: mockActiveJobs.filter(j => j.status !== 'saved' && j.status !== 'failed').length,
};
},
// Analyze pasted code
async analyzeCode(code: string, language: string = 'auto'): Promise<{ language: string; explanation: string; summary: string }> {
const isAirgapped = getAirgapMode();
const serverOnline = !isAirgapped && (await this.checkServerConnection());
if (serverOnline) {
try {
const response = await fetch(`${API_BASE_URL}/analyze-code`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, language }),
});
if (response.ok) {
return await response.json();
}
const body = await response.json();
if (body.error) {
throw new Error(body.error);
}
} catch (err) {
console.error('Server code analysis failed:', err);
if (err instanceof Error && err.message.includes('Ollama')) {
throw err;
}
}
}
// Local fallback: mock analysis with keyword-based language detection
const detectedLang = this._detectCodeLanguage(code);
const lineCount = code.split('\n').length;
const charCount = code.length;
let explanation = `This is a ${detectedLang} code snippet with approximately ${lineCount} lines and ${charCount} characters. `;
explanation += `The code appears to define functions, variables, and logic typical of ${detectedLang} programs. `;
explanation += `Without a live SLM connection, a full explanation requires the PageParse backend server to be running.`;
const summary = `${detectedLang.charAt(0).toUpperCase() + detectedLang.slice(1)} code snippet (${lineCount} lines)`;
return { language: detectedLang, explanation, summary };
},
// Basic client-side code language detection
_detectCodeLanguage(code: string): string {
const s = code.trim();
if (!s) return 'unknown';
if (/^\s*(import |from |def |class |print\()/.test(s)) return 'python';
if (/^\s*(import |export |function |const |let |var |console\.log)/.test(s)) {
if (/:\s*(string|number|boolean|void|any)\s*[=;),]/.test(s)) return 'typescript';
return 'javascript';
}
if (/^\s*(public |private |class |import java\.)/.test(s)) return 'java';
if (/^\s*(#include|int main)/.test(s)) return 'c_cpp';
if (/^\s*(fn |let mut|use std)/.test(s)) return 'rust';
if (/^\s*(package main|func )/.test(s)) return 'go';
if (/^\s*<!DOCTYPE|<html/.test(s)) return 'html';
if (/^\s*[a-zA-Z-]+\s*\{/.test(s)) return 'css';
if (/^\s*(SELECT|INSERT|UPDATE|DELETE|CREATE)/.test(s)) return 'sql';
return 'unknown';
},
// Check Ollama SLM connection status
async checkOllamaStatus(): Promise<{ connected: boolean; url: string; model: string; models: string[]; model_available: boolean; version: string }> {
const isAirgapped = getAirgapMode();
const serverOnline = !isAirgapped && (await this.checkServerConnection());
if (serverOnline) {
try {
const response = await fetch(`${API_BASE_URL}/ollama/status`);
if (response.ok) return await response.json();
} catch (err) {
console.error('Failed to check Ollama status:', err);
}
}
return { connected: false, url: 'http://localhost:11434', model: 'llama3.2:1b', models: [], model_available: false, version: '' };
},
// Configure Ollama connection
async configureOllama(url: string, model: string): Promise<{ url: string; model: string }> {
const response = await fetch(`${API_BASE_URL}/ollama/configure`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, model }),
});
if (response.ok) return await response.json();
throw new Error('Failed to configure Ollama');
},
// Batch upload multiple files
async uploadBatch(
files: File[],
language: string = 'English',
schema_type: string = 'auto',
onProgress?: (job: ProcessingJob) => void
): Promise<ProcessingJob[]> {
const results: ProcessingJob[] = [];
for (const file of files) {
try {
const job = await this.uploadPage(file, language, schema_type, onProgress);
results.push(job);
} catch (err: any) {
results.push({
id: 'err',
filename: file.name,
status: 'failed',
progress: 0,
error: err.message || 'Batch upload failed',
});
}
}
return results;
},
};