|
import React, { useState } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { FileText, Code, Eye, Download, Copy, Check } from 'lucide-react';
|
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
import { marked } from 'marked';
|
|
import jsPDF from 'jspdf';
|
|
|
|
const ResultsPanel = ({ text, previewMode, onPreviewModeChange, fileName }) => {
|
|
const [copied, setCopied] = useState(false);
|
|
const [downloadFormat, setDownloadFormat] = useState('txt');
|
|
|
|
const previewModes = [
|
|
{ id: 'text', label: 'Text', icon: <FileText size={16} /> },
|
|
{ id: 'markdown', label: 'Markdown', icon: <Code size={16} /> },
|
|
{ id: 'preview', label: 'HTML', icon: <Eye size={16} /> },
|
|
{ id: 'json', label: 'JSON', icon: <Code size={16} /> }
|
|
];
|
|
|
|
const downloadFormats = [
|
|
{ id: 'txt', label: '.txt', icon: '📄' },
|
|
{ id: 'md', label: '.md', icon: '📝' },
|
|
{ id: 'html', label: '.html', icon: '🌐' },
|
|
{ id: 'json', label: '.json', icon: '🔧' },
|
|
{ id: 'pdf', label: '.pdf', icon: '📋' }
|
|
];
|
|
|
|
const handleCopy = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
} catch (err) {
|
|
console.error('Failed to copy text:', err);
|
|
}
|
|
};
|
|
|
|
|
|
|
|
const handleDownload = (format = downloadFormat) => {
|
|
const baseFileName = fileName ? fileName.split('.')[0] : 'extracted-text';
|
|
|
|
switch (format) {
|
|
case 'txt':
|
|
downloadFile(text, `${baseFileName}.txt`, 'text/plain');
|
|
break;
|
|
case 'md':
|
|
downloadFile(text, `${baseFileName}.md`, 'text/markdown');
|
|
break;
|
|
case 'html':
|
|
const htmlContent = `
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>${baseFileName} - Luna OCR</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', sans-serif;
|
|
background: #0a0a0f;
|
|
color: #ffffff;
|
|
min-height: 100vh;
|
|
overflow-x: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
/* Modern Animated Background */
|
|
.animated-background {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
pointer-events: none;
|
|
z-index: -1;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.gradient-orb {
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
filter: blur(60px);
|
|
opacity: 0.4;
|
|
}
|
|
|
|
.orb-1 {
|
|
top: 20%;
|
|
left: 10%;
|
|
width: 300px;
|
|
height: 300px;
|
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
animation: float1 25s ease-in-out infinite;
|
|
}
|
|
|
|
.orb-2 {
|
|
top: 60%;
|
|
right: 15%;
|
|
width: 250px;
|
|
height: 250px;
|
|
background: linear-gradient(135deg, #ec4899 0%, #be185d 100%);
|
|
animation: float2 30s ease-in-out infinite;
|
|
}
|
|
|
|
.orb-3 {
|
|
bottom: 30%;
|
|
left: 20%;
|
|
width: 200px;
|
|
height: 200px;
|
|
background: linear-gradient(135deg, #6366f1 0%, #4338ca 100%);
|
|
animation: float3 35s ease-in-out infinite;
|
|
}
|
|
|
|
.orb-4 {
|
|
top: 10%;
|
|
right: 40%;
|
|
width: 180px;
|
|
height: 180px;
|
|
background: linear-gradient(135deg, #14b8a6 0%, #0d9488 100%);
|
|
animation: float4 20s ease-in-out infinite;
|
|
}
|
|
|
|
.orb-5 {
|
|
bottom: 10%;
|
|
right: 30%;
|
|
width: 220px;
|
|
height: 220px;
|
|
background: linear-gradient(135deg, #f472b6 0%, #ec4899 100%);
|
|
animation: float5 28s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes float1 {
|
|
0%, 100% { transform: translate(0, 0) rotate(0deg); }
|
|
25% { transform: translate(-30px, -20px) rotate(90deg); }
|
|
50% { transform: translate(20px, -30px) rotate(180deg); }
|
|
75% { transform: translate(-20px, 20px) rotate(270deg); }
|
|
}
|
|
|
|
@keyframes float2 {
|
|
0%, 100% { transform: translate(0, 0) rotate(0deg); }
|
|
33% { transform: translate(25px, -35px) rotate(120deg); }
|
|
66% { transform: translate(-35px, -25px) rotate(240deg); }
|
|
}
|
|
|
|
@keyframes float3 {
|
|
0%, 100% { transform: translate(0, 0) rotate(0deg); }
|
|
20% { transform: translate(-25px, 30px) rotate(72deg); }
|
|
40% { transform: translate(30px, 25px) rotate(144deg); }
|
|
60% { transform: translate(25px, -30px) rotate(216deg); }
|
|
80% { transform: translate(-30px, -25px) rotate(288deg); }
|
|
}
|
|
|
|
@keyframes float4 {
|
|
0%, 100% { transform: translate(0, 0) rotate(0deg); }
|
|
50% { transform: translate(-20px, 25px) rotate(180deg); }
|
|
}
|
|
|
|
@keyframes float5 {
|
|
0%, 100% { transform: translate(0, 0) rotate(0deg); }
|
|
25% { transform: translate(20px, -15px) rotate(90deg); }
|
|
50% { transform: translate(-15px, 20px) rotate(180deg); }
|
|
75% { transform: translate(-20px, -20px) rotate(270deg); }
|
|
}
|
|
left: 50%;
|
|
width: 60vmin;
|
|
height: 60vmin;
|
|
transform: translate(-50%, -50%);
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.orb-layer {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
border-radius: 50%;
|
|
filter: blur(40px);
|
|
}
|
|
|
|
.orb-layer-1 {
|
|
background: radial-gradient(circle at 30% 30%,
|
|
rgba(156, 67, 254, 0.8) 0%,
|
|
rgba(79, 172, 254, 0.6) 40%,
|
|
rgba(16, 21, 96, 0.4) 70%,
|
|
transparent 100%);
|
|
animation: orbRotate1 20s linear infinite, orbPulse1 8s ease-in-out infinite;
|
|
}
|
|
|
|
.orb-layer-2 {
|
|
background: radial-gradient(circle at 70% 60%,
|
|
rgba(76, 194, 233, 0.7) 0%,
|
|
rgba(139, 92, 246, 0.5) 35%,
|
|
rgba(79, 172, 254, 0.3) 65%,
|
|
transparent 100%);
|
|
animation: orbRotate2 25s linear infinite reverse, orbPulse2 6s ease-in-out infinite;
|
|
}
|
|
|
|
.orb-layer-3 {
|
|
background: radial-gradient(circle at 50% 80%,
|
|
rgba(16, 185, 129, 0.6) 0%,
|
|
rgba(6, 182, 212, 0.4) 30%,
|
|
rgba(139, 92, 246, 0.2) 60%,
|
|
transparent 100%);
|
|
animation: orbRotate3 30s linear infinite, orbPulse3 10s ease-in-out infinite;
|
|
}
|
|
|
|
.orb-core {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
width: 40%;
|
|
height: 40%;
|
|
transform: translate(-50%, -50%);
|
|
background: radial-gradient(circle,
|
|
rgba(156, 67, 254, 0.9) 0%,
|
|
rgba(79, 172, 254, 0.7) 30%,
|
|
rgba(16, 21, 96, 0.5) 60%,
|
|
transparent 100%);
|
|
border-radius: 50%;
|
|
filter: blur(20px);
|
|
animation: orbCore 15s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes orbRotate1 {
|
|
0% { transform: rotate(0deg) scale(1); }
|
|
25% { transform: rotate(90deg) scale(1.1); }
|
|
50% { transform: rotate(180deg) scale(1); }
|
|
75% { transform: rotate(270deg) scale(0.9); }
|
|
100% { transform: rotate(360deg) scale(1); }
|
|
}
|
|
|
|
@keyframes orbRotate2 {
|
|
0% { transform: rotate(0deg) scale(0.9); }
|
|
33% { transform: rotate(120deg) scale(1.2); }
|
|
66% { transform: rotate(240deg) scale(0.8); }
|
|
100% { transform: rotate(360deg) scale(0.9); }
|
|
}
|
|
|
|
@keyframes orbRotate3 {
|
|
0% { transform: rotate(0deg) scale(1.1); }
|
|
20% { transform: rotate(72deg) scale(0.9); }
|
|
40% { transform: rotate(144deg) scale(1.3); }
|
|
60% { transform: rotate(216deg) scale(0.7); }
|
|
80% { transform: rotate(288deg) scale(1.1); }
|
|
100% { transform: rotate(360deg) scale(1.1); }
|
|
}
|
|
|
|
@keyframes orbPulse1 {
|
|
0%, 100% { opacity: 0.8; filter: blur(40px) hue-rotate(0deg); }
|
|
50% { opacity: 0.6; filter: blur(60px) hue-rotate(30deg); }
|
|
}
|
|
|
|
@keyframes orbPulse2 {
|
|
0%, 100% { opacity: 0.7; filter: blur(40px) hue-rotate(0deg); }
|
|
50% { opacity: 0.9; filter: blur(30px) hue-rotate(-20deg); }
|
|
}
|
|
|
|
@keyframes orbPulse3 {
|
|
0%, 100% { opacity: 0.6; filter: blur(40px) hue-rotate(0deg); }
|
|
50% { opacity: 0.4; filter: blur(50px) hue-rotate(15deg); }
|
|
}
|
|
|
|
@keyframes orbCore {
|
|
0%, 100% {
|
|
transform: translate(-50%, -50%) scale(1);
|
|
filter: blur(20px) hue-rotate(0deg);
|
|
}
|
|
25% {
|
|
transform: translate(-50%, -50%) scale(1.2);
|
|
filter: blur(15px) hue-rotate(45deg);
|
|
}
|
|
50% {
|
|
transform: translate(-50%, -50%) scale(0.8);
|
|
filter: blur(25px) hue-rotate(90deg);
|
|
}
|
|
75% {
|
|
transform: translate(-50%, -50%) scale(1.1);
|
|
filter: blur(18px) hue-rotate(135deg);
|
|
}
|
|
}
|
|
|
|
/* Interactive hover effect */
|
|
.orb-container:hover .css-orb {
|
|
animation-duration: 0.5s;
|
|
}
|
|
|
|
.orb-container:hover .orb-layer-1 {
|
|
filter: blur(30px);
|
|
transform: scale(1.2);
|
|
}
|
|
|
|
.orb-container:hover .orb-layer-2 {
|
|
filter: blur(35px);
|
|
transform: scale(0.9);
|
|
}
|
|
|
|
.orb-container:hover .orb-layer-3 {
|
|
filter: blur(45px);
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
/* Modern Glassmorphism Container */
|
|
.glass-container {
|
|
backdrop-filter: blur(30px);
|
|
-webkit-backdrop-filter: blur(30px);
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 32px;
|
|
margin: 24px;
|
|
min-height: calc(100vh - 48px);
|
|
overflow: hidden;
|
|
position: relative;
|
|
box-shadow:
|
|
0 8px 32px rgba(0, 0, 0, 0.3),
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.glass-container::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: linear-gradient(135deg,
|
|
rgba(16, 185, 129, 0.1) 0%,
|
|
rgba(236, 72, 153, 0.08) 35%,
|
|
rgba(99, 102, 241, 0.1) 70%,
|
|
rgba(20, 184, 166, 0.08) 100%);
|
|
border-radius: 32px;
|
|
z-index: -1;
|
|
}
|
|
|
|
/* Modern Header */
|
|
.document-header {
|
|
padding: 40px 48px;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
background: rgba(255, 255, 255, 0.03);
|
|
backdrop-filter: blur(25px);
|
|
position: relative;
|
|
}
|
|
|
|
.document-header::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 1px;
|
|
background: linear-gradient(90deg,
|
|
transparent 0%,
|
|
rgba(16, 185, 129, 0.5) 25%,
|
|
rgba(236, 72, 153, 0.5) 75%,
|
|
transparent 100%);
|
|
}
|
|
|
|
.document-title {
|
|
font-size: 2.75rem;
|
|
font-weight: 800;
|
|
background: linear-gradient(135deg,
|
|
#10b981 0%,
|
|
#ec4899 50%,
|
|
#6366f1 100%);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
margin-bottom: 12px;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
.document-subtitle {
|
|
color: rgba(255, 255, 255, 0.7);
|
|
font-size: 1.1rem;
|
|
font-weight: 500;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
/* Modern Content */
|
|
.document-content {
|
|
padding: 48px;
|
|
line-height: 1.8;
|
|
position: relative;
|
|
}
|
|
|
|
.document-content::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 48px;
|
|
right: 48px;
|
|
height: 1px;
|
|
background: linear-gradient(90deg,
|
|
transparent 0%,
|
|
rgba(255, 255, 255, 0.1) 50%,
|
|
transparent 100%);
|
|
}
|
|
|
|
/* Clean Typography */
|
|
h1, h2, h3, h4, h5, h6 {
|
|
color: rgba(255, 255, 255, 0.95);
|
|
margin: 2.5rem 0 1.5rem 0;
|
|
font-weight: 700;
|
|
letter-spacing: -0.01em;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 2.5rem;
|
|
color: rgba(255, 255, 255, 0.98);
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
h2 {
|
|
font-size: 2rem;
|
|
color: rgba(255, 255, 255, 0.95);
|
|
margin-top: 3rem;
|
|
position: relative;
|
|
padding-bottom: 0.5rem;
|
|
}
|
|
|
|
h2::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 60px;
|
|
height: 2px;
|
|
background: linear-gradient(90deg,
|
|
rgba(16, 185, 129, 0.6) 0%,
|
|
rgba(236, 72, 153, 0.6) 100%);
|
|
border-radius: 1px;
|
|
}
|
|
|
|
h3 {
|
|
font-size: 1.5rem;
|
|
color: rgba(255, 255, 255, 0.92);
|
|
}
|
|
|
|
h4 {
|
|
font-size: 1.25rem;
|
|
color: rgba(255, 255, 255, 0.9);
|
|
}
|
|
|
|
p {
|
|
margin: 1rem 0;
|
|
color: rgba(255, 255, 255, 0.9);
|
|
}
|
|
|
|
/* Enhanced Glassmorphism Tables */
|
|
table {
|
|
width: 100%;
|
|
border-collapse: separate;
|
|
border-spacing: 0;
|
|
margin: 2.5rem 0;
|
|
background: rgba(255, 255, 255, 0.02);
|
|
backdrop-filter: blur(40px);
|
|
-webkit-backdrop-filter: blur(40px);
|
|
border-radius: 24px;
|
|
overflow: hidden;
|
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
box-shadow:
|
|
0 20px 40px rgba(0, 0, 0, 0.3),
|
|
0 8px 16px rgba(0, 0, 0, 0.2),
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
|
position: relative;
|
|
}
|
|
|
|
table::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: linear-gradient(135deg,
|
|
rgba(16, 185, 129, 0.02) 0%,
|
|
rgba(236, 72, 153, 0.02) 50%,
|
|
rgba(99, 102, 241, 0.02) 100%);
|
|
border-radius: 24px;
|
|
z-index: -1;
|
|
}
|
|
|
|
th, td {
|
|
padding: 24px 28px;
|
|
text-align: left;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
|
|
position: relative;
|
|
}
|
|
|
|
th {
|
|
background: rgba(255, 255, 255, 0.04);
|
|
backdrop-filter: blur(20px);
|
|
color: rgba(255, 255, 255, 0.95);
|
|
font-weight: 600;
|
|
font-size: 0.95rem;
|
|
letter-spacing: 0.3px;
|
|
text-transform: uppercase;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
}
|
|
|
|
th::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: linear-gradient(135deg,
|
|
rgba(16, 185, 129, 0.08) 0%,
|
|
rgba(236, 72, 153, 0.08) 100%);
|
|
z-index: -1;
|
|
}
|
|
|
|
th::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 1px;
|
|
background: linear-gradient(90deg,
|
|
transparent 0%,
|
|
rgba(255, 255, 255, 0.1) 50%,
|
|
transparent 100%);
|
|
}
|
|
|
|
td {
|
|
color: rgba(255, 255, 255, 0.9);
|
|
font-weight: 500;
|
|
background: rgba(255, 255, 255, 0.01);
|
|
}
|
|
|
|
tr:hover td {
|
|
background: rgba(255, 255, 255, 0.03);
|
|
color: rgba(255, 255, 255, 0.95);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
tr:hover {
|
|
transform: translateY(-2px);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
tr:hover td::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: linear-gradient(135deg,
|
|
rgba(16, 185, 129, 0.03) 0%,
|
|
rgba(236, 72, 153, 0.03) 100%);
|
|
z-index: -1;
|
|
}
|
|
|
|
/* Links */
|
|
a {
|
|
color: #4facfe;
|
|
text-decoration: none;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
a:hover {
|
|
color: #8b5cf6;
|
|
text-shadow: 0 0 10px rgba(79, 172, 254, 0.5);
|
|
}
|
|
|
|
/* Enhanced Code blocks */
|
|
pre, code {
|
|
background: rgba(0, 0, 0, 0.3);
|
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
border-radius: 12px;
|
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
|
|
backdrop-filter: blur(20px);
|
|
}
|
|
|
|
pre {
|
|
padding: 24px;
|
|
overflow-x: auto;
|
|
margin: 2rem 0;
|
|
box-shadow:
|
|
0 8px 32px rgba(0, 0, 0, 0.2),
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
|
position: relative;
|
|
}
|
|
|
|
pre::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: linear-gradient(135deg,
|
|
rgba(16, 185, 129, 0.02) 0%,
|
|
rgba(99, 102, 241, 0.02) 100%);
|
|
border-radius: 12px;
|
|
z-index: -1;
|
|
}
|
|
|
|
code {
|
|
padding: 6px 10px;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
/* Enhanced Blockquotes */
|
|
blockquote {
|
|
border: none;
|
|
background: rgba(255, 255, 255, 0.03);
|
|
backdrop-filter: blur(30px);
|
|
padding: 28px 32px;
|
|
margin: 2.5rem 0;
|
|
border-radius: 20px;
|
|
border-left: 4px solid transparent;
|
|
background-image: linear-gradient(rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.03)),
|
|
linear-gradient(135deg, #10b981, #ec4899);
|
|
background-origin: border-box;
|
|
background-clip: padding-box, border-box;
|
|
box-shadow:
|
|
0 12px 40px rgba(0, 0, 0, 0.2),
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
|
position: relative;
|
|
}
|
|
|
|
blockquote::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: linear-gradient(135deg,
|
|
rgba(16, 185, 129, 0.03) 0%,
|
|
rgba(236, 72, 153, 0.03) 100%);
|
|
border-radius: 20px;
|
|
z-index: -1;
|
|
}
|
|
|
|
blockquote p {
|
|
color: rgba(255, 255, 255, 0.95);
|
|
font-style: italic;
|
|
font-weight: 500;
|
|
margin: 0;
|
|
}
|
|
|
|
/* Lists */
|
|
ul, ol {
|
|
margin: 1rem 0;
|
|
padding-left: 2rem;
|
|
}
|
|
|
|
li {
|
|
margin: 0.5rem 0;
|
|
color: rgba(255, 255, 255, 0.9);
|
|
}
|
|
|
|
/* Horizontal rules */
|
|
hr {
|
|
border: none;
|
|
height: 1px;
|
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
|
margin: 3rem 0;
|
|
}
|
|
|
|
/* Strong and emphasis */
|
|
strong {
|
|
color: #ffffff;
|
|
font-weight: 600;
|
|
}
|
|
|
|
em {
|
|
color: #4facfe;
|
|
font-style: italic;
|
|
}
|
|
|
|
/* Modern Footer */
|
|
.document-footer {
|
|
padding: 40px 48px;
|
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
|
background: rgba(255, 255, 255, 0.02);
|
|
text-align: center;
|
|
color: rgba(255, 255, 255, 0.6);
|
|
font-size: 0.9rem;
|
|
font-weight: 500;
|
|
position: relative;
|
|
}
|
|
|
|
.document-footer::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 48px;
|
|
right: 48px;
|
|
height: 1px;
|
|
background: linear-gradient(90deg,
|
|
transparent 0%,
|
|
rgba(16, 185, 129, 0.3) 25%,
|
|
rgba(236, 72, 153, 0.3) 75%,
|
|
transparent 100%);
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.glass-container {
|
|
margin: 10px;
|
|
border-radius: 16px;
|
|
}
|
|
|
|
.document-header,
|
|
.document-content,
|
|
.document-footer {
|
|
padding: 24px 20px;
|
|
}
|
|
|
|
.document-title {
|
|
font-size: 2rem;
|
|
}
|
|
|
|
table {
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
th, td {
|
|
padding: 12px 16px;
|
|
}
|
|
}
|
|
|
|
/* Print styles */
|
|
@media print {
|
|
body {
|
|
background: white;
|
|
color: black;
|
|
}
|
|
|
|
.orb-background,
|
|
.glass-container::before {
|
|
display: none;
|
|
}
|
|
|
|
.glass-container {
|
|
background: white;
|
|
border: none;
|
|
box-shadow: none;
|
|
margin: 0;
|
|
}
|
|
}
|
|
</style>
|
|
<script src="https://unpkg.com/[email protected]/dist/ogl.umd.js"></script>
|
|
</head>
|
|
<body>
|
|
<!-- Orb Background -->
|
|
<div class="animated-background">
|
|
<div class="gradient-orb orb-1"></div>
|
|
<div class="gradient-orb orb-2"></div>
|
|
<div class="gradient-orb orb-3"></div>
|
|
<div class="gradient-orb orb-4"></div>
|
|
<div class="gradient-orb orb-5"></div>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<div class="glass-container">
|
|
<header class="document-header">
|
|
<h1 class="document-title">${baseFileName}</h1>
|
|
<p class="document-subtitle">Generated by Luna OCR • ${new Date().toLocaleDateString()}</p>
|
|
</header>
|
|
|
|
<main class="document-content">
|
|
${marked(text)}
|
|
</main>
|
|
|
|
<footer class="document-footer">
|
|
<p>Processed with Luna OCR • Glassmorphism Theme • ${new Date().toLocaleString()}</p>
|
|
</footer>
|
|
</div>
|
|
|
|
<script>
|
|
// Robust Orb.js implementation with fallback
|
|
function initOrb() {
|
|
const container = document.getElementById('orbContainer');
|
|
const fallback = document.getElementById('orbFallback');
|
|
|
|
if (!container) {
|
|
console.error('Orb container not found');
|
|
return;
|
|
}
|
|
|
|
// Check if OGL loaded
|
|
if (typeof OGL === 'undefined') {
|
|
console.log('OGL not loaded, using CSS fallback');
|
|
if (fallback) fallback.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
|
|
const { Renderer, Program, Mesh, Triangle, Vec3 } = OGL;
|
|
|
|
const vert = \`
|
|
precision highp float;
|
|
attribute vec2 position;
|
|
attribute vec2 uv;
|
|
varying vec2 vUv;
|
|
void main() {
|
|
vUv = uv;
|
|
gl_Position = vec4(position, 0.0, 1.0);
|
|
}
|
|
\`;
|
|
|
|
const frag = \`
|
|
precision highp float;
|
|
|
|
uniform float iTime;
|
|
uniform vec3 iResolution;
|
|
uniform float hue;
|
|
uniform float hover;
|
|
uniform float rot;
|
|
uniform float hoverIntensity;
|
|
varying vec2 vUv;
|
|
|
|
vec3 rgb2yiq(vec3 c) {
|
|
float y = dot(c, vec3(0.299, 0.587, 0.114));
|
|
float i = dot(c, vec3(0.596, -0.274, -0.322));
|
|
float q = dot(c, vec3(0.211, -0.523, 0.312));
|
|
return vec3(y, i, q);
|
|
}
|
|
|
|
vec3 yiq2rgb(vec3 c) {
|
|
float r = c.x + 0.956 * c.y + 0.621 * c.z;
|
|
float g = c.x - 0.272 * c.y - 0.647 * c.z;
|
|
float b = c.x - 1.106 * c.y + 1.703 * c.z;
|
|
return vec3(r, g, b);
|
|
}
|
|
|
|
vec3 adjustHue(vec3 color, float hueDeg) {
|
|
float hueRad = hueDeg * 3.14159265 / 180.0;
|
|
vec3 yiq = rgb2yiq(color);
|
|
float cosA = cos(hueRad);
|
|
float sinA = sin(hueRad);
|
|
float i = yiq.y * cosA - yiq.z * sinA;
|
|
float q = yiq.y * sinA + yiq.z * cosA;
|
|
yiq.y = i;
|
|
yiq.z = q;
|
|
return yiq2rgb(yiq);
|
|
}
|
|
|
|
vec3 hash33(vec3 p3) {
|
|
p3 = fract(p3 * vec3(0.1031, 0.11369, 0.13787));
|
|
p3 += dot(p3, p3.yxz + 19.19);
|
|
return -1.0 + 2.0 * fract(vec3(
|
|
p3.x + p3.y,
|
|
p3.x + p3.z,
|
|
p3.y + p3.z
|
|
) * p3.zyx);
|
|
}
|
|
|
|
float snoise3(vec3 p) {
|
|
const float K1 = 0.333333333;
|
|
const float K2 = 0.166666667;
|
|
vec3 i = floor(p + (p.x + p.y + p.z) * K1);
|
|
vec3 d0 = p - (i - (i.x + i.y + i.z) * K2);
|
|
vec3 e = step(vec3(0.0), d0 - d0.yzx);
|
|
vec3 i1 = e * (1.0 - e.zxy);
|
|
vec3 i2 = 1.0 - e.zxy * (1.0 - e);
|
|
vec3 d1 = d0 - (i1 - K2);
|
|
vec3 d2 = d0 - (i2 - K1);
|
|
vec3 d3 = d0 - 0.5;
|
|
vec4 h = max(0.6 - vec4(
|
|
dot(d0, d0),
|
|
dot(d1, d1),
|
|
dot(d2, d2),
|
|
dot(d3, d3)
|
|
), 0.0);
|
|
vec4 n = h * h * h * h * vec4(
|
|
dot(d0, hash33(i)),
|
|
dot(d1, hash33(i + i1)),
|
|
dot(d2, hash33(i + i2)),
|
|
dot(d3, hash33(i + 1.0))
|
|
);
|
|
return dot(vec4(31.316), n);
|
|
}
|
|
|
|
vec4 extractAlpha(vec3 colorIn) {
|
|
float a = max(max(colorIn.r, colorIn.g), colorIn.b);
|
|
return vec4(colorIn.rgb / (a + 1e-5), a);
|
|
}
|
|
|
|
const vec3 baseColor1 = vec3(0.611765, 0.262745, 0.996078);
|
|
const vec3 baseColor2 = vec3(0.298039, 0.760784, 0.913725);
|
|
const vec3 baseColor3 = vec3(0.062745, 0.078431, 0.600000);
|
|
const float innerRadius = 0.6;
|
|
const float noiseScale = 0.65;
|
|
|
|
float light1(float intensity, float attenuation, float dist) {
|
|
return intensity / (1.0 + dist * attenuation);
|
|
}
|
|
float light2(float intensity, float attenuation, float dist) {
|
|
return intensity / (1.0 + dist * dist * attenuation);
|
|
}
|
|
|
|
vec4 draw(vec2 uv) {
|
|
vec3 color1 = adjustHue(baseColor1, hue);
|
|
vec3 color2 = adjustHue(baseColor2, hue);
|
|
vec3 color3 = adjustHue(baseColor3, hue);
|
|
|
|
float ang = atan(uv.y, uv.x);
|
|
float len = length(uv);
|
|
float invLen = len > 0.0 ? 1.0 / len : 0.0;
|
|
|
|
float n0 = snoise3(vec3(uv * noiseScale, iTime * 0.5)) * 0.5 + 0.5;
|
|
float r0 = mix(mix(innerRadius, 1.0, 0.4), mix(innerRadius, 1.0, 0.6), n0);
|
|
float d0 = distance(uv, (r0 * invLen) * uv);
|
|
float v0 = light1(1.0, 10.0, d0);
|
|
v0 *= smoothstep(r0 * 1.05, r0, len);
|
|
float cl = cos(ang + iTime * 2.0) * 0.5 + 0.5;
|
|
|
|
float a = iTime * -1.0;
|
|
vec2 pos = vec2(cos(a), sin(a)) * r0;
|
|
float d = distance(uv, pos);
|
|
float v1 = light2(1.5, 5.0, d);
|
|
v1 *= light1(1.0, 50.0, d0);
|
|
|
|
float v2 = smoothstep(1.0, mix(innerRadius, 1.0, n0 * 0.5), len);
|
|
float v3 = smoothstep(innerRadius, mix(innerRadius, 1.0, 0.5), len);
|
|
|
|
vec3 col = mix(color1, color2, cl);
|
|
col = mix(color3, col, v0);
|
|
col = (col + v1) * v2 * v3;
|
|
col = clamp(col, 0.0, 1.0);
|
|
|
|
return extractAlpha(col);
|
|
}
|
|
|
|
vec4 mainImage(vec2 fragCoord) {
|
|
vec2 center = iResolution.xy * 0.5;
|
|
float size = min(iResolution.x, iResolution.y);
|
|
vec2 uv = (fragCoord - center) / size * 2.0;
|
|
|
|
float angle = rot;
|
|
float s = sin(angle);
|
|
float c = cos(angle);
|
|
uv = vec2(c * uv.x - s * uv.y, s * uv.x + c * uv.y);
|
|
|
|
uv.x += hover * hoverIntensity * 0.1 * sin(uv.y * 10.0 + iTime);
|
|
uv.y += hover * hoverIntensity * 0.1 * sin(uv.x * 10.0 + iTime);
|
|
|
|
return draw(uv);
|
|
}
|
|
|
|
void main() {
|
|
vec2 fragCoord = vUv * iResolution.xy;
|
|
vec4 col = mainImage(fragCoord);
|
|
gl_FragColor = vec4(col.rgb * col.a, col.a);
|
|
}
|
|
\`;
|
|
|
|
// Hide fallback and create WebGL orb
|
|
if (fallback) fallback.style.display = 'none';
|
|
|
|
const renderer = new Renderer({ alpha: true, premultipliedAlpha: false });
|
|
const gl = renderer.gl;
|
|
gl.clearColor(0, 0, 0, 0);
|
|
|
|
// Style the canvas
|
|
gl.canvas.style.position = 'absolute';
|
|
gl.canvas.style.top = '0';
|
|
gl.canvas.style.left = '0';
|
|
gl.canvas.style.width = '100%';
|
|
gl.canvas.style.height = '100%';
|
|
gl.canvas.style.opacity = '0.6';
|
|
|
|
container.appendChild(gl.canvas);
|
|
|
|
const geometry = new Triangle(gl);
|
|
const program = new Program(gl, {
|
|
vertex: vert,
|
|
fragment: frag,
|
|
uniforms: {
|
|
iTime: { value: 0 },
|
|
iResolution: {
|
|
value: new Vec3(
|
|
gl.canvas.width,
|
|
gl.canvas.height,
|
|
gl.canvas.width / gl.canvas.height
|
|
),
|
|
},
|
|
hue: { value: 0 },
|
|
hover: { value: 0 },
|
|
rot: { value: 0 },
|
|
hoverIntensity: { value: 0.2 },
|
|
},
|
|
});
|
|
|
|
const mesh = new Mesh(gl, { geometry, program });
|
|
|
|
function resize() {
|
|
if (!container) return;
|
|
const dpr = window.devicePixelRatio || 1;
|
|
const width = container.clientWidth;
|
|
const height = container.clientHeight;
|
|
renderer.setSize(width * dpr, height * dpr);
|
|
gl.canvas.style.width = width + "px";
|
|
gl.canvas.style.height = height + "px";
|
|
program.uniforms.iResolution.value.set(
|
|
gl.canvas.width,
|
|
gl.canvas.height,
|
|
gl.canvas.width / gl.canvas.height
|
|
);
|
|
}
|
|
window.addEventListener("resize", resize);
|
|
resize();
|
|
|
|
let targetHover = 0;
|
|
let lastTime = 0;
|
|
let currentRot = 0;
|
|
const rotationSpeed = 0.3;
|
|
|
|
const handleMouseMove = (e) => {
|
|
const rect = container.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const y = e.clientY - rect.top;
|
|
const width = rect.width;
|
|
const height = rect.height;
|
|
const size = Math.min(width, height);
|
|
const centerX = width / 2;
|
|
const centerY = height / 2;
|
|
const uvX = ((x - centerX) / size) * 2.0;
|
|
const uvY = ((y - centerY) / size) * 2.0;
|
|
|
|
if (Math.sqrt(uvX * uvX + uvY * uvY) < 0.8) {
|
|
targetHover = 1;
|
|
} else {
|
|
targetHover = 0;
|
|
}
|
|
};
|
|
|
|
const handleMouseLeave = () => {
|
|
targetHover = 0;
|
|
};
|
|
|
|
container.addEventListener("mousemove", handleMouseMove);
|
|
container.addEventListener("mouseleave", handleMouseLeave);
|
|
|
|
const update = (t) => {
|
|
requestAnimationFrame(update);
|
|
const dt = (t - lastTime) * 0.001;
|
|
lastTime = t;
|
|
program.uniforms.iTime.value = t * 0.001;
|
|
program.uniforms.hue.value = 0;
|
|
program.uniforms.hoverIntensity.value = 0.2;
|
|
|
|
const effectiveHover = targetHover;
|
|
program.uniforms.hover.value += (effectiveHover - program.uniforms.hover.value) * 0.1;
|
|
|
|
if (effectiveHover > 0.5) {
|
|
currentRot += dt * rotationSpeed;
|
|
}
|
|
program.uniforms.rot.value = currentRot;
|
|
|
|
renderer.render({ scene: mesh });
|
|
};
|
|
requestAnimationFrame(update);
|
|
|
|
console.log('WebGL Orb initialized successfully!');
|
|
|
|
} catch (error) {
|
|
console.error('WebGL Orb failed, using CSS fallback:', error);
|
|
if (fallback) fallback.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
// Modern animated background - no JavaScript needed!
|
|
</script>
|
|
</body>
|
|
</html>`;
|
|
downloadFile(htmlContent, `${baseFileName}.html`, 'text/html');
|
|
break;
|
|
case 'json':
|
|
const jsonData = {
|
|
metadata: {
|
|
fileName: fileName || 'extracted-text',
|
|
extractedAt: new Date().toISOString(),
|
|
characterCount: text.length,
|
|
lineCount: text.split('\n').length,
|
|
wordCount: text.split(/\s+/).filter(word => word.length > 0).length
|
|
},
|
|
content: {
|
|
rawText: text,
|
|
lines: text.split('\n'),
|
|
paragraphs: text.split('\n\n').filter(p => p.trim().length > 0)
|
|
}
|
|
};
|
|
downloadFile(JSON.stringify(jsonData, null, 2), `${baseFileName}.json`, 'application/json');
|
|
break;
|
|
case 'pdf':
|
|
const pdf = new jsPDF();
|
|
|
|
|
|
pdf.setFont('helvetica', 'bold');
|
|
pdf.setFontSize(20);
|
|
pdf.setTextColor(79, 172, 254);
|
|
|
|
|
|
pdf.text(baseFileName, 20, 30);
|
|
|
|
|
|
pdf.setFont('helvetica', 'normal');
|
|
pdf.setFontSize(10);
|
|
pdf.setTextColor(128, 128, 128);
|
|
pdf.text(`Generated by Luna OCR • ${new Date().toLocaleDateString()}`, 20, 40);
|
|
|
|
|
|
pdf.setDrawColor(79, 172, 254);
|
|
pdf.setLineWidth(0.5);
|
|
pdf.line(20, 45, 190, 45);
|
|
|
|
|
|
pdf.setFont('helvetica', 'normal');
|
|
pdf.setFontSize(11);
|
|
pdf.setTextColor(0, 0, 0);
|
|
|
|
const lines = pdf.splitTextToSize(text, 170);
|
|
pdf.text(lines, 20, 55);
|
|
|
|
|
|
const pageCount = pdf.internal.getNumberOfPages();
|
|
for (let i = 1; i <= pageCount; i++) {
|
|
pdf.setPage(i);
|
|
pdf.setFont('helvetica', 'normal');
|
|
pdf.setFontSize(8);
|
|
pdf.setTextColor(128, 128, 128);
|
|
pdf.text(`Luna OCR • Page ${i} of ${pageCount}`, 20, 285);
|
|
}
|
|
|
|
pdf.save(`${baseFileName}.pdf`);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
|
|
const downloadFile = (content, filename, mimeType) => {
|
|
const blob = new Blob([content], { type: mimeType });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = filename;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
const renderContent = () => {
|
|
switch (previewMode) {
|
|
case 'text':
|
|
|
|
return (
|
|
<div className="text-content">
|
|
<pre className="text-preview">{text}</pre>
|
|
</div>
|
|
);
|
|
case 'markdown':
|
|
marked.setOptions({
|
|
breaks: true,
|
|
gfm: true,
|
|
tables: true,
|
|
headerIds: false,
|
|
mangle: false
|
|
});
|
|
return (
|
|
<div
|
|
className="markdown-plain"
|
|
dangerouslySetInnerHTML={{ __html: marked(text) }}
|
|
/>
|
|
);
|
|
case 'preview':
|
|
marked.setOptions({
|
|
breaks: true,
|
|
gfm: true,
|
|
tables: true,
|
|
headerIds: false,
|
|
mangle: false
|
|
});
|
|
return (
|
|
<div
|
|
className="html-preview"
|
|
dangerouslySetInnerHTML={{ __html: marked(text) }}
|
|
/>
|
|
);
|
|
case 'json':
|
|
const jsonData = {
|
|
metadata: {
|
|
fileName: fileName || 'extracted-text',
|
|
extractedAt: new Date().toISOString(),
|
|
characterCount: text.length,
|
|
lineCount: text.split('\n').length,
|
|
wordCount: text.split(/\s+/).filter(word => word.length > 0).length
|
|
},
|
|
content: {
|
|
rawText: text,
|
|
lines: text.split('\n'),
|
|
paragraphs: text.split('\n\n').filter(p => p.trim().length > 0)
|
|
}
|
|
};
|
|
return (
|
|
<SyntaxHighlighter
|
|
language="json"
|
|
style={atomDark}
|
|
customStyle={{
|
|
background: 'rgba(0, 0, 0, 0.3)',
|
|
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
borderRadius: '12px',
|
|
fontSize: '0.875rem',
|
|
lineHeight: '1.6'
|
|
}}
|
|
>
|
|
{JSON.stringify(jsonData, null, 2)}
|
|
</SyntaxHighlighter>
|
|
);
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="results-container">
|
|
{/* Header Section */}
|
|
<div className="results-header">
|
|
<div className="header-content">
|
|
<div className="header-left">
|
|
<h3>Download & Preview</h3>
|
|
<span className="result-info">
|
|
{text.length} characters • {text.split('\n').length} lines • Ready to download
|
|
</span>
|
|
</div>
|
|
|
|
<div className="header-actions">
|
|
<motion.button
|
|
className="action-button"
|
|
onClick={handleCopy}
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
>
|
|
<AnimatePresence mode="wait">
|
|
{copied ? (
|
|
<motion.div key="copied" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}>
|
|
<Check size={16} />
|
|
</motion.div>
|
|
) : (
|
|
<motion.div key="copy" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}>
|
|
<Copy size={16} />
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
{copied ? 'Copied!' : 'Copy'}
|
|
</motion.button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Unified Preview & Download Section */}
|
|
<div className="categories-section">
|
|
<div className="category-group">
|
|
<div className="category-header">
|
|
<Eye size={20} />
|
|
<span className="category-label">PREVIEW & DOWNLOAD</span>
|
|
</div>
|
|
|
|
<div className="format-buttons">
|
|
{[
|
|
{ id: 'text', label: 'Text', icon: <FileText size={24} />, previewId: 'text' },
|
|
{ id: 'md', label: 'Markdown', icon: <Code size={24} />, previewId: 'markdown' },
|
|
{ id: 'html', label: 'HTML', icon: <Eye size={24} />, previewId: 'preview' },
|
|
{ id: 'json', label: 'JSON', icon: <Code size={24} />, previewId: 'json' }
|
|
].map((format) => (
|
|
<motion.div
|
|
key={format.id}
|
|
className={`format-button-container ${previewMode === format.previewId ? 'active' : ''}`}
|
|
whileHover={{ scale: 1.02 }}
|
|
whileTap={{ scale: 0.98 }}
|
|
>
|
|
<button
|
|
className="format-preview-button"
|
|
onClick={() => format.previewId && onPreviewModeChange(format.previewId)}
|
|
disabled={!format.previewId}
|
|
>
|
|
<div className="format-icon">{format.icon}</div>
|
|
<span className="format-label">{format.label}</span>
|
|
</button>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content Preview */}
|
|
<div className="content-preview">
|
|
<motion.button
|
|
className="content-preview-download"
|
|
onClick={() => {
|
|
const formatMap = {
|
|
'text': 'txt',
|
|
'markdown': 'md',
|
|
'preview': 'html',
|
|
'json': 'json'
|
|
};
|
|
handleDownload(formatMap[previewMode] || 'txt');
|
|
}}
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 0.95 }}
|
|
title={`Download current preview as ${previewMode === 'text' ? 'TXT' : previewMode === 'markdown' ? 'MD' : previewMode === 'preview' ? 'HTML' : 'JSON'}`}
|
|
>
|
|
<Download size={16} />
|
|
</motion.button>
|
|
|
|
<AnimatePresence mode="wait">
|
|
<motion.div
|
|
key={previewMode}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -20 }}
|
|
transition={{ duration: 0.3 }}
|
|
>
|
|
{renderContent()}
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ResultsPanel; |