veela4 commited on
Commit
373c769
·
verified ·
1 Parent(s): df079da

Add files using upload-large-folder tool

Browse files
.gitignore ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+ npm-debug.log*
4
+ yarn-debug.log*
5
+ yarn-error.log*
6
+
7
+ # Production builds
8
+ build/
9
+ dist/
10
+
11
+ # Environment variables
12
+ .env
13
+ .env.local
14
+ .env.development.local
15
+ .env.test.local
16
+ .env.production.local
17
+
18
+ # IDE
19
+ .vscode/
20
+ .idea/
21
+
22
+ # OS
23
+ .DS_Store
24
+ Thumbs.db
25
+
26
+ # Logs
27
+ *.log
.vscode/settings.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "html.autoClosingTags": false
3
+ }
README.md ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Luna OCR
3
+ emoji: 🌙
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: static
7
+ pinned: false
8
+ license: mit
9
+ ---
10
+
11
+ # 🌙 Luna OCR - AI-Powered Text Extraction
12
+
13
+ **The most accurate OCR solution for daily use** - Transform images and PDFs into perfectly formatted text with AI intelligence.
14
+
15
+ ## ✨ What Makes Luna OCR Special
16
+
17
+ Luna OCR isn't just another text extraction tool. It's an **AI-enhanced OCR system** that understands context, preserves formatting, and delivers **insanely accurate results** for your daily workflow.
18
+
19
+ ### 🎯 Key Features
20
+
21
+ - 🤖 **AI-Powered Processing**: Google Gemini 2.5 Flash & Pro integration
22
+ - 📊 **Smart Formatting**: Automatic tables, bold text, and structure detection
23
+ - 🎨 **Multiple Output Formats**: JSON, HTML, Markdown, and plain text
24
+ - � **Data Visgualization**: Interactive tables with sorting and categorization
25
+ - 🖼️ **Universal Support**: Images (JPEG, PNG, WebP) and multi-page PDFs
26
+ - ⚡ **Lightning Fast**: Real-time processing with live progress tracking
27
+ - 🎭 **Beautiful UI**: Modern glassmorphism design with smooth animations
28
+
29
+ ## 🚀 Live Demo
30
+
31
+ Visit: **https://huggingface.co/veela4/luna_ocr**
32
+
33
+ ## 🛠️ Technology Stack
34
+
35
+ - **Frontend**: React 19, Three.js, Framer Motion, Radix UI
36
+ - **Backend**: Node.js, Express, Sharp, Tesseract OCR
37
+ - **AI Engine**: Google Gemini 2.5 Flash & Pro
38
+ - **Design**: Glassmorphism UI with advanced animations
39
+
40
+ ## 📦 Quick Start
41
+
42
+ 1. **Clone the repository**
43
+ ```bash
44
+ git clone https://huggingface.co/veela4/luna_ocr
45
+ cd luna_ocr
46
+ ```
47
+
48
+ 2. **Install dependencies**
49
+ ```bash
50
+ npm install
51
+ cd server && npm install && cd ..
52
+ ```
53
+
54
+ 3. **Set up environment variables**
55
+ ```bash
56
+ # Create .env file in root directory
57
+ echo "GEMINI_API_KEY=your_google_gemini_api_key_here" > .env
58
+ ```
59
+ Get your API key from: https://makersuite.google.com/app/apikey
60
+
61
+ 4. **Start the application**
62
+ ```bash
63
+ npm start
64
+ ```
65
+
66
+ Access the app at `http://localhost:3000`
67
+
68
+ ## 🎯 Use Cases & Examples
69
+
70
+ ### 📋 Document Processing
71
+ - **Receipts & Invoices**: Extract structured data with automatic categorization
72
+ - **Business Cards**: Convert to organized contact information
73
+ - **Forms & Applications**: Preserve table structures and formatting
74
+
75
+ ### 📚 Academic & Research
76
+ - **Research Papers**: Maintain citations, footnotes, and formatting
77
+ - **Handwritten Notes**: Convert to searchable, editable text
78
+ - **Textbooks**: Extract with proper headings and structure
79
+
80
+ ### 💼 Professional Workflows
81
+ - **Meeting Notes**: Convert whiteboards to formatted documents
82
+ - **Legal Documents**: Preserve critical formatting and structure
83
+ - **Technical Manuals**: Maintain code blocks and technical formatting
84
+
85
+ ## 🔧 Output Formats
86
+
87
+ ### 📄 Plain Text
88
+ Clean, readable text extraction
89
+
90
+ ### 📊 JSON Structure
91
+ ```json
92
+ {
93
+ "title": "Document Title",
94
+ "sections": [
95
+ {
96
+ "heading": "Section 1",
97
+ "content": "Formatted content...",
98
+ "tables": [...],
99
+ "metadata": {...}
100
+ }
101
+ ]
102
+ }
103
+ ```
104
+
105
+ ### 🌐 HTML Format
106
+ Fully formatted HTML with proper tags, tables, and styling
107
+
108
+ ### 📝 Markdown
109
+ GitHub-flavored markdown with tables, headers, and formatting
110
+
111
+ ## 🎨 Advanced Features
112
+
113
+ ### 🧠 AI Intelligence
114
+ - **Context Understanding**: Recognizes document types and adjusts processing
115
+ - **Smart Formatting**: Preserves original layout and styling
116
+ - **Error Correction**: Fixes common OCR mistakes using AI
117
+
118
+ ### 📊 Data Processing
119
+ - **Table Recognition**: Converts complex tables to structured data
120
+ - **Sorting & Filtering**: Interactive data manipulation
121
+ - **Export Options**: Multiple format support for different use cases
122
+
123
+ ### 🎭 User Experience
124
+ - **Real-time Preview**: See results as they're processed
125
+ - **Progress Tracking**: Visual feedback for long operations
126
+ - **Responsive Design**: Works perfectly on all devices
127
+
128
+ ## 📡 API Reference
129
+
130
+ ### Process Document
131
+ ```bash
132
+ POST /api/ocr
133
+ Content-Type: multipart/form-data
134
+
135
+ # Response includes structured data in requested format
136
+ ```
137
+
138
+ ### Health Check
139
+ ```bash
140
+ GET /api/health
141
+ # Returns system status and capabilities
142
+ ```
143
+
144
+ ### Processing Status
145
+ ```bash
146
+ GET /api/progress/:sessionId
147
+ # Real-time processing updates
148
+ ```
149
+
150
+ ## 🎯 Why Choose Luna OCR?
151
+
152
+ - ✅ **Accuracy**: AI-enhanced recognition beats traditional OCR
153
+ - ✅ **Speed**: Optimized for daily use with fast processing
154
+ - ✅ **Flexibility**: Multiple output formats for any workflow
155
+ - ✅ **Intelligence**: Context-aware processing and formatting
156
+ - ✅ **Modern**: Beautiful, intuitive interface
157
+ - ✅ **Open Source**: MIT license, customize as needed
158
+
159
+ ## 🤝 Contributing
160
+
161
+ We welcome contributions! Feel free to:
162
+ - Report bugs and suggest features
163
+ - Submit pull requests
164
+ - Improve documentation
165
+ - Share your use cases
166
+
167
+ ## 📝 License
168
+
169
+ MIT License - Use freely in personal and commercial projects
170
+
171
+ ---
172
+
173
+ **Built with ❤️ for developers who demand accuracy**
174
+
175
+ *Transform your document workflow today with Luna OCR*
app.txt ADDED
@@ -0,0 +1,757 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 🌙💛 Luna's OCR Studio - Clean & Professional
3
+ Modern OCR with perfect readability and clean design
4
+ """
5
+
6
+ import gradio as gr
7
+ import os
8
+ from google import genai
9
+ import re
10
+ from PIL import Image, ImageEnhance, ImageFilter
11
+ from pdf2image import convert_from_path
12
+ import tempfile
13
+ import json
14
+ from datetime import datetime
15
+ import time
16
+ import markdown
17
+ import base64
18
+ from io import BytesIO
19
+ import zipfile
20
+ import shutil
21
+
22
+ # === CORE FUNCTIONS ===
23
+
24
+ def enhance_image_quality(image):
25
+ """Real OCR-optimized image enhancement"""
26
+ try:
27
+ import numpy as np
28
+ from PIL import ImageOps, ImageStat
29
+
30
+ if image.mode != 'RGB':
31
+ image = image.convert('RGB')
32
+
33
+ # Calculate adaptive enhancement
34
+ stat = ImageStat.Stat(image)
35
+ mean_brightness = sum(stat.mean) / len(stat.mean)
36
+
37
+ if mean_brightness < 100: # Dark image
38
+ contrast_factor = 1.5
39
+ brightness_factor = 1.2
40
+ elif mean_brightness > 180: # Bright image
41
+ contrast_factor = 1.3
42
+ brightness_factor = 0.9
43
+ else: # Normal image
44
+ contrast_factor = 1.2
45
+ brightness_factor = 1.0
46
+
47
+ # Apply enhancements
48
+ if brightness_factor != 1.0:
49
+ enhancer = ImageEnhance.Brightness(image)
50
+ image = enhancer.enhance(brightness_factor)
51
+
52
+ enhancer = ImageEnhance.Contrast(image)
53
+ image = enhancer.enhance(contrast_factor)
54
+
55
+ enhancer = ImageEnhance.Sharpness(image)
56
+ image = enhancer.enhance(1.3)
57
+
58
+ return image
59
+
60
+ except:
61
+ # Fallback enhancement
62
+ try:
63
+ enhancer = ImageEnhance.Contrast(image)
64
+ image = enhancer.enhance(1.2)
65
+ enhancer = ImageEnhance.Sharpness(image)
66
+ image = enhancer.enhance(1.1)
67
+ return image
68
+ except:
69
+ return image
70
+
71
+ def process_with_gemini(image, api_key, use_structured=False, enhance_quality=True):
72
+ """Process image with Gemini AI"""
73
+ if not api_key:
74
+ return "❌ Please enter your Google API Key"
75
+
76
+ try:
77
+ client = genai.Client(api_key=api_key)
78
+
79
+ # Enhance image if requested
80
+ if enhance_quality:
81
+ image = enhance_image_quality(image)
82
+
83
+ # Choose prompt and model
84
+ if use_structured:
85
+ prompt = """วิเคราะห์และแยกข้อความในรูปภาพนี้อย่างละเอียดและจัดรูปแบบเป็น Markdown:
86
+
87
+ 🎯 **วิธีการประมวลผล:**
88
+ 1. อ่านข้อความทั้งหมดตามลำดับที่ปรากฏ
89
+ 2. แยกประเภทข้อมูล: หัวข้อ, รายละเอียด, ตัวเลข, วันที่, ชื่อ, ที่อยู่
90
+ 3. แก้ไขข้อผิดพลาด OCR และปรับปรุงการเว้นวรรคภาษาไทย
91
+ 4. จัดรูปแบบด้วย Markdown ที่สวยงาม
92
+
93
+ 📋 **รูปแบบที่ต้องการ:**
94
+ - ใช้ ## สำหรับหัวข้อหลัก
95
+ - ใช้ ### สำหรับหัวข้อย่อย
96
+ - ใช้ **text** สำหรับข้อความสำคัญ
97
+ - ใช้ - สำหรับรายการ
98
+ - ใช้ตารางสำหรับข้อมูลที่เป็นระบบ
99
+
100
+ ⚡ **ให้เฉพาะเนื้อหาที่อ่านได้ ไม่ต้องอธิบายกระบวนการ**"""
101
+ model = "gemini-2.5-pro"
102
+ else:
103
+ prompt = """อ่านและแยกข้อความจากรูปภาพนี้อย่างละเอียดและแม่นยำ:
104
+
105
+ 🎯 **คำสั่งการประมวลผล:**
106
+ 1. อ่านข้อความทั้งหมดตามลำดับที่ปรากฏในรูป
107
+ 2. แก้ไขข้อผิดพลาดที่เกิดจาก OCR (ตัวอักษรผิด, การเว้นวรรค)
108
+ 3. จัดรูปแบบให้อ่านง่าย โดยใช้การขึ้นบรรทัดใหม่ที่เหมาะสม
109
+ 4. สำหรับภาษาไทย: ใส่ช่องว่างระหว่างคำให้ถูกต้อง
110
+ 5. สำหรับตัวเลข วันที่ และข้อมูลสำคัญ: แยกบรรทัดให้ชัดเจน
111
+ 6. รักษาโครงสร้างและลำดับเดิมของเอกสาร
112
+ 7. ถ้ามีตาราง: จัดเรียงข้อมูลให้เป็นระเบียบ
113
+
114
+ ⚡ **ให้ผลลัพธ์เป็นข้อความธรรมดาที่สะอาด ไม่ใช้ markdown formatting**"""
115
+ model = "gemini-2.5-flash"
116
+
117
+ # Process with Gemini
118
+ response = client.models.generate_content(
119
+ model=model,
120
+ contents=[prompt, image]
121
+ )
122
+
123
+ result_text = response.text
124
+
125
+ # Clean up result if needed
126
+ if not use_structured:
127
+ result_text = re.sub(r'\*\*(.*?)\*\*', r'\1', result_text)
128
+ result_text = re.sub(r'\*(.*?)\*', r'\1', result_text)
129
+ result_text = re.sub(r'^#+\s*', '', result_text, flags=re.MULTILINE)
130
+
131
+ return result_text
132
+
133
+ except Exception as e:
134
+ return f"❌ Error: {str(e)}"
135
+
136
+ def create_beautiful_html(content, filename, is_structured=False, processing_info=None, image=None):
137
+ """Create beautiful HTML file"""
138
+
139
+ # Convert image to base64 if provided
140
+ image_html = ""
141
+ if image:
142
+ try:
143
+ buffer = BytesIO()
144
+ image.save(buffer, format='PNG')
145
+ img_str = base64.b64encode(buffer.getvalue()).decode()
146
+ image_html = f"""
147
+ <div class="image-container">
148
+ <h3>📷 Processed Image</h3>
149
+ <img src="data:image/png;base64,{img_str}" alt="Processed Image" style="max-width: 100%; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.2);">
150
+ </div>
151
+ """
152
+ except:
153
+ pass
154
+
155
+ # Convert markdown to HTML if structured
156
+ if is_structured:
157
+ try:
158
+ html_content = markdown.markdown(content, extensions=['tables', 'fenced_code'])
159
+ except:
160
+ html_content = content.replace('\n', '<br>')
161
+ else:
162
+ html_content = content.replace('\n', '<br>')
163
+
164
+ # Processing info section
165
+ info_html = ""
166
+ if processing_info:
167
+ info_html = f"""
168
+ <div class="processing-info">
169
+ <h3>📊 Processing Information</h3>
170
+ <div class="info-grid">
171
+ <div class="info-item"><strong>📁 File:</strong> {processing_info.get('filename', 'Unknown')}</div>
172
+ <div class="info-item"><strong>🤖 Model:</strong> {processing_info.get('model', 'Unknown')}</div>
173
+ <div class="info-item"><strong>⏱️ Time:</strong> {processing_info.get('processing_time', 0):.2f}s</div>
174
+ <div class="info-item"><strong>📝 Characters:</strong> {processing_info.get('characters', 0):,}</div>
175
+ <div class="info-item"><strong>✨ Enhanced:</strong> {'Yes' if processing_info.get('enhanced') else 'No'}</div>
176
+ <div class="info-item"><strong>📅 Processed:</strong> {processing_info.get('timestamp', '')[:19]}</div>
177
+ </div>
178
+ </div>
179
+ """
180
+
181
+ html_template = f"""
182
+ <!DOCTYPE html>
183
+ <html lang="en">
184
+ <head>
185
+ <meta charset="UTF-8">
186
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
187
+ <title>OCR Results - {filename}</title>
188
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
189
+ <style>
190
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
191
+ body {{
192
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
193
+ background: #1a1a1a; color: #e0e0e0; line-height: 1.6; min-height: 100vh;
194
+ }}
195
+ .container {{ max-width: 1000px; margin: 0 auto; padding: 32px 20px; }}
196
+ .header {{
197
+ background: #2a2a2a; border: 1px solid #404040; padding: 48px 32px;
198
+ border-radius: 16px; text-align: center; margin-bottom: 32px;
199
+ }}
200
+ .header h1 {{ color: #ffffff; font-size: 2.5em; font-weight: 700; margin-bottom: 16px; }}
201
+ .header p {{ color: #b0b0b0; font-size: 1.2em; font-weight: 500; }}
202
+ .content-section {{
203
+ background: #2a2a2a; border: 1px solid #404040; padding: 32px;
204
+ border-radius: 16px; margin-bottom: 24px;
205
+ }}
206
+ .content-section h2 {{ color: #ffffff; font-weight: 600; margin-bottom: 24px;
207
+ padding-bottom: 12px; border-bottom: 1px solid #404040; }}
208
+ .processing-info {{ background: #333333; border: 1px solid #404040; padding: 24px;
209
+ border-radius: 12px; margin-bottom: 24px; }}
210
+ .info-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
211
+ gap: 16px; margin-top: 16px; }}
212
+ .info-item {{ background: #2a2a2a; border: 1px solid #404040; padding: 16px; border-radius: 8px; }}
213
+ .content {{ background: #333333; padding: 24px; border-radius: 12px;
214
+ border: 1px solid #404040; font-size: 16px; line-height: 1.8; }}
215
+ .image-container {{ text-align: center; margin: 24px 0; padding: 24px;
216
+ background: #333333; border: 1px solid #404040; border-radius: 12px; }}
217
+ .image-container h3 {{ color: #ffffff; font-weight: 600; margin-bottom: 16px; }}
218
+ .footer {{ text-align: center; padding: 32px; margin-top: 40px; background: #2a2a2a;
219
+ border: 1px solid #404040; border-radius: 16px; }}
220
+ .footer h3 {{ color: #ffffff; font-weight: 600; margin-bottom: 16px; }}
221
+ .footer p {{ color: #b0b0b0; font-size: 1.1em; }}
222
+ </style>
223
+ </head>
224
+ <body>
225
+ <div class="container">
226
+ <div class="header">
227
+ <h1>📄 OCR Results</h1>
228
+ <p>Generated by Luna's OCR Studio</p>
229
+ </div>
230
+ {info_html}
231
+ {image_html}
232
+ <div class="content-section">
233
+ <h2>Extracted Content</h2>
234
+ <div class="content">{html_content}</div>
235
+ </div>
236
+ <div class="footer">
237
+ <h3>Made with Luna's OCR Studio</h3>
238
+ <p>Powered by Google Gemini 2.5</p>
239
+ <p style="margin-top: 16px; font-size: 0.9em; color: #888;">
240
+ Generated on {datetime.now().strftime('%Y-%m-%d at %H:%M:%S')}
241
+ </p>
242
+ </div>
243
+ </div>
244
+ </body>
245
+ </html>
246
+ """
247
+ return html_template
248
+
249
+ def create_all_downloads(content, filename, is_structured=False, processing_info=None, image=None):
250
+ """Create all download formats"""
251
+
252
+ base_filename = os.path.splitext(filename)[0]
253
+ files = {}
254
+
255
+ try:
256
+ # 1. Create .txt file (clean text)
257
+ txt_content = content
258
+ if is_structured:
259
+ txt_content = re.sub(r'\*\*(.*?)\*\*', r'\1', txt_content)
260
+ txt_content = re.sub(r'\*(.*?)\*', r'\1', txt_content)
261
+ txt_content = re.sub(r'^#+\s*', '', txt_content, flags=re.MULTILINE)
262
+
263
+ txt_path = tempfile.mktemp(suffix='.txt')
264
+ with open(txt_path, 'w', encoding='utf-8') as f:
265
+ f.write(f"Generated by Luna's OCR Studio\n")
266
+ f.write(f"File: {filename}\n")
267
+ f.write(f"Processed: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
268
+ f.write("="*50 + "\n\n")
269
+ f.write(txt_content)
270
+ files['txt'] = txt_path
271
+
272
+ # 2. Create .md file
273
+ md_content = content if is_structured else f"# OCR Results\n\n{content}"
274
+ md_path = tempfile.mktemp(suffix='.md')
275
+ with open(md_path, 'w', encoding='utf-8') as f:
276
+ f.write(f"<!-- Generated by Luna's OCR Studio -->\n")
277
+ f.write(f"<!-- File: {filename} -->\n")
278
+ f.write(f"<!-- Processed: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} -->\n\n")
279
+ f.write(md_content)
280
+ files['md'] = md_path
281
+
282
+ # 3. Create .html file
283
+ html_content = create_beautiful_html(content, filename, is_structured, processing_info, image)
284
+ html_path = tempfile.mktemp(suffix='.html')
285
+ with open(html_path, 'w', encoding='utf-8') as f:
286
+ f.write(html_content)
287
+ files['html'] = html_path
288
+
289
+ return files
290
+
291
+ except Exception as e:
292
+ print(f"Error creating files: {e}")
293
+ return None
294
+
295
+ def process_document(file, image, api_key, use_structured, enhance_quality):
296
+ """Main processing function - handles both files and images"""
297
+ if not api_key:
298
+ return "❌ Please enter your Google API Key", None, None, None, None, "❌ No API key"
299
+
300
+ if not file and not image:
301
+ return "❌ Please upload a file or paste an image", None, None, None, None, "❌ No input"
302
+
303
+ start_time = time.time()
304
+
305
+ try:
306
+ # Handle file upload
307
+ if file:
308
+ file_path = file.name
309
+ file_ext = os.path.splitext(file_path)[1].lower()
310
+ preview_image = None
311
+
312
+ if file_ext == '.pdf':
313
+ # Convert PDF
314
+ dpi = 400 if enhance_quality else 300
315
+ images = convert_from_path(file_path, dpi=dpi)
316
+ if not images:
317
+ return "❌ Failed to convert PDF", None, None, None, None, "❌ PDF conversion failed"
318
+
319
+ preview_image = images[0]
320
+ all_text = []
321
+
322
+ for i, img in enumerate(images):
323
+ page_text = process_with_gemini(img, api_key, use_structured, enhance_quality)
324
+ if use_structured:
325
+ page_text = f"## 📄 หน้า {i + 1}\n\n{page_text}"
326
+ else:
327
+ page_text = f"=== หน้า {i + 1} ===\n{page_text}"
328
+ all_text.append(page_text)
329
+
330
+ result_text = "\n\n".join(all_text)
331
+ filename = os.path.basename(file_path)
332
+
333
+ elif file_ext in ['.png', '.jpg', '.jpeg', '.webp', '.bmp']:
334
+ img = Image.open(file_path)
335
+ preview_image = img
336
+ result_text = process_with_gemini(img, api_key, use_structured, enhance_quality)
337
+ filename = os.path.basename(file_path)
338
+ else:
339
+ return "❌ Unsupported format. Use PDF, PNG, JPG", None, None, None, None, "❌ Unsupported format"
340
+
341
+ # Handle pasted image
342
+ else:
343
+ preview_image = image
344
+ result_text = process_with_gemini(image, api_key, use_structured, enhance_quality)
345
+ filename = "Clipboard_Image.png"
346
+
347
+ # Create downloads
348
+ processing_time = time.time() - start_time
349
+ processing_info = {
350
+ 'filename': filename,
351
+ 'model': 'Gemini 2.5 Pro' if use_structured else 'Gemini 2.5 Flash',
352
+ 'processing_time': processing_time,
353
+ 'characters': len(result_text),
354
+ 'enhanced': enhance_quality,
355
+ 'timestamp': datetime.now().isoformat()
356
+ }
357
+
358
+ download_files = create_all_downloads(
359
+ result_text,
360
+ filename,
361
+ is_structured=use_structured,
362
+ processing_info=processing_info,
363
+ image=preview_image
364
+ )
365
+
366
+ status = f"✅ Success! File: {filename} • Time: {processing_time:.1f}s • Characters: {len(result_text):,}"
367
+
368
+ if download_files:
369
+ return (
370
+ result_text,
371
+ download_files.get('txt'),
372
+ download_files.get('md'),
373
+ download_files.get('html'),
374
+ preview_image,
375
+ status
376
+ )
377
+ else:
378
+ return result_text, None, None, None, preview_image, status
379
+
380
+ except Exception as e:
381
+ return f"❌ Error: {str(e)}", None, None, None, None, f"❌ Error processing"
382
+
383
+ # === CLEAN, READABLE DESIGN ===
384
+
385
+ css = """
386
+ /* Clean, readable theme with proper contrast */
387
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
388
+
389
+ :root {
390
+ /* Dark theme with excellent readability */
391
+ --bg-primary: #1a1a1a;
392
+ --bg-secondary: #2a2a2a;
393
+ --bg-accent: #333333;
394
+ --text-primary: #ffffff;
395
+ --text-secondary: #b0b0b0;
396
+ --text-muted: #888888;
397
+ --accent-color: #4a9eff;
398
+ --accent-hover: #3d8bdb;
399
+ --success-color: #4caf50;
400
+ --error-color: #f44336;
401
+ --border-color: #404040;
402
+ --border-light: #555555;
403
+
404
+ /* Typography */
405
+ --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
406
+ --font-size-xs: 12px;
407
+ --font-size-sm: 14px;
408
+ --font-size-md: 16px;
409
+ --font-size-lg: 18px;
410
+ --font-size-xl: 24px;
411
+
412
+ /* Spacing */
413
+ --spacing-xs: 4px;
414
+ --spacing-sm: 8px;
415
+ --spacing-md: 16px;
416
+ --spacing-lg: 24px;
417
+ --spacing-xl: 32px;
418
+
419
+ /* Radius */
420
+ --radius-sm: 6px;
421
+ --radius-md: 8px;
422
+ --radius-lg: 12px;
423
+ --radius-xl: 16px;
424
+ }
425
+
426
+ /* Base styles */
427
+ .gradio-container {
428
+ background: var(--bg-primary) !important;
429
+ color: var(--text-primary) !important;
430
+ font-family: var(--font-family) !important;
431
+ }
432
+
433
+ /* Typography */
434
+ h1, h2, h3, h4, h5, h6 {
435
+ font-family: var(--font-family) !important;
436
+ color: var(--text-primary) !important;
437
+ font-weight: 600 !important;
438
+ }
439
+
440
+ /* Clean sections */
441
+ .input-section {
442
+ background: var(--bg-secondary) !important;
443
+ border: 1px solid var(--border-color) !important;
444
+ border-radius: var(--radius-xl) !important;
445
+ padding: var(--spacing-xl) !important;
446
+ margin-bottom: var(--spacing-lg) !important;
447
+ }
448
+
449
+ .output-section {
450
+ background: var(--bg-secondary) !important;
451
+ border: 1px solid var(--border-color) !important;
452
+ border-radius: var(--radius-xl) !important;
453
+ padding: var(--spacing-xl) !important;
454
+ }
455
+
456
+ /* Button styling */
457
+ .primary-button {
458
+ background: var(--accent-color) !important;
459
+ border: none !important;
460
+ color: white !important;
461
+ font-weight: 500 !important;
462
+ font-family: var(--font-family) !important;
463
+ border-radius: var(--radius-lg) !important;
464
+ padding: var(--spacing-md) var(--spacing-xl) !important;
465
+ font-size: var(--font-size-lg) !important;
466
+ transition: all 0.2s ease !important;
467
+ }
468
+
469
+ .primary-button:hover {
470
+ background: var(--accent-hover) !important;
471
+ transform: translateY(-1px) !important;
472
+ }
473
+
474
+ /* Input styling */
475
+ .gradio-textbox textarea,
476
+ .gradio-textbox input {
477
+ background: var(--bg-accent) !important;
478
+ border: 1px solid var(--border-color) !important;
479
+ border-radius: var(--radius-md) !important;
480
+ color: var(--text-primary) !important;
481
+ font-family: var(--font-family) !important;
482
+ font-size: var(--font-size-md) !important;
483
+ padding: var(--spacing-md) !important;
484
+ }
485
+
486
+ .gradio-textbox textarea:focus,
487
+ .gradio-textbox input:focus {
488
+ border-color: var(--accent-color) !important;
489
+ outline: none !important;
490
+ }
491
+
492
+ .gradio-textbox textarea::placeholder,
493
+ .gradio-textbox input::placeholder {
494
+ color: var(--text-muted) !important;
495
+ }
496
+
497
+ /* File upload */
498
+ .gradio-file {
499
+ border: 2px dashed var(--border-light) !important;
500
+ border-radius: var(--radius-xl) !important;
501
+ background: var(--bg-accent) !important;
502
+ padding: var(--spacing-xl) !important;
503
+ transition: all 0.2s ease !important;
504
+ }
505
+
506
+ .gradio-file:hover {
507
+ border-color: var(--accent-color) !important;
508
+ background: rgba(74, 158, 255, 0.1) !important;
509
+ }
510
+
511
+ /* Image upload */
512
+ .gradio-image {
513
+ border: 1px solid var(--border-color) !important;
514
+ border-radius: var(--radius-lg) !important;
515
+ background: var(--bg-accent) !important;
516
+ }
517
+
518
+ /* Checkboxes */
519
+ .gradio-checkbox {
520
+ background: var(--bg-accent) !important;
521
+ border: 1px solid var(--border-color) !important;
522
+ border-radius: var(--radius-md) !important;
523
+ padding: var(--spacing-sm) !important;
524
+ }
525
+
526
+ /* Download section */
527
+ .download-container {
528
+ background: var(--bg-secondary) !important;
529
+ border: 1px solid var(--border-color) !important;
530
+ border-radius: var(--radius-lg) !important;
531
+ padding: var(--spacing-lg) !important;
532
+ margin-top: var(--spacing-lg) !important;
533
+ }
534
+
535
+ .download-item {
536
+ display: flex !important;
537
+ justify-content: space-between !important;
538
+ align-items: center !important;
539
+ padding: var(--spacing-md) 0 !important;
540
+ border-bottom: 1px solid var(--border-color) !important;
541
+ }
542
+
543
+ .download-item:last-child {
544
+ border-bottom: none !important;
545
+ }
546
+
547
+ .download-item:hover {
548
+ background: var(--bg-accent) !important;
549
+ border-radius: var(--radius-sm) !important;
550
+ margin: 0 calc(-1 * var(--spacing-sm)) !important;
551
+ padding: var(--spacing-md) var(--spacing-sm) !important;
552
+ }
553
+
554
+ /* Status styling */
555
+ .status-success {
556
+ color: var(--success-color) !important;
557
+ background: rgba(76, 175, 80, 0.1) !important;
558
+ border: 1px solid rgba(76, 175, 80, 0.3) !important;
559
+ border-radius: var(--radius-md) !important;
560
+ padding: var(--spacing-sm) var(--spacing-md) !important;
561
+ }
562
+
563
+ .status-error {
564
+ color: var(--error-color) !important;
565
+ background: rgba(244, 67, 54, 0.1) !important;
566
+ border: 1px solid rgba(244, 67, 54, 0.3) !important;
567
+ border-radius: var(--radius-md) !important;
568
+ padding: var(--spacing-sm) var(--spacing-md) !important;
569
+ }
570
+
571
+ /* Responsive */
572
+ @media (max-width: 768px) {
573
+ .input-section, .output-section {
574
+ padding: var(--spacing-lg) !important;
575
+ }
576
+
577
+ .primary-button {
578
+ width: 100% !important;
579
+ padding: var(--spacing-md) !important;
580
+ }
581
+ }
582
+ """
583
+
584
+ with gr.Blocks(title="Luna's OCR Studio", css=css, theme=gr.themes.Soft()) as demo:
585
+
586
+ # Clean Header
587
+ gr.HTML("""
588
+ <div style="
589
+ text-align: center;
590
+ padding: 48px 32px;
591
+ background: var(--bg-secondary);
592
+ border: 1px solid var(--border-color);
593
+ border-radius: var(--radius-xl);
594
+ margin-bottom: var(--spacing-xl);
595
+ ">
596
+ <h1 style="
597
+ color: var(--text-primary);
598
+ font-size: var(--font-size-xl);
599
+ font-weight: 700;
600
+ font-family: var(--font-family);
601
+ margin-bottom: var(--spacing-md);
602
+ ">
603
+ 📄 Luna's OCR Studio
604
+ </h1>
605
+ <p style="
606
+ color: var(--text-secondary);
607
+ font-size: var(--font-size-lg);
608
+ font-family: var(--font-family);
609
+ ">
610
+ Extract text from PDFs and images with AI precision
611
+ </p>
612
+ </div>
613
+ """)
614
+
615
+ # Main Interface
616
+ with gr.Row():
617
+ # Input Panel
618
+ with gr.Column(scale=1, elem_classes=["input-section"]):
619
+ gr.Markdown("## API Configuration")
620
+ api_key = gr.Textbox(
621
+ label="Google API Key",
622
+ placeholder="Enter your Google API key here...",
623
+ type="password"
624
+ )
625
+
626
+ gr.Markdown("## Upload Document")
627
+ file_input = gr.File(
628
+ label="Upload PDF or Image File",
629
+ file_types=[".pdf", ".png", ".jpg", ".jpeg", ".webp", ".bmp"]
630
+ )
631
+
632
+ gr.Markdown("**OR**")
633
+
634
+ gr.Markdown("## Paste Image")
635
+ image_input = gr.Image(
636
+ label="Paste from clipboard (Ctrl+V)",
637
+ type="pil"
638
+ )
639
+
640
+ gr.Markdown("## Processing Options")
641
+ use_structured = gr.Checkbox(
642
+ label="Structured Output (Markdown formatting)",
643
+ value=False
644
+ )
645
+ enhance_quality = gr.Checkbox(
646
+ label="Enhance Image Quality",
647
+ value=True
648
+ )
649
+
650
+ # Process Button
651
+ process_btn = gr.Button(
652
+ "Extract Text",
653
+ variant="primary",
654
+ size="lg",
655
+ elem_classes=["primary-button"]
656
+ )
657
+
658
+ # Output Panel
659
+ with gr.Column(scale=2, elem_classes=["output-section"]):
660
+ gr.Markdown("## Status")
661
+ status_output = gr.Textbox(
662
+ value="Ready to extract text from your documents",
663
+ interactive=False,
664
+ show_label=False
665
+ )
666
+
667
+ gr.Markdown("## Extracted Text")
668
+ text_output = gr.Textbox(
669
+ lines=12,
670
+ max_lines=20,
671
+ interactive=False,
672
+ show_label=False,
673
+ placeholder="Your extracted text will appear here..."
674
+ )
675
+
676
+ gr.Markdown("## Image Preview")
677
+ image_preview = gr.Image(
678
+ height=250,
679
+ interactive=False,
680
+ show_label=False
681
+ )
682
+
683
+ # Download Section
684
+ gr.Markdown("## Download Results")
685
+ with gr.Column(elem_classes=["download-container"], visible=False) as download_section:
686
+ gr.HTML("""
687
+ <div style="margin-bottom: var(--spacing-md);">
688
+ <h3 style="color: var(--text-primary); font-family: var(--font-family); margin-bottom: var(--spacing-sm);">
689
+ Available Downloads
690
+ </h3>
691
+ <p style="color: var(--text-secondary); font-family: var(--font-family); font-size: var(--font-size-sm);">
692
+ Choose your preferred format
693
+ </p>
694
+ </div>
695
+ """)
696
+
697
+ with gr.Row(elem_classes=["download-item"]):
698
+ gr.HTML("""
699
+ <div style="display: flex; align-items: center; gap: var(--spacing-sm);">
700
+ <span style="font-size: var(--font-size-lg);">📄</span>
701
+ <div>
702
+ <strong style="color: var(--text-primary); font-family: var(--font-family);">TXT File</strong>
703
+ <br>
704
+ <small style="color: var(--text-secondary); font-family: var(--font-family);">Clean text without formatting</small>
705
+ </div>
706
+ </div>
707
+ """)
708
+ download_txt = gr.File(label="Download", visible=False)
709
+
710
+ with gr.Row(elem_classes=["download-item"]):
711
+ gr.HTML("""
712
+ <div style="display: flex; align-items: center; gap: var(--spacing-sm);">
713
+ <span style="font-size: var(--font-size-lg);">📋</span>
714
+ <div>
715
+ <strong style="color: var(--text-primary); font-family: var(--font-family);">Markdown File</strong>
716
+ <br>
717
+ <small style="color: var(--text-secondary); font-family: var(--font-family);">Formatted with markdown syntax</small>
718
+ </div>
719
+ </div>
720
+ """)
721
+ download_md = gr.File(label="Download", visible=False)
722
+
723
+ with gr.Row(elem_classes=["download-item"]):
724
+ gr.HTML("""
725
+ <div style="display: flex; align-items: center; gap: var(--spacing-sm);">
726
+ <span style="font-size: var(--font-size-lg);">🌐</span>
727
+ <div>
728
+ <strong style="color: var(--text-primary); font-family: var(--font-family);">HTML File</strong>
729
+ <br>
730
+ <small style="color: var(--text-secondary); font-family: var(--font-family);">Beautiful styled web page</small>
731
+ </div>
732
+ </div>
733
+ """)
734
+ download_html = gr.File(label="Download", visible=False)
735
+
736
+ # Event Handlers
737
+ def update_downloads(txt, md, html):
738
+ container_visible = any([txt, md, html])
739
+ return (
740
+ gr.update(visible=container_visible),
741
+ gr.update(value=txt, visible=txt is not None),
742
+ gr.update(value=md, visible=md is not None),
743
+ gr.update(value=html, visible=html is not None)
744
+ )
745
+
746
+ process_btn.click(
747
+ fn=process_document,
748
+ inputs=[file_input, image_input, api_key, use_structured, enhance_quality],
749
+ outputs=[text_output, download_txt, download_md, download_html, image_preview, status_output]
750
+ ).then(
751
+ fn=update_downloads,
752
+ inputs=[download_txt, download_md, download_html],
753
+ outputs=[download_section, download_txt, download_md, download_html]
754
+ )
755
+
756
+ if __name__ == "__main__":
757
+ demo.launch(server_port=7868, share=False, show_error=True)
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "luna-ocr-web",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "dependencies": {
6
+ "@huggingface/hub": "^2.4.0",
7
+ "@radix-ui/react-tabs": "^1.0.4",
8
+ "@react-three/drei": "^10.6.1",
9
+ "@react-three/fiber": "^9.3.0",
10
+ "@react-three/rapier": "^2.1.0",
11
+ "dotenv": "^17.2.1",
12
+ "framer-motion": "^10.16.4",
13
+ "gsap": "^3.13.0",
14
+ "html2canvas": "^1.4.1",
15
+ "jspdf": "^2.5.1",
16
+ "lucide-react": "^0.294.0",
17
+ "marked": "^9.1.2",
18
+ "meshline": "^3.3.1",
19
+ "ogl": "^1.0.11",
20
+ "react": "^19.1.1",
21
+ "react-dom": "^19.1.1",
22
+ "react-dropzone": "^14.2.3",
23
+ "react-scripts": "5.0.1",
24
+ "react-syntax-highlighter": "^15.5.0",
25
+ "three": "^0.178.0"
26
+ },
27
+ "scripts": {
28
+ "start": "concurrently \"npm run server\" \"npm run client\"",
29
+ "client": "react-scripts start",
30
+ "server": "cd server && npm start",
31
+ "build": "react-scripts build",
32
+ "test": "react-scripts test",
33
+ "eject": "react-scripts eject",
34
+ "dev": "concurrently \"npm run server\" \"npm run client\" --names \"SERVER,CLIENT\" --prefix-colors \"blue,green\""
35
+ },
36
+ "eslintConfig": {
37
+ "extends": [
38
+ "react-app",
39
+ "react-app/jest"
40
+ ]
41
+ },
42
+ "browserslist": {
43
+ "production": [
44
+ ">0.2%",
45
+ "not dead",
46
+ "not op_mini all"
47
+ ],
48
+ "development": [
49
+ "last 1 chrome version",
50
+ "last 1 firefox version",
51
+ "last 1 safari version"
52
+ ]
53
+ },
54
+ "devDependencies": {
55
+ "@types/react": "^19.1.9",
56
+ "@types/react-dom": "^19.1.7",
57
+ "concurrently": "^9.2.0"
58
+ }
59
+ }
public/index.html ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <link rel="icon"
7
+ href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🌙💛</text></svg>" />
8
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
9
+ <meta name="theme-color" content="#0a0a0f" />
10
+ <meta name="description" content="Luna OCR - Advanced Text Extraction from PDFs and Images" />
11
+ <title>🌙💛 Luna OCR - Text Extraction</title>
12
+
13
+ <!-- Preload fonts -->
14
+ <link rel="preconnect" href="https://fonts.googleapis.com">
15
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
16
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
17
+ <link href="https://fonts.googleapis.com/css2?family=Martian+Mono:[email protected]&display=swap" rel="stylesheet">
18
+
19
+ <!-- Meta tags for better SEO -->
20
+ <meta property="og:title" content="Luna OCR - Text Extraction" />
21
+ <meta property="og:description"
22
+ content="Extract text from PDFs and images using advanced Luna processing. Support for Thai language and structured formatting." />
23
+ <meta property="og:type" content="website" />
24
+ <meta name="twitter:card" content="summary_large_image" />
25
+
26
+ <style>
27
+ /* Prevent flash of unstyled content */
28
+ body {
29
+ margin: 0;
30
+ background: #141414 !important;
31
+ color: #ffffff;
32
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', sans-serif;
33
+ overflow: hidden;
34
+ }
35
+
36
+ /* Loading screen styles - applied immediately */
37
+ #loading {
38
+ position: fixed !important;
39
+ top: 0 !important;
40
+ left: 0 !important;
41
+ width: 100vw !important;
42
+ height: 100vh !important;
43
+ background: #141414 !important;
44
+ z-index: 99999 !important;
45
+ display: flex !important;
46
+ align-items: center;
47
+ justify-content: center;
48
+ opacity: 1 !important;
49
+ }
50
+
51
+ /* Hide React root initially */
52
+ #root {
53
+ opacity: 0;
54
+ transition: opacity 0.3s ease;
55
+ }
56
+
57
+ /* Show root when loading is done */
58
+ body.loaded #root {
59
+ opacity: 1;
60
+ }
61
+
62
+ body.loaded #loading {
63
+ display: none !important;
64
+ }
65
+ </style>
66
+ </head>
67
+
68
+ <body>
69
+ <noscript>You need to enable JavaScript to run this app.</noscript>
70
+
71
+ <!-- Loading screen -->
72
+ <div id="loading" class="loading-container">
73
+ <div class="a-hole">
74
+ <canvas class="js-canvas"></canvas>
75
+ <div class="aura"></div>
76
+ </div>
77
+ </div>
78
+
79
+ <div id="root"></div>
80
+ <script>
81
+ // Simple loading screen that won't interfere with React
82
+ document.addEventListener('DOMContentLoaded', function () {
83
+ const canvas = document.querySelector('.js-canvas');
84
+ if (!canvas) return;
85
+
86
+ const ctx = canvas.getContext('2d');
87
+ let animationId;
88
+
89
+ // Set canvas size
90
+ function resizeCanvas() {
91
+ const rect = canvas.parentElement.getBoundingClientRect();
92
+ canvas.width = rect.width;
93
+ canvas.height = rect.height;
94
+ }
95
+
96
+ resizeCanvas();
97
+ window.addEventListener('resize', resizeCanvas);
98
+
99
+ // Optimized particle animation - fewer particles for better performance
100
+ const particles = [];
101
+ for (let i = 0; i < 25; i++) {
102
+ particles.push({
103
+ x: Math.random() * canvas.width,
104
+ y: Math.random() * canvas.height,
105
+ vx: (Math.random() - 0.5) * 1.5,
106
+ vy: (Math.random() - 0.5) * 1.5,
107
+ size: Math.random() * 2 + 1,
108
+ opacity: Math.random() * 0.6 + 0.3
109
+ });
110
+ }
111
+
112
+ function animate() {
113
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
114
+
115
+ // Draw particles - all visible immediately
116
+ particles.forEach(particle => {
117
+ particle.x += particle.vx;
118
+ particle.y += particle.vy;
119
+
120
+ // Wrap around edges
121
+ if (particle.x < 0) particle.x = canvas.width;
122
+ if (particle.x > canvas.width) particle.x = 0;
123
+ if (particle.y < 0) particle.y = canvas.height;
124
+ if (particle.y > canvas.height) particle.y = 0;
125
+
126
+ ctx.fillStyle = `rgba(255, 255, 255, ${particle.opacity})`;
127
+ ctx.beginPath();
128
+ ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
129
+ ctx.fill();
130
+ });
131
+
132
+ animationId = requestAnimationFrame(animate);
133
+ }
134
+
135
+ // Start animation after 500ms delay
136
+ setTimeout(() => {
137
+ animate();
138
+ }, 500);
139
+
140
+ // Hide loading screen when React loads
141
+ function hideLoading() {
142
+ cancelAnimationFrame(animationId);
143
+ document.body.classList.add('loaded');
144
+ }
145
+
146
+ // Fast loading screen removal
147
+ let reactLoaded = false;
148
+
149
+ // Check for actual app content (header with Luna OCR)
150
+ const checkReactLoaded = () => {
151
+ const root = document.getElementById('root');
152
+ const appHeader = root?.querySelector('.app-header');
153
+ const logoContent = root?.querySelector('.logo');
154
+
155
+ if (root && root.children.length > 0 && (appHeader || logoContent) && !reactLoaded) {
156
+ reactLoaded = true;
157
+ // Small delay to ensure orb and other components are ready
158
+ setTimeout(hideLoading, 200);
159
+ }
160
+ };
161
+
162
+ // Fast polling every 100ms
163
+ const pollInterval = setInterval(() => {
164
+ checkReactLoaded();
165
+ if (reactLoaded) {
166
+ clearInterval(pollInterval);
167
+ }
168
+ }, 100);
169
+
170
+ // Much faster fallback - hide after 800ms max
171
+ setTimeout(() => {
172
+ if (!reactLoaded) {
173
+ hideLoading();
174
+ }
175
+ }, 800);
176
+
177
+ // Immediate check on DOM ready
178
+ if (document.readyState === 'complete') {
179
+ setTimeout(checkReactLoaded, 100);
180
+ }
181
+ });
182
+ </script>
183
+ </body>
184
+
185
+ </html>
public/sample.html ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Sample Document</title>
7
+ <style>
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
10
+ max-width: 800px;
11
+ margin: 0 auto;
12
+ padding: 40px 20px;
13
+ line-height: 1.6;
14
+ color: #333;
15
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
16
+ min-height: 100vh;
17
+ }
18
+
19
+ .container {
20
+ background: rgba(255, 255, 255, 0.95);
21
+ backdrop-filter: blur(20px);
22
+ border-radius: 20px;
23
+ padding: 40px;
24
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
25
+ }
26
+
27
+ h1 {
28
+ color: #2d3748;
29
+ font-size: 2.5rem;
30
+ margin-bottom: 1rem;
31
+ background: linear-gradient(135deg, #667eea, #764ba2);
32
+ -webkit-background-clip: text;
33
+ -webkit-text-fill-color: transparent;
34
+ background-clip: text;
35
+ }
36
+
37
+ h2 {
38
+ color: #4a5568;
39
+ font-size: 1.5rem;
40
+ margin-top: 2rem;
41
+ margin-bottom: 1rem;
42
+ border-bottom: 2px solid #e2e8f0;
43
+ padding-bottom: 0.5rem;
44
+ }
45
+
46
+ .highlight {
47
+ background: linear-gradient(135deg, #ffd89b 0%, #19547b 100%);
48
+ color: white;
49
+ padding: 20px;
50
+ border-radius: 12px;
51
+ margin: 20px 0;
52
+ }
53
+
54
+ .info-grid {
55
+ display: grid;
56
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
57
+ gap: 20px;
58
+ margin: 30px 0;
59
+ }
60
+
61
+ .info-card {
62
+ background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
63
+ padding: 20px;
64
+ border-radius: 12px;
65
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
66
+ }
67
+
68
+ .info-card h3 {
69
+ margin-top: 0;
70
+ color: #2d3748;
71
+ }
72
+
73
+ table {
74
+ width: 100%;
75
+ border-collapse: collapse;
76
+ margin: 20px 0;
77
+ background: white;
78
+ border-radius: 8px;
79
+ overflow: hidden;
80
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
81
+ }
82
+
83
+ th, td {
84
+ padding: 12px 15px;
85
+ text-align: left;
86
+ border-bottom: 1px solid #e2e8f0;
87
+ }
88
+
89
+ th {
90
+ background: linear-gradient(135deg, #667eea, #764ba2);
91
+ color: white;
92
+ font-weight: 600;
93
+ }
94
+
95
+ tr:hover {
96
+ background-color: #f7fafc;
97
+ }
98
+
99
+ .footer {
100
+ margin-top: 40px;
101
+ padding-top: 20px;
102
+ border-top: 1px solid #e2e8f0;
103
+ text-align: center;
104
+ color: #718096;
105
+ font-size: 0.9rem;
106
+ }
107
+ </style>
108
+ </head>
109
+ <body>
110
+ <div class="container">
111
+ <h1>🌟 Luna OCR Sample Document</h1>
112
+
113
+ <div class="highlight">
114
+ <h2>📋 Document Information</h2>
115
+ <p>This is a sample HTML document designed to showcase the glassmorphism styling and document viewer capabilities of Luna OCR. The document features modern CSS styling with gradients, blur effects, and responsive design.</p>
116
+ </div>
117
+
118
+ <h2>📊 Sample Data Table</h2>
119
+ <table>
120
+ <thead>
121
+ <tr>
122
+ <th>Item</th>
123
+ <th>Category</th>
124
+ <th>Status</th>
125
+ <th>Priority</th>
126
+ </tr>
127
+ </thead>
128
+ <tbody>
129
+ <tr>
130
+ <td>Document Processing</td>
131
+ <td>OCR Technology</td>
132
+ <td>✅ Active</td>
133
+ <td>High</td>
134
+ </tr>
135
+ <tr>
136
+ <td>Glassmorphism UI</td>
137
+ <td>Design System</td>
138
+ <td>✅ Complete</td>
139
+ <td>Medium</td>
140
+ </tr>
141
+ <tr>
142
+ <td>PDF Viewer</td>
143
+ <td>File Handling</td>
144
+ <td>🔄 In Progress</td>
145
+ <td>High</td>
146
+ </tr>
147
+ <tr>
148
+ <td>HTML Viewer</td>
149
+ <td>File Handling</td>
150
+ <td>✅ Complete</td>
151
+ <td>Medium</td>
152
+ </tr>
153
+ </tbody>
154
+ </table>
155
+
156
+ <div class="info-grid">
157
+ <div class="info-card">
158
+ <h3>🎨 Design Features</h3>
159
+ <ul>
160
+ <li>Glassmorphism effects</li>
161
+ <li>Gradient backgrounds</li>
162
+ <li>Smooth animations</li>
163
+ <li>Responsive layout</li>
164
+ </ul>
165
+ </div>
166
+
167
+ <div class="info-card">
168
+ <h3>⚡ Performance</h3>
169
+ <ul>
170
+ <li>Fast rendering</li>
171
+ <li>Optimized CSS</li>
172
+ <li>Minimal JavaScript</li>
173
+ <li>Cross-browser support</li>
174
+ </ul>
175
+ </div>
176
+
177
+ <div class="info-card">
178
+ <h3>🔧 Technical Stack</h3>
179
+ <ul>
180
+ <li>React.js</li>
181
+ <li>Framer Motion</li>
182
+ <li>CSS3 Backdrop Filter</li>
183
+ <li>Modern ES6+</li>
184
+ </ul>
185
+ </div>
186
+ </div>
187
+
188
+ <h2>📝 Sample Content</h2>
189
+ <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
190
+
191
+ <p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
192
+
193
+ <div class="highlight">
194
+ <h3>💡 Key Features</h3>
195
+ <p>This document demonstrates the advanced styling capabilities of the Luna OCR system, including glassmorphism effects, gradient overlays, and modern typography. The viewer supports zoom, rotation, and fullscreen modes for optimal document viewing experience.</p>
196
+ </div>
197
+
198
+ <div class="footer">
199
+ <p>Generated by Luna OCR • Sample HTML Document • Glassmorphism Theme</p>
200
+ </div>
201
+ </div>
202
+ </body>
203
+ </html>
server/README.md ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Luna OCR Backend
2
+
3
+ Real OCR processing backend using Gemini AI for intelligent text extraction and formatting.
4
+
5
+ ## 🚀 Quick Start
6
+
7
+ ### 1. Install Dependencies
8
+ ```bash
9
+ cd server
10
+ npm install
11
+ ```
12
+
13
+ ### 2. Start the Server
14
+ ```bash
15
+ npm start
16
+ # or for development with auto-reload:
17
+ npm run dev
18
+ ```
19
+
20
+ ### 3. Test the API
21
+ ```bash
22
+ curl http://localhost:3001/api/health
23
+ ```
24
+
25
+ ## 📡 API Endpoints
26
+
27
+ ### Health Check
28
+ ```
29
+ GET /api/health
30
+ ```
31
+
32
+ ### OCR Processing
33
+ ```
34
+ POST /api/ocr
35
+ Content-Type: multipart/form-data
36
+
37
+ Parameters:
38
+ - file: Image file (PNG, JPG, WebP) or PDF
39
+ - apiKey: Google Gemini API key
40
+ - mode: "standard" or "structured"
41
+ ```
42
+
43
+ ## 🔧 Configuration
44
+
45
+ ### Environment Variables
46
+ Create a `.env` file (optional):
47
+ ```
48
+ PORT=3001
49
+ MAX_FILE_SIZE=10485760
50
+ ```
51
+
52
+ ### Supported File Types
53
+ - **Images**: PNG, JPG, JPEG, WebP
54
+ - **Documents**: PDF (converted to images)
55
+ - **Max Size**: 10MB per file
56
+
57
+ ## 🎯 Processing Modes
58
+
59
+ ### Standard Mode
60
+ - Uses Gemini 1.5 Flash (faster)
61
+ - Returns clean plain text
62
+ - Good for simple text extraction
63
+
64
+ ### Structured Mode
65
+ - Uses Gemini 1.5 Pro (more intelligent)
66
+ - Returns formatted Markdown
67
+ - Creates tables, headers, lists automatically
68
+ - Perfect for complex documents
69
+
70
+ ## 📊 Response Format
71
+
72
+ ```json
73
+ {
74
+ "success": true,
75
+ "data": {
76
+ "fileName": "document.png",
77
+ "fileSize": 1234567,
78
+ "processingMode": "structured",
79
+ "extractedText": "# Document Title\n\n...",
80
+ "formats": {
81
+ "txt": "plain text version",
82
+ "md": "markdown version",
83
+ "json": { "metadata": {...}, "content": {...} }
84
+ },
85
+ "metadata": {
86
+ "characterCount": 1500,
87
+ "wordCount": 250,
88
+ "lineCount": 45,
89
+ "processedAt": "2024-01-01T12:00:00.000Z"
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ ## 🛠️ Development
96
+
97
+ ### Project Structure
98
+ ```
99
+ server/
100
+ ├── server.js # Main server file
101
+ ├── package.json # Dependencies
102
+ ├── uploads/ # Temporary file storage
103
+ └── README.md # This file
104
+ ```
105
+
106
+ ### Key Features
107
+ - **Image Enhancement**: Automatic image preprocessing for better OCR
108
+ - **Smart Formatting**: Gemini AI creates beautiful Markdown output
109
+ - **Multiple Formats**: Returns TXT, MD, and JSON formats
110
+ - **Error Handling**: Comprehensive error handling and cleanup
111
+ - **File Cleanup**: Automatic temporary file cleanup
112
+
113
+ ## 🔑 Getting Gemini API Key
114
+
115
+ 1. Go to [Google AI Studio](https://makersuite.google.com/app/apikey)
116
+ 2. Create a new API key
117
+ 3. Copy the key and use it in the frontend
118
+
119
+ ## 🚨 Troubleshooting
120
+
121
+ ### Common Issues
122
+
123
+ **"Cannot connect to OCR backend"**
124
+ - Make sure server is running: `npm start`
125
+ - Check port 3001 is not in use
126
+ - Verify no firewall blocking
127
+
128
+ **"Invalid API key"**
129
+ - Check your Gemini API key is correct
130
+ - Ensure API key has proper permissions
131
+ - Try creating a new API key
132
+
133
+ **"File too large"**
134
+ - Maximum file size is 10MB
135
+ - Compress images before uploading
136
+ - For PDFs, try splitting into smaller files
137
+
138
+ **"Processing failed"**
139
+ - Check image quality (not too blurry)
140
+ - Ensure text is clearly visible
141
+ - Try different processing mode
142
+
143
+ ### Debug Mode
144
+ Set `NODE_ENV=development` for detailed logging:
145
+ ```bash
146
+ NODE_ENV=development npm start
147
+ ```
148
+
149
+ ## 📝 Notes
150
+
151
+ - Server runs on port 3001 by default
152
+ - Temporary files are automatically cleaned up
153
+ - CORS is enabled for frontend integration
154
+ - Image enhancement improves OCR accuracy
155
+ - Gemini AI provides intelligent text formatting
156
+
157
+ ## 🔗 Integration
158
+
159
+ The backend integrates seamlessly with the Luna OCR React frontend. Make sure both are running:
160
+
161
+ 1. **Backend**: `cd server && npm start` (port 3001)
162
+ 2. **Frontend**: `npm start` (port 3000)
163
+
164
+ The frontend will automatically call the backend API for real OCR processing!
server/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
server/package.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "luna-ocr-backend",
3
+ "version": "1.0.0",
4
+ "description": "Backend API for Luna OCR with advanced text extraction",
5
+ "main": "server.js",
6
+ "scripts": {
7
+ "start": "node server.js",
8
+ "dev": "nodemon server.js"
9
+ },
10
+ "dependencies": {
11
+ "@google/generative-ai": "^0.2.1",
12
+ "cors": "^2.8.5",
13
+ "express": "^4.18.2",
14
+ "jimp": "^0.22.10",
15
+ "multer": "^1.4.5-lts.1",
16
+ "node-tesseract-ocr": "^2.2.1",
17
+ "pdf-poppler": "^0.2.1",
18
+ "sharp": "^0.32.6"
19
+ },
20
+ "devDependencies": {
21
+ "nodemon": "^3.0.2"
22
+ }
23
+ }
server/server.js ADDED
@@ -0,0 +1,693 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const cors = require('cors');
3
+ const multer = require('multer');
4
+ const sharp = require('sharp');
5
+ const { GoogleGenerativeAI } = require('@google/generative-ai');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ // Server-side API key (more secure for public deployment)
10
+ const GEMINI_API_KEY = process.env.GEMINI_API_KEY || null;
11
+ const pdf = require('pdf-poppler');
12
+
13
+ const app = express();
14
+ const PORT = process.env.PORT || 3002;
15
+
16
+ // Cleanup function for temporary files
17
+ function cleanupTempFiles() {
18
+ const uploadsDir = path.join(__dirname, 'uploads');
19
+
20
+ try {
21
+ if (!fs.existsSync(uploadsDir)) {
22
+ fs.mkdirSync(uploadsDir, { recursive: true });
23
+ console.log('📁 Created uploads directory');
24
+ return;
25
+ }
26
+
27
+ const files = fs.readdirSync(uploadsDir);
28
+ let cleanedCount = 0;
29
+
30
+ files.forEach(file => {
31
+ const filePath = path.join(uploadsDir, file);
32
+ const stats = fs.statSync(filePath);
33
+ const now = new Date();
34
+ const fileAge = now - stats.mtime; // Age in milliseconds
35
+ const maxAge = 30 * 60 * 1000; // 30 minutes in milliseconds
36
+
37
+ // Clean up files older than 30 minutes
38
+ if (fileAge > maxAge) {
39
+ try {
40
+ fs.unlinkSync(filePath);
41
+ cleanedCount++;
42
+ console.log(`🗑️ Cleaned up old temp file: ${file}`);
43
+ } catch (error) {
44
+ console.warn(`⚠️ Could not delete ${file}:`, error.message);
45
+ }
46
+ }
47
+ });
48
+
49
+ if (cleanedCount > 0) {
50
+ console.log(`✅ Cleaned up ${cleanedCount} temporary files`);
51
+ } else {
52
+ console.log('✅ No temporary files to clean up');
53
+ }
54
+
55
+ } catch (error) {
56
+ console.error('❌ Error during cleanup:', error.message);
57
+ }
58
+ }
59
+
60
+ // Run cleanup on server start
61
+ cleanupTempFiles();
62
+
63
+ // Schedule periodic cleanup every 15 minutes
64
+ setInterval(() => {
65
+ console.log('🔄 Running periodic cleanup...');
66
+ cleanupTempFiles();
67
+ }, 15 * 60 * 1000); // 15 minutes
68
+
69
+ // Middleware
70
+ app.use(cors());
71
+ app.use(express.json());
72
+
73
+ // Configure multer for file uploads
74
+ const upload = multer({
75
+ dest: 'uploads/',
76
+ limits: {
77
+ fileSize: 10 * 1024 * 1024, // 10MB limit
78
+ },
79
+ fileFilter: (req, file, cb) => {
80
+ const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
81
+ if (allowedTypes.includes(file.mimetype)) {
82
+ cb(null, true);
83
+ } else {
84
+ cb(new Error('Invalid file type. Only JPEG, PNG, WebP, and PDF are allowed.'));
85
+ }
86
+ }
87
+ });
88
+
89
+ // PDF to image conversion using pdf-poppler
90
+ async function convertPdfToImages(pdfPath) {
91
+ try {
92
+ const outputDir = path.dirname(pdfPath);
93
+
94
+ console.log('Converting PDF to images using pdf-poppler...');
95
+
96
+ const options = {
97
+ format: 'png',
98
+ out_dir: outputDir,
99
+ out_prefix: 'page',
100
+ page: null, // Convert all pages
101
+ scale: 2048 // High resolution for better OCR
102
+ };
103
+
104
+ console.log('PDF conversion options:', options);
105
+
106
+ // Convert PDF to images
107
+ const results = await pdf.convert(pdfPath, options);
108
+
109
+ console.log('PDF conversion results:', results);
110
+
111
+ // Build image paths based on the results
112
+ const imagePaths = [];
113
+ if (Array.isArray(results)) {
114
+ for (let i = 0; i < results.length; i++) {
115
+ const imagePath = path.join(outputDir, `page-${i + 1}.png`);
116
+ if (fs.existsSync(imagePath)) {
117
+ imagePaths.push(imagePath);
118
+ console.log(`Found converted page: ${imagePath}`);
119
+ }
120
+ }
121
+ }
122
+
123
+ // If no images found with the expected naming, try alternative naming
124
+ if (imagePaths.length === 0) {
125
+ console.log('Trying alternative file naming patterns...');
126
+ const files = fs.readdirSync(outputDir);
127
+ const pngFiles = files.filter(file => file.endsWith('.png') && file.startsWith('page'));
128
+
129
+ for (const file of pngFiles) {
130
+ const fullPath = path.join(outputDir, file);
131
+ imagePaths.push(fullPath);
132
+ console.log(`Found PNG file: ${fullPath}`);
133
+ }
134
+ }
135
+
136
+ console.log(`Successfully converted ${imagePaths.length} pages to images`);
137
+ return imagePaths;
138
+ } catch (error) {
139
+ console.error('PDF conversion error:', error);
140
+ throw new Error(`PDF conversion failed: ${error.message}`);
141
+ }
142
+ }
143
+
144
+ // Enhanced image preprocessing
145
+ async function enhanceImageForOCR(imagePath) {
146
+ try {
147
+ // Create a unique enhanced filename to avoid input/output conflict
148
+ const enhancedPath = imagePath + '_enhanced.png';
149
+
150
+ await sharp(imagePath)
151
+ .resize(null, 2000, {
152
+ withoutEnlargement: false,
153
+ kernel: sharp.kernel.lanczos3
154
+ })
155
+ .normalize()
156
+ .sharpen({ sigma: 1.2, flat: 1, jagged: 2 })
157
+ .gamma(1.1)
158
+ .png({ quality: 95, compressionLevel: 6 })
159
+ .toFile(enhancedPath);
160
+
161
+ return enhancedPath;
162
+ } catch (error) {
163
+ console.error('Image enhancement error:', error);
164
+ return imagePath; // Return original if enhancement fails
165
+ }
166
+ }
167
+
168
+ // Gemini OCR processing with intelligent formatting
169
+ async function processWithGemini(imagePath, apiKey, mode = 'standard') {
170
+ try {
171
+ const genAI = new GoogleGenerativeAI(apiKey);
172
+
173
+ // Choose model based on mode (using 2.5 models as specified)
174
+ const modelName = mode === 'structured' ? 'gemini-2.5-flash' : 'gemini-2.5-pro';
175
+ const model = genAI.getGenerativeModel({ model: modelName });
176
+
177
+ // Read and prepare image
178
+ const imageBuffer = fs.readFileSync(imagePath);
179
+ const imageBase64 = imageBuffer.toString('base64');
180
+
181
+ const imagePart = {
182
+ inlineData: {
183
+ data: imageBase64,
184
+ mimeType: 'image/png'
185
+ }
186
+ };
187
+
188
+ // Choose prompt based on mode
189
+ let prompt;
190
+ if (mode === 'structured') {
191
+ prompt = `🇹🇭 **THAI-FOCUSED MARKDOWN OCR - อ่านข้อความไทยและจัดรูปแบบเป็น Markdown**
192
+
193
+ **คำสั่งสำคัญ (CRITICAL INSTRUCTIONS):**
194
+ 1. **อ่านทุกตัวอักษร** - แยกข้อความที่เขียนในรูปภาพทุกตัว
195
+ 2. **ไม่ใช้ตัวอย่างทั่วไป** - ห้ามใช้ "Field | Value" ให้ใช้ข้อความจริงที่เห็น
196
+ 3. **รักษาภาษาเดิม** - ถ้าเป็นไทยให้เป็นไทย ถ้าเป็นอังกฤษให้เป็นอังกฤษ
197
+ 4. **ข้อความจริงเท่านั้น** - ห้ามตีความ แปล หรือสร้างคำอธิบายเอง
198
+
199
+ **วิธีการสแกน:**
200
+ - เริ่มจากซ้ายบน อ่านทีละบรรทัดไปขวาล่าง
201
+ - รวมข้อความทั้งหมด: หัวข้อ เนื้อหา ตัวเลข วันที่ ข้อความเล็ก
202
+ - **สำหรับข้อความไทย: เว้นวรรคระหว่างคำให้ถูกต้อง**
203
+ - สำหรับตาราง: ใช้หัวตารางและข้อมูลจริงที่เห็น
204
+ - สำหรับรายการ: ใช้รายการจริงที่เห็น
205
+
206
+ **กฎการจัดรูปแบบ MARKDOWN:**
207
+ - ## สำหรับหัวข้อใหญ่ (ใช้หัวข้อจริง)
208
+ - ### สำหรับหัวข้อย่อย (ใช้หัวข้อจริง)
209
+ - **ใช้ตารางเสมอสำหรับข้อมูลที่เป็นระบบ:**
210
+ - เห็นตาราง → สร้างตารางด้วยหัวตารางและข้อมูลจริง
211
+ - เห็นรายการ → แปลงเป็นตารางด้วยรายการจริง
212
+ - เห็นคู่ข้อมูล → ใช้คีย์และค่าจริง
213
+ - ใช้ **ตัวหนา** สำหรับข้อความที่เน้นในรูป
214
+ - ใช้ > สำหรับหมายเหตุที่ปรากฏในรูป
215
+
216
+ **ตัวอย่างตาราง:**
217
+ | รายการ | จำนวน | ราคา |
218
+ |--------|--------|------|
219
+ | กาแฟ | 2 แก้ว | 60 บาท |
220
+ | ขนมปัง | 1 ชิ้น | 25 บาท |
221
+
222
+ **ผลลัพธ์: MARKDOWN ที่มีเนื้อหาจริง - ห้ามใช้แม่แบบทั่วไป**`;
223
+ } else {
224
+ prompt = `🇹🇭 **FAST MARKDOWN OCR - SAME FEATURES AS PRO MODE (2.5 FLASH)**
225
+
226
+ **คำสั่งสำคัญ (IDENTICAL TO STRUCTURED MODE):**
227
+ 1. **อ่านทุกตัวอักษร** - แยกข้อความที่เขียนในรูปภาพทุกตัว
228
+ 2. **ไม่ใช้ตัวอย่างทั่วไป** - ห้ามใช้ "Field | Value" ให้ใช้ข้อความจริงที่เห็น
229
+ 3. **รักษาภาษาเดิม** - ถ��าเป็นไทยให้เป็นไทย ถ้าเป็นอังกฤษให้เป็นอังกฤษ
230
+ 4. **ข้อความจริงเท่านั้น** - ห้ามตีความ แปล หรือสร้างคำอธิบายเอง
231
+
232
+ **วิธีการสแกน (SAME AS PRO MODE):**
233
+ - เริ่มจากซ้ายบน อ่านทีละบรรทัดไปขวาล่าง
234
+ - รวมข้อความทั้งหมด: หัวข้อ เนื้อหา ตัวเลข วันที่ ข้อความเล็ก
235
+ - **สำหรับข้อความไทย: เว้นวรรคระหว่างคำให้ถูกต้อง**
236
+ - สำหรับตาราง: ใช้หัวตารางและข้อมูลจริงที่เห็น
237
+ - สำหรับรายการ: ใช้รายการจริงที่เห็น
238
+
239
+ **MARKDOWN FORMATTING RULES (IDENTICAL TO PRO MODE):**
240
+ - ## สำหรับหัวข้อใหญ่ (ใช้หัวข้อจริง)
241
+ - ### สำหรับหัวข้อย่อย (ใช้หัวข้อจริง)
242
+ - **ใช้ตารางเสมอสำหรับข้อมูลที่เป็นระบบ:**
243
+ - เห็นตาราง → สร้างตาราง markdown ด้วยหัวตารางและข้อมูลจริง
244
+ - เห็นรายการ → แปลงเป็นตาราง markdown ด้วยรายการจริง
245
+ - เห็นคู่ข้อมูล → ใช้คีย์และค่าจริงในตาราง markdown
246
+ - ใช้ **ตัวหนา** สำหรับข้อความที่เน้นในรูป
247
+ - ใช้ > สำหรับหมายเหตุที่ปรากฏในรูป
248
+ - ใช้ - สำหรับรายการเมื่อเหมาะสม
249
+
250
+ **การประมวลผลภาษา (SAME AS PRO MODE):**
251
+ - ข้อความไทย: เว้นวรรคให้ถูกต้อง แก้ไขข้อผิดพลาด OCR
252
+ - ข้อความอังกฤษ: รักษาการสะกดและตัวพิมพ์เดิม
253
+ - ตัวเลข: แยกตัวเลข ทศนิยม เปอร์เซ็นต์ รหัสให้แม่นยำ
254
+ - ภาษาผสม: รักษาภาษาเดิมของแต่ละส่วน
255
+
256
+ **ตัวอย่างตาราง MARKDOWN:**
257
+ | รายการ | จำนวน | ราคา |
258
+ |--------|--------|------|
259
+ | กาแฟ อเมริกาโน่ | 2 แก้ว | 120 บาท |
260
+ | ขนมปังโฮลวีท | 1 ชิ้น | 45 บาท |
261
+
262
+ **ผลลัพธ์: MARKDOWN WITH SAME FEATURES AS PRO MODE - JUST FASTER WITH 2.5 FLASH**`;
263
+ }
264
+
265
+ const result = await model.generateContent([prompt, imagePart]);
266
+ const response = await result.response;
267
+ let extractedText = response.text();
268
+
269
+ // Both modes now support the same markdown formatting
270
+ // No cleanup needed - both modes produce markdown output
271
+
272
+ return extractedText;
273
+
274
+ } catch (error) {
275
+ console.error('Gemini processing error:', error);
276
+ throw new Error(`OCR processing failed: ${error.message}`);
277
+ }
278
+ }
279
+
280
+ // Generate different format outputs
281
+ function generateFormats(text, fileName, mode) {
282
+ const baseFileName = fileName.replace(/\.[^/.]+$/, '');
283
+ const timestamp = new Date().toISOString();
284
+
285
+ const formats = {
286
+ // Plain text
287
+ txt: text,
288
+
289
+ // Markdown (always available for both modes)
290
+ md: mode === 'structured' ? text : `# ${baseFileName}\n\n${text}`,
291
+
292
+ // JSON format
293
+ json: {
294
+ metadata: {
295
+ fileName: fileName,
296
+ extractedAt: timestamp,
297
+ characterCount: text.length,
298
+ lineCount: text.split('\n').length,
299
+ wordCount: text.split(/\s+/).filter(word => word.length > 0).length,
300
+ processingMode: mode
301
+ },
302
+ content: {
303
+ rawText: text,
304
+ lines: text.split('\n'),
305
+ paragraphs: text.split('\n\n').filter(p => p.trim().length > 0)
306
+ }
307
+ }
308
+ };
309
+
310
+ return formats;
311
+ }
312
+
313
+ // Progress tracking
314
+ const progressTracking = new Map();
315
+ const consoleLogs = new Map(); // Store console logs per session
316
+
317
+ // Progress endpoint
318
+ app.get('/api/progress/:sessionId', (req, res) => {
319
+ const sessionId = req.params.sessionId;
320
+ const progress = progressTracking.get(sessionId) || { current: 0, total: 0, status: 'Not started' };
321
+ res.json(progress);
322
+ });
323
+
324
+ // Latest progress endpoint (gets the most recent progress)
325
+ app.get('/api/progress-latest', (req, res) => {
326
+ if (progressTracking.size === 0) {
327
+ return res.json({ current: 0, total: 0, status: 'No active processing' });
328
+ }
329
+
330
+ // Get the most recent progress entry
331
+ const entries = Array.from(progressTracking.entries());
332
+ const latestEntry = entries[entries.length - 1];
333
+ // Sending progress data to frontend
334
+ res.json(latestEntry[1]);
335
+ });
336
+
337
+ // Main OCR endpoint
338
+ app.post('/api/ocr', upload.single('file'), async (req, res) => {
339
+ const sessionId = Date.now().toString();
340
+
341
+ try {
342
+ const { mode = 'standard' } = req.body;
343
+
344
+ // Use server-side API key if available, otherwise require from client
345
+ const apiKey = GEMINI_API_KEY || req.body.apiKey;
346
+
347
+ if (!apiKey) {
348
+ return res.status(400).json({
349
+ success: false,
350
+ error: GEMINI_API_KEY ? 'Server API key not configured' : 'Google API Key is required'
351
+ });
352
+ }
353
+
354
+ if (!req.file) {
355
+ return res.status(400).json({
356
+ success: false,
357
+ error: 'No file uploaded'
358
+ });
359
+ }
360
+
361
+ let extractedText = '';
362
+ let imagePaths = [];
363
+
364
+ // Console log capture helper
365
+ const addConsoleLog = (message) => {
366
+ if (!consoleLogs.has(sessionId)) {
367
+ consoleLogs.set(sessionId, []);
368
+ }
369
+ const logs = consoleLogs.get(sessionId);
370
+ logs.push({
371
+ timestamp: new Date().toISOString(),
372
+ message: message
373
+ });
374
+ // Keep only last 20 logs to prevent memory issues
375
+ if (logs.length > 20) {
376
+ logs.shift();
377
+ }
378
+ console.log(message);
379
+ };
380
+
381
+ addConsoleLog(`🚀 Processing file: ${req.file.originalname} in ${mode} mode`);
382
+
383
+ // Progress update helper
384
+ const updateProgress = (current, total, status, details = {}) => {
385
+ const progressData = {
386
+ current,
387
+ total,
388
+ status,
389
+ sessionId,
390
+ fileName: req.file.originalname,
391
+ consoleLogs: consoleLogs.get(sessionId) || [],
392
+ ...details
393
+ };
394
+ progressTracking.set(sessionId, progressData);
395
+ addConsoleLog(`Progress: ${current}/${total} - ${status}`);
396
+ };
397
+
398
+ // Handle PDF files
399
+ if (req.file.mimetype === 'application/pdf') {
400
+ addConsoleLog('🔄 Starting PDF processing...');
401
+ addConsoleLog(`📄 PDF file: ${req.file.originalname}`);
402
+ addConsoleLog(`📊 File size: ${(req.file.size / 1024).toFixed(2)} KB`);
403
+
404
+ updateProgress(1, 10, '🖼️ Converting PDF to images...');
405
+ const pdfImagePaths = await convertPdfToImages(req.file.path);
406
+
407
+ if (pdfImagePaths.length === 0) {
408
+ throw new Error('No pages could be extracted from PDF. The PDF might be corrupted or empty.');
409
+ }
410
+
411
+ addConsoleLog(`✅ Converted ${pdfImagePaths.length} pages to images`);
412
+
413
+ // Calculate total steps: 2 initial + (2 steps per page)
414
+ const totalSteps = 2 + (pdfImagePaths.length * 2);
415
+ updateProgress(2, totalSteps, `📋 Found ${pdfImagePaths.length} pages to process`, {
416
+ totalPages: pdfImagePaths.length,
417
+ currentPage: 0,
418
+ totalCharacters: 0
419
+ });
420
+
421
+ // Process each page with OCR
422
+ for (let i = 0; i < pdfImagePaths.length; i++) {
423
+ const currentPage = i + 1;
424
+ const totalPages = pdfImagePaths.length;
425
+
426
+ addConsoleLog(`🔍 Processing page ${currentPage}/${totalPages} (${Math.round((currentPage / totalPages) * 100)}%)`);
427
+
428
+ // Update progress for enhancement step
429
+ const enhanceStep = 2 + (i * 2) + 1;
430
+ updateProgress(enhanceStep, totalSteps, `🔧 Enhancing page ${currentPage}/${totalPages}`, {
431
+ totalPages,
432
+ currentPage,
433
+ totalCharacters: extractedText.length,
434
+ phase: 'enhancing'
435
+ });
436
+
437
+ addConsoleLog(`📝 Enhancing image: page-${currentPage}.png`);
438
+ const enhancedImagePath = await enhanceImageForOCR(pdfImagePaths[i]);
439
+
440
+ // Update progress for OCR step
441
+ const ocrStep = 2 + (i * 2) + 2;
442
+ updateProgress(ocrStep, totalSteps, `🤖 Running OCR on page ${currentPage}/${totalPages}`, {
443
+ totalPages,
444
+ currentPage,
445
+ totalCharacters: extractedText.length,
446
+ phase: 'ocr'
447
+ });
448
+
449
+ addConsoleLog(`🤖 Running OCR on page ${currentPage} with Gemini ${mode === 'structured' ? '2.5-Pro' : '2.5-Flash'}`);
450
+ const pageText = await processWithGemini(enhancedImagePath, apiKey, mode);
451
+
452
+ addConsoleLog(`✅ Page ${currentPage} processed - ${pageText.length} characters extracted`);
453
+
454
+ if (mode === 'structured') {
455
+ extractedText += `\n\n## Page ${currentPage}\n\n${pageText}`;
456
+ } else {
457
+ extractedText += `\n\n--- Page ${currentPage} ---\n\n${pageText}`;
458
+ }
459
+
460
+ // Update progress with completed page info
461
+ updateProgress(ocrStep, totalSteps, `✅ Page ${currentPage}/${totalPages} completed - ${pageText.length} chars`, {
462
+ totalPages,
463
+ currentPage,
464
+ totalCharacters: extractedText.length,
465
+ pageCharacters: pageText.length,
466
+ phase: 'completed'
467
+ });
468
+
469
+ addConsoleLog(`📊 Total extracted so far: ${extractedText.length} characters`);
470
+
471
+ imagePaths.push(enhancedImagePath);
472
+ }
473
+
474
+ addConsoleLog('🧹 Cleaning up temporary files...');
475
+ // Clean up PDF image files
476
+ pdfImagePaths.forEach((imagePath, index) => {
477
+ try {
478
+ if (fs.existsSync(imagePath)) {
479
+ fs.unlinkSync(imagePath);
480
+ addConsoleLog(`🗑️ Cleaned up page-${index + 1}.png`);
481
+ }
482
+ } catch (error) {
483
+ addConsoleLog(`⚠️ Cleanup warning: ${error.message}`);
484
+ }
485
+ });
486
+
487
+ } else {
488
+ // Handle regular image files
489
+ addConsoleLog('🖼️ Processing single image file...');
490
+ addConsoleLog(`📄 File: ${req.file.originalname}`);
491
+ addConsoleLog(`📊 Size: ${(req.file.size / 1024).toFixed(2)} KB`);
492
+
493
+ updateProgress(1, 3, '🔧 Enhancing image for better OCR...', {
494
+ totalPages: 1,
495
+ currentPage: 1,
496
+ totalCharacters: 0,
497
+ phase: 'enhancing'
498
+ });
499
+ const enhancedImagePath = await enhanceImageForOCR(req.file.path);
500
+
501
+ updateProgress(2, 3, `🤖 Running OCR with Gemini ${mode === 'structured' ? '2.5-Pro' : '2.5-Flash'}...`, {
502
+ totalPages: 1,
503
+ currentPage: 1,
504
+ totalCharacters: 0,
505
+ phase: 'ocr'
506
+ });
507
+ extractedText = await processWithGemini(enhancedImagePath, apiKey, mode);
508
+
509
+ updateProgress(3, 3, `✅ OCR completed - ${extractedText.length} characters extracted`, {
510
+ totalPages: 1,
511
+ currentPage: 1,
512
+ totalCharacters: extractedText.length,
513
+ pageCharacters: extractedText.length,
514
+ phase: 'completed'
515
+ });
516
+ addConsoleLog(`✅ OCR completed - ${extractedText.length} characters extracted`);
517
+ imagePaths.push(enhancedImagePath);
518
+ }
519
+
520
+ // Generate all formats
521
+ const formats = generateFormats(extractedText, req.file.originalname, mode);
522
+
523
+ // Comprehensive cleanup of all temporary files
524
+ const filesToCleanup = [req.file.path, ...imagePaths];
525
+ let cleanedFiles = 0;
526
+
527
+ filesToCleanup.forEach(filePath => {
528
+ if (filePath && fs.existsSync(filePath)) {
529
+ try {
530
+ fs.unlinkSync(filePath);
531
+ cleanedFiles++;
532
+ addConsoleLog(`🗑️ Cleaned up: ${path.basename(filePath)}`);
533
+ } catch (error) {
534
+ addConsoleLog(`⚠️ Cleanup warning for ${path.basename(filePath)}: ${error.message}`);
535
+ }
536
+ }
537
+ });
538
+
539
+ addConsoleLog(`✅ Cleanup complete: ${cleanedFiles} files removed`);
540
+
541
+ // Final progress update
542
+ const finalProgress = progressTracking.get(sessionId);
543
+ if (finalProgress) {
544
+ updateProgress(finalProgress.total, finalProgress.total, '✅ Processing complete!');
545
+ }
546
+
547
+ // Return success response
548
+ res.json({
549
+ success: true,
550
+ sessionId: sessionId,
551
+ data: {
552
+ fileName: req.file.originalname,
553
+ fileSize: req.file.size,
554
+ processingMode: mode,
555
+ extractedText: extractedText,
556
+ formats: formats,
557
+ metadata: {
558
+ characterCount: extractedText.length,
559
+ wordCount: extractedText.split(/\s+/).filter(word => word.length > 0).length,
560
+ lineCount: extractedText.split('\n').length,
561
+ processedAt: new Date().toISOString()
562
+ }
563
+ }
564
+ });
565
+
566
+ // Clean up progress tracking and console logs after a delay
567
+ setTimeout(() => {
568
+ progressTracking.delete(sessionId);
569
+ consoleLogs.delete(sessionId);
570
+ }, 30000); // Clean up after 30 seconds
571
+
572
+ } catch (error) {
573
+ console.error('OCR processing error:', error);
574
+
575
+ // Comprehensive cleanup on error
576
+ const filesToCleanup = [];
577
+ if (req.file && req.file.path) filesToCleanup.push(req.file.path);
578
+ if (imagePaths && imagePaths.length > 0) filesToCleanup.push(...imagePaths);
579
+
580
+ let cleanedFiles = 0;
581
+ filesToCleanup.forEach(filePath => {
582
+ if (filePath && fs.existsSync(filePath)) {
583
+ try {
584
+ fs.unlinkSync(filePath);
585
+ cleanedFiles++;
586
+ console.log(`🗑️ Error cleanup: ${path.basename(filePath)}`);
587
+ } catch (cleanupError) {
588
+ console.warn(`⚠️ Error cleanup warning for ${path.basename(filePath)}:`, cleanupError.message);
589
+ }
590
+ }
591
+ });
592
+
593
+ if (cleanedFiles > 0) {
594
+ console.log(`✅ Error cleanup complete: ${cleanedFiles} files removed`);
595
+ }
596
+
597
+ res.status(500).json({
598
+ success: false,
599
+ error: error.message || 'Internal server error during OCR processing'
600
+ });
601
+ }
602
+ });
603
+
604
+ // Root endpoint
605
+ app.get('/', (req, res) => {
606
+ res.json({
607
+ success: true,
608
+ message: '🌙 Luna OCR Backend API',
609
+ version: '1.0.0',
610
+ endpoints: {
611
+ health: '/api/health',
612
+ ocr: '/api/ocr (POST)'
613
+ },
614
+ status: 'Running',
615
+ port: PORT,
616
+ timestamp: new Date().toISOString()
617
+ });
618
+ });
619
+
620
+ // Health check endpoint
621
+ app.get('/api/health', (req, res) => {
622
+ res.json({
623
+ success: true,
624
+ message: 'Luna OCR Backend is running!',
625
+ hasApiKey: !!GEMINI_API_KEY,
626
+ requiresUserApiKey: !GEMINI_API_KEY,
627
+ timestamp: new Date().toISOString()
628
+ });
629
+ });
630
+
631
+ // Manual cleanup endpoint
632
+ app.post('/api/cleanup', (req, res) => {
633
+ try {
634
+ console.log('🧹 Manual cleanup requested...');
635
+ cleanupTempFiles();
636
+
637
+ res.json({
638
+ success: true,
639
+ message: 'Cleanup completed successfully',
640
+ timestamp: new Date().toISOString()
641
+ });
642
+ } catch (error) {
643
+ console.error('Manual cleanup error:', error);
644
+ res.status(500).json({
645
+ success: false,
646
+ error: 'Cleanup failed: ' + error.message
647
+ });
648
+ }
649
+ });
650
+
651
+ // Error handling middleware
652
+ app.use((error, req, res, next) => {
653
+ if (error instanceof multer.MulterError) {
654
+ if (error.code === 'LIMIT_FILE_SIZE') {
655
+ return res.status(400).json({
656
+ success: false,
657
+ error: 'File too large. Maximum size is 10MB.'
658
+ });
659
+ }
660
+ }
661
+
662
+ console.error('Unhandled error:', error);
663
+ res.status(500).json({
664
+ success: false,
665
+ error: 'Internal server error'
666
+ });
667
+ });
668
+
669
+ // Graceful shutdown cleanup
670
+ process.on('SIGINT', () => {
671
+ console.log('\n🛑 Received SIGINT, cleaning up before shutdown...');
672
+ cleanupTempFiles();
673
+ console.log('� LuRna OCR Backend shutting down gracefully');
674
+ process.exit(0);
675
+ });
676
+
677
+ process.on('SIGTERM', () => {
678
+ console.log('\n🛑 Received SIGTERM, cleaning up before shutdown...');
679
+ cleanupTempFiles();
680
+ console.log('👋 Luna OCR Backend shutting down gracefully');
681
+ process.exit(0);
682
+ });
683
+
684
+ // Start server
685
+ app.listen(PORT, () => {
686
+ console.log(`🚀 Luna OCR Backend running on port ${PORT}`);
687
+ console.log(`📡 Health check: http://localhost:${PORT}/api/health`);
688
+ console.log(`🔍 OCR endpoint: http://localhost:${PORT}/api/ocr`);
689
+ console.log(`🧹 Cleanup endpoint: http://localhost:${PORT}/api/cleanup`);
690
+ console.log(`⏰ Automatic cleanup runs every 15 minutes`);
691
+ });
692
+
693
+ module.exports = app;
src/App.js ADDED
@@ -0,0 +1,420 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useCallback, useRef, useEffect } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { useDropzone } from 'react-dropzone';
4
+ import FlowingMenu from './components/FlowingMenu';
5
+ import FileUploader from './components/FileUploader';
6
+ import PreviewPanel from './components/PreviewPanel';
7
+ import ResultsPanel from './components/ResultsPanel';
8
+ import Orb from './components/Orb';
9
+ import TrueFocus from './components/TrueFocus';
10
+ import LoadingScreen from './components/LoadingScreen';
11
+ import ProcessingProgress from './components/ProcessingProgress';
12
+ import ApiKeyEncryption from './utils/encryption';
13
+ import './index.css';
14
+
15
+ function App() {
16
+ const [apiKey, setApiKey] = useState(() => {
17
+ // Load encrypted API key from localStorage on initialization
18
+ return ApiKeyEncryption.retrieveApiKey() || '';
19
+ });
20
+
21
+ // Handle API key changes and save encrypted to localStorage
22
+ const handleApiKeyChange = (newApiKey) => {
23
+ setApiKey(newApiKey);
24
+
25
+ // Store encrypted API key
26
+ if (newApiKey.trim()) {
27
+ ApiKeyEncryption.storeApiKey(newApiKey);
28
+ console.log('🔐 API key encrypted and stored securely');
29
+ } else {
30
+ ApiKeyEncryption.clearApiKey();
31
+ console.log('🗑️ Encrypted API key cleared');
32
+ }
33
+ };
34
+
35
+ const [uploadedFile, setUploadedFile] = useState(null);
36
+ const [extractedText, setExtractedText] = useState('');
37
+ const [isProcessing, setIsProcessing] = useState(false);
38
+ const [processingProgress, setProcessingProgress] = useState({ current: 0, total: 0, status: '', fileName: '' });
39
+ const [processingMode, setProcessingMode] = useState('standard');
40
+ const [activeTab, setActiveTab] = useState('upload');
41
+ const [previewMode, setPreviewMode] = useState('text');
42
+ const [showLoading, setShowLoading] = useState(true);
43
+ const fileInputRef = useRef(null);
44
+
45
+ // Check if server has API key configured
46
+ const [serverHasApiKey, setServerHasApiKey] = useState(false);
47
+
48
+ // Migrate old unencrypted API key to encrypted storage
49
+ useEffect(() => {
50
+ const migrateOldApiKey = () => {
51
+ const oldApiKey = localStorage.getItem('gemini-api-key');
52
+ if (oldApiKey && !ApiKeyEncryption.retrieveApiKey()) {
53
+ console.log('🔄 Migrating old API key to encrypted storage...');
54
+ ApiKeyEncryption.storeApiKey(oldApiKey);
55
+ localStorage.removeItem('gemini-api-key'); // Remove old unencrypted key
56
+ setApiKey(oldApiKey);
57
+ console.log('✅ API key migration completed');
58
+ }
59
+ };
60
+
61
+ migrateOldApiKey();
62
+ }, []);
63
+
64
+ // Cleanup temp files on app load/reload and check server API key
65
+ useEffect(() => {
66
+ const initializeApp = async () => {
67
+ try {
68
+ // Check if server has API key configured
69
+ const healthResponse = await fetch('http://localhost:3002/api/health');
70
+ if (healthResponse.ok) {
71
+ const healthData = await healthResponse.json();
72
+ setServerHasApiKey(healthData.hasApiKey || false);
73
+ }
74
+
75
+ // Cleanup temp files
76
+ await fetch('http://localhost:3002/api/cleanup', { method: 'POST' });
77
+ console.log('✅ Cleanup completed on app load');
78
+ } catch (error) {
79
+ console.log('⚠️ App initialization failed (server might be starting):', error.message);
80
+ }
81
+ };
82
+
83
+ // Run initialization after a short delay to ensure server is ready
84
+ setTimeout(initializeApp, 2000);
85
+ }, []);
86
+
87
+ const menuItems = [
88
+ { id: 'upload', label: 'Upload', icon: '↑' },
89
+ { id: 'preview', label: 'Preview', icon: '○' },
90
+ { id: 'results', label: 'RESULTS', icon: '↓' }
91
+ ];
92
+
93
+ const onDrop = useCallback(async (acceptedFiles) => {
94
+ const file = acceptedFiles[0];
95
+ if (file) {
96
+ // Trigger cleanup before processing new file
97
+ try {
98
+ await fetch('http://localhost:3002/api/cleanup', { method: 'POST' });
99
+ console.log('✅ Pre-upload cleanup completed');
100
+ } catch (error) {
101
+ console.log('⚠️ Pre-upload cleanup failed:', error.message);
102
+ }
103
+
104
+ setUploadedFile(file);
105
+ setActiveTab('preview');
106
+ }
107
+ }, []);
108
+
109
+ const { getRootProps, getInputProps, isDragActive } = useDropzone({
110
+ onDrop,
111
+ accept: {
112
+ 'image/*': ['.png', '.jpg', '.jpeg'],
113
+ 'application/pdf': ['.pdf'],
114
+ 'text/html': ['.html', '.htm']
115
+ },
116
+ multiple: false
117
+ });
118
+
119
+ const handlePaste = useCallback((e) => {
120
+ const items = e.clipboardData?.items;
121
+ if (items) {
122
+ for (let item of items) {
123
+ if (item.type.indexOf('image') !== -1) {
124
+ const file = item.getAsFile();
125
+ if (file) {
126
+ setUploadedFile(file);
127
+ setActiveTab('preview');
128
+ }
129
+ }
130
+ }
131
+ }
132
+ }, []);
133
+
134
+ const handleFileSelect = () => {
135
+ fileInputRef.current?.click();
136
+ };
137
+
138
+ const handleFileChange = (e) => {
139
+ const file = e.target.files?.[0];
140
+ if (file) {
141
+ setUploadedFile(file);
142
+ setActiveTab('preview');
143
+ }
144
+ };
145
+
146
+ const processFile = async () => {
147
+ if (!uploadedFile || !apiKey) return;
148
+
149
+ setIsProcessing(true);
150
+
151
+ // Initialize progress
152
+ setProcessingProgress({
153
+ current: 0,
154
+ total: 1,
155
+ status: '🔄 Initializing...',
156
+ fileName: uploadedFile.name
157
+ });
158
+
159
+ let progressInterval = null;
160
+
161
+ try {
162
+ // Create FormData for file upload
163
+ const formData = new FormData();
164
+ formData.append('file', uploadedFile);
165
+ formData.append('apiKey', apiKey);
166
+ formData.append('mode', processingMode);
167
+
168
+ console.log('Processing file:', uploadedFile.name, 'Mode:', processingMode);
169
+
170
+ // Start the OCR request
171
+ const ocrPromise = fetch('http://localhost:3002/api/ocr', {
172
+ method: 'POST',
173
+ body: formData,
174
+ });
175
+
176
+ // Start progress polling immediately
177
+ progressInterval = setInterval(async () => {
178
+ try {
179
+ // We'll get the sessionId from the response, but for now poll a generic endpoint
180
+ // In a real implementation, you'd start polling after getting the sessionId
181
+ const progressResponse = await fetch(`http://localhost:3002/api/progress-latest`);
182
+ if (progressResponse.ok) {
183
+ const progressData = await progressResponse.json();
184
+ // Progress data received and processed
185
+ if (progressData.current > 0) {
186
+ setProcessingProgress({
187
+ current: progressData.current,
188
+ total: progressData.total,
189
+ status: progressData.status,
190
+ fileName: progressData.fileName || uploadedFile.name,
191
+ totalPages: progressData.totalPages || 1,
192
+ currentPage: progressData.currentPage || 1,
193
+ totalCharacters: progressData.totalCharacters || 0,
194
+ pageCharacters: progressData.pageCharacters || 0,
195
+ phase: progressData.phase || 'processing',
196
+ consoleLogs: progressData.consoleLogs || []
197
+ });
198
+ }
199
+ }
200
+ } catch (error) {
201
+ // Ignore progress polling errors
202
+ console.log('Progress polling error (ignored):', error.message);
203
+ }
204
+ }, 1000); // Poll every 1 second to reduce spam
205
+
206
+ // Wait for OCR to complete
207
+ const response = await ocrPromise;
208
+ const result = await response.json();
209
+
210
+ // Clear progress polling
211
+ if (progressInterval) {
212
+ clearInterval(progressInterval);
213
+ progressInterval = null;
214
+ }
215
+
216
+ if (result.success) {
217
+ // Final progress update
218
+ setProcessingProgress(prev => ({
219
+ ...prev,
220
+ current: prev.total,
221
+ status: '✅ Processing complete!'
222
+ }));
223
+
224
+ await new Promise(resolve => setTimeout(resolve, 500));
225
+
226
+ // Use the extracted text from Gemini
227
+ setExtractedText(result.data.extractedText);
228
+ setActiveTab('results');
229
+
230
+ console.log('✅ OCR Success:', {
231
+ fileName: result.data.fileName,
232
+ characters: result.data.metadata.characterCount,
233
+ words: result.data.metadata.wordCount,
234
+ mode: result.data.processingMode
235
+ });
236
+ } else {
237
+ console.error('❌ OCR Error:', result.error);
238
+ alert(`OCR Error: ${result.error}`);
239
+ }
240
+ } catch (error) {
241
+ console.error('❌ Network Error:', error);
242
+
243
+ // Clear progress polling on error
244
+ if (progressInterval) {
245
+ clearInterval(progressInterval);
246
+ progressInterval = null;
247
+ }
248
+
249
+ // Check if backend is running
250
+ if (error.message.includes('fetch')) {
251
+ alert(`Network Error: Cannot connect to OCR backend.
252
+
253
+ Please make sure:
254
+ 1. Backend server is running on port 3002
255
+ 2. Run: cd server && npm install && npm start
256
+ 3. Check console for any backend errors
257
+ 4. Visit http://localhost:3002 to verify backend is running
258
+
259
+ Error: ${error.message}`);
260
+ } else {
261
+ alert(`Processing Error: ${error.message}`);
262
+ }
263
+ } finally {
264
+ // Clean up
265
+ if (progressInterval) {
266
+ clearInterval(progressInterval);
267
+ }
268
+ setIsProcessing(false);
269
+ setProcessingProgress({ current: 0, total: 0, status: '', fileName: '' });
270
+ }
271
+ };
272
+
273
+ return (
274
+ <div className="app" onPaste={handlePaste}>
275
+ {/* Loading Screen */}
276
+ {showLoading && (
277
+ <LoadingScreen onComplete={() => setShowLoading(false)} />
278
+ )}
279
+
280
+ {/* Giant Central Orb - Fades in after loading */}
281
+ <div className={`giant-orb-background ${!showLoading ? 'loaded' : ''}`}>
282
+ <div className="giant-orb-container">
283
+ <Orb hue={0} hoverIntensity={0.5} rotateOnHover={true} forceHoverState={false} />
284
+ </div>
285
+ </div>
286
+
287
+ <div className="glass-container">
288
+ <header className="app-header">
289
+ <motion.div
290
+ className="logo"
291
+ initial={{ opacity: 0, y: -20 }}
292
+ animate={{ opacity: 1, y: 0 }}
293
+ transition={{ duration: 0.8 }}
294
+ >
295
+ <TrueFocus
296
+ sentence="Luna OCR"
297
+ manualMode={true}
298
+ blurAmount={8.5}
299
+ borderColor="#ffffff"
300
+ glowColor="rgba(255, 255, 255, 0.8)"
301
+ animationDuration={0.5}
302
+ pauseBetweenAnimations={2}
303
+ />
304
+ </motion.div>
305
+
306
+ <motion.div
307
+ className="api-key-input"
308
+ initial={{ opacity: 0, x: 20 }}
309
+ animate={{ opacity: 1, x: 0 }}
310
+ transition={{ duration: 0.8, delay: 0.2 }}
311
+ >
312
+ {!serverHasApiKey && (
313
+ <div className="api-key-container">
314
+ <input
315
+ type="password"
316
+ placeholder={apiKey ? "API Key loaded from storage" : "Enter Google API Key..."}
317
+ value={apiKey}
318
+ onChange={(e) => handleApiKeyChange(e.target.value)}
319
+ className="glass-input"
320
+ />
321
+ {apiKey && (
322
+ <button
323
+ type="button"
324
+ onClick={() => handleApiKeyChange('')}
325
+ className="clear-api-key-btn"
326
+ title="Clear saved API key"
327
+ >
328
+ ×
329
+ </button>
330
+ )}
331
+ </div>
332
+ )}
333
+
334
+ {serverHasApiKey && (
335
+ <div className="server-api-notice">
336
+ <p>🔒 API key configured on server - no user key required</p>
337
+ </div>
338
+ )}
339
+ </motion.div>
340
+ </header>
341
+
342
+ <FlowingMenu
343
+ items={menuItems}
344
+ activeItem={activeTab}
345
+ onItemClick={setActiveTab}
346
+ />
347
+
348
+ <main className="app-main">
349
+ <AnimatePresence mode="wait">
350
+ {activeTab === 'upload' && (
351
+ <motion.div
352
+ key="upload"
353
+ initial={{ opacity: 0, x: -20 }}
354
+ animate={{ opacity: 1, x: 0 }}
355
+ exit={{ opacity: 0, x: 20 }}
356
+ className="tab-content"
357
+ >
358
+ <FileUploader
359
+ getRootProps={getRootProps}
360
+ getInputProps={getInputProps}
361
+ isDragActive={isDragActive}
362
+ onFileSelect={handleFileSelect}
363
+ fileInputRef={fileInputRef}
364
+ onFileChange={handleFileChange}
365
+ />
366
+ </motion.div>
367
+ )}
368
+
369
+
370
+
371
+ {activeTab === 'preview' && uploadedFile && (
372
+ <motion.div
373
+ key="preview"
374
+ initial={{ opacity: 0, x: -20 }}
375
+ animate={{ opacity: 1, x: 0 }}
376
+ exit={{ opacity: 0, x: 20 }}
377
+ className="tab-content"
378
+ >
379
+ <PreviewPanel
380
+ file={uploadedFile}
381
+ processingMode={processingMode}
382
+ onModeChange={setProcessingMode}
383
+ onProcess={processFile}
384
+ isProcessing={isProcessing}
385
+ apiKey={apiKey}
386
+ />
387
+ </motion.div>
388
+ )}
389
+
390
+ {activeTab === 'results' && extractedText && (
391
+ <motion.div
392
+ key="results"
393
+ initial={{ opacity: 0, x: -20 }}
394
+ animate={{ opacity: 1, x: 0 }}
395
+ exit={{ opacity: 0, x: 20 }}
396
+ className="tab-content"
397
+ >
398
+ <ResultsPanel
399
+ text={extractedText}
400
+ previewMode={previewMode}
401
+ onPreviewModeChange={setPreviewMode}
402
+ fileName={uploadedFile?.name}
403
+ />
404
+ </motion.div>
405
+ )}
406
+ </AnimatePresence>
407
+ </main>
408
+ </div>
409
+
410
+ {/* Processing Progress Overlay */}
411
+ <ProcessingProgress
412
+ progress={processingProgress}
413
+ isVisible={isProcessing}
414
+ />
415
+
416
+ </div>
417
+ );
418
+ }
419
+
420
+ export default App;
src/components/AHoleLoader.css ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* AHole Loader Styles - Adapted from CodePen */
2
+ .a-hole-loader {
3
+ width: 100%;
4
+ height: 100%;
5
+ background: #141414;
6
+ }
7
+
8
+ a-hole {
9
+ position: absolute;
10
+ top: 0;
11
+ left: 0;
12
+ margin: 0;
13
+ padding: 0;
14
+ width: 100%;
15
+ height: 100%;
16
+ overflow: hidden;
17
+ }
18
+
19
+ a-hole::before {
20
+ position: absolute;
21
+ top: 50%;
22
+ left: 50%;
23
+ z-index: 2;
24
+ display: block;
25
+ width: 150%;
26
+ height: 140%;
27
+ background: radial-gradient(ellipse at 50% 55%, transparent 10%, black 50%);
28
+ transform: translate3d(-50%, -50%, 0);
29
+ content: "";
30
+ }
31
+
32
+ a-hole::after {
33
+ position: absolute;
34
+ top: 50%;
35
+ left: 50%;
36
+ z-index: 5;
37
+ display: block;
38
+ width: 100%;
39
+ height: 100%;
40
+ background: radial-gradient(ellipse at 50% 75%,
41
+ #a900ff 20%,
42
+ transparent 75%);
43
+ mix-blend-mode: overlay;
44
+ transform: translate3d(-50%, -50%, 0);
45
+ content: "";
46
+ }
47
+
48
+ @keyframes aura-glow {
49
+ 0% {
50
+ background-position: 0 100%;
51
+ }
52
+
53
+ 100% {
54
+ background-position: 0 300%;
55
+ }
56
+ }
57
+
58
+ a-hole .aura {
59
+ position: absolute;
60
+ top: -71.5%;
61
+ left: 50%;
62
+ z-index: 3;
63
+ width: 30%;
64
+ height: 140%;
65
+ background: linear-gradient(20deg,
66
+ #00f8f1,
67
+ #ffbd1e20 16.5%,
68
+ #fe848f 33%,
69
+ #fe848f20 49.5%,
70
+ #00f8f1 66%,
71
+ #00f8f160 85.5%,
72
+ #ffbd1e 100%) 0 100% / 100% 200%;
73
+ border-radius: 0 0 100% 100%;
74
+ filter: blur(50px);
75
+ mix-blend-mode: plus-lighter;
76
+ opacity: 0.75;
77
+ transform: translate3d(-50%, 0, 0);
78
+ animation: aura-glow 5s infinite linear;
79
+ }
80
+
81
+ a-hole .overlay {
82
+ position: absolute;
83
+ top: 0;
84
+ left: 0;
85
+ z-index: 10;
86
+ width: 100%;
87
+ height: 100%;
88
+ background: repeating-linear-gradient(transparent,
89
+ transparent 1px,
90
+ white 1px,
91
+ white 2px);
92
+ mix-blend-mode: overlay;
93
+ opacity: 0.5;
94
+ }
95
+
96
+ a-hole canvas {
97
+ display: block;
98
+ width: 100%;
99
+ height: 100%;
100
+ }
101
+
102
+ /* Loading Screen Integration */
103
+ .loading-screen {
104
+ position: fixed;
105
+ top: 0;
106
+ left: 0;
107
+ width: 100%;
108
+ height: 100%;
109
+ background: #141414;
110
+ z-index: 9999;
111
+ display: flex;
112
+ flex-direction: column;
113
+ align-items: center;
114
+ justify-content: center;
115
+ }
116
+
117
+ .loading-content {
118
+ position: relative;
119
+ z-index: 10;
120
+ text-align: center;
121
+ color: white;
122
+ margin-bottom: 40px;
123
+ }
124
+
125
+ .loading-title {
126
+ font-size: 2.5rem;
127
+ font-weight: 700;
128
+ margin-bottom: 16px;
129
+ background: linear-gradient(45deg, #00f8f1, #a900ff, #fe848f);
130
+ -webkit-background-clip: text;
131
+ -webkit-text-fill-color: transparent;
132
+ background-clip: text;
133
+ animation: gradient-shift 3s ease-in-out infinite;
134
+ }
135
+
136
+ .loading-subtitle {
137
+ font-size: 1rem;
138
+ color: rgba(255, 255, 255, 0.7);
139
+ font-weight: 400;
140
+ }
141
+
142
+ .a-hole-container {
143
+ width: 100vw;
144
+ height: 100vh;
145
+ position: absolute;
146
+ top: 0;
147
+ left: 0;
148
+ }
149
+
150
+ @keyframes gradient-shift {
151
+
152
+ 0%,
153
+ 100% {
154
+ background-position: 0% 50%;
155
+ }
156
+
157
+ 50% {
158
+ background-position: 100% 50%;
159
+ }
160
+ }
161
+
162
+ /* Pixelated Loading Indicator - Bottom Left */
163
+ .pixelated-loading {
164
+ position: absolute;
165
+ bottom: 40px;
166
+ left: 40px;
167
+ z-index: 15;
168
+ display: flex;
169
+ align-items: center;
170
+ gap: 12px;
171
+ font-family: 'Courier New', monospace;
172
+ }
173
+
174
+ .loading-dots {
175
+ display: flex;
176
+ gap: 4px;
177
+ }
178
+
179
+ .dot {
180
+ width: 8px;
181
+ height: 8px;
182
+ background: #00f8f1;
183
+ border-radius: 0;
184
+ /* Pixelated - no rounded corners */
185
+ animation: pixel-pulse 1.2s ease-in-out infinite;
186
+ box-shadow: 0 0 4px #00f8f1;
187
+ transition: background 0.3s ease, box-shadow 0.3s ease;
188
+ }
189
+
190
+ .dot:nth-child(1) {
191
+ animation-delay: 0s;
192
+ }
193
+
194
+ .dot:nth-child(2) {
195
+ animation-delay: 0.3s;
196
+ }
197
+
198
+ .dot:nth-child(3) {
199
+ animation-delay: 0.6s;
200
+ }
201
+
202
+ .dot:nth-child(4) {
203
+ animation-delay: 0.9s;
204
+ }
205
+
206
+ .loading-text {
207
+ color: #00f8f1;
208
+ font-size: 12px;
209
+ font-weight: bold;
210
+ letter-spacing: 2px;
211
+ text-shadow: 0 0 8px #00f8f1;
212
+ animation: text-flicker 2s ease-in-out infinite;
213
+ transition: color 0.3s ease, text-shadow 0.3s ease;
214
+ }
215
+
216
+ @keyframes pixel-pulse {
217
+
218
+ 0%,
219
+ 100% {
220
+ opacity: 0.3;
221
+ transform: scale(1);
222
+ }
223
+
224
+ 50% {
225
+ opacity: 1;
226
+ transform: scale(1.2);
227
+ }
228
+ }
229
+
230
+ @keyframes text-flicker {
231
+
232
+ 0%,
233
+ 100% {
234
+ opacity: 1;
235
+ }
236
+
237
+ 50% {
238
+ opacity: 0.7;
239
+ }
240
+ }
241
+
242
+ /* Responsive adjustments */
243
+ @media (max-width: 768px) {
244
+ .loading-title {
245
+ font-size: 2rem;
246
+ }
247
+
248
+ .loading-subtitle {
249
+ font-size: 0.9rem;
250
+ }
251
+
252
+ .pixelated-loading {
253
+ bottom: 20px;
254
+ left: 20px;
255
+ }
256
+
257
+ .dot {
258
+ width: 6px;
259
+ height: 6px;
260
+ }
261
+
262
+ .loading-text {
263
+ font-size: 10px;
264
+ }
265
+ }
src/components/AHoleLoader.js ADDED
@@ -0,0 +1,404 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useRef } from 'react';
2
+
3
+ class AHole extends HTMLElement {
4
+ connectedCallback() {
5
+ // Elements
6
+ this.canvas = this.querySelector(".js-canvas");
7
+ this.ctx = this.canvas.getContext("2d");
8
+
9
+ this.discs = [];
10
+ this.lines = [];
11
+
12
+ // Init
13
+ this.setSize();
14
+ this.setDiscs();
15
+ this.setLines();
16
+ this.setParticles();
17
+
18
+ this.bindEvents();
19
+
20
+ // RAF
21
+ this.animationId = requestAnimationFrame(this.tick.bind(this));
22
+ }
23
+
24
+ disconnectedCallback() {
25
+ if (this.animationId) {
26
+ cancelAnimationFrame(this.animationId);
27
+ }
28
+ window.removeEventListener("resize", this.onResize.bind(this));
29
+ }
30
+
31
+ bindEvents() {
32
+ this.onResize = this.onResize.bind(this);
33
+ window.addEventListener("resize", this.onResize);
34
+ }
35
+
36
+ onResize() {
37
+ this.setSize();
38
+ this.setDiscs();
39
+ this.setLines();
40
+ this.setParticles();
41
+ }
42
+
43
+ setSize() {
44
+ this.rect = this.getBoundingClientRect();
45
+
46
+ this.render = {
47
+ width: this.rect.width,
48
+ height: this.rect.height,
49
+ dpi: window.devicePixelRatio
50
+ };
51
+
52
+ this.canvas.width = this.render.width * this.render.dpi;
53
+ this.canvas.height = this.render.height * this.render.dpi;
54
+ }
55
+
56
+ setDiscs() {
57
+ const { width, height } = this.rect;
58
+
59
+ this.discs = [];
60
+
61
+ this.startDisc = {
62
+ x: width * 0.5,
63
+ y: height * 0.45,
64
+ w: width * 0.75,
65
+ h: height * 0.7
66
+ };
67
+
68
+ this.endDisc = {
69
+ x: width * 0.5,
70
+ y: height * 0.95,
71
+ w: 0,
72
+ h: 0
73
+ };
74
+
75
+ const totalDiscs = 100;
76
+
77
+ let prevBottom = height;
78
+ this.clip = {};
79
+
80
+ for (let i = 0; i < totalDiscs; i++) {
81
+ const p = i / totalDiscs;
82
+
83
+ const disc = this.tweenDisc({
84
+ p
85
+ });
86
+
87
+ const bottom = disc.y + disc.h;
88
+
89
+ if (bottom <= prevBottom) {
90
+ this.clip = {
91
+ disc: { ...disc },
92
+ i
93
+ };
94
+ }
95
+
96
+ prevBottom = bottom;
97
+
98
+ this.discs.push(disc);
99
+ }
100
+
101
+ this.clip.path = new Path2D();
102
+ this.clip.path.ellipse(
103
+ this.clip.disc.x,
104
+ this.clip.disc.y,
105
+ this.clip.disc.w,
106
+ this.clip.disc.h,
107
+ 0,
108
+ 0,
109
+ Math.PI * 2
110
+ );
111
+ this.clip.path.rect(
112
+ this.clip.disc.x - this.clip.disc.w,
113
+ 0,
114
+ this.clip.disc.w * 2,
115
+ this.clip.disc.y
116
+ );
117
+ }
118
+
119
+ setLines() {
120
+ const { width, height } = this.rect;
121
+
122
+ this.lines = [];
123
+
124
+ const totalLines = 100;
125
+ const linesAngle = (Math.PI * 2) / totalLines;
126
+
127
+ for (let i = 0; i < totalLines; i++) {
128
+ this.lines.push([]);
129
+ }
130
+
131
+ this.discs.forEach((disc) => {
132
+ for (let i = 0; i < totalLines; i++) {
133
+ const angle = i * linesAngle;
134
+
135
+ const p = {
136
+ x: disc.x + Math.cos(angle) * disc.w,
137
+ y: disc.y + Math.sin(angle) * disc.h
138
+ };
139
+
140
+ this.lines[i].push(p);
141
+ }
142
+ });
143
+
144
+ this.linesCanvas = new OffscreenCanvas(width, height);
145
+ const ctx = this.linesCanvas.getContext("2d");
146
+
147
+ this.lines.forEach((line) => {
148
+ ctx.save();
149
+
150
+ let lineIsIn = false;
151
+ line.forEach((p1, j) => {
152
+ if (j === 0) {
153
+ return;
154
+ }
155
+
156
+ const p0 = line[j - 1];
157
+
158
+ if (
159
+ !lineIsIn &&
160
+ (ctx.isPointInPath(this.clip.path, p1.x, p1.y) ||
161
+ ctx.isPointInStroke(this.clip.path, p1.x, p1.y))
162
+ ) {
163
+ lineIsIn = true;
164
+ } else if (lineIsIn) {
165
+ ctx.clip(this.clip.path);
166
+ }
167
+
168
+ ctx.beginPath();
169
+
170
+ ctx.moveTo(p0.x, p0.y);
171
+ ctx.lineTo(p1.x, p1.y);
172
+
173
+ ctx.strokeStyle = "#444";
174
+ ctx.lineWidth = 2;
175
+ ctx.stroke();
176
+
177
+ ctx.closePath();
178
+ });
179
+
180
+ ctx.restore();
181
+ });
182
+
183
+ this.linesCtx = ctx;
184
+ }
185
+
186
+ setParticles() {
187
+ const { width, height } = this.rect;
188
+
189
+ this.particles = [];
190
+
191
+ this.particleArea = {
192
+ sw: this.clip.disc.w * 0.5,
193
+ ew: this.clip.disc.w * 2,
194
+ h: height * 0.85
195
+ };
196
+ this.particleArea.sx = (width - this.particleArea.sw) / 2;
197
+ this.particleArea.ex = (width - this.particleArea.ew) / 2;
198
+
199
+ const totalParticles = 100;
200
+
201
+ for (let i = 0; i < totalParticles; i++) {
202
+ const particle = this.initParticle(true);
203
+ this.particles.push(particle);
204
+ }
205
+ }
206
+
207
+ initParticle(start = false) {
208
+ const sx = this.particleArea.sx + this.particleArea.sw * Math.random();
209
+ const ex = this.particleArea.ex + this.particleArea.ew * Math.random();
210
+ const dx = ex - sx;
211
+ const y = start ? this.particleArea.h * Math.random() : this.particleArea.h;
212
+ const r = 0.5 + Math.random() * 4;
213
+ const vy = 0.5 + Math.random();
214
+
215
+ return {
216
+ x: sx,
217
+ sx,
218
+ dx,
219
+ y,
220
+ vy,
221
+ p: 0,
222
+ r,
223
+ c: `rgba(255, 255, 255, ${Math.random()})`
224
+ };
225
+ }
226
+
227
+ tweenValue(start, end, p, ease = false) {
228
+ const delta = end - start;
229
+
230
+ // Simple easing functions
231
+ let easeFn;
232
+ if (ease === "inExpo") {
233
+ easeFn = (t) => t === 0 ? 0 : Math.pow(2, 10 * (t - 1));
234
+ } else {
235
+ easeFn = (t) => t; // linear
236
+ }
237
+
238
+ return start + delta * easeFn(p);
239
+ }
240
+
241
+ drawDiscs() {
242
+ const { ctx } = this;
243
+
244
+ ctx.strokeStyle = "#444";
245
+ ctx.lineWidth = 2;
246
+
247
+ // Outer disc
248
+ const outerDisc = this.startDisc;
249
+
250
+ ctx.beginPath();
251
+
252
+ ctx.ellipse(
253
+ outerDisc.x,
254
+ outerDisc.y,
255
+ outerDisc.w,
256
+ outerDisc.h,
257
+ 0,
258
+ 0,
259
+ Math.PI * 2
260
+ );
261
+ ctx.stroke();
262
+
263
+ ctx.closePath();
264
+
265
+ // Discs
266
+ this.discs.forEach((disc, i) => {
267
+ if (i % 5 !== 0) {
268
+ return;
269
+ }
270
+
271
+ if (disc.w < this.clip.disc.w - 5) {
272
+ ctx.save();
273
+ ctx.clip(this.clip.path);
274
+ }
275
+
276
+ ctx.beginPath();
277
+
278
+ ctx.ellipse(disc.x, disc.y, disc.w, disc.h, 0, 0, Math.PI * 2);
279
+ ctx.stroke();
280
+
281
+ ctx.closePath();
282
+
283
+ if (disc.w < this.clip.disc.w - 5) {
284
+ ctx.restore();
285
+ }
286
+ });
287
+ }
288
+
289
+ drawLines() {
290
+ const { ctx, linesCanvas } = this;
291
+ ctx.drawImage(linesCanvas, 0, 0);
292
+ }
293
+
294
+ drawParticles() {
295
+ const { ctx } = this;
296
+
297
+ ctx.save();
298
+ ctx.clip(this.clip.path);
299
+
300
+ this.particles.forEach((particle) => {
301
+ ctx.fillStyle = particle.c;
302
+ ctx.beginPath();
303
+ ctx.rect(particle.x, particle.y, particle.r, particle.r);
304
+ ctx.closePath();
305
+ ctx.fill();
306
+ });
307
+
308
+ ctx.restore();
309
+ }
310
+
311
+ moveDiscs() {
312
+ this.discs.forEach((disc) => {
313
+ disc.p = (disc.p + 0.001) % 1;
314
+ this.tweenDisc(disc);
315
+ });
316
+ }
317
+
318
+ moveParticles() {
319
+ this.particles.forEach((particle) => {
320
+ particle.p = 1 - particle.y / this.particleArea.h;
321
+ particle.x = particle.sx + particle.dx * particle.p;
322
+ particle.y -= particle.vy;
323
+
324
+ if (particle.y < 0) {
325
+ particle.y = this.initParticle().y;
326
+ }
327
+ });
328
+ }
329
+
330
+ tweenDisc(disc) {
331
+ disc.x = this.tweenValue(this.startDisc.x, this.endDisc.x, disc.p);
332
+ disc.y = this.tweenValue(
333
+ this.startDisc.y,
334
+ this.endDisc.y,
335
+ disc.p,
336
+ "inExpo"
337
+ );
338
+
339
+ disc.w = this.tweenValue(this.startDisc.w, this.endDisc.w, disc.p);
340
+ disc.h = this.tweenValue(this.startDisc.h, this.endDisc.h, disc.p);
341
+
342
+ return disc;
343
+ }
344
+
345
+ tick() {
346
+ const { ctx } = this;
347
+
348
+ ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
349
+
350
+ ctx.save();
351
+ ctx.scale(this.render.dpi, this.render.dpi);
352
+
353
+ this.moveDiscs();
354
+ this.moveParticles();
355
+
356
+ this.drawDiscs();
357
+ this.drawLines();
358
+ this.drawParticles();
359
+
360
+ ctx.restore();
361
+
362
+ this.animationId = requestAnimationFrame(this.tick.bind(this));
363
+ }
364
+ }
365
+
366
+ // Define custom element
367
+ if (!customElements.get('a-hole')) {
368
+ customElements.define("a-hole", AHole);
369
+ }
370
+
371
+ const AHoleLoader = ({ className = "", style = {} }) => {
372
+ const containerRef = useRef(null);
373
+
374
+ useEffect(() => {
375
+ // Force a resize event to ensure proper initialization
376
+ const timer = setTimeout(() => {
377
+ window.dispatchEvent(new Event('resize'));
378
+ }, 100);
379
+
380
+ return () => clearTimeout(timer);
381
+ }, []);
382
+
383
+ return (
384
+ <div
385
+ ref={containerRef}
386
+ className={`a-hole-loader ${className}`}
387
+ style={{
388
+ position: 'relative',
389
+ width: '100%',
390
+ height: '100%',
391
+ overflow: 'hidden',
392
+ ...style
393
+ }}
394
+ >
395
+ <a-hole>
396
+ <canvas className="js-canvas"></canvas>
397
+ <div className="aura"></div>
398
+ <div className="overlay"></div>
399
+ </a-hole>
400
+ </div>
401
+ );
402
+ };
403
+
404
+ export default AHoleLoader;
src/components/DocumentViewer.js ADDED
@@ -0,0 +1,552 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import {
4
+ FileText, Download, Eye, ZoomIn, ZoomOut, RotateCw,
5
+ Maximize2, ArrowLeft, Settings, Printer, Share2
6
+ } from 'lucide-react';
7
+
8
+ const DocumentViewer = ({ file, onBack }) => {
9
+ const [viewMode, setViewMode] = useState('preview');
10
+ const [zoom, setZoom] = useState(1);
11
+ const [rotation, setRotation] = useState(0);
12
+ const [isFullscreen, setIsFullscreen] = useState(false);
13
+ const [documentUrl, setDocumentUrl] = useState(null);
14
+ const [isLoading, setIsLoading] = useState(true);
15
+ const viewerRef = useRef(null);
16
+
17
+ useEffect(() => {
18
+ if (file) {
19
+ const url = URL.createObjectURL(file);
20
+ setDocumentUrl(url);
21
+ setIsLoading(false);
22
+
23
+ return () => URL.revokeObjectURL(url);
24
+ }
25
+ }, [file]);
26
+
27
+ const handleZoomIn = () => setZoom(prev => Math.min(prev + 0.25, 3));
28
+ const handleZoomOut = () => setZoom(prev => Math.max(prev - 0.25, 0.5));
29
+ const handleRotate = () => setRotation(prev => (prev + 90) % 360);
30
+
31
+ const toggleFullscreen = async () => {
32
+ try {
33
+ if (!document.fullscreenElement) {
34
+ await viewerRef.current?.requestFullscreen();
35
+ setIsFullscreen(true);
36
+ } else {
37
+ await document.exitFullscreen();
38
+ setIsFullscreen(false);
39
+ }
40
+ } catch (error) {
41
+ setIsFullscreen(!isFullscreen);
42
+ }
43
+ };
44
+
45
+ const handleDownload = () => {
46
+ if (documentUrl) {
47
+ const a = document.createElement('a');
48
+ a.href = documentUrl;
49
+ a.download = file.name;
50
+ document.body.appendChild(a);
51
+ a.click();
52
+ document.body.removeChild(a);
53
+ }
54
+ };
55
+
56
+ const renderPDFViewer = () => (
57
+ <div className="pdf-viewer-container">
58
+ <div className="pdf-embed-wrapper" style={{
59
+ transform: `scale(${zoom}) rotate(${rotation}deg)`,
60
+ transformOrigin: 'center center'
61
+ }}>
62
+ <embed
63
+ src={documentUrl}
64
+ type="application/pdf"
65
+ className="pdf-embed"
66
+ title={file.name}
67
+ />
68
+ </div>
69
+ </div>
70
+ );
71
+
72
+ const renderHTMLViewer = () => (
73
+ <div className="html-viewer-container">
74
+ <div className="html-content-wrapper" style={{
75
+ transform: `scale(${zoom}) rotate(${rotation}deg)`,
76
+ transformOrigin: 'center center'
77
+ }}>
78
+ <iframe
79
+ src={documentUrl}
80
+ className="html-iframe"
81
+ title={file.name}
82
+ sandbox="allow-same-origin allow-scripts"
83
+ />
84
+ </div>
85
+ </div>
86
+ );
87
+
88
+ if (!file) return null;
89
+
90
+ const isPDF = file.type === 'application/pdf';
91
+ const isHTML = file.type === 'text/html' || file.name.toLowerCase().endsWith('.html');
92
+
93
+ return (
94
+ <div className={`document-viewer ${isFullscreen ? 'fullscreen' : ''}`} ref={viewerRef}>
95
+ {/* Glassmorphism Background */}
96
+ <div className="viewer-background">
97
+ <div className="orb-background-container">
98
+ <div className="floating-orb orb-1"></div>
99
+ <div className="floating-orb orb-2"></div>
100
+ <div className="floating-orb orb-3"></div>
101
+ </div>
102
+ </div>
103
+
104
+ {/* Header Controls */}
105
+ <motion.header
106
+ className="viewer-header glass-panel"
107
+ initial={{ opacity: 0, y: -20 }}
108
+ animate={{ opacity: 1, y: 0 }}
109
+ transition={{ duration: 0.6 }}
110
+ >
111
+ <div className="header-left">
112
+ <motion.button
113
+ className="control-btn back-btn"
114
+ onClick={onBack}
115
+ whileHover={{ scale: 1.05 }}
116
+ whileTap={{ scale: 0.95 }}
117
+ >
118
+ <ArrowLeft size={18} />
119
+ Back
120
+ </motion.button>
121
+
122
+ <div className="document-info">
123
+ <h2 className="document-title">{file.name}</h2>
124
+ <div className="document-meta">
125
+ <span className="format-badge">
126
+ {isPDF ? 'PDF' : 'HTML'}
127
+ </span>
128
+ <span className="file-size">
129
+ {(file.size / 1024 / 1024).toFixed(2)} MB
130
+ </span>
131
+ </div>
132
+ </div>
133
+ </div>
134
+
135
+ <div className="header-controls">
136
+ <div className="zoom-controls glass-group">
137
+ <motion.button
138
+ className="control-btn"
139
+ onClick={handleZoomOut}
140
+ disabled={zoom <= 0.5}
141
+ whileHover={{ scale: 1.05 }}
142
+ whileTap={{ scale: 0.95 }}
143
+ >
144
+ <ZoomOut size={16} />
145
+ </motion.button>
146
+ <span className="zoom-display">{Math.round(zoom * 100)}%</span>
147
+ <motion.button
148
+ className="control-btn"
149
+ onClick={handleZoomIn}
150
+ disabled={zoom >= 3}
151
+ whileHover={{ scale: 1.05 }}
152
+ whileTap={{ scale: 0.95 }}
153
+ >
154
+ <ZoomIn size={16} />
155
+ </motion.button>
156
+ </div>
157
+
158
+ <motion.button
159
+ className="control-btn"
160
+ onClick={handleRotate}
161
+ whileHover={{ scale: 1.05 }}
162
+ whileTap={{ scale: 0.95 }}
163
+ >
164
+ <RotateCw size={16} />
165
+ </motion.button>
166
+
167
+ <motion.button
168
+ className="control-btn"
169
+ onClick={toggleFullscreen}
170
+ whileHover={{ scale: 1.05 }}
171
+ whileTap={{ scale: 0.95 }}
172
+ >
173
+ <Maximize2 size={16} />
174
+ </motion.button>
175
+
176
+ <motion.button
177
+ className="control-btn primary"
178
+ onClick={handleDownload}
179
+ whileHover={{ scale: 1.05 }}
180
+ whileTap={{ scale: 0.95 }}
181
+ >
182
+ <Download size={16} />
183
+ Download
184
+ </motion.button>
185
+ </div>
186
+ </motion.header>
187
+
188
+ {/* Document Content */}
189
+ <motion.main
190
+ className="viewer-content glass-panel"
191
+ initial={{ opacity: 0, scale: 0.95 }}
192
+ animate={{ opacity: 1, scale: 1 }}
193
+ transition={{ duration: 0.6, delay: 0.1 }}
194
+ >
195
+ <AnimatePresence mode="wait">
196
+ {isLoading ? (
197
+ <motion.div
198
+ key="loading"
199
+ className="loading-state"
200
+ initial={{ opacity: 0 }}
201
+ animate={{ opacity: 1 }}
202
+ exit={{ opacity: 0 }}
203
+ >
204
+ <div className="loading-spinner"></div>
205
+ <p>Loading document...</p>
206
+ </motion.div>
207
+ ) : (
208
+ <motion.div
209
+ key="content"
210
+ className="document-content"
211
+ initial={{ opacity: 0 }}
212
+ animate={{ opacity: 1 }}
213
+ exit={{ opacity: 0 }}
214
+ >
215
+ {isPDF ? renderPDFViewer() : isHTML ? renderHTMLViewer() : (
216
+ <div className="unsupported-format">
217
+ <FileText size={48} />
218
+ <p>Unsupported file format</p>
219
+ </div>
220
+ )}
221
+ </motion.div>
222
+ )}
223
+ </AnimatePresence>
224
+ </motion.main>
225
+
226
+ <style jsx>{`
227
+ .document-viewer {
228
+ position: fixed;
229
+ top: 0;
230
+ left: 0;
231
+ width: 100vw;
232
+ height: 100vh;
233
+ background: #0f0f23;
234
+ z-index: 1000;
235
+ display: flex;
236
+ flex-direction: column;
237
+ overflow: hidden;
238
+ }
239
+
240
+ .document-viewer.fullscreen {
241
+ z-index: 9999;
242
+ }
243
+
244
+ .viewer-background {
245
+ position: absolute;
246
+ top: 0;
247
+ left: 0;
248
+ width: 100%;
249
+ height: 100%;
250
+ pointer-events: none;
251
+ z-index: -1;
252
+ }
253
+
254
+ .orb-background-container {
255
+ position: relative;
256
+ width: 100%;
257
+ height: 100%;
258
+ overflow: hidden;
259
+ }
260
+
261
+ .floating-orb {
262
+ position: absolute;
263
+ border-radius: 50%;
264
+ filter: blur(80px);
265
+ opacity: 0.3;
266
+ animation: float 20s ease-in-out infinite;
267
+ }
268
+
269
+ .orb-1 {
270
+ top: 10%;
271
+ right: 20%;
272
+ width: 300px;
273
+ height: 300px;
274
+ background: linear-gradient(45deg, #4facfe, #8b5cf6);
275
+ animation-delay: 0s;
276
+ }
277
+
278
+ .orb-2 {
279
+ bottom: 20%;
280
+ left: 15%;
281
+ width: 250px;
282
+ height: 250px;
283
+ background: linear-gradient(45deg, #f093fb, #f5576c);
284
+ animation-delay: -7s;
285
+ }
286
+
287
+ .orb-3 {
288
+ top: 50%;
289
+ left: 50%;
290
+ width: 200px;
291
+ height: 200px;
292
+ background: linear-gradient(45deg, #06b6d4, #10b981);
293
+ animation-delay: -14s;
294
+ }
295
+
296
+ @keyframes float {
297
+ 0%, 100% { transform: translate(0, 0) rotate(0deg); }
298
+ 25% { transform: translate(-20px, -30px) rotate(90deg); }
299
+ 50% { transform: translate(10px, -20px) rotate(180deg); }
300
+ 75% { transform: translate(-10px, 10px) rotate(270deg); }
301
+ }
302
+
303
+ .viewer-header {
304
+ display: flex;
305
+ justify-content: space-between;
306
+ align-items: center;
307
+ padding: 16px 24px;
308
+ margin: 12px;
309
+ margin-bottom: 0;
310
+ border-radius: 16px;
311
+ backdrop-filter: blur(25px);
312
+ -webkit-backdrop-filter: blur(25px);
313
+ background: rgba(0, 0, 0, 0.2);
314
+ border: 1px solid rgba(255, 255, 255, 0.1);
315
+ flex-shrink: 0;
316
+ }
317
+
318
+ .header-left {
319
+ display: flex;
320
+ align-items: center;
321
+ gap: 20px;
322
+ }
323
+
324
+ .back-btn {
325
+ display: flex;
326
+ align-items: center;
327
+ gap: 8px;
328
+ padding: 8px 16px;
329
+ background: rgba(255, 255, 255, 0.05);
330
+ border: 1px solid rgba(255, 255, 255, 0.1);
331
+ border-radius: 8px;
332
+ color: #ffffff;
333
+ font-size: 0.875rem;
334
+ cursor: pointer;
335
+ transition: all 0.3s ease;
336
+ }
337
+
338
+ .back-btn:hover {
339
+ background: rgba(255, 255, 255, 0.1);
340
+ border-color: rgba(79, 172, 254, 0.3);
341
+ }
342
+
343
+ .document-info {
344
+ display: flex;
345
+ flex-direction: column;
346
+ gap: 4px;
347
+ }
348
+
349
+ .document-title {
350
+ font-size: 1.25rem;
351
+ font-weight: 600;
352
+ color: #ffffff;
353
+ margin: 0;
354
+ }
355
+
356
+ .document-meta {
357
+ display: flex;
358
+ align-items: center;
359
+ gap: 12px;
360
+ font-size: 0.75rem;
361
+ }
362
+
363
+ .format-badge {
364
+ background: rgba(79, 172, 254, 0.2);
365
+ color: #4facfe;
366
+ padding: 4px 12px;
367
+ border-radius: 6px;
368
+ font-weight: 500;
369
+ font-size: 0.75rem;
370
+ letter-spacing: 0.5px;
371
+ }
372
+
373
+ .file-size {
374
+ color: rgba(255, 255, 255, 0.6);
375
+ }
376
+
377
+ .header-controls {
378
+ display: flex;
379
+ align-items: center;
380
+ gap: 12px;
381
+ }
382
+
383
+ .glass-group {
384
+ display: flex;
385
+ align-items: center;
386
+ gap: 8px;
387
+ background: rgba(255, 255, 255, 0.05);
388
+ border: 1px solid rgba(255, 255, 255, 0.1);
389
+ border-radius: 8px;
390
+ padding: 4px 8px;
391
+ }
392
+
393
+ .control-btn {
394
+ display: flex;
395
+ align-items: center;
396
+ gap: 6px;
397
+ padding: 8px 12px;
398
+ background: rgba(255, 255, 255, 0.05);
399
+ border: 1px solid rgba(255, 255, 255, 0.1);
400
+ border-radius: 6px;
401
+ color: rgba(255, 255, 255, 0.8);
402
+ font-size: 0.875rem;
403
+ cursor: pointer;
404
+ transition: all 0.3s ease;
405
+ }
406
+
407
+ .control-btn:hover:not(:disabled) {
408
+ background: rgba(255, 255, 255, 0.1);
409
+ color: #ffffff;
410
+ border-color: rgba(79, 172, 254, 0.3);
411
+ }
412
+
413
+ .control-btn:disabled {
414
+ opacity: 0.5;
415
+ cursor: not-allowed;
416
+ }
417
+
418
+ .control-btn.primary {
419
+ background: linear-gradient(135deg, #4facfe, #8b5cf6);
420
+ border-color: rgba(79, 172, 254, 0.3);
421
+ color: #ffffff;
422
+ }
423
+
424
+ .control-btn.primary:hover {
425
+ box-shadow: 0 4px 15px rgba(79, 172, 254, 0.3);
426
+ }
427
+
428
+ .zoom-display {
429
+ font-size: 0.75rem;
430
+ color: rgba(255, 255, 255, 0.8);
431
+ min-width: 40px;
432
+ text-align: center;
433
+ }
434
+
435
+ .viewer-content {
436
+ flex: 1;
437
+ margin: 12px;
438
+ margin-top: 0;
439
+ border-radius: 16px;
440
+ backdrop-filter: blur(25px);
441
+ -webkit-backdrop-filter: blur(25px);
442
+ background: rgba(0, 0, 0, 0.1);
443
+ border: 1px solid rgba(255, 255, 255, 0.1);
444
+ overflow: hidden;
445
+ position: relative;
446
+ }
447
+
448
+ .document-content {
449
+ width: 100%;
450
+ height: 100%;
451
+ display: flex;
452
+ align-items: center;
453
+ justify-content: center;
454
+ overflow: auto;
455
+ }
456
+
457
+ .pdf-viewer-container,
458
+ .html-viewer-container {
459
+ width: 100%;
460
+ height: 100%;
461
+ display: flex;
462
+ align-items: center;
463
+ justify-content: center;
464
+ padding: 20px;
465
+ }
466
+
467
+ .pdf-embed-wrapper,
468
+ .html-content-wrapper {
469
+ transition: transform 0.3s ease;
470
+ border-radius: 12px;
471
+ overflow: hidden;
472
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
473
+ }
474
+
475
+ .pdf-embed {
476
+ width: 80vw;
477
+ height: 80vh;
478
+ border: none;
479
+ border-radius: 12px;
480
+ background: rgba(255, 255, 255, 0.95);
481
+ }
482
+
483
+ .html-iframe {
484
+ width: 80vw;
485
+ height: 80vh;
486
+ border: none;
487
+ border-radius: 12px;
488
+ background: rgba(255, 255, 255, 0.95);
489
+ }
490
+
491
+ .loading-state {
492
+ display: flex;
493
+ flex-direction: column;
494
+ align-items: center;
495
+ justify-content: center;
496
+ gap: 16px;
497
+ color: rgba(255, 255, 255, 0.8);
498
+ }
499
+
500
+ .loading-spinner {
501
+ width: 40px;
502
+ height: 40px;
503
+ border: 3px solid rgba(79, 172, 254, 0.3);
504
+ border-top: 3px solid #4facfe;
505
+ border-radius: 50%;
506
+ animation: spin 1s linear infinite;
507
+ }
508
+
509
+ @keyframes spin {
510
+ 0% { transform: rotate(0deg); }
511
+ 100% { transform: rotate(360deg); }
512
+ }
513
+
514
+ .unsupported-format {
515
+ display: flex;
516
+ flex-direction: column;
517
+ align-items: center;
518
+ justify-content: center;
519
+ gap: 16px;
520
+ color: rgba(255, 255, 255, 0.6);
521
+ }
522
+
523
+ @media (max-width: 768px) {
524
+ .viewer-header {
525
+ flex-direction: column;
526
+ gap: 16px;
527
+ padding: 16px;
528
+ }
529
+
530
+ .header-left {
531
+ width: 100%;
532
+ justify-content: space-between;
533
+ }
534
+
535
+ .header-controls {
536
+ width: 100%;
537
+ justify-content: center;
538
+ flex-wrap: wrap;
539
+ }
540
+
541
+ .pdf-embed,
542
+ .html-iframe {
543
+ width: 95vw;
544
+ height: 70vh;
545
+ }
546
+ }
547
+ `}</style>
548
+ </div>
549
+ );
550
+ };
551
+
552
+ export default DocumentViewer;
src/components/FileUploader.js ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { Upload, FileText, Clipboard } from 'lucide-react';
4
+
5
+ const FileUploader = ({
6
+ getRootProps,
7
+ getInputProps,
8
+ isDragActive,
9
+ onFileSelect,
10
+ fileInputRef,
11
+ onFileChange
12
+ }) => {
13
+
14
+ const uploadMethods = [
15
+ {
16
+ id: 'drag',
17
+ icon: <Upload size={24} />,
18
+ title: 'Drag & Drop',
19
+ description: 'Drag files directly here',
20
+ action: 'drag-drop'
21
+ },
22
+ {
23
+ id: 'browse',
24
+ icon: <FileText size={24} />,
25
+ title: 'Browse Files',
26
+ description: 'Select from computer',
27
+ action: 'browse'
28
+ }
29
+ ];
30
+
31
+ const handleMethodClick = (method) => {
32
+ if (method.action === 'browse') {
33
+ onFileSelect();
34
+ }
35
+ };
36
+
37
+ return (
38
+ <div className="file-uploader">
39
+ <motion.div
40
+ className={`upload-zone glass-panel ${isDragActive ? 'drag-active' : ''}`}
41
+ initial={{ opacity: 0, scale: 0.9 }}
42
+ animate={{ opacity: 1, scale: 1 }}
43
+ transition={{ duration: 0.5 }}
44
+ onDragOver={(e) => e.preventDefault()}
45
+ onDrop={(e) => {
46
+ e.preventDefault();
47
+ const files = e.dataTransfer.files;
48
+ if (files.length > 0) {
49
+ const file = files[0];
50
+ onFileChange({ target: { files: [file] } });
51
+ }
52
+ }}
53
+ >
54
+ <input {...getInputProps()} />
55
+ <input
56
+ ref={fileInputRef}
57
+ type="file"
58
+ onChange={onFileChange}
59
+ accept=".pdf,.png,.jpg,.jpeg,.html,.htm"
60
+ style={{ display: 'none' }}
61
+ />
62
+
63
+ <div className="upload-content">
64
+ <div className="upload-text">
65
+ <h3>{isDragActive ? 'Drop files here' : 'Document Processing'}</h3>
66
+ <p>{isDragActive ? 'Release to process your document' : 'Drag & drop files or click to browse'}</p>
67
+ </div>
68
+
69
+ <div className="upload-actions">
70
+ <div className="button-group">
71
+ <motion.button
72
+ className="primary-upload-btn"
73
+ onClick={onFileSelect}
74
+ whileHover={{ scale: 1.05 }}
75
+ whileTap={{ scale: 0.95 }}
76
+ initial={{ opacity: 0, y: 20 }}
77
+ animate={{ opacity: 1, y: 0 }}
78
+ transition={{ duration: 0.5 }}
79
+ >
80
+ <Upload size={20} />
81
+ Upload Files
82
+ </motion.button>
83
+
84
+ <div className="button-divider"></div>
85
+
86
+ <motion.button
87
+ className="paste-btn"
88
+ onClick={() => {
89
+ // Trigger paste functionality
90
+ document.execCommand('paste');
91
+ }}
92
+ whileHover={{ scale: 1.05 }}
93
+ whileTap={{ scale: 0.95 }}
94
+ initial={{ opacity: 0, y: 20 }}
95
+ animate={{ opacity: 1, y: 0 }}
96
+ transition={{ duration: 0.5, delay: 0.1 }}
97
+ >
98
+ <Clipboard size={18} />
99
+ Paste Image
100
+ </motion.button>
101
+ </div>
102
+ </div>
103
+
104
+ <div className="supported-formats">
105
+ <p>Supported: PDF, HTML, PNG, JPG, JPEG</p>
106
+ </div>
107
+ </div>
108
+ </motion.div>
109
+
110
+ <style jsx>{`
111
+ .file-uploader {
112
+ width: 100%;
113
+ height: 100%;
114
+ }
115
+
116
+ .upload-zone {
117
+ min-height: 300px;
118
+ display: flex;
119
+ flex-direction: column;
120
+ align-items: center;
121
+ justify-content: center;
122
+ transition: all 0.3s ease;
123
+ position: relative;
124
+ overflow: hidden;
125
+ cursor: default;
126
+ }
127
+
128
+ .upload-zone.drag-active {
129
+ border-color: var(--accent-primary);
130
+ background: rgba(99, 102, 241, 0.05);
131
+ transform: scale(1.02);
132
+ }
133
+
134
+ .upload-content {
135
+ text-align: center;
136
+ width: 100%;
137
+ max-width: 400px;
138
+ }
139
+
140
+ .upload-actions {
141
+ margin: 32px 0;
142
+ }
143
+
144
+ .button-group {
145
+ display: flex;
146
+ flex-direction: column;
147
+ gap: 16px;
148
+ justify-content: center;
149
+ align-items: center;
150
+ }
151
+
152
+ .button-divider {
153
+ width: 60px;
154
+ height: 1px;
155
+ background: rgba(255, 255, 255, 0.15);
156
+ }
157
+
158
+ .primary-upload-btn {
159
+ background: linear-gradient(135deg, #8b5cf6 0%, #3b82f6 100%);
160
+ border: none;
161
+ border-radius: 12px;
162
+ padding: 18px 40px;
163
+ color: white;
164
+ font-size: 1.1rem;
165
+ font-weight: 600;
166
+ cursor: pointer;
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 14px;
170
+ box-shadow: 0 4px 20px rgba(139, 92, 246, 0.4);
171
+ transition: all 0.3s ease;
172
+ }
173
+
174
+ .primary-upload-btn:hover {
175
+ box-shadow: 0 6px 25px rgba(139, 92, 246, 0.6);
176
+ transform: translateY(-2px);
177
+ }
178
+
179
+ .paste-btn {
180
+ background: transparent;
181
+ border: 1px solid rgba(255, 255, 255, 0.1);
182
+ border-radius: 8px;
183
+ padding: 12px 20px;
184
+ color: rgba(255, 255, 255, 0.6);
185
+ font-size: 0.85rem;
186
+ font-weight: 400;
187
+ cursor: pointer;
188
+ display: flex;
189
+ align-items: center;
190
+ gap: 8px;
191
+ transition: all 0.3s ease;
192
+ }
193
+
194
+ .paste-btn:hover {
195
+ background: rgba(255, 255, 255, 0.05);
196
+ border-color: rgba(255, 255, 255, 0.15);
197
+ color: rgba(255, 255, 255, 0.8);
198
+ }
199
+
200
+ .upload-divider {
201
+ margin: 24px 0;
202
+ position: relative;
203
+ color: rgba(255, 255, 255, 0.5);
204
+ font-size: 0.875rem;
205
+ }
206
+
207
+ .upload-divider::before {
208
+ content: '';
209
+ position: absolute;
210
+ top: 50%;
211
+ left: 0;
212
+ right: 0;
213
+ height: 1px;
214
+ background: rgba(255, 255, 255, 0.1);
215
+ z-index: 0;
216
+ }
217
+
218
+ .upload-divider span {
219
+ background: rgba(0, 0, 0, 0.3);
220
+ padding: 0 16px;
221
+ position: relative;
222
+ z-index: 1;
223
+ }
224
+
225
+ .secondary-upload-btn {
226
+ background: rgba(255, 255, 255, 0.05);
227
+ border: 1px solid rgba(255, 255, 255, 0.1);
228
+ border-radius: 8px;
229
+ padding: 12px 20px;
230
+ color: rgba(255, 255, 255, 0.7);
231
+ font-size: 0.875rem;
232
+ cursor: pointer;
233
+ display: flex;
234
+ align-items: center;
235
+ gap: 8px;
236
+ margin: 0 auto;
237
+ transition: all 0.3s ease;
238
+ }
239
+
240
+ .secondary-upload-btn:hover {
241
+ background: rgba(255, 255, 255, 0.08);
242
+ color: rgba(255, 255, 255, 0.9);
243
+ border-color: rgba(255, 255, 255, 0.2);
244
+ }
245
+
246
+ .secondary-upload-btn.disabled {
247
+ opacity: 0.4;
248
+ cursor: not-allowed;
249
+ pointer-events: none;
250
+ }
251
+
252
+ .supported-formats {
253
+ margin-top: 24px;
254
+ }
255
+
256
+ .supported-formats p {
257
+ color: rgba(255, 255, 255, 0.4);
258
+ font-size: 0.75rem;
259
+ margin: 0;
260
+ }
261
+
262
+ .upload-icon {
263
+ margin-bottom: 16px;
264
+ color: var(--accent-primary);
265
+ }
266
+
267
+ .drag-indicator {
268
+ font-size: 3rem;
269
+ filter: drop-shadow(0 0 20px rgba(99, 102, 241, 0.5));
270
+ }
271
+
272
+ .upload-svg {
273
+ filter: drop-shadow(0 0 10px rgba(99, 102, 241, 0.3));
274
+ }
275
+
276
+ .upload-text h3 {
277
+ font-size: 1.25rem;
278
+ font-weight: 600;
279
+ margin-bottom: 8px;
280
+ color: var(--text-primary);
281
+ }
282
+
283
+ .upload-text p {
284
+ color: var(--text-secondary);
285
+ font-size: 0.875rem;
286
+ max-width: 400px;
287
+ margin: 0 auto;
288
+ }
289
+
290
+ .upload-methods {
291
+ display: grid;
292
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
293
+ gap: 16px;
294
+ width: 100%;
295
+ max-width: 800px;
296
+ }
297
+
298
+ .upload-method {
299
+ display: flex;
300
+ align-items: center;
301
+ gap: 16px;
302
+ cursor: pointer;
303
+ transition: all 0.3s ease;
304
+ position: relative;
305
+ overflow: hidden;
306
+ }
307
+
308
+ .method-icon {
309
+ color: var(--accent-secondary);
310
+ flex-shrink: 0;
311
+ }
312
+
313
+ .method-content h4 {
314
+ font-size: 0.875rem;
315
+ font-weight: 600;
316
+ margin-bottom: 4px;
317
+ color: var(--text-primary);
318
+ }
319
+
320
+ .method-content p {
321
+ font-size: 0.75rem;
322
+ color: var(--text-muted);
323
+ }
324
+
325
+ @media (max-width: 768px) {
326
+ .upload-methods {
327
+ grid-template-columns: 1fr;
328
+ }
329
+
330
+ .upload-method {
331
+ justify-content: center;
332
+ text-align: center;
333
+ flex-direction: column;
334
+ gap: 8px;
335
+ }
336
+ }
337
+ `}</style>
338
+ </div>
339
+ );
340
+ };
341
+
342
+ export default FileUploader;
src/components/FlowingMenu.js ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { gsap } from 'gsap';
3
+
4
+ function FlowingMenu({ items = [], activeItem, onItemClick }) {
5
+ return (
6
+ <div className="flowing-menu-container">
7
+ <nav className="flowing-menu-nav">
8
+ {items.map((item, idx) => (
9
+ <MenuItem
10
+ key={item.id}
11
+ link="#"
12
+ text={item.label}
13
+ image={`https://picsum.photos/600/400?random=${idx + 1}`}
14
+ isActive={activeItem === item.id}
15
+ onClick={() => onItemClick(item.id)}
16
+ icon={item.icon}
17
+ />
18
+ ))}
19
+ </nav>
20
+ </div>
21
+ );
22
+ }
23
+
24
+ function MenuItem({ link, text, image, isActive, onClick, icon }) {
25
+ const itemRef = React.useRef(null);
26
+ const marqueeRef = React.useRef(null);
27
+ const marqueeInnerRef = React.useRef(null);
28
+
29
+ const animationDefaults = { duration: 0.6, ease: 'expo' };
30
+
31
+ const findClosestEdge = (mouseX, mouseY, width, height) => {
32
+ const topEdgeDist = (mouseX - width / 2) ** 2 + mouseY ** 2;
33
+ const bottomEdgeDist = (mouseX - width / 2) ** 2 + (mouseY - height) ** 2;
34
+ return topEdgeDist < bottomEdgeDist ? 'top' : 'bottom';
35
+ };
36
+
37
+ const handleMouseEnter = (ev) => {
38
+ if (!itemRef.current || !marqueeRef.current || !marqueeInnerRef.current) return;
39
+
40
+ const rect = itemRef.current.getBoundingClientRect();
41
+ const edge = findClosestEdge(
42
+ ev.clientX - rect.left,
43
+ ev.clientY - rect.top,
44
+ rect.width,
45
+ rect.height
46
+ );
47
+
48
+ gsap.timeline({ defaults: animationDefaults })
49
+ .set(marqueeRef.current, { y: edge === 'top' ? '-101%' : '101%' })
50
+ .set(marqueeInnerRef.current, { y: edge === 'top' ? '101%' : '-101%' })
51
+ .to([marqueeRef.current, marqueeInnerRef.current], { y: '0%' });
52
+ };
53
+
54
+ const handleMouseLeave = (ev) => {
55
+ if (!itemRef.current || !marqueeRef.current || !marqueeInnerRef.current) return;
56
+
57
+ const rect = itemRef.current.getBoundingClientRect();
58
+ const edge = findClosestEdge(
59
+ ev.clientX - rect.left,
60
+ ev.clientY - rect.top,
61
+ rect.width,
62
+ rect.height
63
+ );
64
+
65
+ gsap.timeline({ defaults: animationDefaults })
66
+ .to(marqueeRef.current, { y: edge === 'top' ? '-101%' : '101%' })
67
+ .to(marqueeInnerRef.current, { y: edge === 'top' ? '101%' : '-101%' });
68
+ };
69
+
70
+ const repeatedMarqueeContent = [];
71
+
72
+ return (
73
+ <div className={`flowing-menu-item ${isActive ? 'active' : ''}`} ref={itemRef}>
74
+ <a
75
+ className="menu-item-link"
76
+ href={link}
77
+ onClick={(e) => {
78
+ e.preventDefault();
79
+ onClick();
80
+ }}
81
+ onMouseEnter={handleMouseEnter}
82
+ onMouseLeave={handleMouseLeave}
83
+ >
84
+ <span className="menu-icon">{icon}</span>
85
+ <span className="menu-text">{text}</span>
86
+ </a>
87
+ <div className="marquee-overlay" ref={marqueeRef}>
88
+ <div className="marquee-inner" ref={marqueeInnerRef}>
89
+ <div className="marquee-content">
90
+ {repeatedMarqueeContent}
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ );
96
+ }
97
+
98
+ export default FlowingMenu;
src/components/LoadingScreen.js ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState } from 'react';
2
+ import AHoleLoader from './AHoleLoader';
3
+ import './AHoleLoader.css';
4
+
5
+ const LoadingScreen = ({ onComplete }) => {
6
+ const [isVisible, setIsVisible] = useState(true);
7
+
8
+ useEffect(() => {
9
+ // Cycling loading messages and colors
10
+ const messages = [
11
+ { text: "LOADING...", color: "#00f8f1" },
12
+ { text: "MADE BY LUNA...", color: "#ff6b9d" },
13
+ { text: "OCR EXTRACTOR...", color: "#4facfe" },
14
+ { text: "LUNA POWERED...", color: "#a855f7" },
15
+ { text: "TEXT EXTRACTION...", color: "#10b981" }
16
+ ];
17
+
18
+ let messageIndex = 0;
19
+ const textElement = document.getElementById('loading-text');
20
+
21
+ const cycleMessages = () => {
22
+ if (textElement) {
23
+ const currentMessage = messages[messageIndex];
24
+ textElement.textContent = currentMessage.text;
25
+ textElement.style.color = currentMessage.color;
26
+ textElement.style.textShadow = `0 0 8px ${currentMessage.color}`;
27
+
28
+ // Update dots color to match
29
+ const dots = document.querySelectorAll('.dot');
30
+ dots.forEach(dot => {
31
+ dot.style.background = currentMessage.color;
32
+ dot.style.boxShadow = `0 0 4px ${currentMessage.color}`;
33
+ });
34
+
35
+ messageIndex = (messageIndex + 1) % messages.length;
36
+ }
37
+ };
38
+
39
+ // Start cycling immediately
40
+ cycleMessages();
41
+ const messageTimer = setInterval(cycleMessages, 800);
42
+
43
+ // Auto-hide after 4 seconds
44
+ const timer = setTimeout(() => {
45
+ setIsVisible(false);
46
+ setTimeout(() => {
47
+ onComplete?.();
48
+ }, 500); // Wait for fade out animation
49
+ }, 4000);
50
+
51
+ return () => {
52
+ clearTimeout(timer);
53
+ clearInterval(messageTimer);
54
+ };
55
+ }, [onComplete]);
56
+
57
+ if (!isVisible) {
58
+ return (
59
+ <div
60
+ className="loading-screen"
61
+ style={{
62
+ opacity: 0,
63
+ transition: 'opacity 0.5s ease',
64
+ pointerEvents: 'none'
65
+ }}
66
+ />
67
+ );
68
+ }
69
+
70
+ return (
71
+ <div className="loading-screen">
72
+ <div className="a-hole-container">
73
+ <AHoleLoader />
74
+ </div>
75
+
76
+ {/* Pixelated loading indicator in bottom left */}
77
+ <div className="pixelated-loading">
78
+ <div className="loading-dots">
79
+ <div className="dot"></div>
80
+ <div className="dot"></div>
81
+ <div className="dot"></div>
82
+ <div className="dot"></div>
83
+ </div>
84
+ <span className="loading-text" id="loading-text">LOADING...</span>
85
+ </div>
86
+ </div>
87
+ );
88
+ };
89
+
90
+ export default LoadingScreen;
src/components/Orb.js ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useRef } from "react";
2
+ import { Renderer, Program, Mesh, Triangle, Vec3 } from "ogl";
3
+
4
+ const vert = /* glsl */ `
5
+ precision highp float;
6
+ attribute vec2 position;
7
+ attribute vec2 uv;
8
+ varying vec2 vUv;
9
+ void main() {
10
+ vUv = uv;
11
+ gl_Position = vec4(position, 0.0, 1.0);
12
+ }
13
+ `;
14
+
15
+ const frag = /* glsl */ `
16
+ precision highp float;
17
+
18
+ uniform float iTime;
19
+ uniform vec3 iResolution;
20
+ uniform float hue;
21
+ uniform float hover;
22
+ uniform float rot;
23
+ uniform float hoverIntensity;
24
+ varying vec2 vUv;
25
+
26
+ vec3 rgb2yiq(vec3 c) {
27
+ float y = dot(c, vec3(0.299, 0.587, 0.114));
28
+ float i = dot(c, vec3(0.596, -0.274, -0.322));
29
+ float q = dot(c, vec3(0.211, -0.523, 0.312));
30
+ return vec3(y, i, q);
31
+ }
32
+
33
+ vec3 yiq2rgb(vec3 c) {
34
+ float r = c.x + 0.956 * c.y + 0.621 * c.z;
35
+ float g = c.x - 0.272 * c.y - 0.647 * c.z;
36
+ float b = c.x - 1.106 * c.y + 1.703 * c.z;
37
+ return vec3(r, g, b);
38
+ }
39
+
40
+ vec3 adjustHue(vec3 color, float hueDeg) {
41
+ float hueRad = hueDeg * 3.14159265 / 180.0;
42
+ vec3 yiq = rgb2yiq(color);
43
+ float cosA = cos(hueRad);
44
+ float sinA = sin(hueRad);
45
+ float i = yiq.y * cosA - yiq.z * sinA;
46
+ float q = yiq.y * sinA + yiq.z * cosA;
47
+ yiq.y = i;
48
+ yiq.z = q;
49
+ return yiq2rgb(yiq);
50
+ }
51
+
52
+ vec3 hash33(vec3 p3) {
53
+ p3 = fract(p3 * vec3(0.1031, 0.11369, 0.13787));
54
+ p3 += dot(p3, p3.yxz + 19.19);
55
+ return -1.0 + 2.0 * fract(vec3(
56
+ p3.x + p3.y,
57
+ p3.x + p3.z,
58
+ p3.y + p3.z
59
+ ) * p3.zyx);
60
+ }
61
+
62
+ float snoise3(vec3 p) {
63
+ const float K1 = 0.333333333;
64
+ const float K2 = 0.166666667;
65
+ vec3 i = floor(p + (p.x + p.y + p.z) * K1);
66
+ vec3 d0 = p - (i - (i.x + i.y + i.z) * K2);
67
+ vec3 e = step(vec3(0.0), d0 - d0.yzx);
68
+ vec3 i1 = e * (1.0 - e.zxy);
69
+ vec3 i2 = 1.0 - e.zxy * (1.0 - e);
70
+ vec3 d1 = d0 - (i1 - K2);
71
+ vec3 d2 = d0 - (i2 - K1);
72
+ vec3 d3 = d0 - 0.5;
73
+ vec4 h = max(0.6 - vec4(
74
+ dot(d0, d0),
75
+ dot(d1, d1),
76
+ dot(d2, d2),
77
+ dot(d3, d3)
78
+ ), 0.0);
79
+ vec4 n = h * h * h * h * vec4(
80
+ dot(d0, hash33(i)),
81
+ dot(d1, hash33(i + i1)),
82
+ dot(d2, hash33(i + i2)),
83
+ dot(d3, hash33(i + 1.0))
84
+ );
85
+ return dot(vec4(31.316), n);
86
+ }
87
+
88
+ vec4 extractAlpha(vec3 colorIn) {
89
+ float a = max(max(colorIn.r, colorIn.g), colorIn.b);
90
+ return vec4(colorIn.rgb / (a + 1e-5), a);
91
+ }
92
+
93
+ const vec3 baseColor1 = vec3(0.611765, 0.262745, 0.996078);
94
+ const vec3 baseColor2 = vec3(0.298039, 0.760784, 0.913725);
95
+ const vec3 baseColor3 = vec3(0.062745, 0.078431, 0.600000);
96
+ const float innerRadius = 0.6;
97
+ const float noiseScale = 0.65;
98
+
99
+ float light1(float intensity, float attenuation, float dist) {
100
+ return intensity / (1.0 + dist * attenuation);
101
+ }
102
+ float light2(float intensity, float attenuation, float dist) {
103
+ return intensity / (1.0 + dist * dist * attenuation);
104
+ }
105
+
106
+ vec4 draw(vec2 uv) {
107
+ vec3 color1 = adjustHue(baseColor1, hue);
108
+ vec3 color2 = adjustHue(baseColor2, hue);
109
+ vec3 color3 = adjustHue(baseColor3, hue);
110
+
111
+ float ang = atan(uv.y, uv.x);
112
+ float len = length(uv);
113
+ float invLen = len > 0.0 ? 1.0 / len : 0.0;
114
+
115
+ float n0 = snoise3(vec3(uv * noiseScale, iTime * 0.5)) * 0.5 + 0.5;
116
+ float r0 = mix(mix(innerRadius, 1.0, 0.4), mix(innerRadius, 1.0, 0.6), n0);
117
+ float d0 = distance(uv, (r0 * invLen) * uv);
118
+ float v0 = light1(1.0, 10.0, d0);
119
+ v0 *= smoothstep(r0 * 1.05, r0, len);
120
+ float cl = cos(ang + iTime * 2.0) * 0.5 + 0.5;
121
+
122
+ float a = iTime * -1.0;
123
+ vec2 pos = vec2(cos(a), sin(a)) * r0;
124
+ float d = distance(uv, pos);
125
+ float v1 = light2(1.5, 5.0, d);
126
+ v1 *= light1(1.0, 50.0, d0);
127
+
128
+ float v2 = smoothstep(1.0, mix(innerRadius, 1.0, n0 * 0.5), len);
129
+ float v3 = smoothstep(innerRadius, mix(innerRadius, 1.0, 0.5), len);
130
+
131
+ vec3 col = mix(color1, color2, cl);
132
+ col = mix(color3, col, v0);
133
+ col = (col + v1) * v2 * v3;
134
+ col = clamp(col, 0.0, 1.0);
135
+
136
+ return extractAlpha(col);
137
+ }
138
+
139
+ vec4 mainImage(vec2 fragCoord) {
140
+ vec2 center = iResolution.xy * 0.5;
141
+ float size = min(iResolution.x, iResolution.y);
142
+ vec2 uv = (fragCoord - center) / size * 2.0;
143
+
144
+ float angle = rot;
145
+ float s = sin(angle);
146
+ float c = cos(angle);
147
+ uv = vec2(c * uv.x - s * uv.y, s * uv.x + c * uv.y);
148
+
149
+ uv.x += hover * hoverIntensity * 0.1 * sin(uv.y * 10.0 + iTime);
150
+ uv.y += hover * hoverIntensity * 0.1 * sin(uv.x * 10.0 + iTime);
151
+
152
+ return draw(uv);
153
+ }
154
+
155
+ void main() {
156
+ vec2 fragCoord = vUv * iResolution.xy;
157
+ vec4 col = mainImage(fragCoord);
158
+ gl_FragColor = vec4(col.rgb * col.a, col.a);
159
+ }
160
+ `;
161
+
162
+ export default function Orb({
163
+ hue = 0,
164
+ hoverIntensity = 0.2,
165
+ rotateOnHover = true,
166
+ forceHoverState = false,
167
+ }) {
168
+ const ctnDom = useRef(null);
169
+
170
+ useEffect(() => {
171
+ const container = ctnDom.current;
172
+ if (!container) return;
173
+
174
+ const renderer = new Renderer({ alpha: true, premultipliedAlpha: false });
175
+ const gl = renderer.gl;
176
+ gl.clearColor(0, 0, 0, 0);
177
+ container.appendChild(gl.canvas);
178
+
179
+ const geometry = new Triangle(gl);
180
+ const program = new Program(gl, {
181
+ vertex: vert,
182
+ fragment: frag,
183
+ uniforms: {
184
+ iTime: { value: 0 },
185
+ iResolution: {
186
+ value: new Vec3(
187
+ gl.canvas.width,
188
+ gl.canvas.height,
189
+ gl.canvas.width / gl.canvas.height
190
+ ),
191
+ },
192
+ hue: { value: hue },
193
+ hover: { value: 0 },
194
+ rot: { value: 0 },
195
+ hoverIntensity: { value: hoverIntensity },
196
+ },
197
+ });
198
+
199
+ const mesh = new Mesh(gl, { geometry, program });
200
+
201
+ function resize() {
202
+ if (!container) return;
203
+ const dpr = window.devicePixelRatio || 1;
204
+ const width = container.clientWidth;
205
+ const height = container.clientHeight;
206
+ renderer.setSize(width * dpr, height * dpr);
207
+ gl.canvas.style.width = width + "px";
208
+ gl.canvas.style.height = height + "px";
209
+ program.uniforms.iResolution.value.set(
210
+ gl.canvas.width,
211
+ gl.canvas.height,
212
+ gl.canvas.width / gl.canvas.height
213
+ );
214
+ }
215
+ window.addEventListener("resize", resize);
216
+ resize();
217
+
218
+ let targetHover = 0;
219
+ let lastTime = 0;
220
+ let currentRot = 0;
221
+ const rotationSpeed = 0.3;
222
+
223
+ const handleMouseMove = (e) => {
224
+ const rect = container.getBoundingClientRect();
225
+ const x = e.clientX - rect.left;
226
+ const y = e.clientY - rect.top;
227
+ const width = rect.width;
228
+ const height = rect.height;
229
+ const size = Math.min(width, height);
230
+ const centerX = width / 2;
231
+ const centerY = height / 2;
232
+ const uvX = ((x - centerX) / size) * 2.0;
233
+ const uvY = ((y - centerY) / size) * 2.0;
234
+
235
+ if (Math.sqrt(uvX * uvX + uvY * uvY) < 0.8) {
236
+ targetHover = 1;
237
+ } else {
238
+ targetHover = 0;
239
+ }
240
+ };
241
+
242
+ const handleMouseLeave = () => {
243
+ targetHover = 0;
244
+ };
245
+
246
+ container.addEventListener("mousemove", handleMouseMove);
247
+ container.addEventListener("mouseleave", handleMouseLeave);
248
+
249
+ let rafId;
250
+ const update = (t) => {
251
+ rafId = requestAnimationFrame(update);
252
+ const dt = (t - lastTime) * 0.001;
253
+ lastTime = t;
254
+ program.uniforms.iTime.value = t * 0.001;
255
+ program.uniforms.hue.value = hue;
256
+ program.uniforms.hoverIntensity.value = hoverIntensity;
257
+
258
+ const effectiveHover = forceHoverState ? 1 : targetHover;
259
+ program.uniforms.hover.value += (effectiveHover - program.uniforms.hover.value) * 0.1;
260
+
261
+ if (rotateOnHover && effectiveHover > 0.5) {
262
+ currentRot += dt * rotationSpeed;
263
+ }
264
+ program.uniforms.rot.value = currentRot;
265
+
266
+ renderer.render({ scene: mesh });
267
+ };
268
+ rafId = requestAnimationFrame(update);
269
+
270
+ return () => {
271
+ cancelAnimationFrame(rafId);
272
+ window.removeEventListener("resize", resize);
273
+ container.removeEventListener("mousemove", handleMouseMove);
274
+ container.removeEventListener("mouseleave", handleMouseLeave);
275
+ if (container.contains(gl.canvas)) {
276
+ container.removeChild(gl.canvas);
277
+ }
278
+ gl.getExtension("WEBGL_lose_context")?.loseContext();
279
+ };
280
+ }, [hue, hoverIntensity, rotateOnHover, forceHoverState]);
281
+
282
+ return <div ref={ctnDom} className="orb-container" />;
283
+ }
284
+
285
+ // CSS for orb container
286
+ const orbCSS = `
287
+ .orb-container {
288
+ position: relative;
289
+ z-index: 0;
290
+ width: 100%;
291
+ height: 100%;
292
+ }
293
+ `;
294
+
295
+ // Inject CSS
296
+ if (typeof document !== 'undefined') {
297
+ const style = document.createElement('style');
298
+ style.textContent = orbCSS;
299
+ document.head.appendChild(style);
300
+ }
src/components/PreviewPanel.js ADDED
@@ -0,0 +1,543 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { Eye, Zap, Brain, Settings, ExternalLink, FileText } from 'lucide-react';
4
+ import DocumentViewer from './DocumentViewer';
5
+
6
+ const PreviewPanel = ({
7
+ file,
8
+ processingMode,
9
+ onModeChange,
10
+ onProcess,
11
+ isProcessing,
12
+ apiKey
13
+ }) => {
14
+ const [previewUrl, setPreviewUrl] = useState(null);
15
+ const [fileInfo, setFileInfo] = useState(null);
16
+ const [showDocumentViewer, setShowDocumentViewer] = useState(false);
17
+ // OCR borders for future use
18
+ // const [showOcrBorders, setShowOcrBorders] = useState(false);
19
+
20
+ useEffect(() => {
21
+ if (file) {
22
+ const url = URL.createObjectURL(file);
23
+ setPreviewUrl(url);
24
+
25
+ setFileInfo({
26
+ name: file.name,
27
+ size: (file.size / 1024 / 1024).toFixed(2),
28
+ type: file.type,
29
+ lastModified: new Date(file.lastModified).toLocaleString()
30
+ });
31
+
32
+ return () => URL.revokeObjectURL(url);
33
+ }
34
+ }, [file]);
35
+
36
+ const processingModes = [
37
+ {
38
+ id: 'standard',
39
+ name: 'Standard Mode',
40
+ model: 'Gemini 2.5 Flash',
41
+ icon: <Zap size={20} />,
42
+ description: 'Fast processing',
43
+ features: ['Same features as Pro', 'Faster speed', 'Slightly less accuracy'],
44
+ gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
45
+ },
46
+ {
47
+ id: 'structured',
48
+ name: 'Structured Mode',
49
+ model: 'Gemini 2.5 Pro',
50
+ icon: <Brain size={20} />,
51
+ description: 'Maximum accuracy',
52
+ features: ['Best accuracy', 'Advanced formatting', 'Slower processing'],
53
+ gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)'
54
+ }
55
+ ];
56
+
57
+ return (
58
+ <>
59
+ {showDocumentViewer && (
60
+ <DocumentViewer
61
+ file={file}
62
+ onBack={() => setShowDocumentViewer(false)}
63
+ />
64
+ )}
65
+
66
+ <div className="preview-panel">
67
+ <div className="preview-grid">
68
+ {/* File Preview */}
69
+ <motion.div
70
+ className="preview-section glass-panel"
71
+ initial={{ opacity: 0, x: -20 }}
72
+ animate={{ opacity: 1, x: 0 }}
73
+ transition={{ duration: 0.5 }}
74
+ >
75
+ <div className="section-header">
76
+ <Eye size={20} />
77
+ <h3>File Preview</h3>
78
+ </div>
79
+
80
+ <div className="preview-container">
81
+ {previewUrl && (
82
+ <motion.div
83
+ className="preview-image-container"
84
+ initial={{ scale: 0.9, opacity: 0 }}
85
+ animate={{ scale: 1, opacity: 1 }}
86
+ transition={{ duration: 0.3 }}
87
+ >
88
+ {file.type === 'application/pdf' ? (
89
+ <div className="pdf-preview glass-preview">
90
+ <div className="preview-content">
91
+ <div className="document-icon">
92
+ <FileText size={48} />
93
+ </div>
94
+ <p className="preview-title">PDF Document</p>
95
+ <span className="pdf-note">Ready for processing</span>
96
+ <motion.button
97
+ className="preview-button"
98
+ onClick={() => setShowDocumentViewer(true)}
99
+ whileHover={{ scale: 1.05 }}
100
+ whileTap={{ scale: 0.95 }}
101
+ >
102
+ <ExternalLink size={16} />
103
+ Open Viewer
104
+ </motion.button>
105
+ </div>
106
+ </div>
107
+ ) : file.type === 'text/html' || file.name.toLowerCase().endsWith('.html') ? (
108
+ <div className="html-preview glass-preview">
109
+ <div className="preview-content">
110
+ <div className="document-icon">
111
+ <FileText size={48} />
112
+ </div>
113
+ <p className="preview-title">HTML Document</p>
114
+ <span className="html-note">Ready for processing</span>
115
+ <motion.button
116
+ className="preview-button"
117
+ onClick={() => setShowDocumentViewer(true)}
118
+ whileHover={{ scale: 1.05 }}
119
+ whileTap={{ scale: 0.95 }}
120
+ >
121
+ <ExternalLink size={16} />
122
+ Open Viewer
123
+ </motion.button>
124
+ </div>
125
+ </div>
126
+ ) : (
127
+ <img
128
+ src={previewUrl}
129
+ alt="Preview"
130
+ className="preview-image"
131
+ />
132
+ )}
133
+ </motion.div>
134
+ )}
135
+
136
+ {fileInfo && (
137
+ <div className="file-info nested-panel">
138
+ <div className="info-grid">
139
+ <div className="info-item">
140
+ <span className="info-label">Name:</span>
141
+ <span className="info-value">{fileInfo.name}</span>
142
+ </div>
143
+ <div className="info-item">
144
+ <span className="info-label">Size:</span>
145
+ <span className="info-value">{fileInfo.size} MB</span>
146
+ </div>
147
+ <div className="info-item">
148
+ <span className="info-label">Type:</span>
149
+ <span className="info-value">{fileInfo.type}</span>
150
+ </div>
151
+ <div className="info-item">
152
+ <span className="info-label">Modified:</span>
153
+ <span className="info-value">{fileInfo.lastModified}</span>
154
+ </div>
155
+ </div>
156
+ </div>
157
+ )}
158
+ </div>
159
+ </motion.div>
160
+
161
+ {/* Processing Options */}
162
+ <motion.div
163
+ className="options-section glass-panel"
164
+ initial={{ opacity: 0, x: 20 }}
165
+ animate={{ opacity: 1, x: 0 }}
166
+ transition={{ duration: 0.5, delay: 0.1 }}
167
+ >
168
+ <div className="section-header">
169
+ <Settings size={20} />
170
+ <h3>Processing Options</h3>
171
+ </div>
172
+
173
+ <div className="processing-modes">
174
+ {processingModes.map((mode, index) => (
175
+ <motion.div
176
+ key={mode.id}
177
+ className={`mode-card nested-panel ${processingMode === mode.id ? 'active' : ''}`}
178
+ onClick={() => onModeChange(mode.id)}
179
+ whileHover={{ scale: 1.02, y: -2 }}
180
+ whileTap={{ scale: 0.98 }}
181
+ initial={{ opacity: 0, y: 20 }}
182
+ animate={{ opacity: 1, y: 0 }}
183
+ transition={{ delay: index * 0.1 }}
184
+ >
185
+ <div className="mode-header">
186
+ <div className="mode-icon" style={{ background: mode.gradient }}>
187
+ {mode.icon}
188
+ </div>
189
+ <div className="mode-info">
190
+ <h4>{mode.name}</h4>
191
+ <span className="mode-model">{mode.model}</span>
192
+ </div>
193
+ </div>
194
+
195
+ <p className="mode-description">{mode.description}</p>
196
+
197
+ <div className="mode-features">
198
+ {mode.features.map((feature, idx) => (
199
+ <span key={idx} className="feature-tag">
200
+ {feature}
201
+ </span>
202
+ ))}
203
+ </div>
204
+
205
+ {processingMode === mode.id && (
206
+ <motion.div
207
+ className="mode-indicator"
208
+ layoutId="modeIndicator"
209
+ style={{
210
+ position: 'absolute',
211
+ top: 0,
212
+ left: 0,
213
+ right: 0,
214
+ bottom: 0,
215
+ background: 'rgba(99, 102, 241, 0.1)',
216
+ borderRadius: '12px',
217
+ zIndex: -1,
218
+ border: '1px solid rgba(99, 102, 241, 0.3)'
219
+ }}
220
+ transition={{
221
+ type: "spring",
222
+ stiffness: 500,
223
+ damping: 30
224
+ }}
225
+ />
226
+ )}
227
+ </motion.div>
228
+ ))}
229
+ </div>
230
+
231
+ <motion.button
232
+ className="process-button flowing-button"
233
+ onClick={onProcess}
234
+ disabled={!apiKey || isProcessing}
235
+ whileHover={{ scale: 1.05 }}
236
+ whileTap={{ scale: 0.95 }}
237
+ style={{
238
+ width: '100%',
239
+ marginTop: '24px',
240
+ background: isProcessing
241
+ ? 'linear-gradient(135deg, #6b7280 0%, #4b5563 100%)'
242
+ : 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
243
+ }}
244
+ >
245
+ <AnimatePresence mode="wait">
246
+ {isProcessing ? (
247
+ <motion.div
248
+ key="processing"
249
+ initial={{ opacity: 0 }}
250
+ animate={{ opacity: 1 }}
251
+ exit={{ opacity: 0 }}
252
+ className="button-content"
253
+ >
254
+ <motion.div
255
+ className="processing-spinner"
256
+ animate={{ rotate: 360 }}
257
+ transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
258
+ >
259
+
260
+ </motion.div>
261
+ Processing...
262
+ </motion.div>
263
+ ) : (
264
+ <motion.div
265
+ key="ready"
266
+ initial={{ opacity: 0 }}
267
+ animate={{ opacity: 1 }}
268
+ exit={{ opacity: 0 }}
269
+ className="button-content"
270
+ >
271
+ 🚀 Extract Text
272
+ </motion.div>
273
+ )}
274
+ </AnimatePresence>
275
+ </motion.button>
276
+
277
+ {!apiKey && (
278
+ <motion.p
279
+ className="api-warning"
280
+ initial={{ opacity: 0 }}
281
+ animate={{ opacity: 1 }}
282
+ style={{
283
+ color: 'rgba(251, 86, 7, 0.8)',
284
+ fontSize: '0.75rem',
285
+ textAlign: 'center',
286
+ marginTop: '8px'
287
+ }}
288
+ >
289
+ Please enter your Google API Key to continue
290
+ </motion.p>
291
+ )}
292
+ </motion.div>
293
+ </div>
294
+
295
+ <style jsx>{`
296
+ .preview-panel {
297
+ width: 100%;
298
+ min-height: auto;
299
+ padding-bottom: 40px;
300
+ }
301
+
302
+ .preview-grid {
303
+ display: grid;
304
+ grid-template-columns: 1fr 1fr;
305
+ gap: 24px;
306
+ height: 100%;
307
+ }
308
+
309
+ .section-header {
310
+ display: flex;
311
+ align-items: center;
312
+ gap: 12px;
313
+ margin-bottom: 24px;
314
+ color: var(--text-primary);
315
+ }
316
+
317
+ .section-header h3 {
318
+ font-size: 1.25rem;
319
+ font-weight: 600;
320
+ }
321
+
322
+ .preview-container {
323
+ display: flex;
324
+ flex-direction: column;
325
+ gap: 16px;
326
+ }
327
+
328
+ .preview-image-container {
329
+ border-radius: 12px;
330
+ overflow: hidden;
331
+ background: rgba(100, 100, 100, 0.1);
332
+ backdrop-filter: blur(25px);
333
+ -webkit-backdrop-filter: blur(25px);
334
+ }
335
+
336
+ .preview-image {
337
+ width: 100%;
338
+ height: 300px;
339
+ object-fit: contain;
340
+ background: rgba(100, 100, 100, 0.1);
341
+ }
342
+
343
+ .pdf-preview {
344
+ height: 300px;
345
+ display: flex;
346
+ flex-direction: column;
347
+ align-items: center;
348
+ justify-content: center;
349
+ gap: 12px;
350
+ background: rgba(100, 100, 100, 0.1);
351
+ }
352
+
353
+ .pdf-icon {
354
+ font-size: 4rem;
355
+ filter: drop-shadow(0 0 20px rgba(99, 102, 241, 0.3));
356
+ }
357
+
358
+ .pdf-note {
359
+ font-size: 0.75rem;
360
+ color: var(--text-muted);
361
+ }
362
+
363
+ .file-info {
364
+ padding: 16px;
365
+ }
366
+
367
+ .info-grid {
368
+ display: grid;
369
+ grid-template-columns: 1fr 1fr;
370
+ gap: 12px;
371
+ }
372
+
373
+ .info-item {
374
+ display: flex;
375
+ flex-direction: column;
376
+ gap: 4px;
377
+ }
378
+
379
+ .info-label {
380
+ font-size: 0.75rem;
381
+ color: var(--text-muted);
382
+ font-weight: 500;
383
+ }
384
+
385
+ .info-value {
386
+ font-size: 0.875rem;
387
+ color: var(--text-secondary);
388
+ word-break: break-all;
389
+ }
390
+
391
+ .processing-modes {
392
+ display: flex;
393
+ flex-direction: column;
394
+ gap: 16px;
395
+ margin-bottom: 24px;
396
+ }
397
+
398
+ .mode-card {
399
+ cursor: pointer;
400
+ transition: all 0.3s ease;
401
+ position: relative;
402
+ overflow: hidden;
403
+ }
404
+
405
+ .mode-card.active {
406
+ border-color: rgba(99, 102, 241, 0.3);
407
+ }
408
+
409
+ .mode-header {
410
+ display: flex;
411
+ align-items: center;
412
+ gap: 12px;
413
+ margin-bottom: 12px;
414
+ }
415
+
416
+ .mode-icon {
417
+ width: 40px;
418
+ height: 40px;
419
+ border-radius: 10px;
420
+ display: flex;
421
+ align-items: center;
422
+ justify-content: center;
423
+ color: white;
424
+ }
425
+
426
+ .mode-info h4 {
427
+ font-size: 1rem;
428
+ font-weight: 600;
429
+ color: var(--text-primary);
430
+ margin-bottom: 2px;
431
+ }
432
+
433
+ .mode-model {
434
+ font-size: 0.75rem;
435
+ color: var(--text-muted);
436
+ }
437
+
438
+ .mode-description {
439
+ font-size: 0.875rem;
440
+ color: var(--text-secondary);
441
+ margin-bottom: 12px;
442
+ }
443
+
444
+ .mode-features {
445
+ display: flex;
446
+ flex-wrap: wrap;
447
+ gap: 6px;
448
+ }
449
+
450
+ .feature-tag {
451
+ background: rgba(100, 100, 100, 0.1);
452
+ color: rgba(255, 255, 255, 0.8);
453
+ padding: 4px 8px;
454
+ border-radius: 6px;
455
+ font-size: 0.75rem;
456
+ font-weight: 500;
457
+ backdrop-filter: blur(10px);
458
+ }
459
+
460
+ .button-content {
461
+ display: flex;
462
+ align-items: center;
463
+ justify-content: center;
464
+ gap: 8px;
465
+ }
466
+
467
+ .processing-spinner {
468
+ font-size: 1.2rem;
469
+ }
470
+
471
+ @media (max-width: 768px) {
472
+ .preview-grid {
473
+ grid-template-columns: 1fr;
474
+ }
475
+
476
+ .info-grid {
477
+ grid-template-columns: 1fr;
478
+ }
479
+ }
480
+
481
+ .glass-preview {
482
+ background: rgba(0, 0, 0, 0.2);
483
+ backdrop-filter: blur(25px);
484
+ -webkit-backdrop-filter: blur(25px);
485
+ border: 1px solid rgba(255, 255, 255, 0.1);
486
+ border-radius: 16px;
487
+ overflow: hidden;
488
+ }
489
+
490
+ .preview-content {
491
+ display: flex;
492
+ flex-direction: column;
493
+ align-items: center;
494
+ justify-content: center;
495
+ gap: 12px;
496
+ padding: 32px;
497
+ }
498
+
499
+ .document-icon {
500
+ color: rgba(79, 172, 254, 0.8);
501
+ filter: drop-shadow(0 0 20px rgba(79, 172, 254, 0.4));
502
+ }
503
+
504
+ .preview-title {
505
+ font-size: 1.25rem;
506
+ font-weight: 600;
507
+ color: var(--text-primary);
508
+ margin: 0;
509
+ }
510
+
511
+ .pdf-note, .html-note {
512
+ font-size: 0.875rem;
513
+ color: var(--text-muted);
514
+ margin: 0;
515
+ }
516
+
517
+ .preview-button {
518
+ display: flex;
519
+ align-items: center;
520
+ gap: 8px;
521
+ padding: 12px 20px;
522
+ background: linear-gradient(135deg, #4facfe, #8b5cf6);
523
+ border: none;
524
+ border-radius: 8px;
525
+ color: white;
526
+ font-size: 0.875rem;
527
+ font-weight: 500;
528
+ cursor: pointer;
529
+ transition: all 0.3s ease;
530
+ margin-top: 8px;
531
+ }
532
+
533
+ .preview-button:hover {
534
+ box-shadow: 0 4px 15px rgba(79, 172, 254, 0.3);
535
+ transform: translateY(-1px);
536
+ }
537
+ `}</style>
538
+ </div>
539
+ </>
540
+ );
541
+ };
542
+
543
+ export default PreviewPanel;
src/components/ProcessingProgress.js ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+
4
+ const ProcessingProgress = ({ progress, isVisible }) => {
5
+ if (!isVisible) return null;
6
+
7
+ const {
8
+ current,
9
+ total,
10
+ status,
11
+ fileName,
12
+ totalPages,
13
+ currentPage,
14
+ totalCharacters,
15
+ pageCharacters,
16
+ phase,
17
+ consoleLogs = []
18
+ } = progress;
19
+ const percentage = total > 0 ? Math.round((current / total) * 100) : 0;
20
+
21
+ return (
22
+ <motion.div
23
+ className="processing-progress-overlay"
24
+ initial={{ opacity: 0 }}
25
+ animate={{ opacity: 1 }}
26
+ exit={{ opacity: 0 }}
27
+ >
28
+ <div className="processing-progress-container">
29
+ <div className="progress-content">
30
+ <div className="progress-header">
31
+ <h3>🌙 Luna Processing</h3>
32
+ <div className="progress-file">
33
+ 📄 {fileName || 'Processing file...'}
34
+ </div>
35
+ </div>
36
+
37
+ <div className="progress-bar-container">
38
+ <div className="progress-bar">
39
+ <motion.div
40
+ className="progress-fill"
41
+ initial={{ width: 0 }}
42
+ animate={{ width: `${percentage}%` }}
43
+ transition={{ duration: 0.5, ease: "easeOut" }}
44
+ />
45
+ </div>
46
+ <div className="progress-percentage">{percentage}%</div>
47
+ </div>
48
+
49
+ <div className="progress-status">
50
+ {status && (
51
+ <motion.div
52
+ key={status}
53
+ initial={{ opacity: 0, y: 10 }}
54
+ animate={{ opacity: 1, y: 0 }}
55
+ className="status-text"
56
+ >
57
+ {status}
58
+ </motion.div>
59
+ )}
60
+ </div>
61
+
62
+ {totalPages > 1 && (
63
+ <div className="progress-details">
64
+ <div className="progress-counter">
65
+ <span className="counter-current">{currentPage || 0}</span>
66
+ <span className="counter-separator">/</span>
67
+ <span className="counter-total">{totalPages}</span>
68
+ <span className="counter-label">pages</span>
69
+ </div>
70
+
71
+ {totalCharacters > 0 && (
72
+ <div className="progress-stats">
73
+ <div className="stat-item">
74
+ <span className="stat-label">Total Characters:</span>
75
+ <span className="stat-value">{totalCharacters.toLocaleString()}</span>
76
+ </div>
77
+ {pageCharacters > 0 && (
78
+ <div className="stat-item">
79
+ <span className="stat-label">Last Page:</span>
80
+ <span className="stat-value">{pageCharacters.toLocaleString()} chars</span>
81
+ </div>
82
+ )}
83
+ </div>
84
+ )}
85
+ </div>
86
+ )}
87
+
88
+ {consoleLogs.length > 0 && (
89
+ <div className="console-logs">
90
+ <div className="console-header">📋 Live Process Log</div>
91
+ <div className="console-content">
92
+ {consoleLogs.slice(-6).map((log, index) => (
93
+ <div key={index} className="console-line">
94
+ <span className="console-time">
95
+ {new Date(log.timestamp).toLocaleTimeString('th-TH', {
96
+ hour12: false,
97
+ hour: '2-digit',
98
+ minute: '2-digit',
99
+ second: '2-digit'
100
+ })}
101
+ </span>
102
+ <span className="console-message">{log.message}</span>
103
+ </div>
104
+ ))}
105
+ </div>
106
+ </div>
107
+ )}
108
+
109
+ <div className="processing-animation">
110
+ <div className="processing-dots">
111
+ <div className="dot"></div>
112
+ <div className="dot"></div>
113
+ <div className="dot"></div>
114
+ </div>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ </motion.div>
119
+ );
120
+ };
121
+
122
+ export default ProcessingProgress;
src/components/ResultsPanel.js ADDED
@@ -0,0 +1,1340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { FileText, Code, Eye, Download, Copy, Check } from 'lucide-react';
4
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
5
+ import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
6
+ import { marked } from 'marked';
7
+ import jsPDF from 'jspdf';
8
+
9
+ const ResultsPanel = ({ text, previewMode, onPreviewModeChange, fileName }) => {
10
+ const [copied, setCopied] = useState(false);
11
+ const [downloadFormat, setDownloadFormat] = useState('txt');
12
+
13
+ const previewModes = [
14
+ { id: 'text', label: 'Text', icon: <FileText size={16} /> },
15
+ { id: 'markdown', label: 'Markdown', icon: <Code size={16} /> },
16
+ { id: 'preview', label: 'HTML', icon: <Eye size={16} /> },
17
+ { id: 'json', label: 'JSON', icon: <Code size={16} /> }
18
+ ];
19
+
20
+ const downloadFormats = [
21
+ { id: 'txt', label: '.txt', icon: '📄' },
22
+ { id: 'md', label: '.md', icon: '📝' },
23
+ { id: 'html', label: '.html', icon: '🌐' },
24
+ { id: 'json', label: '.json', icon: '🔧' },
25
+ { id: 'pdf', label: '.pdf', icon: '📋' }
26
+ ];
27
+
28
+ const handleCopy = async () => {
29
+ try {
30
+ await navigator.clipboard.writeText(text);
31
+ setCopied(true);
32
+ setTimeout(() => setCopied(false), 2000);
33
+ } catch (err) {
34
+ console.error('Failed to copy text:', err);
35
+ }
36
+ };
37
+
38
+
39
+
40
+ const handleDownload = (format = downloadFormat) => {
41
+ const baseFileName = fileName ? fileName.split('.')[0] : 'extracted-text';
42
+
43
+ switch (format) {
44
+ case 'txt':
45
+ downloadFile(text, `${baseFileName}.txt`, 'text/plain');
46
+ break;
47
+ case 'md':
48
+ downloadFile(text, `${baseFileName}.md`, 'text/markdown');
49
+ break;
50
+ case 'html':
51
+ const htmlContent = `
52
+ <!DOCTYPE html>
53
+ <html lang="en">
54
+ <head>
55
+ <meta charset="UTF-8">
56
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
57
+ <title>${baseFileName} - Luna OCR</title>
58
+ <style>
59
+ * {
60
+ margin: 0;
61
+ padding: 0;
62
+ box-sizing: border-box;
63
+ }
64
+
65
+ body {
66
+ font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', sans-serif;
67
+ background: #0a0a0f;
68
+ color: #ffffff;
69
+ min-height: 100vh;
70
+ overflow-x: hidden;
71
+ position: relative;
72
+ }
73
+
74
+ /* Modern Animated Background */
75
+ .animated-background {
76
+ position: fixed;
77
+ top: 0;
78
+ left: 0;
79
+ width: 100%;
80
+ height: 100%;
81
+ pointer-events: none;
82
+ z-index: -1;
83
+ overflow: hidden;
84
+ }
85
+
86
+ .gradient-orb {
87
+ position: absolute;
88
+ border-radius: 50%;
89
+ filter: blur(60px);
90
+ opacity: 0.4;
91
+ }
92
+
93
+ .orb-1 {
94
+ top: 20%;
95
+ left: 10%;
96
+ width: 300px;
97
+ height: 300px;
98
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
99
+ animation: float1 25s ease-in-out infinite;
100
+ }
101
+
102
+ .orb-2 {
103
+ top: 60%;
104
+ right: 15%;
105
+ width: 250px;
106
+ height: 250px;
107
+ background: linear-gradient(135deg, #ec4899 0%, #be185d 100%);
108
+ animation: float2 30s ease-in-out infinite;
109
+ }
110
+
111
+ .orb-3 {
112
+ bottom: 30%;
113
+ left: 20%;
114
+ width: 200px;
115
+ height: 200px;
116
+ background: linear-gradient(135deg, #6366f1 0%, #4338ca 100%);
117
+ animation: float3 35s ease-in-out infinite;
118
+ }
119
+
120
+ .orb-4 {
121
+ top: 10%;
122
+ right: 40%;
123
+ width: 180px;
124
+ height: 180px;
125
+ background: linear-gradient(135deg, #14b8a6 0%, #0d9488 100%);
126
+ animation: float4 20s ease-in-out infinite;
127
+ }
128
+
129
+ .orb-5 {
130
+ bottom: 10%;
131
+ right: 30%;
132
+ width: 220px;
133
+ height: 220px;
134
+ background: linear-gradient(135deg, #f472b6 0%, #ec4899 100%);
135
+ animation: float5 28s ease-in-out infinite;
136
+ }
137
+
138
+ @keyframes float1 {
139
+ 0%, 100% { transform: translate(0, 0) rotate(0deg); }
140
+ 25% { transform: translate(-30px, -20px) rotate(90deg); }
141
+ 50% { transform: translate(20px, -30px) rotate(180deg); }
142
+ 75% { transform: translate(-20px, 20px) rotate(270deg); }
143
+ }
144
+
145
+ @keyframes float2 {
146
+ 0%, 100% { transform: translate(0, 0) rotate(0deg); }
147
+ 33% { transform: translate(25px, -35px) rotate(120deg); }
148
+ 66% { transform: translate(-35px, -25px) rotate(240deg); }
149
+ }
150
+
151
+ @keyframes float3 {
152
+ 0%, 100% { transform: translate(0, 0) rotate(0deg); }
153
+ 20% { transform: translate(-25px, 30px) rotate(72deg); }
154
+ 40% { transform: translate(30px, 25px) rotate(144deg); }
155
+ 60% { transform: translate(25px, -30px) rotate(216deg); }
156
+ 80% { transform: translate(-30px, -25px) rotate(288deg); }
157
+ }
158
+
159
+ @keyframes float4 {
160
+ 0%, 100% { transform: translate(0, 0) rotate(0deg); }
161
+ 50% { transform: translate(-20px, 25px) rotate(180deg); }
162
+ }
163
+
164
+ @keyframes float5 {
165
+ 0%, 100% { transform: translate(0, 0) rotate(0deg); }
166
+ 25% { transform: translate(20px, -15px) rotate(90deg); }
167
+ 50% { transform: translate(-15px, 20px) rotate(180deg); }
168
+ 75% { transform: translate(-20px, -20px) rotate(270deg); }
169
+ }
170
+ left: 50%;
171
+ width: 60vmin;
172
+ height: 60vmin;
173
+ transform: translate(-50%, -50%);
174
+ opacity: 0.7;
175
+ }
176
+
177
+ .orb-layer {
178
+ position: absolute;
179
+ top: 0;
180
+ left: 0;
181
+ width: 100%;
182
+ height: 100%;
183
+ border-radius: 50%;
184
+ filter: blur(40px);
185
+ }
186
+
187
+ .orb-layer-1 {
188
+ background: radial-gradient(circle at 30% 30%,
189
+ rgba(156, 67, 254, 0.8) 0%,
190
+ rgba(79, 172, 254, 0.6) 40%,
191
+ rgba(16, 21, 96, 0.4) 70%,
192
+ transparent 100%);
193
+ animation: orbRotate1 20s linear infinite, orbPulse1 8s ease-in-out infinite;
194
+ }
195
+
196
+ .orb-layer-2 {
197
+ background: radial-gradient(circle at 70% 60%,
198
+ rgba(76, 194, 233, 0.7) 0%,
199
+ rgba(139, 92, 246, 0.5) 35%,
200
+ rgba(79, 172, 254, 0.3) 65%,
201
+ transparent 100%);
202
+ animation: orbRotate2 25s linear infinite reverse, orbPulse2 6s ease-in-out infinite;
203
+ }
204
+
205
+ .orb-layer-3 {
206
+ background: radial-gradient(circle at 50% 80%,
207
+ rgba(16, 185, 129, 0.6) 0%,
208
+ rgba(6, 182, 212, 0.4) 30%,
209
+ rgba(139, 92, 246, 0.2) 60%,
210
+ transparent 100%);
211
+ animation: orbRotate3 30s linear infinite, orbPulse3 10s ease-in-out infinite;
212
+ }
213
+
214
+ .orb-core {
215
+ position: absolute;
216
+ top: 50%;
217
+ left: 50%;
218
+ width: 40%;
219
+ height: 40%;
220
+ transform: translate(-50%, -50%);
221
+ background: radial-gradient(circle,
222
+ rgba(156, 67, 254, 0.9) 0%,
223
+ rgba(79, 172, 254, 0.7) 30%,
224
+ rgba(16, 21, 96, 0.5) 60%,
225
+ transparent 100%);
226
+ border-radius: 50%;
227
+ filter: blur(20px);
228
+ animation: orbCore 15s ease-in-out infinite;
229
+ }
230
+
231
+ @keyframes orbRotate1 {
232
+ 0% { transform: rotate(0deg) scale(1); }
233
+ 25% { transform: rotate(90deg) scale(1.1); }
234
+ 50% { transform: rotate(180deg) scale(1); }
235
+ 75% { transform: rotate(270deg) scale(0.9); }
236
+ 100% { transform: rotate(360deg) scale(1); }
237
+ }
238
+
239
+ @keyframes orbRotate2 {
240
+ 0% { transform: rotate(0deg) scale(0.9); }
241
+ 33% { transform: rotate(120deg) scale(1.2); }
242
+ 66% { transform: rotate(240deg) scale(0.8); }
243
+ 100% { transform: rotate(360deg) scale(0.9); }
244
+ }
245
+
246
+ @keyframes orbRotate3 {
247
+ 0% { transform: rotate(0deg) scale(1.1); }
248
+ 20% { transform: rotate(72deg) scale(0.9); }
249
+ 40% { transform: rotate(144deg) scale(1.3); }
250
+ 60% { transform: rotate(216deg) scale(0.7); }
251
+ 80% { transform: rotate(288deg) scale(1.1); }
252
+ 100% { transform: rotate(360deg) scale(1.1); }
253
+ }
254
+
255
+ @keyframes orbPulse1 {
256
+ 0%, 100% { opacity: 0.8; filter: blur(40px) hue-rotate(0deg); }
257
+ 50% { opacity: 0.6; filter: blur(60px) hue-rotate(30deg); }
258
+ }
259
+
260
+ @keyframes orbPulse2 {
261
+ 0%, 100% { opacity: 0.7; filter: blur(40px) hue-rotate(0deg); }
262
+ 50% { opacity: 0.9; filter: blur(30px) hue-rotate(-20deg); }
263
+ }
264
+
265
+ @keyframes orbPulse3 {
266
+ 0%, 100% { opacity: 0.6; filter: blur(40px) hue-rotate(0deg); }
267
+ 50% { opacity: 0.4; filter: blur(50px) hue-rotate(15deg); }
268
+ }
269
+
270
+ @keyframes orbCore {
271
+ 0%, 100% {
272
+ transform: translate(-50%, -50%) scale(1);
273
+ filter: blur(20px) hue-rotate(0deg);
274
+ }
275
+ 25% {
276
+ transform: translate(-50%, -50%) scale(1.2);
277
+ filter: blur(15px) hue-rotate(45deg);
278
+ }
279
+ 50% {
280
+ transform: translate(-50%, -50%) scale(0.8);
281
+ filter: blur(25px) hue-rotate(90deg);
282
+ }
283
+ 75% {
284
+ transform: translate(-50%, -50%) scale(1.1);
285
+ filter: blur(18px) hue-rotate(135deg);
286
+ }
287
+ }
288
+
289
+ /* Interactive hover effect */
290
+ .orb-container:hover .css-orb {
291
+ animation-duration: 0.5s;
292
+ }
293
+
294
+ .orb-container:hover .orb-layer-1 {
295
+ filter: blur(30px);
296
+ transform: scale(1.2);
297
+ }
298
+
299
+ .orb-container:hover .orb-layer-2 {
300
+ filter: blur(35px);
301
+ transform: scale(0.9);
302
+ }
303
+
304
+ .orb-container:hover .orb-layer-3 {
305
+ filter: blur(45px);
306
+ transform: scale(1.1);
307
+ }
308
+
309
+ /* Modern Glassmorphism Container */
310
+ .glass-container {
311
+ backdrop-filter: blur(30px);
312
+ -webkit-backdrop-filter: blur(30px);
313
+ background: rgba(255, 255, 255, 0.05);
314
+ border: 1px solid rgba(255, 255, 255, 0.1);
315
+ border-radius: 32px;
316
+ margin: 24px;
317
+ min-height: calc(100vh - 48px);
318
+ overflow: hidden;
319
+ position: relative;
320
+ box-shadow:
321
+ 0 8px 32px rgba(0, 0, 0, 0.3),
322
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
323
+ }
324
+
325
+ .glass-container::before {
326
+ content: '';
327
+ position: absolute;
328
+ top: 0;
329
+ left: 0;
330
+ right: 0;
331
+ bottom: 0;
332
+ background: linear-gradient(135deg,
333
+ rgba(16, 185, 129, 0.1) 0%,
334
+ rgba(236, 72, 153, 0.08) 35%,
335
+ rgba(99, 102, 241, 0.1) 70%,
336
+ rgba(20, 184, 166, 0.08) 100%);
337
+ border-radius: 32px;
338
+ z-index: -1;
339
+ }
340
+
341
+ /* Modern Header */
342
+ .document-header {
343
+ padding: 40px 48px;
344
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
345
+ background: rgba(255, 255, 255, 0.03);
346
+ backdrop-filter: blur(25px);
347
+ position: relative;
348
+ }
349
+
350
+ .document-header::before {
351
+ content: '';
352
+ position: absolute;
353
+ top: 0;
354
+ left: 0;
355
+ right: 0;
356
+ height: 1px;
357
+ background: linear-gradient(90deg,
358
+ transparent 0%,
359
+ rgba(16, 185, 129, 0.5) 25%,
360
+ rgba(236, 72, 153, 0.5) 75%,
361
+ transparent 100%);
362
+ }
363
+
364
+ .document-title {
365
+ font-size: 2.75rem;
366
+ font-weight: 800;
367
+ background: linear-gradient(135deg,
368
+ #10b981 0%,
369
+ #ec4899 50%,
370
+ #6366f1 100%);
371
+ -webkit-background-clip: text;
372
+ -webkit-text-fill-color: transparent;
373
+ background-clip: text;
374
+ margin-bottom: 12px;
375
+ letter-spacing: -0.02em;
376
+ }
377
+
378
+ .document-subtitle {
379
+ color: rgba(255, 255, 255, 0.7);
380
+ font-size: 1.1rem;
381
+ font-weight: 500;
382
+ opacity: 0.9;
383
+ }
384
+
385
+ /* Modern Content */
386
+ .document-content {
387
+ padding: 48px;
388
+ line-height: 1.8;
389
+ position: relative;
390
+ }
391
+
392
+ .document-content::before {
393
+ content: '';
394
+ position: absolute;
395
+ top: 0;
396
+ left: 48px;
397
+ right: 48px;
398
+ height: 1px;
399
+ background: linear-gradient(90deg,
400
+ transparent 0%,
401
+ rgba(255, 255, 255, 0.1) 50%,
402
+ transparent 100%);
403
+ }
404
+
405
+ /* Clean Typography */
406
+ h1, h2, h3, h4, h5, h6 {
407
+ color: rgba(255, 255, 255, 0.95);
408
+ margin: 2.5rem 0 1.5rem 0;
409
+ font-weight: 700;
410
+ letter-spacing: -0.01em;
411
+ line-height: 1.3;
412
+ }
413
+
414
+ h1 {
415
+ font-size: 2.5rem;
416
+ color: rgba(255, 255, 255, 0.98);
417
+ margin-bottom: 2rem;
418
+ }
419
+
420
+ h2 {
421
+ font-size: 2rem;
422
+ color: rgba(255, 255, 255, 0.95);
423
+ margin-top: 3rem;
424
+ position: relative;
425
+ padding-bottom: 0.5rem;
426
+ }
427
+
428
+ h2::after {
429
+ content: '';
430
+ position: absolute;
431
+ bottom: 0;
432
+ left: 0;
433
+ width: 60px;
434
+ height: 2px;
435
+ background: linear-gradient(90deg,
436
+ rgba(16, 185, 129, 0.6) 0%,
437
+ rgba(236, 72, 153, 0.6) 100%);
438
+ border-radius: 1px;
439
+ }
440
+
441
+ h3 {
442
+ font-size: 1.5rem;
443
+ color: rgba(255, 255, 255, 0.92);
444
+ }
445
+
446
+ h4 {
447
+ font-size: 1.25rem;
448
+ color: rgba(255, 255, 255, 0.9);
449
+ }
450
+
451
+ p {
452
+ margin: 1rem 0;
453
+ color: rgba(255, 255, 255, 0.9);
454
+ }
455
+
456
+ /* Enhanced Glassmorphism Tables */
457
+ table {
458
+ width: 100%;
459
+ border-collapse: separate;
460
+ border-spacing: 0;
461
+ margin: 2.5rem 0;
462
+ background: rgba(255, 255, 255, 0.02);
463
+ backdrop-filter: blur(40px);
464
+ -webkit-backdrop-filter: blur(40px);
465
+ border-radius: 24px;
466
+ overflow: hidden;
467
+ border: 1px solid rgba(255, 255, 255, 0.06);
468
+ box-shadow:
469
+ 0 20px 40px rgba(0, 0, 0, 0.3),
470
+ 0 8px 16px rgba(0, 0, 0, 0.2),
471
+ inset 0 1px 0 rgba(255, 255, 255, 0.05);
472
+ position: relative;
473
+ }
474
+
475
+ table::before {
476
+ content: '';
477
+ position: absolute;
478
+ top: 0;
479
+ left: 0;
480
+ right: 0;
481
+ bottom: 0;
482
+ background: linear-gradient(135deg,
483
+ rgba(16, 185, 129, 0.02) 0%,
484
+ rgba(236, 72, 153, 0.02) 50%,
485
+ rgba(99, 102, 241, 0.02) 100%);
486
+ border-radius: 24px;
487
+ z-index: -1;
488
+ }
489
+
490
+ th, td {
491
+ padding: 24px 28px;
492
+ text-align: left;
493
+ border-bottom: 1px solid rgba(255, 255, 255, 0.03);
494
+ position: relative;
495
+ }
496
+
497
+ th {
498
+ background: rgba(255, 255, 255, 0.04);
499
+ backdrop-filter: blur(20px);
500
+ color: rgba(255, 255, 255, 0.95);
501
+ font-weight: 600;
502
+ font-size: 0.95rem;
503
+ letter-spacing: 0.3px;
504
+ text-transform: uppercase;
505
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
506
+ }
507
+
508
+ th::before {
509
+ content: '';
510
+ position: absolute;
511
+ top: 0;
512
+ left: 0;
513
+ right: 0;
514
+ bottom: 0;
515
+ background: linear-gradient(135deg,
516
+ rgba(16, 185, 129, 0.08) 0%,
517
+ rgba(236, 72, 153, 0.08) 100%);
518
+ z-index: -1;
519
+ }
520
+
521
+ th::after {
522
+ content: '';
523
+ position: absolute;
524
+ top: 0;
525
+ left: 0;
526
+ right: 0;
527
+ height: 1px;
528
+ background: linear-gradient(90deg,
529
+ transparent 0%,
530
+ rgba(255, 255, 255, 0.1) 50%,
531
+ transparent 100%);
532
+ }
533
+
534
+ td {
535
+ color: rgba(255, 255, 255, 0.9);
536
+ font-weight: 500;
537
+ background: rgba(255, 255, 255, 0.01);
538
+ }
539
+
540
+ tr:hover td {
541
+ background: rgba(255, 255, 255, 0.03);
542
+ color: rgba(255, 255, 255, 0.95);
543
+ transition: all 0.3s ease;
544
+ }
545
+
546
+ tr:hover {
547
+ transform: translateY(-2px);
548
+ transition: all 0.3s ease;
549
+ }
550
+
551
+ tr:hover td::before {
552
+ content: '';
553
+ position: absolute;
554
+ top: 0;
555
+ left: 0;
556
+ right: 0;
557
+ bottom: 0;
558
+ background: linear-gradient(135deg,
559
+ rgba(16, 185, 129, 0.03) 0%,
560
+ rgba(236, 72, 153, 0.03) 100%);
561
+ z-index: -1;
562
+ }
563
+
564
+ /* Links */
565
+ a {
566
+ color: #4facfe;
567
+ text-decoration: none;
568
+ transition: all 0.3s ease;
569
+ }
570
+
571
+ a:hover {
572
+ color: #8b5cf6;
573
+ text-shadow: 0 0 10px rgba(79, 172, 254, 0.5);
574
+ }
575
+
576
+ /* Enhanced Code blocks */
577
+ pre, code {
578
+ background: rgba(0, 0, 0, 0.3);
579
+ border: 1px solid rgba(255, 255, 255, 0.06);
580
+ border-radius: 12px;
581
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
582
+ backdrop-filter: blur(20px);
583
+ }
584
+
585
+ pre {
586
+ padding: 24px;
587
+ overflow-x: auto;
588
+ margin: 2rem 0;
589
+ box-shadow:
590
+ 0 8px 32px rgba(0, 0, 0, 0.2),
591
+ inset 0 1px 0 rgba(255, 255, 255, 0.05);
592
+ position: relative;
593
+ }
594
+
595
+ pre::before {
596
+ content: '';
597
+ position: absolute;
598
+ top: 0;
599
+ left: 0;
600
+ right: 0;
601
+ bottom: 0;
602
+ background: linear-gradient(135deg,
603
+ rgba(16, 185, 129, 0.02) 0%,
604
+ rgba(99, 102, 241, 0.02) 100%);
605
+ border-radius: 12px;
606
+ z-index: -1;
607
+ }
608
+
609
+ code {
610
+ padding: 6px 10px;
611
+ font-size: 0.875rem;
612
+ }
613
+
614
+ /* Enhanced Blockquotes */
615
+ blockquote {
616
+ border: none;
617
+ background: rgba(255, 255, 255, 0.03);
618
+ backdrop-filter: blur(30px);
619
+ padding: 28px 32px;
620
+ margin: 2.5rem 0;
621
+ border-radius: 20px;
622
+ border-left: 4px solid transparent;
623
+ background-image: linear-gradient(rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.03)),
624
+ linear-gradient(135deg, #10b981, #ec4899);
625
+ background-origin: border-box;
626
+ background-clip: padding-box, border-box;
627
+ box-shadow:
628
+ 0 12px 40px rgba(0, 0, 0, 0.2),
629
+ inset 0 1px 0 rgba(255, 255, 255, 0.08);
630
+ position: relative;
631
+ }
632
+
633
+ blockquote::before {
634
+ content: '';
635
+ position: absolute;
636
+ top: 0;
637
+ left: 0;
638
+ right: 0;
639
+ bottom: 0;
640
+ background: linear-gradient(135deg,
641
+ rgba(16, 185, 129, 0.03) 0%,
642
+ rgba(236, 72, 153, 0.03) 100%);
643
+ border-radius: 20px;
644
+ z-index: -1;
645
+ }
646
+
647
+ blockquote p {
648
+ color: rgba(255, 255, 255, 0.95);
649
+ font-style: italic;
650
+ font-weight: 500;
651
+ margin: 0;
652
+ }
653
+
654
+ /* Lists */
655
+ ul, ol {
656
+ margin: 1rem 0;
657
+ padding-left: 2rem;
658
+ }
659
+
660
+ li {
661
+ margin: 0.5rem 0;
662
+ color: rgba(255, 255, 255, 0.9);
663
+ }
664
+
665
+ /* Horizontal rules */
666
+ hr {
667
+ border: none;
668
+ height: 1px;
669
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
670
+ margin: 3rem 0;
671
+ }
672
+
673
+ /* Strong and emphasis */
674
+ strong {
675
+ color: #ffffff;
676
+ font-weight: 600;
677
+ }
678
+
679
+ em {
680
+ color: #4facfe;
681
+ font-style: italic;
682
+ }
683
+
684
+ /* Modern Footer */
685
+ .document-footer {
686
+ padding: 40px 48px;
687
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
688
+ background: rgba(255, 255, 255, 0.02);
689
+ text-align: center;
690
+ color: rgba(255, 255, 255, 0.6);
691
+ font-size: 0.9rem;
692
+ font-weight: 500;
693
+ position: relative;
694
+ }
695
+
696
+ .document-footer::before {
697
+ content: '';
698
+ position: absolute;
699
+ top: 0;
700
+ left: 48px;
701
+ right: 48px;
702
+ height: 1px;
703
+ background: linear-gradient(90deg,
704
+ transparent 0%,
705
+ rgba(16, 185, 129, 0.3) 25%,
706
+ rgba(236, 72, 153, 0.3) 75%,
707
+ transparent 100%);
708
+ }
709
+
710
+ /* Responsive */
711
+ @media (max-width: 768px) {
712
+ .glass-container {
713
+ margin: 10px;
714
+ border-radius: 16px;
715
+ }
716
+
717
+ .document-header,
718
+ .document-content,
719
+ .document-footer {
720
+ padding: 24px 20px;
721
+ }
722
+
723
+ .document-title {
724
+ font-size: 2rem;
725
+ }
726
+
727
+ table {
728
+ font-size: 0.875rem;
729
+ }
730
+
731
+ th, td {
732
+ padding: 12px 16px;
733
+ }
734
+ }
735
+
736
+ /* Print styles */
737
+ @media print {
738
+ body {
739
+ background: white;
740
+ color: black;
741
+ }
742
+
743
+ .orb-background,
744
+ .glass-container::before {
745
+ display: none;
746
+ }
747
+
748
+ .glass-container {
749
+ background: white;
750
+ border: none;
751
+ box-shadow: none;
752
+ margin: 0;
753
+ }
754
+ }
755
+ </style>
756
+ <script src="https://unpkg.com/[email protected]/dist/ogl.umd.js"></script>
757
+ </head>
758
+ <body>
759
+ <!-- Orb Background -->
760
+ <div class="animated-background">
761
+ <div class="gradient-orb orb-1"></div>
762
+ <div class="gradient-orb orb-2"></div>
763
+ <div class="gradient-orb orb-3"></div>
764
+ <div class="gradient-orb orb-4"></div>
765
+ <div class="gradient-orb orb-5"></div>
766
+ </div>
767
+
768
+ <!-- Main Content -->
769
+ <div class="glass-container">
770
+ <header class="document-header">
771
+ <h1 class="document-title">${baseFileName}</h1>
772
+ <p class="document-subtitle">Generated by Luna OCR • ${new Date().toLocaleDateString()}</p>
773
+ </header>
774
+
775
+ <main class="document-content">
776
+ ${marked(text)}
777
+ </main>
778
+
779
+ <footer class="document-footer">
780
+ <p>Processed with Luna OCR • Glassmorphism Theme • ${new Date().toLocaleString()}</p>
781
+ </footer>
782
+ </div>
783
+
784
+ <script>
785
+ // Robust Orb.js implementation with fallback
786
+ function initOrb() {
787
+ const container = document.getElementById('orbContainer');
788
+ const fallback = document.getElementById('orbFallback');
789
+
790
+ if (!container) {
791
+ console.error('Orb container not found');
792
+ return;
793
+ }
794
+
795
+ // Check if OGL loaded
796
+ if (typeof OGL === 'undefined') {
797
+ console.log('OGL not loaded, using CSS fallback');
798
+ if (fallback) fallback.style.display = 'block';
799
+ return;
800
+ }
801
+
802
+ try {
803
+
804
+ const { Renderer, Program, Mesh, Triangle, Vec3 } = OGL;
805
+
806
+ const vert = \`
807
+ precision highp float;
808
+ attribute vec2 position;
809
+ attribute vec2 uv;
810
+ varying vec2 vUv;
811
+ void main() {
812
+ vUv = uv;
813
+ gl_Position = vec4(position, 0.0, 1.0);
814
+ }
815
+ \`;
816
+
817
+ const frag = \`
818
+ precision highp float;
819
+
820
+ uniform float iTime;
821
+ uniform vec3 iResolution;
822
+ uniform float hue;
823
+ uniform float hover;
824
+ uniform float rot;
825
+ uniform float hoverIntensity;
826
+ varying vec2 vUv;
827
+
828
+ vec3 rgb2yiq(vec3 c) {
829
+ float y = dot(c, vec3(0.299, 0.587, 0.114));
830
+ float i = dot(c, vec3(0.596, -0.274, -0.322));
831
+ float q = dot(c, vec3(0.211, -0.523, 0.312));
832
+ return vec3(y, i, q);
833
+ }
834
+
835
+ vec3 yiq2rgb(vec3 c) {
836
+ float r = c.x + 0.956 * c.y + 0.621 * c.z;
837
+ float g = c.x - 0.272 * c.y - 0.647 * c.z;
838
+ float b = c.x - 1.106 * c.y + 1.703 * c.z;
839
+ return vec3(r, g, b);
840
+ }
841
+
842
+ vec3 adjustHue(vec3 color, float hueDeg) {
843
+ float hueRad = hueDeg * 3.14159265 / 180.0;
844
+ vec3 yiq = rgb2yiq(color);
845
+ float cosA = cos(hueRad);
846
+ float sinA = sin(hueRad);
847
+ float i = yiq.y * cosA - yiq.z * sinA;
848
+ float q = yiq.y * sinA + yiq.z * cosA;
849
+ yiq.y = i;
850
+ yiq.z = q;
851
+ return yiq2rgb(yiq);
852
+ }
853
+
854
+ vec3 hash33(vec3 p3) {
855
+ p3 = fract(p3 * vec3(0.1031, 0.11369, 0.13787));
856
+ p3 += dot(p3, p3.yxz + 19.19);
857
+ return -1.0 + 2.0 * fract(vec3(
858
+ p3.x + p3.y,
859
+ p3.x + p3.z,
860
+ p3.y + p3.z
861
+ ) * p3.zyx);
862
+ }
863
+
864
+ float snoise3(vec3 p) {
865
+ const float K1 = 0.333333333;
866
+ const float K2 = 0.166666667;
867
+ vec3 i = floor(p + (p.x + p.y + p.z) * K1);
868
+ vec3 d0 = p - (i - (i.x + i.y + i.z) * K2);
869
+ vec3 e = step(vec3(0.0), d0 - d0.yzx);
870
+ vec3 i1 = e * (1.0 - e.zxy);
871
+ vec3 i2 = 1.0 - e.zxy * (1.0 - e);
872
+ vec3 d1 = d0 - (i1 - K2);
873
+ vec3 d2 = d0 - (i2 - K1);
874
+ vec3 d3 = d0 - 0.5;
875
+ vec4 h = max(0.6 - vec4(
876
+ dot(d0, d0),
877
+ dot(d1, d1),
878
+ dot(d2, d2),
879
+ dot(d3, d3)
880
+ ), 0.0);
881
+ vec4 n = h * h * h * h * vec4(
882
+ dot(d0, hash33(i)),
883
+ dot(d1, hash33(i + i1)),
884
+ dot(d2, hash33(i + i2)),
885
+ dot(d3, hash33(i + 1.0))
886
+ );
887
+ return dot(vec4(31.316), n);
888
+ }
889
+
890
+ vec4 extractAlpha(vec3 colorIn) {
891
+ float a = max(max(colorIn.r, colorIn.g), colorIn.b);
892
+ return vec4(colorIn.rgb / (a + 1e-5), a);
893
+ }
894
+
895
+ const vec3 baseColor1 = vec3(0.611765, 0.262745, 0.996078);
896
+ const vec3 baseColor2 = vec3(0.298039, 0.760784, 0.913725);
897
+ const vec3 baseColor3 = vec3(0.062745, 0.078431, 0.600000);
898
+ const float innerRadius = 0.6;
899
+ const float noiseScale = 0.65;
900
+
901
+ float light1(float intensity, float attenuation, float dist) {
902
+ return intensity / (1.0 + dist * attenuation);
903
+ }
904
+ float light2(float intensity, float attenuation, float dist) {
905
+ return intensity / (1.0 + dist * dist * attenuation);
906
+ }
907
+
908
+ vec4 draw(vec2 uv) {
909
+ vec3 color1 = adjustHue(baseColor1, hue);
910
+ vec3 color2 = adjustHue(baseColor2, hue);
911
+ vec3 color3 = adjustHue(baseColor3, hue);
912
+
913
+ float ang = atan(uv.y, uv.x);
914
+ float len = length(uv);
915
+ float invLen = len > 0.0 ? 1.0 / len : 0.0;
916
+
917
+ float n0 = snoise3(vec3(uv * noiseScale, iTime * 0.5)) * 0.5 + 0.5;
918
+ float r0 = mix(mix(innerRadius, 1.0, 0.4), mix(innerRadius, 1.0, 0.6), n0);
919
+ float d0 = distance(uv, (r0 * invLen) * uv);
920
+ float v0 = light1(1.0, 10.0, d0);
921
+ v0 *= smoothstep(r0 * 1.05, r0, len);
922
+ float cl = cos(ang + iTime * 2.0) * 0.5 + 0.5;
923
+
924
+ float a = iTime * -1.0;
925
+ vec2 pos = vec2(cos(a), sin(a)) * r0;
926
+ float d = distance(uv, pos);
927
+ float v1 = light2(1.5, 5.0, d);
928
+ v1 *= light1(1.0, 50.0, d0);
929
+
930
+ float v2 = smoothstep(1.0, mix(innerRadius, 1.0, n0 * 0.5), len);
931
+ float v3 = smoothstep(innerRadius, mix(innerRadius, 1.0, 0.5), len);
932
+
933
+ vec3 col = mix(color1, color2, cl);
934
+ col = mix(color3, col, v0);
935
+ col = (col + v1) * v2 * v3;
936
+ col = clamp(col, 0.0, 1.0);
937
+
938
+ return extractAlpha(col);
939
+ }
940
+
941
+ vec4 mainImage(vec2 fragCoord) {
942
+ vec2 center = iResolution.xy * 0.5;
943
+ float size = min(iResolution.x, iResolution.y);
944
+ vec2 uv = (fragCoord - center) / size * 2.0;
945
+
946
+ float angle = rot;
947
+ float s = sin(angle);
948
+ float c = cos(angle);
949
+ uv = vec2(c * uv.x - s * uv.y, s * uv.x + c * uv.y);
950
+
951
+ uv.x += hover * hoverIntensity * 0.1 * sin(uv.y * 10.0 + iTime);
952
+ uv.y += hover * hoverIntensity * 0.1 * sin(uv.x * 10.0 + iTime);
953
+
954
+ return draw(uv);
955
+ }
956
+
957
+ void main() {
958
+ vec2 fragCoord = vUv * iResolution.xy;
959
+ vec4 col = mainImage(fragCoord);
960
+ gl_FragColor = vec4(col.rgb * col.a, col.a);
961
+ }
962
+ \`;
963
+
964
+ // Hide fallback and create WebGL orb
965
+ if (fallback) fallback.style.display = 'none';
966
+
967
+ const renderer = new Renderer({ alpha: true, premultipliedAlpha: false });
968
+ const gl = renderer.gl;
969
+ gl.clearColor(0, 0, 0, 0);
970
+
971
+ // Style the canvas
972
+ gl.canvas.style.position = 'absolute';
973
+ gl.canvas.style.top = '0';
974
+ gl.canvas.style.left = '0';
975
+ gl.canvas.style.width = '100%';
976
+ gl.canvas.style.height = '100%';
977
+ gl.canvas.style.opacity = '0.6';
978
+
979
+ container.appendChild(gl.canvas);
980
+
981
+ const geometry = new Triangle(gl);
982
+ const program = new Program(gl, {
983
+ vertex: vert,
984
+ fragment: frag,
985
+ uniforms: {
986
+ iTime: { value: 0 },
987
+ iResolution: {
988
+ value: new Vec3(
989
+ gl.canvas.width,
990
+ gl.canvas.height,
991
+ gl.canvas.width / gl.canvas.height
992
+ ),
993
+ },
994
+ hue: { value: 0 },
995
+ hover: { value: 0 },
996
+ rot: { value: 0 },
997
+ hoverIntensity: { value: 0.2 },
998
+ },
999
+ });
1000
+
1001
+ const mesh = new Mesh(gl, { geometry, program });
1002
+
1003
+ function resize() {
1004
+ if (!container) return;
1005
+ const dpr = window.devicePixelRatio || 1;
1006
+ const width = container.clientWidth;
1007
+ const height = container.clientHeight;
1008
+ renderer.setSize(width * dpr, height * dpr);
1009
+ gl.canvas.style.width = width + "px";
1010
+ gl.canvas.style.height = height + "px";
1011
+ program.uniforms.iResolution.value.set(
1012
+ gl.canvas.width,
1013
+ gl.canvas.height,
1014
+ gl.canvas.width / gl.canvas.height
1015
+ );
1016
+ }
1017
+ window.addEventListener("resize", resize);
1018
+ resize();
1019
+
1020
+ let targetHover = 0;
1021
+ let lastTime = 0;
1022
+ let currentRot = 0;
1023
+ const rotationSpeed = 0.3;
1024
+
1025
+ const handleMouseMove = (e) => {
1026
+ const rect = container.getBoundingClientRect();
1027
+ const x = e.clientX - rect.left;
1028
+ const y = e.clientY - rect.top;
1029
+ const width = rect.width;
1030
+ const height = rect.height;
1031
+ const size = Math.min(width, height);
1032
+ const centerX = width / 2;
1033
+ const centerY = height / 2;
1034
+ const uvX = ((x - centerX) / size) * 2.0;
1035
+ const uvY = ((y - centerY) / size) * 2.0;
1036
+
1037
+ if (Math.sqrt(uvX * uvX + uvY * uvY) < 0.8) {
1038
+ targetHover = 1;
1039
+ } else {
1040
+ targetHover = 0;
1041
+ }
1042
+ };
1043
+
1044
+ const handleMouseLeave = () => {
1045
+ targetHover = 0;
1046
+ };
1047
+
1048
+ container.addEventListener("mousemove", handleMouseMove);
1049
+ container.addEventListener("mouseleave", handleMouseLeave);
1050
+
1051
+ const update = (t) => {
1052
+ requestAnimationFrame(update);
1053
+ const dt = (t - lastTime) * 0.001;
1054
+ lastTime = t;
1055
+ program.uniforms.iTime.value = t * 0.001;
1056
+ program.uniforms.hue.value = 0;
1057
+ program.uniforms.hoverIntensity.value = 0.2;
1058
+
1059
+ const effectiveHover = targetHover;
1060
+ program.uniforms.hover.value += (effectiveHover - program.uniforms.hover.value) * 0.1;
1061
+
1062
+ if (effectiveHover > 0.5) {
1063
+ currentRot += dt * rotationSpeed;
1064
+ }
1065
+ program.uniforms.rot.value = currentRot;
1066
+
1067
+ renderer.render({ scene: mesh });
1068
+ };
1069
+ requestAnimationFrame(update);
1070
+
1071
+ console.log('WebGL Orb initialized successfully!');
1072
+
1073
+ } catch (error) {
1074
+ console.error('WebGL Orb failed, using CSS fallback:', error);
1075
+ if (fallback) fallback.style.display = 'block';
1076
+ }
1077
+ }
1078
+
1079
+ // Modern animated background - no JavaScript needed!
1080
+ </script>
1081
+ </body>
1082
+ </html>`;
1083
+ downloadFile(htmlContent, `${baseFileName}.html`, 'text/html');
1084
+ break;
1085
+ case 'json':
1086
+ const jsonData = {
1087
+ metadata: {
1088
+ fileName: fileName || 'extracted-text',
1089
+ extractedAt: new Date().toISOString(),
1090
+ characterCount: text.length,
1091
+ lineCount: text.split('\n').length,
1092
+ wordCount: text.split(/\s+/).filter(word => word.length > 0).length
1093
+ },
1094
+ content: {
1095
+ rawText: text,
1096
+ lines: text.split('\n'),
1097
+ paragraphs: text.split('\n\n').filter(p => p.trim().length > 0)
1098
+ }
1099
+ };
1100
+ downloadFile(JSON.stringify(jsonData, null, 2), `${baseFileName}.json`, 'application/json');
1101
+ break;
1102
+ case 'pdf':
1103
+ const pdf = new jsPDF();
1104
+
1105
+ // Set up styling
1106
+ pdf.setFont('helvetica', 'bold');
1107
+ pdf.setFontSize(20);
1108
+ pdf.setTextColor(79, 172, 254); // Luna OCR blue
1109
+
1110
+ // Add title
1111
+ pdf.text(baseFileName, 20, 30);
1112
+
1113
+ // Add subtitle
1114
+ pdf.setFont('helvetica', 'normal');
1115
+ pdf.setFontSize(10);
1116
+ pdf.setTextColor(128, 128, 128);
1117
+ pdf.text(`Generated by Luna OCR • ${new Date().toLocaleDateString()}`, 20, 40);
1118
+
1119
+ // Add separator line
1120
+ pdf.setDrawColor(79, 172, 254);
1121
+ pdf.setLineWidth(0.5);
1122
+ pdf.line(20, 45, 190, 45);
1123
+
1124
+ // Add content
1125
+ pdf.setFont('helvetica', 'normal');
1126
+ pdf.setFontSize(11);
1127
+ pdf.setTextColor(0, 0, 0);
1128
+
1129
+ const lines = pdf.splitTextToSize(text, 170);
1130
+ pdf.text(lines, 20, 55);
1131
+
1132
+ // Add footer
1133
+ const pageCount = pdf.internal.getNumberOfPages();
1134
+ for (let i = 1; i <= pageCount; i++) {
1135
+ pdf.setPage(i);
1136
+ pdf.setFont('helvetica', 'normal');
1137
+ pdf.setFontSize(8);
1138
+ pdf.setTextColor(128, 128, 128);
1139
+ pdf.text(`Luna OCR • Page ${i} of ${pageCount}`, 20, 285);
1140
+ }
1141
+
1142
+ pdf.save(`${baseFileName}.pdf`);
1143
+ break;
1144
+ default:
1145
+ break;
1146
+ }
1147
+ };
1148
+
1149
+ const downloadFile = (content, filename, mimeType) => {
1150
+ const blob = new Blob([content], { type: mimeType });
1151
+ const url = URL.createObjectURL(blob);
1152
+ const link = document.createElement('a');
1153
+ link.href = url;
1154
+ link.download = filename;
1155
+ document.body.appendChild(link);
1156
+ link.click();
1157
+ document.body.removeChild(link);
1158
+ URL.revokeObjectURL(url);
1159
+ };
1160
+
1161
+ const renderContent = () => {
1162
+ switch (previewMode) {
1163
+ case 'text':
1164
+ // Always show raw text in text mode
1165
+ return (
1166
+ <div className="text-content">
1167
+ <pre className="text-preview">{text}</pre>
1168
+ </div>
1169
+ );
1170
+ case 'markdown':
1171
+ marked.setOptions({
1172
+ breaks: true,
1173
+ gfm: true,
1174
+ tables: true,
1175
+ headerIds: false,
1176
+ mangle: false
1177
+ });
1178
+ return (
1179
+ <div
1180
+ className="markdown-plain"
1181
+ dangerouslySetInnerHTML={{ __html: marked(text) }}
1182
+ />
1183
+ );
1184
+ case 'preview':
1185
+ marked.setOptions({
1186
+ breaks: true,
1187
+ gfm: true,
1188
+ tables: true,
1189
+ headerIds: false,
1190
+ mangle: false
1191
+ });
1192
+ return (
1193
+ <div
1194
+ className="html-preview"
1195
+ dangerouslySetInnerHTML={{ __html: marked(text) }}
1196
+ />
1197
+ );
1198
+ case 'json':
1199
+ const jsonData = {
1200
+ metadata: {
1201
+ fileName: fileName || 'extracted-text',
1202
+ extractedAt: new Date().toISOString(),
1203
+ characterCount: text.length,
1204
+ lineCount: text.split('\n').length,
1205
+ wordCount: text.split(/\s+/).filter(word => word.length > 0).length
1206
+ },
1207
+ content: {
1208
+ rawText: text,
1209
+ lines: text.split('\n'),
1210
+ paragraphs: text.split('\n\n').filter(p => p.trim().length > 0)
1211
+ }
1212
+ };
1213
+ return (
1214
+ <SyntaxHighlighter
1215
+ language="json"
1216
+ style={atomDark}
1217
+ customStyle={{
1218
+ background: 'rgba(0, 0, 0, 0.3)',
1219
+ border: '1px solid rgba(255, 255, 255, 0.1)',
1220
+ borderRadius: '12px',
1221
+ fontSize: '0.875rem',
1222
+ lineHeight: '1.6'
1223
+ }}
1224
+ >
1225
+ {JSON.stringify(jsonData, null, 2)}
1226
+ </SyntaxHighlighter>
1227
+ );
1228
+ default:
1229
+ return null;
1230
+ }
1231
+ };
1232
+
1233
+ return (
1234
+ <div className="results-container">
1235
+ {/* Header Section */}
1236
+ <div className="results-header">
1237
+ <div className="header-content">
1238
+ <div className="header-left">
1239
+ <h3>Download & Preview</h3>
1240
+ <span className="result-info">
1241
+ {text.length} characters • {text.split('\n').length} lines • Ready to download
1242
+ </span>
1243
+ </div>
1244
+
1245
+ <div className="header-actions">
1246
+ <motion.button
1247
+ className="action-button"
1248
+ onClick={handleCopy}
1249
+ whileHover={{ scale: 1.05 }}
1250
+ whileTap={{ scale: 0.95 }}
1251
+ >
1252
+ <AnimatePresence mode="wait">
1253
+ {copied ? (
1254
+ <motion.div key="copied" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}>
1255
+ <Check size={16} />
1256
+ </motion.div>
1257
+ ) : (
1258
+ <motion.div key="copy" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}>
1259
+ <Copy size={16} />
1260
+ </motion.div>
1261
+ )}
1262
+ </AnimatePresence>
1263
+ {copied ? 'Copied!' : 'Copy'}
1264
+ </motion.button>
1265
+ </div>
1266
+ </div>
1267
+ </div>
1268
+
1269
+ {/* Unified Preview & Download Section */}
1270
+ <div className="categories-section">
1271
+ <div className="category-group">
1272
+ <div className="category-header">
1273
+ <Eye size={20} />
1274
+ <span className="category-label">PREVIEW & DOWNLOAD</span>
1275
+ </div>
1276
+
1277
+ <div className="format-buttons">
1278
+ {[
1279
+ { id: 'text', label: 'Text', icon: <FileText size={24} />, previewId: 'text' },
1280
+ { id: 'md', label: 'Markdown', icon: <Code size={24} />, previewId: 'markdown' },
1281
+ { id: 'html', label: 'HTML', icon: <Eye size={24} />, previewId: 'preview' },
1282
+ { id: 'json', label: 'JSON', icon: <Code size={24} />, previewId: 'json' }
1283
+ ].map((format) => (
1284
+ <motion.div
1285
+ key={format.id}
1286
+ className={`format-button-container ${previewMode === format.previewId ? 'active' : ''}`}
1287
+ whileHover={{ scale: 1.02 }}
1288
+ whileTap={{ scale: 0.98 }}
1289
+ >
1290
+ <button
1291
+ className="format-preview-button"
1292
+ onClick={() => format.previewId && onPreviewModeChange(format.previewId)}
1293
+ disabled={!format.previewId}
1294
+ >
1295
+ <div className="format-icon">{format.icon}</div>
1296
+ <span className="format-label">{format.label}</span>
1297
+ </button>
1298
+ </motion.div>
1299
+ ))}
1300
+ </div>
1301
+ </div>
1302
+ </div>
1303
+
1304
+ {/* Content Preview */}
1305
+ <div className="content-preview">
1306
+ <motion.button
1307
+ className="content-preview-download"
1308
+ onClick={() => {
1309
+ const formatMap = {
1310
+ 'text': 'txt',
1311
+ 'markdown': 'md',
1312
+ 'preview': 'html',
1313
+ 'json': 'json'
1314
+ };
1315
+ handleDownload(formatMap[previewMode] || 'txt');
1316
+ }}
1317
+ whileHover={{ scale: 1.05 }}
1318
+ whileTap={{ scale: 0.95 }}
1319
+ title={`Download current preview as ${previewMode === 'text' ? 'TXT' : previewMode === 'markdown' ? 'MD' : previewMode === 'preview' ? 'HTML' : 'JSON'}`}
1320
+ >
1321
+ <Download size={16} />
1322
+ </motion.button>
1323
+
1324
+ <AnimatePresence mode="wait">
1325
+ <motion.div
1326
+ key={previewMode}
1327
+ initial={{ opacity: 0, y: 20 }}
1328
+ animate={{ opacity: 1, y: 0 }}
1329
+ exit={{ opacity: 0, y: -20 }}
1330
+ transition={{ duration: 0.3 }}
1331
+ >
1332
+ {renderContent()}
1333
+ </motion.div>
1334
+ </AnimatePresence>
1335
+ </div>
1336
+ </div>
1337
+ );
1338
+ };
1339
+
1340
+ export default ResultsPanel;
src/components/TextViewer.js ADDED
@@ -0,0 +1,809 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { FileText, Code, Eye, ArrowLeft, Download, Copy, Check, Maximize2, ZoomIn, ZoomOut, Search } from 'lucide-react';
4
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
5
+ import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
6
+ import { marked } from 'marked';
7
+
8
+ const TextViewer = () => {
9
+ const [text, setText] = useState('');
10
+ const [fileName, setFileName] = useState('');
11
+ const [viewMode, setViewMode] = useState('text');
12
+ const [copied, setCopied] = useState(false);
13
+ const [fontSize, setFontSize] = useState(16);
14
+ const [searchTerm, setSearchTerm] = useState('');
15
+ const [isSearching, setIsSearching] = useState(false);
16
+
17
+ useEffect(() => {
18
+ // Get text from URL params or localStorage
19
+ const urlParams = new URLSearchParams(window.location.search);
20
+ const textParam = urlParams.get('text');
21
+ const fileParam = urlParams.get('file');
22
+
23
+ if (textParam) {
24
+ setText(decodeURIComponent(textParam));
25
+ } else {
26
+ const savedText = localStorage.getItem('extractedText');
27
+ if (savedText) setText(savedText);
28
+ }
29
+
30
+ if (fileParam) {
31
+ setFileName(decodeURIComponent(fileParam));
32
+ } else {
33
+ setFileName(localStorage.getItem('fileName') || 'extracted-text');
34
+ }
35
+ }, []);
36
+
37
+ const viewModes = [
38
+ { id: 'text', label: 'Text', icon: <FileText size={18} />, ext: '.txt' },
39
+ { id: 'markdown', label: 'Markdown', icon: <Code size={18} />, ext: '.md' },
40
+ { id: 'html', label: 'Rendered', icon: <Eye size={18} />, ext: '.html' },
41
+ { id: 'json', label: 'JSON', icon: <Code size={18} />, ext: '.json' }
42
+ ];
43
+
44
+ const handleCopy = async () => {
45
+ try {
46
+ await navigator.clipboard.writeText(text);
47
+ setCopied(true);
48
+ setTimeout(() => setCopied(false), 2000);
49
+ } catch (err) {
50
+ console.error('Failed to copy text:', err);
51
+ }
52
+ };
53
+
54
+ const handleBack = () => {
55
+ window.history.back();
56
+ };
57
+
58
+ const adjustFontSize = (delta) => {
59
+ setFontSize(prev => Math.max(12, Math.min(24, prev + delta)));
60
+ }; cons
61
+ t renderContent = () => {
62
+ const baseStyle = { fontSize: `${fontSize}px`, lineHeight: 1.7 };
63
+
64
+ switch (viewMode) {
65
+ case 'text':
66
+ return (
67
+ <div className="reader-content" style={baseStyle}>
68
+ <pre className="text-reader">{text}</pre>
69
+ </div>
70
+ );
71
+ case 'markdown':
72
+ return (
73
+ <div className="reader-content">
74
+ <SyntaxHighlighter
75
+ language="markdown"
76
+ style={atomDark}
77
+ customStyle={{
78
+ ...baseStyle,
79
+ background: 'transparent',
80
+ border: 'none',
81
+ padding: 0,
82
+ margin: 0
83
+ }}
84
+ >
85
+ {text}
86
+ </SyntaxHighlighter>
87
+ </div>
88
+ );
89
+ case 'html':
90
+ return (
91
+ <div
92
+ className="reader-content html-reader"
93
+ style={baseStyle}
94
+ dangerouslySetInnerHTML={{ __html: marked(text) }}
95
+ />
96
+ );
97
+ case 'json':
98
+ const jsonData = {
99
+ metadata: {
100
+ fileName: fileName || 'extracted-text',
101
+ extractedAt: new Date().toISOString(),
102
+ characterCount: text.length,
103
+ lineCount: text.split('\n').length,
104
+ wordCount: text.split(/\s+/).filter(word => word.length > 0).length
105
+ },
106
+ content: {
107
+ rawText: text,
108
+ lines: text.split('\n'),
109
+ paragraphs: text.split('\n\n').filter(p => p.trim().length > 0)
110
+ }
111
+ };
112
+ return (
113
+ <div className="reader-content">
114
+ <SyntaxHighlighter
115
+ language="json"
116
+ style={atomDark}
117
+ customStyle={{
118
+ ...baseStyle,
119
+ background: 'transparent',
120
+ border: 'none',
121
+ padding: 0,
122
+ margin: 0
123
+ }}
124
+ >
125
+ {JSON.stringify(jsonData, null, 2)}
126
+ </SyntaxHighlighter>
127
+ </div>
128
+ );
129
+ default:
130
+ return null;
131
+ }
132
+ }; return
133
+ (
134
+ <div className="text-viewer-app">
135
+ {/* Same background as main app */}
136
+ <div className="viewer-background">
137
+ <div className="giant-orb-background">
138
+ <div className="giant-orb-container">
139
+ <div className="gradient-orb orb-1"></div>
140
+ <div className="gradient-orb orb-2"></div>
141
+ <div className="gradient-orb orb-3"></div>
142
+ </div>
143
+ </div>
144
+ </div>
145
+
146
+ <div className="viewer-glass-container">
147
+ {/* Professional Header */}
148
+ <motion.header
149
+ className="viewer-header"
150
+ initial={{ opacity: 0, y: -20 }}
151
+ animate={{ opacity: 1, y: 0 }}
152
+ transition={{ duration: 0.6 }}
153
+ >
154
+ <div className="header-left">
155
+ <motion.button
156
+ className="back-button"
157
+ onClick={handleBack}
158
+ whileHover={{ scale: 1.05 }}
159
+ whileTap={{ scale: 0.95 }}
160
+ >
161
+ <ArrowLeft size={18} />
162
+ Back
163
+ </motion.button>
164
+
165
+ <div className="document-info">
166
+ <h1 className="document-title">{fileName}</h1>
167
+ <span className="document-stats">
168
+ {text.length} chars • {text.split('\n').length} lines • {text.split(/\s+/).filter(w => w.length > 0).length} words
169
+ </span>
170
+ </div>
171
+ </div>
172
+
173
+ <div className="header-actions">
174
+ <div className="search-container">
175
+ <Search size={16} />
176
+ <input
177
+ type="text"
178
+ placeholder="Search in document..."
179
+ value={searchTerm}
180
+ onChange={(e) => setSearchTerm(e.target.value)}
181
+ className="search-input"
182
+ />
183
+ </div>
184
+
185
+ <div className="font-controls">
186
+ <button onClick={() => adjustFontSize(-2)} className="font-btn">
187
+ <ZoomOut size={16} />
188
+ </button>
189
+ <span className="font-size">{fontSize}px</span>
190
+ <button onClick={() => adjustFontSize(2)} className="font-btn">
191
+ <ZoomIn size={16} />
192
+ </button>
193
+ </div>
194
+
195
+ <motion.button
196
+ className="action-btn"
197
+ onClick={handleCopy}
198
+ whileHover={{ scale: 1.05 }}
199
+ whileTap={{ scale: 0.95 }}
200
+ >
201
+ <AnimatePresence mode="wait">
202
+ {copied ? (
203
+ <motion.div key="copied" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}>
204
+ <Check size={16} />
205
+ </motion.div>
206
+ ) : (
207
+ <motion.div key="copy" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}>
208
+ <Copy size={16} />
209
+ </motion.div>
210
+ )}
211
+ </AnimatePresence>
212
+ {copied ? 'Copied!' : 'Copy'}
213
+ </motion.button>
214
+ </div>
215
+ </motion.header>
216
+ {/* Format Selector */}
217
+ <motion.div
218
+ className="format-selector"
219
+ initial={{ opacity: 0, y: 20 }}
220
+ animate={{ opacity: 1, y: 0 }}
221
+ transition={{ duration: 0.6, delay: 0.1 }}
222
+ >
223
+ <div className="format-tabs">
224
+ {viewModes.map((mode, index) => (
225
+ <motion.button
226
+ key={mode.id}
227
+ className={`format-tab ${viewMode === mode.id ? 'active' : ''}`}
228
+ onClick={() => setViewMode(mode.id)}
229
+ whileHover={{ scale: 1.02 }}
230
+ whileTap={{ scale: 0.98 }}
231
+ initial={{ opacity: 0, x: -20 }}
232
+ animate={{ opacity: 1, x: 0 }}
233
+ transition={{ delay: index * 0.1 }}
234
+ >
235
+ {mode.icon}
236
+ <span className="tab-label">{mode.label}</span>
237
+ <span className="tab-ext">{mode.ext}</span>
238
+
239
+ {viewMode === mode.id && (
240
+ <motion.div
241
+ className="tab-indicator"
242
+ layoutId="viewModeIndicator"
243
+ transition={{ type: "spring", stiffness: 500, damping: 30 }}
244
+ />
245
+ )}
246
+ </motion.button>
247
+ ))}
248
+ </div>
249
+ </motion.div>
250
+
251
+ {/* Professional Reader */}
252
+ <motion.main
253
+ className="reader-main"
254
+ initial={{ opacity: 0, y: 20 }}
255
+ animate={{ opacity: 1, y: 0 }}
256
+ transition={{ duration: 0.6, delay: 0.2 }}
257
+ >
258
+ <div className="reader-panel">
259
+ <AnimatePresence mode="wait">
260
+ <motion.div
261
+ key={viewMode}
262
+ initial={{ opacity: 0, x: 20 }}
263
+ animate={{ opacity: 1, x: 0 }}
264
+ exit={{ opacity: 0, x: -20 }}
265
+ transition={{ duration: 0.3 }}
266
+ className="content-wrapper"
267
+ >
268
+ {renderContent()}
269
+ </motion.div>
270
+ </AnimatePresence>
271
+ </div>
272
+ </motion.main>
273
+ </div>
274
+ <style jsx>{`
275
+ .text-viewer-app {
276
+ min-height: 100vh;
277
+ background: #0f0f23;
278
+ color: #ffffff;
279
+ font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif;
280
+ position: relative;
281
+ overflow-x: hidden;
282
+ }
283
+
284
+ .viewer-background {
285
+ position: fixed;
286
+ top: 0;
287
+ left: 0;
288
+ width: 100%;
289
+ height: 100%;
290
+ pointer-events: none;
291
+ z-index: -1;
292
+ }
293
+
294
+ .giant-orb-background {
295
+ position: fixed;
296
+ top: 0;
297
+ left: 0;
298
+ width: 100%;
299
+ height: 100%;
300
+ pointer-events: none;
301
+ z-index: -10;
302
+ display: flex;
303
+ align-items: center;
304
+ justify-content: center;
305
+ }
306
+
307
+ .giant-orb-container {
308
+ width: 100vmin;
309
+ height: 100vmin;
310
+ position: relative;
311
+ pointer-events: none;
312
+ transform: translateY(-5vh);
313
+ }
314
+
315
+ .gradient-orb {
316
+ position: absolute;
317
+ border-radius: 50%;
318
+ filter: blur(100px);
319
+ opacity: 0.3;
320
+ }
321
+
322
+ .orb-1 {
323
+ top: 10%;
324
+ right: 20%;
325
+ width: 300px;
326
+ height: 300px;
327
+ background: linear-gradient(45deg, #4facfe, #8b5cf6);
328
+ animation: float1 20s ease-in-out infinite;
329
+ }
330
+
331
+ .orb-2 {
332
+ bottom: 20%;
333
+ left: 15%;
334
+ width: 250px;
335
+ height: 250px;
336
+ background: linear-gradient(45deg, #f093fb, #f5576c);
337
+ animation: float2 25s ease-in-out infinite;
338
+ }
339
+
340
+ .orb-3 {
341
+ top: 50%;
342
+ left: 50%;
343
+ width: 200px;
344
+ height: 200px;
345
+ background: linear-gradient(45deg, #06b6d4, #10b981);
346
+ animation: float3 30s ease-in-out infinite;
347
+ }
348
+
349
+ @keyframes float1 {
350
+ 0%, 100% { transform: translate(0, 0) rotate(0deg); }
351
+ 25% { transform: translate(-20px, -30px) rotate(90deg); }
352
+ 50% { transform: translate(10px, -20px) rotate(180deg); }
353
+ 75% { transform: translate(-10px, 10px) rotate(270deg); }
354
+ }
355
+
356
+ @keyframes float2 {
357
+ 0%, 100% { transform: translate(0, 0) rotate(0deg); }
358
+ 33% { transform: translate(30px, -20px) rotate(120deg); }
359
+ 66% { transform: translate(-20px, -40px) rotate(240deg); }
360
+ }
361
+
362
+ @keyframes float3 {
363
+ 0%, 100% { transform: translate(-50%, -50%) rotate(0deg); }
364
+ 20% { transform: translate(-65%, -35%) rotate(72deg); }
365
+ 40% { transform: translate(-35%, -35%) rotate(144deg); }
366
+ 60% { transform: translate(-35%, -65%) rotate(216deg); }
367
+ 80% { transform: translate(-65%, -65%) rotate(288deg); }
368
+ }
369
+ .viewer-glass-container {
370
+ backdrop-filter: blur(25px);
371
+ -webkit-backdrop-filter: blur(25px);
372
+ background: rgba(0, 0, 0, 0.1);
373
+ border: none;
374
+ border-radius: 16px;
375
+ margin: 12px;
376
+ min-height: calc(100vh - 24px);
377
+ overflow: hidden;
378
+ position: relative;
379
+ z-index: 10;
380
+ display: flex;
381
+ flex-direction: column;
382
+ }
383
+
384
+ .viewer-header {
385
+ display: flex;
386
+ justify-content: space-between;
387
+ align-items: center;
388
+ padding: 20px 24px;
389
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
390
+ flex-shrink: 0;
391
+ }
392
+
393
+ .header-left {
394
+ display: flex;
395
+ align-items: center;
396
+ gap: 20px;
397
+ }
398
+
399
+ .back-button {
400
+ background: rgba(255, 255, 255, 0.05);
401
+ border: 1px solid rgba(255, 255, 255, 0.1);
402
+ border-radius: 8px;
403
+ padding: 8px 16px;
404
+ color: #ffffff;
405
+ cursor: pointer;
406
+ display: flex;
407
+ align-items: center;
408
+ gap: 6px;
409
+ font-size: 0.85rem;
410
+ transition: all 0.3s ease;
411
+ backdrop-filter: blur(10px);
412
+ }
413
+
414
+ .back-button:hover {
415
+ background: rgba(255, 255, 255, 0.08);
416
+ border-color: rgba(79, 172, 254, 0.3);
417
+ }
418
+
419
+ .document-info {
420
+ display: flex;
421
+ flex-direction: column;
422
+ gap: 4px;
423
+ }
424
+
425
+ .document-title {
426
+ font-size: 1.25rem;
427
+ font-weight: 600;
428
+ color: #ffffff;
429
+ margin: 0;
430
+ }
431
+
432
+ .document-stats {
433
+ font-size: 0.75rem;
434
+ color: rgba(255, 255, 255, 0.6);
435
+ }
436
+
437
+ .header-actions {
438
+ display: flex;
439
+ align-items: center;
440
+ gap: 16px;
441
+ }
442
+
443
+ .search-container {
444
+ display: flex;
445
+ align-items: center;
446
+ gap: 8px;
447
+ background: rgba(255, 255, 255, 0.05);
448
+ border: 1px solid rgba(255, 255, 255, 0.1);
449
+ border-radius: 8px;
450
+ padding: 6px 12px;
451
+ backdrop-filter: blur(10px);
452
+ }
453
+
454
+ .search-input {
455
+ background: none;
456
+ border: none;
457
+ color: #ffffff;
458
+ font-size: 0.85rem;
459
+ outline: none;
460
+ width: 200px;
461
+ }
462
+
463
+ .search-input::placeholder {
464
+ color: rgba(255, 255, 255, 0.5);
465
+ }
466
+
467
+ .font-controls {
468
+ display: flex;
469
+ align-items: center;
470
+ gap: 8px;
471
+ background: rgba(255, 255, 255, 0.05);
472
+ border: 1px solid rgba(255, 255, 255, 0.1);
473
+ border-radius: 8px;
474
+ padding: 4px 8px;
475
+ backdrop-filter: blur(10px);
476
+ }
477
+
478
+ .font-btn {
479
+ background: none;
480
+ border: none;
481
+ color: rgba(255, 255, 255, 0.7);
482
+ cursor: pointer;
483
+ padding: 4px;
484
+ border-radius: 4px;
485
+ transition: all 0.3s ease;
486
+ }
487
+
488
+ .font-btn:hover {
489
+ color: #ffffff;
490
+ background: rgba(255, 255, 255, 0.1);
491
+ }
492
+
493
+ .font-size {
494
+ font-size: 0.75rem;
495
+ color: rgba(255, 255, 255, 0.8);
496
+ min-width: 32px;
497
+ text-align: center;
498
+ }
499
+
500
+ .action-btn {
501
+ background: rgba(255, 255, 255, 0.05);
502
+ border: 1px solid rgba(255, 255, 255, 0.1);
503
+ border-radius: 8px;
504
+ padding: 8px 16px;
505
+ color: #ffffff;
506
+ cursor: pointer;
507
+ display: flex;
508
+ align-items: center;
509
+ gap: 6px;
510
+ font-size: 0.85rem;
511
+ transition: all 0.3s ease;
512
+ backdrop-filter: blur(10px);
513
+ }
514
+
515
+ .action-btn:hover {
516
+ background: rgba(255, 255, 255, 0.08);
517
+ border-color: rgba(79, 172, 254, 0.3);
518
+ }
519
+ .format-selector {
520
+ display: flex;
521
+ justify-content: center;
522
+ padding: 16px 24px;
523
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
524
+ flex-shrink: 0;
525
+ }
526
+
527
+ .format-tabs {
528
+ display: flex;
529
+ background: rgba(0, 0, 0, 0.3);
530
+ border: 1px solid rgba(255, 255, 255, 0.1);
531
+ border-radius: 12px;
532
+ padding: 4px;
533
+ backdrop-filter: blur(20px);
534
+ }
535
+
536
+ .format-tab {
537
+ background: none;
538
+ border: none;
539
+ padding: 12px 20px;
540
+ border-radius: 8px;
541
+ cursor: pointer;
542
+ display: flex;
543
+ align-items: center;
544
+ gap: 8px;
545
+ font-size: 0.85rem;
546
+ color: rgba(255, 255, 255, 0.7);
547
+ transition: all 0.3s ease;
548
+ position: relative;
549
+ min-width: 120px;
550
+ justify-content: center;
551
+ }
552
+
553
+ .format-tab:hover {
554
+ color: #ffffff;
555
+ background: rgba(255, 255, 255, 0.05);
556
+ }
557
+
558
+ .format-tab.active {
559
+ color: #ffffff;
560
+ background: rgba(255, 255, 255, 0.08);
561
+ }
562
+
563
+ .tab-label {
564
+ font-weight: 500;
565
+ }
566
+
567
+ .tab-ext {
568
+ font-size: 0.75rem;
569
+ color: rgba(255, 255, 255, 0.5);
570
+ font-weight: 400;
571
+ }
572
+
573
+ .tab-indicator {
574
+ position: absolute;
575
+ bottom: 0;
576
+ left: 0;
577
+ right: 0;
578
+ height: 2px;
579
+ background: linear-gradient(90deg, #4facfe, #8b5cf6);
580
+ border-radius: 1px;
581
+ }
582
+
583
+ .reader-main {
584
+ flex: 1;
585
+ overflow: hidden;
586
+ display: flex;
587
+ flex-direction: column;
588
+ }
589
+
590
+ .reader-panel {
591
+ flex: 1;
592
+ overflow: auto;
593
+ padding: 0;
594
+ background: rgba(255, 255, 255, 0.02);
595
+ margin: 16px 24px 24px;
596
+ border-radius: 12px;
597
+ backdrop-filter: blur(10px);
598
+ }
599
+
600
+ .content-wrapper {
601
+ height: 100%;
602
+ overflow: auto;
603
+ }
604
+
605
+ .reader-content {
606
+ padding: 32px;
607
+ max-width: 900px;
608
+ margin: 0 auto;
609
+ line-height: 1.8;
610
+ }
611
+
612
+ .text-reader {
613
+ background: none;
614
+ border: none;
615
+ color: #ffffff;
616
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
617
+ white-space: pre-wrap;
618
+ word-wrap: break-word;
619
+ width: 100%;
620
+ margin: 0;
621
+ padding: 0;
622
+ line-height: inherit;
623
+ } .ht
624
+ ml-reader {
625
+ color: #ffffff;
626
+ font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif;
627
+ }
628
+
629
+ .html-reader h1,
630
+ .html-reader h2,
631
+ .html-reader h3,
632
+ .html-reader h4,
633
+ .html-reader h5,
634
+ .html-reader h6 {
635
+ color: #ffffff;
636
+ margin-bottom: 24px;
637
+ margin-top: 40px;
638
+ font-weight: 600;
639
+ line-height: 1.3;
640
+ }
641
+
642
+ .html-reader h1 {
643
+ font-size: 2.5rem;
644
+ background: linear-gradient(45deg, #4facfe, #8b5cf6);
645
+ -webkit-background-clip: text;
646
+ -webkit-text-fill-color: transparent;
647
+ background-clip: text;
648
+ border-bottom: 2px solid rgba(79, 172, 254, 0.3);
649
+ padding-bottom: 16px;
650
+ }
651
+
652
+ .html-reader h2 {
653
+ font-size: 2rem;
654
+ color: #4facfe;
655
+ border-left: 4px solid #4facfe;
656
+ padding-left: 16px;
657
+ }
658
+
659
+ .html-reader h3 {
660
+ font-size: 1.5rem;
661
+ color: #8b5cf6;
662
+ }
663
+
664
+ .html-reader p {
665
+ margin-bottom: 20px;
666
+ color: rgba(255, 255, 255, 0.9);
667
+ text-align: justify;
668
+ }
669
+
670
+ .html-reader strong,
671
+ .html-reader b {
672
+ color: #ffffff;
673
+ font-weight: 700;
674
+ text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
675
+ }
676
+
677
+ .html-reader em,
678
+ .html-reader i {
679
+ color: rgba(255, 255, 255, 0.95);
680
+ font-style: italic;
681
+ }
682
+
683
+ .html-reader ul,
684
+ .html-reader ol {
685
+ margin-bottom: 20px;
686
+ padding-left: 32px;
687
+ }
688
+
689
+ .html-reader li {
690
+ margin-bottom: 12px;
691
+ color: rgba(255, 255, 255, 0.9);
692
+ position: relative;
693
+ }
694
+
695
+ .html-reader ul li::marker {
696
+ color: #4facfe;
697
+ }
698
+
699
+ .html-reader ol li::marker {
700
+ color: #8b5cf6;
701
+ font-weight: 600;
702
+ }
703
+
704
+ .html-reader table {
705
+ width: 100%;
706
+ border-collapse: collapse;
707
+ margin: 32px 0;
708
+ background: rgba(0, 0, 0, 0.2);
709
+ border-radius: 12px;
710
+ overflow: hidden;
711
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
712
+ }
713
+
714
+ .html-reader th,
715
+ .html-reader td {
716
+ border: 1px solid rgba(255, 255, 255, 0.1);
717
+ padding: 16px 20px;
718
+ text-align: left;
719
+ vertical-align: top;
720
+ }
721
+
722
+ .html-reader th {
723
+ background: linear-gradient(135deg, rgba(79, 172, 254, 0.2), rgba(139, 92, 246, 0.2));
724
+ font-weight: 700;
725
+ color: #ffffff;
726
+ text-transform: uppercase;
727
+ font-size: 0.85rem;
728
+ letter-spacing: 0.5px;
729
+ }
730
+
731
+ .html-reader td {
732
+ background: rgba(255, 255, 255, 0.02);
733
+ }
734
+
735
+ .html-reader tr:nth-child(even) td {
736
+ background: rgba(255, 255, 255, 0.04);
737
+ }
738
+
739
+ .html-reader tr:hover td {
740
+ background: rgba(79, 172, 254, 0.05);
741
+ }
742
+
743
+ .html-reader blockquote {
744
+ border-left: 4px solid #4facfe;
745
+ padding: 20px 24px;
746
+ margin: 24px 0;
747
+ background: rgba(79, 172, 254, 0.05);
748
+ border-radius: 0 8px 8px 0;
749
+ font-style: italic;
750
+ color: rgba(255, 255, 255, 0.9);
751
+ position: relative;
752
+ }
753
+
754
+ .html-reader blockquote::before {
755
+ content: '"';
756
+ font-size: 4rem;
757
+ color: rgba(79, 172, 254, 0.3);
758
+ position: absolute;
759
+ top: -10px;
760
+ left: 10px;
761
+ font-family: serif;
762
+ }
763
+
764
+ .html-reader code {
765
+ background: rgba(0, 0, 0, 0.4);
766
+ padding: 4px 8px;
767
+ border-radius: 6px;
768
+ font-family: 'SF Mono', Monaco, monospace;
769
+ font-size: 0.9em;
770
+ color: #4facfe;
771
+ border: 1px solid rgba(79, 172, 254, 0.2);
772
+ }
773
+
774
+ .html-reader pre {
775
+ background: rgba(0, 0, 0, 0.4);
776
+ padding: 24px;
777
+ border-radius: 12px;
778
+ overflow-x: auto;
779
+ margin: 24px 0;
780
+ border: 1px solid rgba(255, 255, 255, 0.1);
781
+ position: relative;
782
+ }
783
+
784
+ .html-reader pre code {
785
+ background: none;
786
+ padding: 0;
787
+ border: none;
788
+ color: rgba(255, 255, 255, 0.9);
789
+ }
790
+
791
+ .html-reader a {
792
+ color: #4facfe;
793
+ text-decoration: none;
794
+ border-bottom: 1px solid rgba(79, 172, 254, 0.3);
795
+ transition: all 0.3s ease;
796
+ }
797
+
798
+ .html-reader a:hover {
799
+ color: #8b5cf6;
800
+ border-bottom-color: rgba(139, 92, 246, 0.5);
801
+ text-shadow: 0 0 8px rgba(139, 92, 246, 0.3);
802
+ }
803
+
804
+ .html-reader hr {
805
+ border: none;
806
+ height: 2px;
807
+ background: linear-gradient(90deg, transparent, rgba(79, 172, 254, 0.5), transparent);
808
+ margin: 40px 0;
809
+ }
src/components/TrueFocus.js ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { motion } from "framer-motion";
3
+
4
+ const TrueFocus = ({
5
+ sentence = "Luna OCR",
6
+ manualMode = false,
7
+ blurAmount = 0,
8
+ borderColor = "#8b5cf6",
9
+ glowColor = "rgba(139, 92, 246, 0.6)",
10
+ animationDuration = 0.8,
11
+ pauseBetweenAnimations = 1.5,
12
+ }) => {
13
+ const words = sentence.split(" ");
14
+ const [currentIndex, setCurrentIndex] = useState(0);
15
+ const [lastActiveIndex, setLastActiveIndex] = useState(null);
16
+ const containerRef = useRef(null);
17
+ const wordRefs = useRef([]);
18
+ const [focusRect, setFocusRect] = useState({ x: 0, y: 0, width: 0, height: 0 });
19
+
20
+ useEffect(() => {
21
+ if (!manualMode) {
22
+ const interval = setInterval(() => {
23
+ setCurrentIndex((prev) => (prev + 1) % words.length);
24
+ }, (animationDuration + pauseBetweenAnimations) * 1000);
25
+
26
+ return () => clearInterval(interval);
27
+ }
28
+ }, [manualMode, animationDuration, pauseBetweenAnimations, words.length]);
29
+
30
+ useEffect(() => {
31
+ if (currentIndex === null || currentIndex === -1) return;
32
+
33
+ if (!wordRefs.current[currentIndex] || !containerRef.current) return;
34
+
35
+ const parentRect = containerRef.current.getBoundingClientRect();
36
+ const activeRect = wordRefs.current[currentIndex].getBoundingClientRect();
37
+
38
+ setFocusRect({
39
+ x: activeRect.left - parentRect.left,
40
+ y: activeRect.top - parentRect.top,
41
+ width: activeRect.width,
42
+ height: activeRect.height,
43
+ });
44
+ }, [currentIndex, words.length]);
45
+
46
+ const handleMouseEnter = (index) => {
47
+ if (manualMode) {
48
+ setLastActiveIndex(index);
49
+ setCurrentIndex(index);
50
+ }
51
+ };
52
+
53
+ const handleMouseLeave = () => {
54
+ if (manualMode) {
55
+ setCurrentIndex(lastActiveIndex);
56
+ }
57
+ };
58
+
59
+ return (
60
+ <div className="focus-container" ref={containerRef}>
61
+ {words.map((word, index) => {
62
+ const isActive = index === currentIndex;
63
+ return (
64
+ <span
65
+ key={index}
66
+ ref={(el) => (wordRefs.current[index] = el)}
67
+ className={`focus-word ${manualMode ? "manual" : ""} ${isActive && !manualMode ? "active" : ""}`}
68
+ style={{
69
+ filter: manualMode
70
+ ? isActive
71
+ ? `blur(0px)`
72
+ : `blur(${blurAmount}px)`
73
+ : isActive
74
+ ? `blur(0px)`
75
+ : `blur(${blurAmount}px)`,
76
+ "--border-color": borderColor,
77
+ "--glow-color": glowColor,
78
+ transition: `filter ${animationDuration}s ease`,
79
+ }}
80
+ onMouseEnter={() => handleMouseEnter(index)}
81
+ onMouseLeave={handleMouseLeave}
82
+ >
83
+ {word}
84
+ </span>
85
+ );
86
+ })}
87
+
88
+ <motion.div
89
+ className="focus-frame"
90
+ animate={{
91
+ x: focusRect.x,
92
+ y: focusRect.y,
93
+ width: focusRect.width,
94
+ height: focusRect.height,
95
+ opacity: currentIndex >= 0 ? 1 : 0,
96
+ }}
97
+ transition={{
98
+ duration: animationDuration,
99
+ }}
100
+ style={{
101
+ "--border-color": borderColor,
102
+ "--glow-color": glowColor,
103
+ }}
104
+ >
105
+ <span className="corner top-left"></span>
106
+ <span className="corner top-right"></span>
107
+ <span className="corner bottom-left"></span>
108
+ <span className="corner bottom-right"></span>
109
+ </motion.div>
110
+ </div>
111
+ );
112
+ };
113
+
114
+ export default TrueFocus;
src/index.css ADDED
@@ -0,0 +1,2241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
2
+
3
+ * {
4
+ margin: 0;
5
+ padding: 0;
6
+ box-sizing: border-box;
7
+ }
8
+
9
+ :root {
10
+ --bg-primary: #000000;
11
+ --bg-secondary: #111111;
12
+ --glass-bg: rgba(255, 255, 255, 0.08);
13
+ --glass-border: transparent;
14
+ --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
15
+ --text-primary: #ffffff;
16
+ --text-secondary: rgba(255, 255, 255, 0.8);
17
+ --text-muted: rgba(255, 255, 255, 0.6);
18
+ --accent-primary: #4facfe;
19
+ --accent-secondary: #8b5cf6;
20
+ --accent-tertiary: #06b6d4;
21
+ --gradient-primary: linear-gradient(135deg, #4facfe 0%, #8b5cf6 100%);
22
+ --gradient-secondary: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
23
+ --gradient-tertiary: linear-gradient(135deg, #06b6d4 0%, #4facfe 100%);
24
+ --gradient-iridescent: linear-gradient(45deg, #4facfe, #8b5cf6, #06b6d4, #10b981, #f59e0b, #ef4444);
25
+ }
26
+
27
+ body {
28
+ font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif;
29
+ background: var(--bg-primary);
30
+ color: var(--text-primary);
31
+ overflow-x: hidden;
32
+ overflow-y: auto;
33
+ min-height: 100vh;
34
+ }
35
+
36
+ /* Universal fix - force same colors in all browsers */
37
+ body {
38
+ background: #0f0f23 !important;
39
+ }
40
+
41
+ .app::before {
42
+ background: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 50%, #16213e 100%) !important;
43
+ }
44
+
45
+ .glass-container {
46
+ background: rgba(0, 0, 0, 0.3) !important;
47
+ backdrop-filter: none !important;
48
+ -webkit-backdrop-filter: none !important;
49
+ }
50
+
51
+ .glass-panel {
52
+ background: rgba(100, 100, 100, 0.1) !important;
53
+ backdrop-filter: blur(25px) !important;
54
+ -webkit-backdrop-filter: blur(25px) !important;
55
+ }
56
+
57
+ .app {
58
+ min-height: 100vh;
59
+ display: flex;
60
+ flex-direction: column;
61
+ position: relative;
62
+ overflow-y: auto;
63
+ }
64
+
65
+ .app::before {
66
+ content: '';
67
+ position: fixed;
68
+ top: 0;
69
+ left: 0;
70
+ right: 0;
71
+ bottom: 0;
72
+ background: var(--gradient-iridescent);
73
+ opacity: 0.03;
74
+ pointer-events: none;
75
+ z-index: -1;
76
+ }
77
+
78
+ .glass-container {
79
+ backdrop-filter: blur(25px);
80
+ -webkit-backdrop-filter: blur(25px);
81
+ background: rgba(0, 0, 0, 0.1);
82
+ border: none;
83
+ border-radius: 16px;
84
+ box-shadow: none;
85
+ margin: 12px;
86
+ min-height: auto;
87
+ overflow: visible;
88
+ position: relative;
89
+ z-index: 10;
90
+ flex: 1;
91
+ }
92
+
93
+ .glass-container::before {
94
+ content: '';
95
+ position: absolute;
96
+ top: -1px;
97
+ left: -1px;
98
+ right: -1px;
99
+ bottom: -1px;
100
+ background: linear-gradient(45deg,
101
+ rgba(138, 43, 226, 0.2) 0%,
102
+ rgba(59, 130, 246, 0.15) 50%,
103
+ rgba(138, 43, 226, 0.2) 100%);
104
+ border-radius: 24px;
105
+ z-index: -1;
106
+ }
107
+
108
+ .app-header {
109
+ padding: 20px 24px;
110
+ display: flex;
111
+ justify-content: space-between;
112
+ align-items: center;
113
+ border-bottom: 1px solid var(--glass-border);
114
+ }
115
+
116
+ .logo {
117
+ display: flex;
118
+ align-items: center;
119
+ gap: 16px;
120
+ }
121
+
122
+ .logo-icon {
123
+ font-size: 2.5rem;
124
+ filter: drop-shadow(0 0 20px rgba(255, 193, 7, 0.5));
125
+ }
126
+
127
+ .logo h1 {
128
+ font-size: 2rem;
129
+ font-weight: 700;
130
+ filter: drop-shadow(0 0 10px rgba(255, 107, 53, 0.3));
131
+ }
132
+
133
+ .logo-subtitle {
134
+ font-size: 0.875rem;
135
+ color: var(--text-muted);
136
+ font-weight: 400;
137
+ }
138
+
139
+ @keyframes gradient-shift {
140
+
141
+ 0%,
142
+ 100% {
143
+ background-position: 0% 50%;
144
+ }
145
+
146
+ 50% {
147
+ background-position: 100% 50%;
148
+ }
149
+ }
150
+
151
+ /* Loading Screen Styles */
152
+ .loading-container {
153
+ position: fixed !important;
154
+ top: 0 !important;
155
+ left: 0 !important;
156
+ width: 100vw !important;
157
+ height: 100vh !important;
158
+ background: #141414 !important;
159
+ z-index: 99999 !important;
160
+ display: flex !important;
161
+ align-items: center;
162
+ justify-content: center;
163
+ opacity: 1 !important;
164
+ }
165
+
166
+ @keyframes loading-fade-in {
167
+ 0% {
168
+ opacity: 0;
169
+ }
170
+ 100% {
171
+ opacity: 1;
172
+ }
173
+ }
174
+
175
+ .a-hole {
176
+ position: absolute;
177
+ top: 0;
178
+ left: 0;
179
+ margin: 0;
180
+ padding: 0;
181
+ width: 100%;
182
+ height: 100%;
183
+ overflow: hidden;
184
+ opacity: 0;
185
+ animation: hole-appear 0.2s ease-out 0.5s forwards;
186
+ }
187
+
188
+ @keyframes hole-appear {
189
+ 0% {
190
+ opacity: 0;
191
+ }
192
+ 100% {
193
+ opacity: 1;
194
+ }
195
+ }
196
+
197
+ .a-hole::before {
198
+ position: absolute;
199
+ top: 50%;
200
+ left: 50%;
201
+ z-index: 2;
202
+ display: block;
203
+ width: 150%;
204
+ height: 140%;
205
+ background: radial-gradient(ellipse at 50% 55%, transparent 10%, black 50%);
206
+ transform: translate3d(-50%, -50%, 0);
207
+ content: "";
208
+ }
209
+
210
+ .a-hole::after {
211
+ position: absolute;
212
+ top: 50%;
213
+ left: 50%;
214
+ z-index: 5;
215
+ display: block;
216
+ width: 100%;
217
+ height: 100%;
218
+ background: radial-gradient(ellipse at 50% 75%, #a900ff 20%, transparent 75%);
219
+ mix-blend-mode: overlay;
220
+ transform: translate3d(-50%, -50%, 0);
221
+ content: "";
222
+ }
223
+
224
+
225
+
226
+ @keyframes aura-glow {
227
+ 0% {
228
+ background-position: 0 100%;
229
+ }
230
+ 100% {
231
+ background-position: 0 300%;
232
+ }
233
+ }
234
+
235
+ .aura {
236
+ position: absolute;
237
+ top: -71.5%;
238
+ left: 50%;
239
+ z-index: 3;
240
+ width: 30%;
241
+ height: 140%;
242
+ background: linear-gradient(
243
+ 20deg,
244
+ #00f8f1,
245
+ #ffbd1e20 16.5%,
246
+ #fe848f 33%,
247
+ #fe848f20 49.5%,
248
+ #00f8f1 66%,
249
+ #00f8f160 85.5%,
250
+ #ffbd1e 100%
251
+ )
252
+ 0 100% / 100% 200%;
253
+ border-radius: 0 0 100% 100%;
254
+ filter: blur(50px);
255
+ mix-blend-mode: plus-lighter;
256
+ opacity: 0;
257
+ transform: translate3d(-50%, 0, 0);
258
+ animation: aura-appear 0.2s ease-out 0.5s forwards, aura-glow 5s infinite linear 0.7s;
259
+ }
260
+
261
+ @keyframes aura-appear {
262
+ 0% {
263
+ opacity: 0;
264
+ }
265
+ 100% {
266
+ opacity: 0.75;
267
+ }
268
+ }
269
+
270
+ .overlay {
271
+ position: absolute;
272
+ top: 0;
273
+ left: 0;
274
+ z-index: 10;
275
+ width: 100%;
276
+ height: 100%;
277
+ background: transparent;
278
+ opacity: 0;
279
+ }
280
+
281
+ .a-hole canvas {
282
+ display: block;
283
+ width: 100%;
284
+ height: 100%;
285
+ }
286
+
287
+ /* TrueFocus Animation Styles */
288
+ .focus-container {
289
+ position: relative;
290
+ display: flex;
291
+ gap: 0.5em;
292
+ justify-content: center;
293
+ align-items: center;
294
+ flex-wrap: wrap;
295
+ }
296
+
297
+ .focus-word {
298
+ position: relative;
299
+ font-size: 1.75rem;
300
+ font-weight: 300;
301
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', 'SF Pro Display', system-ui, sans-serif;
302
+ cursor: pointer;
303
+ color: #ffffff;
304
+ letter-spacing: 0.05em;
305
+ text-shadow: 0 0 20px rgba(255, 255, 255, 0.3);
306
+ transition: filter 0.3s ease, color 0.3s ease;
307
+ }
308
+
309
+ .focus-word.active {
310
+ filter: blur(0);
311
+ }
312
+
313
+ .focus-frame {
314
+ position: absolute;
315
+ top: 0;
316
+ left: 0;
317
+ pointer-events: none;
318
+ box-sizing: content-box;
319
+ border: none;
320
+ }
321
+
322
+ .corner {
323
+ position: absolute;
324
+ width: 0.75rem;
325
+ height: 0.75rem;
326
+ border: 2.5px solid var(--border-color, #ffffff);
327
+ filter: drop-shadow(0px 0px 8px var(--glow-color, rgba(255, 255, 255, 0.8)));
328
+ border-radius: 2px;
329
+ transition: none;
330
+ }
331
+
332
+ .top-left {
333
+ top: -8px;
334
+ left: -8px;
335
+ border-right: none;
336
+ border-bottom: none;
337
+ }
338
+
339
+ .top-right {
340
+ top: -8px;
341
+ right: -8px;
342
+ border-left: none;
343
+ border-bottom: none;
344
+ }
345
+
346
+ .bottom-left {
347
+ bottom: -8px;
348
+ left: -8px;
349
+ border-right: none;
350
+ border-top: none;
351
+ }
352
+
353
+ .bottom-right {
354
+ bottom: -8px;
355
+ right: -8px;
356
+ border-left: none;
357
+ border-top: none;
358
+ }
359
+
360
+
361
+
362
+
363
+
364
+ .api-key-input {
365
+ position: relative;
366
+ }
367
+
368
+ /* API Key Container */
369
+ .api-key-container {
370
+ position: relative;
371
+ width: 300px;
372
+ }
373
+
374
+ .glass-input {
375
+ background: rgba(255, 255, 255, 0.05);
376
+ border: 1px solid var(--glass-border);
377
+ border-radius: 12px;
378
+ padding: 12px 16px;
379
+ padding-right: 45px; /* Make room for clear button */
380
+ color: var(--text-primary);
381
+ font-size: 0.875rem;
382
+ width: 100%;
383
+ backdrop-filter: blur(10px);
384
+ transition: all 0.3s ease;
385
+ }
386
+
387
+ .glass-input:focus {
388
+ outline: none;
389
+ border-color: var(--accent-primary);
390
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
391
+ background: rgba(255, 255, 255, 0.08);
392
+ }
393
+
394
+ .glass-input::placeholder {
395
+ color: var(--text-muted);
396
+ }
397
+
398
+ /* Clear API Key Button */
399
+ .clear-api-key-btn {
400
+ position: absolute;
401
+ right: 8px;
402
+ top: 50%;
403
+ transform: translateY(-50%);
404
+ background: rgba(255, 255, 255, 0.1);
405
+ border: 1px solid rgba(255, 255, 255, 0.2);
406
+ border-radius: 50%;
407
+ width: 24px;
408
+ height: 24px;
409
+ color: rgba(255, 255, 255, 0.7);
410
+ font-size: 16px;
411
+ font-weight: bold;
412
+ cursor: pointer;
413
+ display: flex;
414
+ align-items: center;
415
+ justify-content: center;
416
+ transition: all 0.3s ease;
417
+ backdrop-filter: blur(10px);
418
+ }
419
+
420
+ .clear-api-key-btn:hover {
421
+ background: rgba(255, 255, 255, 0.2);
422
+ color: #ff6b6b;
423
+ border-color: rgba(255, 107, 107, 0.3);
424
+ transform: translateY(-50%) scale(1.1);
425
+ }
426
+
427
+ /* Server API Key Notice */
428
+ .server-api-notice {
429
+ background: rgba(34, 197, 94, 0.1);
430
+ border: 1px solid rgba(34, 197, 94, 0.3);
431
+ border-radius: 12px;
432
+ padding: 12px 16px;
433
+ margin-bottom: 20px;
434
+ text-align: center;
435
+ }
436
+
437
+ .server-api-notice p {
438
+ color: rgba(34, 197, 94, 0.9);
439
+ margin: 0;
440
+ font-size: 0.875rem;
441
+ font-weight: 500;
442
+ }
443
+
444
+ /* Processing Progress Overlay */
445
+ .processing-progress-overlay {
446
+ position: fixed;
447
+ top: 0;
448
+ left: 0;
449
+ right: 0;
450
+ bottom: 0;
451
+ background: rgba(0, 0, 0, 0.8);
452
+ backdrop-filter: blur(10px);
453
+ display: flex;
454
+ align-items: center;
455
+ justify-content: center;
456
+ z-index: 9999;
457
+ }
458
+
459
+ .processing-progress-container {
460
+ background: rgba(255, 255, 255, 0.1);
461
+ border: 1px solid rgba(255, 255, 255, 0.2);
462
+ border-radius: 20px;
463
+ padding: 40px;
464
+ min-width: 400px;
465
+ max-width: 500px;
466
+ backdrop-filter: blur(20px);
467
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
468
+ }
469
+
470
+ .progress-content {
471
+ text-align: center;
472
+ }
473
+
474
+ .progress-header h3 {
475
+ color: var(--text-primary);
476
+ font-size: 1.5rem;
477
+ margin-bottom: 10px;
478
+ font-weight: 600;
479
+ }
480
+
481
+ .progress-file {
482
+ color: var(--text-secondary);
483
+ font-size: 0.9rem;
484
+ margin-bottom: 30px;
485
+ padding: 8px 16px;
486
+ background: rgba(255, 255, 255, 0.05);
487
+ border-radius: 8px;
488
+ border: 1px solid rgba(255, 255, 255, 0.1);
489
+ }
490
+
491
+ .progress-bar-container {
492
+ display: flex;
493
+ align-items: center;
494
+ gap: 15px;
495
+ margin-bottom: 20px;
496
+ }
497
+
498
+ .progress-bar {
499
+ flex: 1;
500
+ height: 8px;
501
+ background: rgba(255, 255, 255, 0.1);
502
+ border-radius: 4px;
503
+ overflow: hidden;
504
+ border: 1px solid rgba(255, 255, 255, 0.2);
505
+ }
506
+
507
+ .progress-fill {
508
+ height: 100%;
509
+ background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
510
+ border-radius: 4px;
511
+ box-shadow: 0 0 10px rgba(79, 172, 254, 0.5);
512
+ }
513
+
514
+ .progress-percentage {
515
+ color: var(--text-primary);
516
+ font-weight: 600;
517
+ font-size: 1.1rem;
518
+ min-width: 45px;
519
+ }
520
+
521
+ .progress-status {
522
+ min-height: 24px;
523
+ margin-bottom: 20px;
524
+ }
525
+
526
+ .status-text {
527
+ color: var(--text-secondary);
528
+ font-size: 0.9rem;
529
+ padding: 6px 12px;
530
+ background: rgba(255, 255, 255, 0.05);
531
+ border-radius: 6px;
532
+ display: inline-block;
533
+ }
534
+
535
+ .progress-details {
536
+ margin-bottom: 25px;
537
+ }
538
+
539
+ .progress-counter {
540
+ display: flex;
541
+ align-items: center;
542
+ justify-content: center;
543
+ gap: 5px;
544
+ margin-bottom: 15px;
545
+ font-size: 1.2rem;
546
+ }
547
+
548
+ .counter-current {
549
+ color: var(--accent-primary);
550
+ font-weight: 700;
551
+ font-size: 1.4rem;
552
+ }
553
+
554
+ .counter-separator {
555
+ color: var(--text-muted);
556
+ font-weight: 300;
557
+ }
558
+
559
+ .counter-total {
560
+ color: var(--text-secondary);
561
+ font-weight: 600;
562
+ }
563
+
564
+ .counter-label {
565
+ color: var(--text-muted);
566
+ font-size: 0.9rem;
567
+ margin-left: 5px;
568
+ }
569
+
570
+ .progress-stats {
571
+ display: flex;
572
+ flex-direction: column;
573
+ gap: 8px;
574
+ padding: 12px;
575
+ background: rgba(255, 255, 255, 0.05);
576
+ border-radius: 8px;
577
+ border: 1px solid rgba(255, 255, 255, 0.1);
578
+ }
579
+
580
+ .stat-item {
581
+ display: flex;
582
+ justify-content: space-between;
583
+ align-items: center;
584
+ font-size: 0.85rem;
585
+ }
586
+
587
+ .stat-label {
588
+ color: var(--text-secondary);
589
+ }
590
+
591
+ .stat-value {
592
+ color: var(--accent-primary);
593
+ font-weight: 600;
594
+ font-family: 'Courier New', monospace;
595
+ }
596
+
597
+ /* Console Logs */
598
+ .console-logs {
599
+ margin: 20px 0;
600
+ background: rgba(0, 0, 0, 0.3);
601
+ border: 1px solid rgba(255, 255, 255, 0.1);
602
+ border-radius: 8px;
603
+ overflow: hidden;
604
+ }
605
+
606
+ .console-header {
607
+ background: rgba(255, 255, 255, 0.1);
608
+ padding: 8px 12px;
609
+ font-size: 0.8rem;
610
+ font-weight: 600;
611
+ color: var(--text-secondary);
612
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
613
+ }
614
+
615
+ .console-content {
616
+ max-height: 120px;
617
+ overflow-y: auto;
618
+ padding: 8px;
619
+ }
620
+
621
+ .console-line {
622
+ display: flex;
623
+ gap: 8px;
624
+ margin-bottom: 4px;
625
+ font-size: 0.75rem;
626
+ font-family: 'Courier New', monospace;
627
+ }
628
+
629
+ .console-time {
630
+ color: var(--text-muted);
631
+ min-width: 60px;
632
+ flex-shrink: 0;
633
+ }
634
+
635
+ .console-message {
636
+ color: var(--text-primary);
637
+ flex: 1;
638
+ word-break: break-word;
639
+ }
640
+
641
+ .processing-animation {
642
+ display: flex;
643
+ justify-content: center;
644
+ }
645
+
646
+ .processing-dots {
647
+ display: flex;
648
+ gap: 8px;
649
+ }
650
+
651
+ .processing-dots .dot {
652
+ width: 8px;
653
+ height: 8px;
654
+ background: var(--accent-primary);
655
+ border-radius: 50%;
656
+ animation: processingPulse 1.5s ease-in-out infinite;
657
+ }
658
+
659
+ .processing-dots .dot:nth-child(2) {
660
+ animation-delay: 0.2s;
661
+ }
662
+
663
+ .processing-dots .dot:nth-child(3) {
664
+ animation-delay: 0.4s;
665
+ }
666
+
667
+ @keyframes processingPulse {
668
+ 0%, 80%, 100% {
669
+ transform: scale(0.8);
670
+ opacity: 0.5;
671
+ }
672
+ 40% {
673
+ transform: scale(1.2);
674
+ opacity: 1;
675
+ }
676
+ }
677
+
678
+
679
+
680
+ .menu-container {
681
+ display: flex;
682
+ background: rgba(0, 0, 0, 0.4) !important;
683
+ border: 1px solid rgba(255, 255, 255, 0.1) !important;
684
+ border-radius: 16px;
685
+ padding: 8px;
686
+ backdrop-filter: blur(20px) !important;
687
+ -webkit-backdrop-filter: blur(20px) !important;
688
+ position: relative;
689
+ overflow: hidden;
690
+ }
691
+
692
+ .menu-container::before {
693
+ content: '';
694
+ position: absolute;
695
+ top: 0;
696
+ left: 0;
697
+ right: 0;
698
+ bottom: 0;
699
+ background: var(--gradient-iridescent);
700
+ opacity: 0.05;
701
+ border-radius: 16px;
702
+ }
703
+
704
+ .menu-item {
705
+ position: relative;
706
+ padding: 12px 24px;
707
+ border-radius: 12px;
708
+ cursor: pointer;
709
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
710
+ display: flex;
711
+ align-items: center;
712
+ gap: 8px;
713
+ font-weight: 500;
714
+ font-size: 0.875rem;
715
+ color: var(--text-secondary);
716
+ z-index: 2;
717
+ }
718
+
719
+ .menu-item:hover {
720
+ color: var(--text-primary);
721
+ transform: translateY(-1px);
722
+ }
723
+
724
+ .menu-item.active {
725
+ color: var(--text-primary);
726
+ background: rgba(255, 255, 255, 0.1);
727
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
728
+ }
729
+
730
+ .menu-item-icon {
731
+ font-size: 1rem;
732
+ }
733
+
734
+ .app-main {
735
+ flex: 1;
736
+ padding: 0 24px 24px;
737
+ position: relative;
738
+ overflow-y: auto;
739
+ min-height: 0;
740
+ }
741
+
742
+ .tab-content {
743
+ min-height: auto;
744
+ display: flex;
745
+ flex-direction: column;
746
+ padding-bottom: 40px;
747
+ }
748
+
749
+ .glass-panel {
750
+ background: rgba(100, 100, 100, 0.1);
751
+ border-radius: 12px;
752
+ padding: 20px;
753
+ backdrop-filter: blur(25px);
754
+ -webkit-backdrop-filter: blur(25px);
755
+ position: relative;
756
+ overflow: hidden;
757
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
758
+ }
759
+
760
+
761
+ .glass-panel::before {
762
+ content: '';
763
+ position: absolute;
764
+ top: 0;
765
+ left: 0;
766
+ right: 0;
767
+ height: 1px;
768
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
769
+ }
770
+
771
+ .nested-panel {
772
+ background: transparent;
773
+ border-radius: 8px;
774
+ padding: 16px;
775
+ margin: 12px 0;
776
+ backdrop-filter: blur(15px);
777
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
778
+ }
779
+
780
+ .flowing-button {
781
+ background: var(--gradient-primary);
782
+ border: none;
783
+ border-radius: 12px;
784
+ padding: 12px 24px;
785
+ color: white;
786
+ font-weight: 600;
787
+ cursor: pointer;
788
+ transition: all 0.3s ease;
789
+ position: relative;
790
+ overflow: hidden;
791
+ }
792
+
793
+ .flowing-button::before {
794
+ content: '';
795
+ position: absolute;
796
+ top: 0;
797
+ left: -100%;
798
+ width: 100%;
799
+ height: 100%;
800
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
801
+ transition: left 0.5s ease;
802
+ }
803
+
804
+ .flowing-button:hover::before {
805
+ left: 100%;
806
+ }
807
+
808
+ .flowing-button:hover {
809
+ transform: translateY(-2px);
810
+ box-shadow: 0 8px 25px rgba(99, 102, 241, 0.3);
811
+ }
812
+
813
+ .flowing-button:disabled {
814
+ opacity: 0.5;
815
+ cursor: not-allowed;
816
+ transform: none;
817
+ }
818
+
819
+ @media (max-width: 768px) {
820
+ .glass-container {
821
+ margin: 10px;
822
+ min-height: calc(100vh - 20px);
823
+ border-radius: 16px;
824
+ }
825
+
826
+ .app-header {
827
+ padding: 24px 20px;
828
+ flex-direction: column;
829
+ gap: 16px;
830
+ }
831
+
832
+ .glass-input {
833
+ width: 100%;
834
+ }
835
+
836
+ .app-main {
837
+ padding: 0 20px 20px;
838
+ }
839
+
840
+ .flowing-menu {
841
+ padding: 16px 20px;
842
+ }
843
+
844
+ .menu-container {
845
+ width: 100%;
846
+ justify-content: space-around;
847
+ }
848
+
849
+ .menu-item {
850
+ flex: 1;
851
+ justify-content: center;
852
+ padding: 12px 8px;
853
+ }
854
+ }
855
+
856
+ /* Iridescence Container */
857
+ .iridescence-container {
858
+ width: 100%;
859
+ height: 100%;
860
+ position: absolute;
861
+ top: 0;
862
+ left: 0;
863
+ pointer-events: none;
864
+ }
865
+
866
+ .iridescence-container canvas {
867
+ width: 100% !important;
868
+ height: 100% !important;
869
+ }
870
+
871
+
872
+ .flowing-menu-container {
873
+ width: 100%;
874
+ height: 200px;
875
+ overflow: hidden;
876
+ padding: 24px 40px;
877
+ }
878
+
879
+ .flowing-menu-nav {
880
+ display: flex;
881
+ flex-direction: column;
882
+ height: 100%;
883
+ margin: 0;
884
+ padding: 0;
885
+ }
886
+
887
+ .flowing-menu-item {
888
+ flex: 1;
889
+ position: relative;
890
+ overflow: hidden;
891
+ text-align: center;
892
+ box-shadow: 0 -1px 0 0 #fff;
893
+ }
894
+
895
+ .flowing-menu-item.active {
896
+ background: rgba(255, 255, 255, 0.1);
897
+ }
898
+
899
+ .menu-item-link {
900
+ display: flex;
901
+ align-items: center;
902
+ justify-content: center;
903
+ height: 100%;
904
+ position: relative;
905
+ cursor: pointer;
906
+ text-transform: uppercase;
907
+ text-decoration: none;
908
+ font-weight: 600;
909
+ color: white;
910
+ font-size: 2rem;
911
+ transition: color 0.3s ease;
912
+ }
913
+
914
+ .menu-item-link:hover {
915
+ color: #ffffff;
916
+ }
917
+
918
+ .menu-item-link:focus {
919
+ color: white;
920
+ }
921
+
922
+ .menu-icon {
923
+ margin-right: 0.5rem;
924
+ font-size: 1.5rem;
925
+ }
926
+
927
+ .menu-text {
928
+ font-size: 1.2rem;
929
+ }
930
+
931
+
932
+
933
+ /* Lanyard Styles */
934
+ .lanyard-wrapper {
935
+ position: absolute;
936
+ top: 20px;
937
+ right: 20px;
938
+ width: 200px;
939
+ height: 300px;
940
+ z-index: 10;
941
+ pointer-events: auto;
942
+ }
943
+
944
+ /* Professional Logo Styles */
945
+ .logo-content h1 {
946
+ font-size: 1.75rem;
947
+ font-weight: 700;
948
+ color: white;
949
+ text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
950
+ margin-bottom: 4px;
951
+ }
952
+
953
+ .logo-subtitle {
954
+ font-size: 0.8rem;
955
+ color: rgba(255, 255, 255, 0.7);
956
+ font-weight: 400;
957
+ }
958
+
959
+ /* More Professional Upload Styles */
960
+ .upload-zone {
961
+ border: none;
962
+ border-radius: 16px;
963
+ transition: all 0.3s ease;
964
+ }
965
+
966
+ .upload-zone.drag-active {
967
+ background: rgba(79, 172, 254, 0.05);
968
+ }
969
+
970
+ .upload-svg {
971
+ color: rgba(255, 255, 255, 0.6);
972
+ filter: drop-shadow(0 0 10px rgba(79, 172, 254, 0.2));
973
+ }
974
+
975
+ .upload-methods {
976
+ margin-top: 32px;
977
+ }
978
+
979
+ .upload-method {
980
+ background: rgba(255, 255, 255, 0.02);
981
+ border: none;
982
+ transition: all 0.3s ease;
983
+ }
984
+
985
+ .upload-method:hover {
986
+ background: rgba(255, 255, 255, 0.04);
987
+ border-color: rgba(79, 172, 254, 0.3);
988
+ transform: translateY(-1px);
989
+ }
990
+
991
+ .method-icon {
992
+ color: rgba(79, 172, 254, 0.8);
993
+ }
994
+
995
+ /*
996
+ Simple Lanyard Styles */
997
+ .lanyard-wrapper {
998
+ position: fixed;
999
+ top: 60px;
1000
+ right: 40px;
1001
+ width: 120px;
1002
+ height: 200px;
1003
+ z-index: 10;
1004
+ pointer-events: none;
1005
+ }
1006
+
1007
+ .lanyard-card {
1008
+ position: relative;
1009
+ width: 100%;
1010
+ height: 100%;
1011
+ }
1012
+
1013
+ .lanyard-string {
1014
+ width: 2px;
1015
+ height: 80px;
1016
+ background: linear-gradient(to bottom, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.3));
1017
+ margin: 0 auto;
1018
+ border-radius: 1px;
1019
+ }
1020
+
1021
+ .card {
1022
+ width: 80px;
1023
+ height: 100px;
1024
+ background: rgba(0, 0, 0, 0.8);
1025
+ border: none;
1026
+ border-radius: 8px;
1027
+ margin: 0 auto;
1028
+ backdrop-filter: blur(10px);
1029
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
1030
+ }
1031
+
1032
+ .card-content {
1033
+ padding: 12px;
1034
+ text-align: center;
1035
+ height: 100%;
1036
+ display: flex;
1037
+ flex-direction: column;
1038
+ justify-content: center;
1039
+ align-items: center;
1040
+ gap: 8px;
1041
+ }
1042
+
1043
+ .card-logo {
1044
+ font-size: 1.2rem;
1045
+ font-weight: bold;
1046
+ color: #4facfe;
1047
+ text-shadow: 0 0 10px rgba(79, 172, 254, 0.5);
1048
+ }
1049
+
1050
+ .card-text {
1051
+ font-size: 0.6rem;
1052
+ color: rgba(255, 255, 255, 0.8);
1053
+ font-weight: 500;
1054
+ }
1055
+
1056
+ /* Interactive Pullable Lanyard Styles */
1057
+ .lanyard-wrapper {
1058
+ position: fixed;
1059
+ top: 40px;
1060
+ right: 40px;
1061
+ width: 120px;
1062
+ height: 250px;
1063
+ z-index: 10;
1064
+ pointer-events: auto;
1065
+ }
1066
+
1067
+ .lanyard-string-container {
1068
+ position: relative;
1069
+ width: 100%;
1070
+ height: 100%;
1071
+ display: flex;
1072
+ flex-direction: column;
1073
+ align-items: center;
1074
+ }
1075
+
1076
+ .lanyard-string {
1077
+ width: 2px;
1078
+ background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.4));
1079
+ border-radius: 1px;
1080
+ box-shadow: 0 0 4px rgba(255, 255, 255, 0.3);
1081
+ }
1082
+
1083
+ .lanyard-card {
1084
+ position: relative;
1085
+ margin-top: 4px;
1086
+ }
1087
+
1088
+ .card {
1089
+ width: 85px;
1090
+ height: 110px;
1091
+ background: rgba(0, 0, 0, 0.85);
1092
+ border: 1px solid rgba(79, 172, 254, 0.3);
1093
+ border-radius: 10px;
1094
+ backdrop-filter: blur(15px);
1095
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4), 0 0 20px rgba(79, 172, 254, 0.1);
1096
+ transition: all 0.3s ease;
1097
+ }
1098
+
1099
+ .card:hover {
1100
+ border-color: rgba(79, 172, 254, 0.5);
1101
+ box-shadow: 0 12px 35px rgba(0, 0, 0, 0.5), 0 0 30px rgba(79, 172, 254, 0.2);
1102
+ }
1103
+
1104
+ .card-content {
1105
+ padding: 16px 12px;
1106
+ text-align: center;
1107
+ height: 100%;
1108
+ display: flex;
1109
+ flex-direction: column;
1110
+ justify-content: center;
1111
+ align-items: center;
1112
+ gap: 10px;
1113
+ }
1114
+
1115
+ .card-logo {
1116
+ font-size: 1.4rem;
1117
+ font-weight: bold;
1118
+ color: #4facfe;
1119
+ text-shadow: 0 0 15px rgba(79, 172, 254, 0.6);
1120
+ letter-spacing: 1px;
1121
+ }
1122
+
1123
+ .card-text {
1124
+ font-size: 0.65rem;
1125
+ color: rgba(255, 255, 255, 0.9);
1126
+ font-weight: 500;
1127
+ letter-spacing: 0.5px;
1128
+ }
1129
+
1130
+ /* ReactBits Physics Lanyard Override */
1131
+ .lanyard-wrapper {
1132
+ position: fixed !important;
1133
+ top: 40px !important;
1134
+ right: 40px !important;
1135
+ width: 150px !important;
1136
+ height: 300px !important;
1137
+ z-index: 10 !important;
1138
+ pointer-events: auto !important;
1139
+ }
1140
+
1141
+
1142
+ .flowing-menu-container {
1143
+ width: 100% !important;
1144
+ height: 50px !important;
1145
+ overflow: hidden !important;
1146
+ padding: 8px 24px !important;
1147
+ display: flex !important;
1148
+ justify-content: center !important;
1149
+ }
1150
+
1151
+ .flowing-menu-nav {
1152
+ display: flex !important;
1153
+ flex-direction: row !important;
1154
+ height: 100% !important;
1155
+ background: rgba(100, 100, 100, 0.1) !important;
1156
+ border: none !important;
1157
+ border-radius: 16px !important;
1158
+ backdrop-filter: blur(20px) !important;
1159
+ overflow: hidden !important;
1160
+ position: relative !important;
1161
+ min-width: 400px !important;
1162
+ }
1163
+
1164
+ .flowing-menu-item {
1165
+ flex: 1 !important;
1166
+ position: relative !important;
1167
+ overflow: hidden !important;
1168
+ text-align: center !important;
1169
+ border-right: none !important;
1170
+ }
1171
+
1172
+ .flowing-menu-item:last-child {
1173
+ border-right: none !important;
1174
+ }
1175
+
1176
+ .flowing-menu-item.active {
1177
+ background: transparent !important;
1178
+ border: none !important;
1179
+ box-shadow: none !important;
1180
+ }
1181
+
1182
+ .menu-item-link {
1183
+ display: flex !important;
1184
+ align-items: center !important;
1185
+ justify-content: center !important;
1186
+ height: 100% !important;
1187
+ position: relative !important;
1188
+ cursor: pointer !important;
1189
+ text-decoration: none !important;
1190
+ font-weight: 500 !important;
1191
+ color: rgba(255, 255, 255, 0.7) !important;
1192
+ font-size: 0.75rem !important;
1193
+ transition: all 0.3s ease !important;
1194
+ gap: 4px !important;
1195
+ padding: 8px 12px !important;
1196
+ }
1197
+
1198
+ .menu-item-link:hover {
1199
+ color: #ffffff !important;
1200
+ background: linear-gradient(135deg,
1201
+ rgba(138, 43, 226, 0.1) 0%,
1202
+ rgba(59, 130, 246, 0.08) 50%,
1203
+ rgba(138, 43, 226, 0.05) 100%) !important;
1204
+ box-shadow: 0 0 15px rgba(138, 43, 226, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.05) !important;
1205
+ }
1206
+
1207
+ .menu-icon {
1208
+ font-size: 1.2rem !important;
1209
+ background: linear-gradient(45deg, #8b5cf6, #3b82f6);
1210
+ -webkit-background-clip: text !important;
1211
+ -webkit-text-fill-color: transparent !important;
1212
+ background-clip: text !important;
1213
+ opacity: 0.9 !important;
1214
+ }
1215
+
1216
+ .menu-text {
1217
+ font-size: 0.875rem !important;
1218
+ font-weight: 500 !important;
1219
+ }
1220
+
1221
+ /* Smooth Physics Lanyard Styles */
1222
+ .lanyard-string-container {
1223
+ position: relative;
1224
+ width: 100%;
1225
+ height: 100%;
1226
+ display: flex;
1227
+ flex-direction: column;
1228
+ align-items: center;
1229
+ }
1230
+
1231
+ .lanyard-string {
1232
+ width: 2px;
1233
+ background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.4));
1234
+ border-radius: 1px;
1235
+ box-shadow: 0 0 4px rgba(255, 255, 255, 0.3);
1236
+ }
1237
+
1238
+ .lanyard-card {
1239
+ position: relative;
1240
+ margin-top: 4px;
1241
+ }
1242
+
1243
+ .card {
1244
+ width: 90px;
1245
+ height: 120px;
1246
+ background: rgba(0, 0, 0, 0.9);
1247
+ border: 1px solid rgba(79, 172, 254, 0.4);
1248
+ border-radius: 12px;
1249
+ backdrop-filter: blur(20px);
1250
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4), 0 0 20px rgba(79, 172, 254, 0.15);
1251
+ transition: all 0.3s ease;
1252
+ }
1253
+
1254
+ .card:hover {
1255
+ border-color: rgba(79, 172, 254, 0.6);
1256
+ box-shadow: 0 12px 35px rgba(0, 0, 0, 0.5), 0 0 30px rgba(79, 172, 254, 0.25);
1257
+ }
1258
+
1259
+ .card-content {
1260
+ padding: 16px 12px;
1261
+ text-align: center;
1262
+ height: 100%;
1263
+ display: flex;
1264
+ flex-direction: column;
1265
+ justify-content: center;
1266
+ align-items: center;
1267
+ gap: 8px;
1268
+ }
1269
+
1270
+ .card-logo {
1271
+ font-size: 1.5rem;
1272
+ font-weight: bold;
1273
+ color: #4facfe;
1274
+ text-shadow: 0 0 15px rgba(79, 172, 254, 0.6);
1275
+ letter-spacing: 1px;
1276
+ }
1277
+
1278
+ .card-text {
1279
+ font-size: 0.7rem;
1280
+ color: rgba(255, 255, 255, 0.9);
1281
+ font-weight: 600;
1282
+ letter-spacing: 0.5px;
1283
+ }
1284
+
1285
+ .card-subtitle {
1286
+ font-size: 0.6rem;
1287
+ color: rgba(255, 255, 255, 0.6);
1288
+ font-weight: 400;
1289
+ letter-spacing: 0.5px;
1290
+ }
1291
+
1292
+ /* Simple Visible Lanyard Styles */
1293
+ .simple-lanyard-wrapper {
1294
+ position: fixed !important;
1295
+ top: 60px !important;
1296
+ right: 40px !important;
1297
+ width: 120px !important;
1298
+ height: 250px !important;
1299
+ z-index: 1000 !important;
1300
+ pointer-events: auto !important;
1301
+ }
1302
+
1303
+ .lanyard-container {
1304
+ position: relative;
1305
+ width: 100%;
1306
+ height: 100%;
1307
+ display: flex;
1308
+ flex-direction: column;
1309
+ align-items: center;
1310
+ }
1311
+
1312
+ .lanyard-string {
1313
+ width: 3px;
1314
+ background: linear-gradient(to bottom,
1315
+ rgba(255, 255, 255, 0.9) 0%,
1316
+ rgba(255, 255, 255, 0.7) 50%,
1317
+ rgba(255, 255, 255, 0.5) 100%);
1318
+ border-radius: 2px;
1319
+ box-shadow: 0 0 8px rgba(255, 255, 255, 0.3);
1320
+ margin-bottom: 8px;
1321
+ }
1322
+
1323
+ .lanyard-card {
1324
+ position: relative;
1325
+ width: 90px;
1326
+ height: 120px;
1327
+ }
1328
+
1329
+ .card-face {
1330
+ width: 100%;
1331
+ height: 100%;
1332
+ background: linear-gradient(135deg,
1333
+ rgba(0, 0, 0, 0.9) 0%,
1334
+ rgba(20, 20, 40, 0.9) 100%);
1335
+ border: 2px solid rgba(79, 172, 254, 0.4);
1336
+ border-radius: 12px;
1337
+ backdrop-filter: blur(20px);
1338
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6), 0 0 20px rgba(79, 172, 254, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.1);
1339
+ display: flex;
1340
+ flex-direction: column;
1341
+ padding: 12px;
1342
+ transition: all 0.3s ease;
1343
+ }
1344
+
1345
+ .card-face:hover {
1346
+ border-color: rgba(79, 172, 254, 0.6);
1347
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.7), 0 0 30px rgba(79, 172, 254, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2);
1348
+ }
1349
+
1350
+ .card-header {
1351
+ display: flex;
1352
+ justify-content: center;
1353
+ margin-bottom: 8px;
1354
+ }
1355
+
1356
+ .card-logo {
1357
+ font-size: 1.5rem;
1358
+ font-weight: bold;
1359
+ color: #4facfe;
1360
+ text-shadow: 0 0 15px rgba(79, 172, 254, 0.8);
1361
+ letter-spacing: 2px;
1362
+ }
1363
+
1364
+ .card-body {
1365
+ flex: 1;
1366
+ display: flex;
1367
+ flex-direction: column;
1368
+ justify-content: center;
1369
+ align-items: center;
1370
+ text-align: center;
1371
+ gap: 4px;
1372
+ }
1373
+
1374
+ .card-title {
1375
+ font-size: 0.75rem;
1376
+ font-weight: 600;
1377
+ color: rgba(255, 255, 255, 0.95);
1378
+ letter-spacing: 0.5px;
1379
+ }
1380
+
1381
+ .card-subtitle {
1382
+ font-size: 0.6rem;
1383
+ color: rgba(255, 255, 255, 0.6);
1384
+ font-weight: 400;
1385
+ }
1386
+
1387
+ .card-footer {
1388
+ display: flex;
1389
+ justify-content: center;
1390
+ gap: 4px;
1391
+ margin-top: 8px;
1392
+ }
1393
+
1394
+ .card-dot {
1395
+ width: 4px;
1396
+ height: 4px;
1397
+ background: rgba(79, 172, 254, 0.6);
1398
+ border-radius: 50%;
1399
+ box-shadow: 0 0 4px rgba(79, 172, 254, 0.4);
1400
+ }
1401
+
1402
+ /* Giant Central Orb - Hidden until loading complete */
1403
+ .giant-orb-background {
1404
+ position: fixed;
1405
+ top: 0;
1406
+ left: 0;
1407
+ width: 100%;
1408
+ height: 100%;
1409
+ pointer-events: none;
1410
+ z-index: -10;
1411
+ display: flex;
1412
+ align-items: center;
1413
+ justify-content: center;
1414
+ opacity: 0;
1415
+ transition: opacity 0.8s ease-out;
1416
+ }
1417
+
1418
+ .giant-orb-background.loaded {
1419
+ opacity: 1;
1420
+ }
1421
+
1422
+ .giant-orb-container {
1423
+ width: 100vmin;
1424
+ height: 100vmin;
1425
+ position: relative;
1426
+ pointer-events: none;
1427
+ transform: translateY(-5vh);
1428
+ }
1429
+
1430
+ .orb {
1431
+ position: absolute;
1432
+ pointer-events: auto;
1433
+ }
1434
+
1435
+ .orb-1 {
1436
+ top: 10%;
1437
+ right: 15%;
1438
+ width: 300px;
1439
+ height: 300px;
1440
+ animation: float1 20s ease-in-out infinite;
1441
+ }
1442
+
1443
+ .orb-2 {
1444
+ bottom: 20%;
1445
+ left: 10%;
1446
+ width: 250px;
1447
+ height: 250px;
1448
+ animation: float2 25s ease-in-out infinite;
1449
+ }
1450
+
1451
+ .orb-3 {
1452
+ top: 60%;
1453
+ right: 60%;
1454
+ width: 200px;
1455
+ height: 200px;
1456
+ animation: float3 30s ease-in-out infinite;
1457
+ }
1458
+
1459
+ @keyframes float1 {
1460
+
1461
+ 0%,
1462
+ 100% {
1463
+ transform: translate(0, 0) rotate(0deg);
1464
+ }
1465
+
1466
+ 25% {
1467
+ transform: translate(-20px, -30px) rotate(90deg);
1468
+ }
1469
+
1470
+ 50% {
1471
+ transform: translate(10px, -20px) rotate(180deg);
1472
+ }
1473
+
1474
+ 75% {
1475
+ transform: translate(-10px, 10px) rotate(270deg);
1476
+ }
1477
+ }
1478
+
1479
+ @keyframes float2 {
1480
+
1481
+ 0%,
1482
+ 100% {
1483
+ transform: translate(0, 0) rotate(0deg);
1484
+ }
1485
+
1486
+ 33% {
1487
+ transform: translate(30px, -20px) rotate(120deg);
1488
+ }
1489
+
1490
+ 66% {
1491
+ transform: translate(-20px, -40px) rotate(240deg);
1492
+ }
1493
+ }
1494
+
1495
+ @keyframes float3 {
1496
+
1497
+ 0%,
1498
+ 100% {
1499
+ transform: translate(0, 0) rotate(0deg);
1500
+ }
1501
+
1502
+ 20% {
1503
+ transform: translate(-15px, 25px) rotate(72deg);
1504
+ }
1505
+
1506
+ 40% {
1507
+ transform: translate(25px, 15px) rotate(144deg);
1508
+ }
1509
+
1510
+ 60% {
1511
+ transform: translate(20px, -25px) rotate(216deg);
1512
+ }
1513
+
1514
+ 80% {
1515
+ transform: translate(-25px, -15px) rotate(288deg);
1516
+ }
1517
+ }
1518
+
1519
+ .orb-container {
1520
+ position: relative;
1521
+ z-index: 0;
1522
+ width: 100%;
1523
+ height: 100%;
1524
+ }
1525
+
1526
+ /* Apple-Style Glass Container */
1527
+ .glass-container {
1528
+ backdrop-filter: blur(40px);
1529
+ -webkit-backdrop-filter: blur(40px);
1530
+ background: rgba(0, 0, 0, 0.1);
1531
+ border: none;
1532
+ border-radius: 24px;
1533
+ box-shadow: none;
1534
+ margin: 20px;
1535
+ min-height: calc(100vh - 40px);
1536
+ overflow: hidden;
1537
+ position: relative;
1538
+ }
1539
+
1540
+ .glass-panel {
1541
+ background: rgba(255, 255, 255, 0.7);
1542
+ border: 1px solid rgba(0, 0, 0, 0.06);
1543
+ border-radius: 16px;
1544
+ padding: 32px;
1545
+ backdrop-filter: blur(20px);
1546
+ position: relative;
1547
+ overflow: hidden;
1548
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.1);
1549
+ }
1550
+
1551
+ .nested-panel {
1552
+ background: transparent;
1553
+ border: 1px solid rgba(0, 0, 0, 0.04);
1554
+ border-radius: 12px;
1555
+ padding: 24px;
1556
+ margin: 16px 0;
1557
+ backdrop-filter: blur(10px);
1558
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.03);
1559
+ }
1560
+
1561
+ /* Fixed Lanyard - Make it VERY visible */
1562
+ .simple-lanyard-wrapper {
1563
+ position: fixed !important;
1564
+ top: 80px !important;
1565
+ right: 50px !important;
1566
+ width: 100px !important;
1567
+ height: 200px !important;
1568
+ z-index: 9999 !important;
1569
+ pointer-events: auto !important;
1570
+ }
1571
+
1572
+ .lanyard-string {
1573
+ width: 4px !important;
1574
+ background: linear-gradient(to bottom,
1575
+ rgba(59, 130, 246, 0.9) 0%,
1576
+ rgba(139, 92, 246, 0.7) 100%) !important;
1577
+ border-radius: 2px !important;
1578
+ box-shadow: 0 0 10px rgba(59, 130, 246, 0.5) !important;
1579
+ margin-bottom: 8px !important;
1580
+ }
1581
+
1582
+ .card-face {
1583
+ background: rgba(255, 255, 255, 0.95) !important;
1584
+ border: 2px solid rgba(59, 130, 246, 0.3) !important;
1585
+ color: var(--text-primary) !important;
1586
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15) !important, 0 0 20px rgba(59, 130, 246, 0.2) !important;
1587
+ }
1588
+
1589
+ .card-logo {
1590
+ color: #3b82f6 !important;
1591
+ text-shadow: 0 0 15px rgba(59, 130, 246, 0.6) !important;
1592
+ }
1593
+
1594
+ .card-title {
1595
+ color: var(--text-primary) !important;
1596
+ }
1597
+
1598
+ .card-subtitle {
1599
+ color: var(--text-secondary) !important;
1600
+ }
1601
+
1602
+ .card-dot {
1603
+ background: rgba(59, 130, 246, 0.8) !important;
1604
+ box-shadow: 0 0 6px rgba(59, 130, 246, 0.6) !important;
1605
+ }
1606
+ /*
1607
+ Results Panel Styles */
1608
+ .results-container {
1609
+ display: flex;
1610
+ flex-direction: column;
1611
+ gap: 24px;
1612
+ height: 100%;
1613
+ }
1614
+
1615
+ .results-header {
1616
+ background: rgba(255, 255, 255, 0.05);
1617
+ border: 1px solid rgba(255, 255, 255, 0.1);
1618
+ border-radius: 16px;
1619
+ padding: 24px;
1620
+ backdrop-filter: blur(20px);
1621
+ }
1622
+
1623
+ .header-content {
1624
+ display: flex;
1625
+ justify-content: space-between;
1626
+ align-items: flex-start;
1627
+ gap: 24px;
1628
+ }
1629
+
1630
+ .header-left h3 {
1631
+ font-size: 1.5rem;
1632
+ font-weight: 600;
1633
+ color: var(--text-primary);
1634
+ margin: 0 0 8px 0;
1635
+ background: var(--gradient-primary);
1636
+ -webkit-background-clip: text;
1637
+ -webkit-text-fill-color: transparent;
1638
+ background-clip: text;
1639
+ }
1640
+
1641
+ .result-info {
1642
+ font-size: 0.875rem;
1643
+ color: var(--text-muted);
1644
+ }
1645
+
1646
+ .header-actions {
1647
+ display: flex;
1648
+ gap: 12px;
1649
+ }
1650
+
1651
+ .action-button {
1652
+ display: flex;
1653
+ align-items: center;
1654
+ gap: 8px;
1655
+ padding: 12px 20px;
1656
+ background: rgba(255, 255, 255, 0.05);
1657
+ border: 1px solid rgba(255, 255, 255, 0.1);
1658
+ border-radius: 12px;
1659
+ color: var(--text-primary);
1660
+ font-size: 0.875rem;
1661
+ font-weight: 500;
1662
+ cursor: pointer;
1663
+ transition: all 0.3s ease;
1664
+ backdrop-filter: blur(10px);
1665
+ }
1666
+
1667
+ .action-button:hover {
1668
+ background: rgba(255, 255, 255, 0.08);
1669
+ border-color: var(--accent-primary);
1670
+ transform: translateY(-1px);
1671
+ }
1672
+
1673
+
1674
+
1675
+ /* Categories Section */
1676
+ .categories-section {
1677
+ display: flex;
1678
+ gap: 24px;
1679
+ }
1680
+
1681
+ .category-group {
1682
+ flex: 1;
1683
+ background: rgba(255, 255, 255, 0.05);
1684
+ border: 1px solid rgba(255, 255, 255, 0.1);
1685
+ border-radius: 16px;
1686
+ padding: 24px;
1687
+ backdrop-filter: blur(20px);
1688
+ }
1689
+
1690
+ .category-header {
1691
+ display: flex;
1692
+ align-items: center;
1693
+ gap: 12px;
1694
+ margin-bottom: 20px;
1695
+ color: var(--text-primary);
1696
+ }
1697
+
1698
+ .category-label {
1699
+ font-size: 0.75rem;
1700
+ font-weight: 600;
1701
+ letter-spacing: 1px;
1702
+ text-transform: uppercase;
1703
+ color: var(--text-muted);
1704
+ }
1705
+
1706
+ /* Mode Buttons */
1707
+ .mode-buttons {
1708
+ display: flex;
1709
+ flex-direction: column;
1710
+ gap: 8px;
1711
+ }
1712
+
1713
+ .mode-button {
1714
+ display: flex;
1715
+ align-items: center;
1716
+ gap: 12px;
1717
+ padding: 12px 16px;
1718
+ background: rgba(255, 255, 255, 0.02);
1719
+ border: 1px solid rgba(255, 255, 255, 0.05);
1720
+ border-radius: 12px;
1721
+ color: var(--text-secondary);
1722
+ font-size: 0.875rem;
1723
+ font-weight: 500;
1724
+ cursor: pointer;
1725
+ transition: all 0.3s ease;
1726
+ text-align: left;
1727
+ }
1728
+
1729
+ .mode-button:hover {
1730
+ background: rgba(255, 255, 255, 0.05);
1731
+ border-color: rgba(255, 255, 255, 0.1);
1732
+ color: var(--text-primary);
1733
+ }
1734
+
1735
+ .mode-button.active {
1736
+ background: var(--gradient-primary);
1737
+ border-color: var(--accent-primary);
1738
+ color: white;
1739
+ box-shadow: 0 4px 15px rgba(79, 172, 254, 0.3);
1740
+ }
1741
+
1742
+ /* Download Buttons */
1743
+ .download-buttons {
1744
+ display: grid;
1745
+ grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
1746
+ gap: 12px;
1747
+ }
1748
+
1749
+ .download-button {
1750
+ display: flex;
1751
+ flex-direction: column;
1752
+ align-items: center;
1753
+ gap: 8px;
1754
+ padding: 16px 12px;
1755
+ background: rgba(255, 255, 255, 0.02);
1756
+ border: 1px solid rgba(255, 255, 255, 0.05);
1757
+ border-radius: 12px;
1758
+ color: var(--text-secondary);
1759
+ font-size: 0.75rem;
1760
+ font-weight: 500;
1761
+ cursor: pointer;
1762
+ transition: all 0.3s ease;
1763
+ text-align: center;
1764
+ min-height: 80px;
1765
+ }
1766
+
1767
+ .download-button:hover {
1768
+ background: rgba(255, 255, 255, 0.05);
1769
+ border-color: rgba(255, 255, 255, 0.1);
1770
+ color: var(--text-primary);
1771
+ transform: translateY(-2px);
1772
+ }
1773
+
1774
+ .download-button.active {
1775
+ background: var(--gradient-secondary);
1776
+ border-color: var(--accent-secondary);
1777
+ color: white;
1778
+ box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
1779
+ }
1780
+
1781
+ .format-icon {
1782
+ display: flex;
1783
+ align-items: center;
1784
+ justify-content: center;
1785
+ color: rgba(255, 255, 255, 0.8);
1786
+ }
1787
+
1788
+ .format-button-container.active .format-icon {
1789
+ color: white;
1790
+ }
1791
+
1792
+ /* Unified Format Buttons */
1793
+ .format-buttons {
1794
+ display: grid;
1795
+ grid-template-columns: repeat(4, 1fr);
1796
+ gap: 16px;
1797
+ margin: 0;
1798
+ }
1799
+
1800
+ .format-button-container {
1801
+ display: flex;
1802
+ align-items: stretch;
1803
+ background: rgba(100, 100, 100, 0.1);
1804
+ border: 1px solid rgba(255, 255, 255, 0.1);
1805
+ border-radius: 12px;
1806
+ overflow: hidden;
1807
+ transition: all 0.3s ease;
1808
+ backdrop-filter: blur(15px);
1809
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
1810
+ }
1811
+
1812
+ .format-button-container:hover {
1813
+ background: rgba(255, 255, 255, 0.05);
1814
+ border-color: rgba(255, 255, 255, 0.15);
1815
+ transform: translateY(-1px);
1816
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
1817
+ }
1818
+
1819
+ .format-button-container.active {
1820
+ background: var(--gradient-primary);
1821
+ border-color: var(--accent-primary);
1822
+ box-shadow: 0 4px 15px rgba(79, 172, 254, 0.3);
1823
+ }
1824
+
1825
+ .format-preview-button {
1826
+ width: 100%;
1827
+ display: flex;
1828
+ flex-direction: column;
1829
+ align-items: center;
1830
+ justify-content: center;
1831
+ gap: 8px;
1832
+ padding: 20px 16px;
1833
+ background: none;
1834
+ border: none;
1835
+ color: var(--text-secondary);
1836
+ cursor: pointer;
1837
+ transition: all 0.3s ease;
1838
+ min-height: 100px;
1839
+ }
1840
+
1841
+ .format-preview-button:disabled {
1842
+ opacity: 0.5;
1843
+ cursor: not-allowed;
1844
+ }
1845
+
1846
+ .format-button-container.active .format-preview-button {
1847
+ color: white;
1848
+ }
1849
+
1850
+ .format-label {
1851
+ font-size: 0.875rem;
1852
+ font-weight: 500;
1853
+ text-align: center;
1854
+ }
1855
+
1856
+ .format-download-button {
1857
+ display: none;
1858
+ }
1859
+
1860
+ .format-download-button:hover {
1861
+ background: rgba(255, 255, 255, 0.15);
1862
+ color: #ffffff;
1863
+ transform: scale(1.1);
1864
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
1865
+ }
1866
+
1867
+ .format-button-container.active .format-download-button {
1868
+ background: rgba(255, 255, 255, 0.2);
1869
+ color: white;
1870
+ }
1871
+
1872
+ /* Content Preview */
1873
+ .content-preview {
1874
+ position: relative;
1875
+ flex: 1;
1876
+ background: rgba(255, 255, 255, 0.05);
1877
+ border: 1px solid rgba(255, 255, 255, 0.1);
1878
+ border-radius: 16px;
1879
+ padding: 24px 60px 24px 24px;
1880
+ backdrop-filter: blur(20px);
1881
+ overflow: hidden;
1882
+ }
1883
+
1884
+ .content-preview-download {
1885
+ position: absolute;
1886
+ top: 16px;
1887
+ right: 16px;
1888
+ display: flex;
1889
+ align-items: center;
1890
+ justify-content: center;
1891
+ width: 36px;
1892
+ height: 36px;
1893
+ background: rgba(255, 255, 255, 0.1);
1894
+ border: 1px solid rgba(255, 255, 255, 0.1);
1895
+ border-radius: 8px;
1896
+ color: rgba(255, 255, 255, 0.8);
1897
+ cursor: pointer;
1898
+ transition: all 0.3s ease;
1899
+ z-index: 10;
1900
+ backdrop-filter: blur(10px);
1901
+ }
1902
+
1903
+ .content-preview-download:hover {
1904
+ background: rgba(255, 255, 255, 0.15);
1905
+ border-color: rgba(79, 172, 254, 0.3);
1906
+ color: #4facfe;
1907
+ transform: scale(1.05);
1908
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
1909
+ }
1910
+
1911
+ .text-content {
1912
+ background: rgba(0, 0, 0, 0.2);
1913
+ border: 1px solid rgba(255, 255, 255, 0.1);
1914
+ border-radius: 12px;
1915
+ padding: 20px;
1916
+ overflow: auto;
1917
+ max-height: 500px;
1918
+ }
1919
+
1920
+ .text-preview {
1921
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
1922
+ font-size: 0.875rem;
1923
+ line-height: 1.6;
1924
+ color: var(--text-primary);
1925
+ white-space: pre-wrap;
1926
+ margin: 0;
1927
+ }
1928
+
1929
+ /* HTML Preview Styles */
1930
+ .html-preview {
1931
+ background: rgba(0, 0, 0, 0.2);
1932
+ color: var(--text-primary);
1933
+ border: 1px solid rgba(255, 255, 255, 0.1);
1934
+ border-radius: 12px;
1935
+ padding: 24px;
1936
+ overflow: auto;
1937
+ max-height: 500px;
1938
+ line-height: 1.6;
1939
+ }
1940
+
1941
+ /* Headers are now styled in the emoji fix section below */
1942
+
1943
+ .html-preview p {
1944
+ margin: 16px 0;
1945
+ color: var(--text-secondary);
1946
+ }
1947
+
1948
+ .html-preview ul,
1949
+ .html-preview ol {
1950
+ margin: 16px 0;
1951
+ padding-left: 24px;
1952
+ }
1953
+
1954
+ .html-preview li {
1955
+ margin: 8px 0;
1956
+ color: var(--text-secondary);
1957
+ }
1958
+
1959
+ .html-preview table {
1960
+ width: 100%;
1961
+ border-collapse: collapse;
1962
+ background: rgba(0, 0, 0, 0.3);
1963
+ border: 1px solid rgba(255, 255, 255, 0.1);
1964
+ border-radius: 8px;
1965
+ overflow: hidden;
1966
+ margin: 24px 0;
1967
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
1968
+ }
1969
+
1970
+ .html-preview th,
1971
+ .html-preview td {
1972
+ padding: 12px 16px;
1973
+ text-align: left;
1974
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
1975
+ }
1976
+
1977
+ .html-preview th {
1978
+ background: linear-gradient(135deg, #4facfe, #8b5cf6);
1979
+ color: white;
1980
+ font-weight: 600;
1981
+ font-size: 0.875rem;
1982
+ text-transform: uppercase;
1983
+ letter-spacing: 0.5px;
1984
+ }
1985
+
1986
+ .html-preview td {
1987
+ color: var(--text-primary);
1988
+ }
1989
+
1990
+ .html-preview tr:hover {
1991
+ background: rgba(79, 172, 254, 0.1);
1992
+ }
1993
+
1994
+ .html-preview code {
1995
+ background: rgba(79, 172, 254, 0.1);
1996
+ color: #4facfe;
1997
+ padding: 2px 6px;
1998
+ border-radius: 4px;
1999
+ font-family: 'SF Mono', Monaco, monospace;
2000
+ font-size: 0.875em;
2001
+ }
2002
+
2003
+ .html-preview pre {
2004
+ background: rgba(0, 0, 0, 0.4);
2005
+ border: 1px solid rgba(255, 255, 255, 0.1);
2006
+ border-radius: 8px;
2007
+ padding: 16px;
2008
+ overflow-x: auto;
2009
+ margin: 16px 0;
2010
+ }
2011
+
2012
+ .html-preview pre code {
2013
+ background: none;
2014
+ color: var(--text-primary);
2015
+ padding: 0;
2016
+ }
2017
+
2018
+ .html-preview blockquote {
2019
+ border-left: 4px solid var(--accent-primary);
2020
+ background: rgba(79, 172, 254, 0.1);
2021
+ padding: 16px 20px;
2022
+ margin: 16px 0;
2023
+ border-radius: 0 8px 8px 0;
2024
+ font-style: italic;
2025
+ color: var(--text-secondary);
2026
+ }
2027
+
2028
+ .html-preview strong {
2029
+ color: var(--accent-primary);
2030
+ font-weight: 600;
2031
+ }
2032
+
2033
+ .html-preview em {
2034
+ color: var(--accent-secondary);
2035
+ font-style: italic;
2036
+ }
2037
+
2038
+ .html-preview a {
2039
+ color: var(--accent-primary);
2040
+ text-decoration: none;
2041
+ border-bottom: 1px solid rgba(79, 172, 254, 0.3);
2042
+ transition: all 0.3s ease;
2043
+ }
2044
+
2045
+ .html-preview a:hover {
2046
+ border-bottom-color: var(--accent-primary);
2047
+ color: var(--accent-secondary);
2048
+ }
2049
+
2050
+ /* Fix emoji rendering in HTML preview */
2051
+ .html-preview {
2052
+ font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
2053
+ }
2054
+
2055
+ /* Force proper emoji rendering - override all text styling */
2056
+ .html-preview * {
2057
+ font-variant-emoji: emoji !important;
2058
+ }
2059
+
2060
+ /* Completely reset styling for any element that might contain emojis */
2061
+ .html-preview h1,
2062
+ .html-preview h2,
2063
+ .html-preview h3,
2064
+ .html-preview h4,
2065
+ .html-preview h5,
2066
+ .html-preview h6,
2067
+ .html-preview p,
2068
+ .html-preview td,
2069
+ .html-preview th,
2070
+ .html-preview li,
2071
+ .html-preview span,
2072
+ .html-preview div {
2073
+ background: none !important;
2074
+ -webkit-background-clip: initial !important;
2075
+ -webkit-text-fill-color: initial !important;
2076
+ background-clip: initial !important;
2077
+ color: var(--text-primary) !important;
2078
+ }
2079
+
2080
+ /* Specific fix for headers to maintain their styling but not affect emojis */
2081
+ .html-preview h1 {
2082
+ font-size: 2rem;
2083
+ font-weight: 600;
2084
+ color: var(--text-primary) !important;
2085
+ }
2086
+ .html-preview h2 {
2087
+ font-size: 1.5rem;
2088
+ font-weight: 600;
2089
+ color: var(--text-primary) !important;
2090
+ }
2091
+ .html-preview h3 {
2092
+ font-size: 1.25rem;
2093
+ font-weight: 600;
2094
+ color: var(--text-primary) !important;
2095
+ }
2096
+
2097
+ /* Responsive Design */
2098
+ @media (max-width: 768px) {
2099
+ .categories-section {
2100
+ flex-direction: column;
2101
+ }
2102
+
2103
+ .header-content {
2104
+ flex-direction: column;
2105
+ gap: 16px;
2106
+ }
2107
+
2108
+ .download-buttons {
2109
+ grid-template-columns: repeat(2, 1fr);
2110
+ }
2111
+
2112
+ .format-buttons {
2113
+ grid-template-columns: repeat(2, 1fr);
2114
+ gap: 12px;
2115
+ }
2116
+
2117
+ .mode-buttons {
2118
+ display: grid;
2119
+ grid-template-columns: repeat(2, 1fr);
2120
+ gap: 8px;
2121
+ }
2122
+ }
2123
+
2124
+ @media (max-width: 480px) {
2125
+ .results-container {
2126
+ gap: 16px;
2127
+ }
2128
+
2129
+ .results-header,
2130
+ .category-group {
2131
+ padding: 16px;
2132
+ }
2133
+
2134
+ .content-preview {
2135
+ padding: 16px 50px 16px 16px;
2136
+ }
2137
+
2138
+ .download-buttons {
2139
+ grid-template-columns: 1fr;
2140
+ }
2141
+
2142
+ .format-buttons {
2143
+ grid-template-columns: repeat(2, 1fr);
2144
+ }
2145
+
2146
+ .mode-buttons {
2147
+ grid-template-columns: 1fr;
2148
+ }
2149
+ }/* Plain
2150
+ Markdown Preview - No Styling */
2151
+ .markdown-plain {
2152
+ background: rgba(0, 0, 0, 0.2);
2153
+ color: var(--text-primary);
2154
+ border: 1px solid rgba(255, 255, 255, 0.1);
2155
+ border-radius: 12px;
2156
+ padding: 24px;
2157
+ overflow: auto;
2158
+ max-height: 500px;
2159
+ line-height: 1.6;
2160
+ font-family: monospace;
2161
+ }
2162
+
2163
+ .markdown-plain h1,
2164
+ .markdown-plain h2,
2165
+ .markdown-plain h3,
2166
+ .markdown-plain h4,
2167
+ .markdown-plain h5,
2168
+ .markdown-plain h6 {
2169
+ color: var(--text-primary);
2170
+ margin: 16px 0 8px 0;
2171
+ font-weight: normal;
2172
+ font-size: 1rem;
2173
+ }
2174
+
2175
+ .markdown-plain p {
2176
+ margin: 8px 0;
2177
+ color: var(--text-primary);
2178
+ }
2179
+
2180
+ .markdown-plain table {
2181
+ border-collapse: collapse;
2182
+ margin: 16px 0;
2183
+ font-family: monospace;
2184
+ }
2185
+
2186
+ .markdown-plain th,
2187
+ .markdown-plain td {
2188
+ border: 1px solid var(--text-muted);
2189
+ padding: 8px 12px;
2190
+ text-align: left;
2191
+ color: var(--text-primary);
2192
+ background: transparent;
2193
+ }
2194
+
2195
+ .markdown-plain th {
2196
+ font-weight: normal;
2197
+ background: transparent;
2198
+ }
2199
+
2200
+ .markdown-plain tr:hover {
2201
+ background: transparent;
2202
+ }
2203
+
2204
+ .markdown-plain strong {
2205
+ font-weight: bold;
2206
+ color: var(--text-primary);
2207
+ }
2208
+
2209
+ .markdown-plain em {
2210
+ font-style: italic;
2211
+ color: var(--text-primary);
2212
+ }
2213
+
2214
+ .markdown-plain ul,
2215
+ .markdown-plain ol {
2216
+ margin: 8px 0;
2217
+ padding-left: 20px;
2218
+ color: var(--text-primary);
2219
+ }
2220
+
2221
+ .markdown-plain li {
2222
+ margin: 4px 0;
2223
+ color: var(--text-primary);
2224
+ }.mark
2225
+ div.markdown-plain a,
2226
+ div.markdown-plain a:link,
2227
+ div.markdown-plain a:visited {
2228
+ color: #ff9500 !important;
2229
+ text-decoration: none !important;
2230
+ border-bottom: none !important;
2231
+ border: none !important;
2232
+ }
2233
+
2234
+ div.markdown-plain a:hover {
2235
+ color: #ffb347 !important;
2236
+ text-decoration: underline !important;
2237
+ }
2238
+
2239
+ div.markdown-plain a:active {
2240
+ color: #cc7700 !important;
2241
+ }
src/index.js ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+
5
+ const root = ReactDOM.createRoot(document.getElementById('root'));
6
+ root.render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>
10
+ );
src/utils/encryption.js ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Secure encryption utility for API key storage
2
+ // Uses device-specific salt and multiple encryption layers
3
+
4
+ class ApiKeyEncryption {
5
+ constructor() {
6
+ // Generate a device-specific salt based on browser fingerprint
7
+ this.salt = this.generateDeviceSalt();
8
+ this.additionalSalt = 'Luna-OCR-2025-Security-Salt';
9
+ }
10
+
11
+ // Generate a consistent salt based on device characteristics
12
+ generateDeviceSalt() {
13
+ const canvas = document.createElement('canvas');
14
+ const ctx = canvas.getContext('2d');
15
+ ctx.textBaseline = 'top';
16
+ ctx.font = '14px Arial';
17
+ ctx.fillText('Luna OCR Device Salt', 2, 2);
18
+
19
+ const fingerprint = [
20
+ navigator.userAgent,
21
+ navigator.language,
22
+ window.screen.width + 'x' + window.screen.height,
23
+ new Date().getTimezoneOffset(),
24
+ canvas.toDataURL()
25
+ ].join('|');
26
+
27
+ return this.simpleHash(fingerprint);
28
+ }
29
+
30
+ // Simple hash function
31
+ simpleHash(str) {
32
+ let hash = 0;
33
+ for (let i = 0; i < str.length; i++) {
34
+ const char = str.charCodeAt(i);
35
+ hash = ((hash << 5) - hash) + char;
36
+ hash = hash & hash; // Convert to 32-bit integer
37
+ }
38
+ return Math.abs(hash).toString(36);
39
+ }
40
+
41
+ // Multi-layer encryption with salt and additional security
42
+ encrypt(text) {
43
+ if (!text || text.trim() === '') return '';
44
+
45
+ // Layer 1: Add timestamp and random padding
46
+ const timestamp = Date.now().toString(36);
47
+ const randomPadding = Math.random().toString(36).substring(2);
48
+ const paddedText = `${timestamp}:${text}:${randomPadding}`;
49
+
50
+ // Layer 2: XOR with device salt
51
+ const saltKey = this.salt + this.additionalSalt;
52
+ let encrypted = '';
53
+
54
+ for (let i = 0; i < paddedText.length; i++) {
55
+ const textChar = paddedText.charCodeAt(i);
56
+ const saltChar = saltKey.charCodeAt(i % saltKey.length);
57
+ const encryptedChar = textChar ^ saltChar;
58
+ encrypted += String.fromCharCode(encryptedChar);
59
+ }
60
+
61
+ // Layer 3: Base64 encode with additional obfuscation
62
+ const base64 = btoa(encrypted);
63
+ const obfuscated = base64.split('').reverse().join('');
64
+
65
+ return obfuscated;
66
+ }
67
+
68
+ // Multi-layer decryption
69
+ decrypt(encryptedText) {
70
+ if (!encryptedText || encryptedText.trim() === '') return '';
71
+
72
+ try {
73
+ // Layer 1: Reverse obfuscation and Base64 decode
74
+ const deobfuscated = encryptedText.split('').reverse().join('');
75
+ const encrypted = atob(deobfuscated);
76
+
77
+ // Layer 2: XOR decryption with device salt
78
+ const saltKey = this.salt + this.additionalSalt;
79
+ let decrypted = '';
80
+
81
+ for (let i = 0; i < encrypted.length; i++) {
82
+ const encryptedChar = encrypted.charCodeAt(i);
83
+ const saltChar = saltKey.charCodeAt(i % saltKey.length);
84
+ const decryptedChar = encryptedChar ^ saltChar;
85
+ decrypted += String.fromCharCode(decryptedChar);
86
+ }
87
+
88
+ // Layer 3: Extract original text from padded format
89
+ const parts = decrypted.split(':');
90
+ if (parts.length >= 3) {
91
+ // Remove timestamp and random padding, return original text
92
+ return parts.slice(1, -1).join(':');
93
+ }
94
+
95
+ return decrypted; // Fallback for old format
96
+ } catch (error) {
97
+ console.warn('Failed to decrypt API key:', error);
98
+ return '';
99
+ }
100
+ }
101
+
102
+ // Store encrypted API key with obfuscated key name
103
+ storeApiKey(apiKey) {
104
+ if (!apiKey || apiKey.trim() === '') {
105
+ localStorage.removeItem('luna_secure_config_v2');
106
+ return;
107
+ }
108
+
109
+ const encrypted = this.encrypt(apiKey);
110
+ localStorage.setItem('luna_secure_config_v2', encrypted);
111
+ }
112
+
113
+ // Retrieve and decrypt API key
114
+ retrieveApiKey() {
115
+ const encrypted = localStorage.getItem('luna_secure_config_v2');
116
+ if (!encrypted) return '';
117
+
118
+ return this.decrypt(encrypted);
119
+ }
120
+
121
+ // Clear stored API key
122
+ clearApiKey() {
123
+ localStorage.removeItem('luna_secure_config_v2');
124
+ }
125
+
126
+ // Check if API key exists
127
+ hasStoredApiKey() {
128
+ return !!localStorage.getItem('luna_secure_config_v2');
129
+ }
130
+ }
131
+
132
+ export default new ApiKeyEncryption();
start-dev.js ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require('child_process');
4
+ const path = require('path');
5
+
6
+ console.log('🚀 Starting Luna OCR Development Environment...\n');
7
+
8
+ // Start backend server
9
+ console.log('📡 Starting backend server...');
10
+ const backend = spawn('npm', ['start'], {
11
+ cwd: path.join(__dirname, 'server'),
12
+ stdio: 'inherit',
13
+ shell: true
14
+ });
15
+
16
+ // Wait a bit for backend to start
17
+ setTimeout(() => {
18
+ console.log('\n🎨 Starting frontend...');
19
+ const frontend = spawn('npm', ['start'], {
20
+ stdio: 'inherit',
21
+ shell: true
22
+ });
23
+
24
+ // Handle process termination
25
+ process.on('SIGINT', () => {
26
+ console.log('\n🛑 Shutting down...');
27
+ backend.kill();
28
+ frontend.kill();
29
+ process.exit();
30
+ });
31
+
32
+ frontend.on('close', (code) => {
33
+ console.log(`Frontend exited with code ${code}`);
34
+ backend.kill();
35
+ });
36
+
37
+ backend.on('close', (code) => {
38
+ console.log(`Backend exited with code ${code}`);
39
+ frontend.kill();
40
+ });
41
+
42
+ }, 2000);
43
+
44
+ backend.on('close', (code) => {
45
+ console.log(`Backend exited with code ${code}`);
46
+ });