luna_ocr / src /components /ResultsPanel.js
veela4's picture
Add files using upload-large-folder tool
373c769 verified
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();
// Set up styling
pdf.setFont('helvetica', 'bold');
pdf.setFontSize(20);
pdf.setTextColor(79, 172, 254); // Luna OCR blue
// Add title
pdf.text(baseFileName, 20, 30);
// Add subtitle
pdf.setFont('helvetica', 'normal');
pdf.setFontSize(10);
pdf.setTextColor(128, 128, 128);
pdf.text(`Generated by Luna OCR • ${new Date().toLocaleDateString()}`, 20, 40);
// Add separator line
pdf.setDrawColor(79, 172, 254);
pdf.setLineWidth(0.5);
pdf.line(20, 45, 190, 45);
// Add content
pdf.setFont('helvetica', 'normal');
pdf.setFontSize(11);
pdf.setTextColor(0, 0, 0);
const lines = pdf.splitTextToSize(text, 170);
pdf.text(lines, 20, 55);
// Add footer
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':
// Always show raw text in text mode
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;