Migueldiaz1 commited on
Commit
611896f
verified
1 Parent(s): bacd740

Upload 16 files

Browse files
.gitattributes CHANGED
@@ -35,3 +35,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  metadata.json filter=lfs diff=lfs merge=lfs -text
37
  metadata_text.json filter=lfs diff=lfs merge=lfs -text
 
 
 
 
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  metadata.json filter=lfs diff=lfs merge=lfs -text
37
  metadata_text.json filter=lfs diff=lfs merge=lfs -text
38
+ frontend/public/images/atlases.png filter=lfs diff=lfs merge=lfs -text
39
+ frontend/public/images/internet.png filter=lfs diff=lfs merge=lfs -text
40
+ frontend/public/images/PIPELINE.png filter=lfs diff=lfs merge=lfs -text
frontend/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>mirage-web</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "mirage-web",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "clsx": "^2.1.1",
14
+ "framer-motion": "^12.23.24",
15
+ "lucide-react": "^0.554.0",
16
+ "react": "^19.2.0",
17
+ "react-dom": "^19.2.0",
18
+ "tailwind-merge": "^3.4.0"
19
+ },
20
+ "devDependencies": {
21
+ "@eslint/js": "^9.39.1",
22
+ "@types/react": "^19.2.5",
23
+ "@types/react-dom": "^19.2.3",
24
+ "@vitejs/plugin-react": "^5.1.1",
25
+ "autoprefixer": "^10.4.22",
26
+ "eslint": "^9.39.1",
27
+ "eslint-plugin-react-hooks": "^7.0.1",
28
+ "eslint-plugin-react-refresh": "^0.4.24",
29
+ "globals": "^16.5.0",
30
+ "postcss": "^8.5.6",
31
+ "tailwindcss": "^3.4.17",
32
+ "vite": "npm:[email protected]"
33
+ },
34
+ "overrides": {
35
+ "vite": "npm:[email protected]"
36
+ }
37
+ }
frontend/postcss.config.cjs ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
frontend/public/images/PIPELINE.png ADDED

Git LFS Details

  • SHA256: 69f7f36624c7ecddb980265deb8c7766f2048dcba1cd274851fd85aab68ae5fb
  • Pointer size: 131 Bytes
  • Size of remote file: 430 kB
frontend/public/images/atlases.png ADDED

Git LFS Details

  • SHA256: c17b57a815d588ec4b9eec242b01cc197978515a20e9597cc181443f8febb55c
  • Pointer size: 131 Bytes
  • Size of remote file: 433 kB
frontend/public/images/dual_search.png ADDED
frontend/public/images/internet.png ADDED

Git LFS Details

  • SHA256: 15c349cd7e85bb24c6ef42be746a2948477336b8b3b9c911d6b0579c543098a3
  • Pointer size: 131 Bytes
  • Size of remote file: 425 kB
frontend/public/vite.svg ADDED
frontend/src/App.css ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #root {
2
+ max-width: 1280px;
3
+ margin: 0 auto;
4
+ padding: 2rem;
5
+ text-align: center;
6
+ }
7
+
8
+ .logo {
9
+ height: 6em;
10
+ padding: 1.5em;
11
+ will-change: filter;
12
+ transition: filter 300ms;
13
+ }
14
+ .logo:hover {
15
+ filter: drop-shadow(0 0 2em #646cffaa);
16
+ }
17
+ .logo.react:hover {
18
+ filter: drop-shadow(0 0 2em #61dafbaa);
19
+ }
20
+
21
+ @keyframes logo-spin {
22
+ from {
23
+ transform: rotate(0deg);
24
+ }
25
+ to {
26
+ transform: rotate(360deg);
27
+ }
28
+ }
29
+
30
+ @media (prefers-reduced-motion: no-preference) {
31
+ a:nth-of-type(2) .logo {
32
+ animation: logo-spin infinite 20s linear;
33
+ }
34
+ }
35
+
36
+ .card {
37
+ padding: 2em;
38
+ }
39
+
40
+ .read-the-docs {
41
+ color: #888;
42
+ }
frontend/src/App.jsx ADDED
@@ -0,0 +1,1212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useMemo, useRef } from 'react';
2
+ import {
3
+ Search, Plus, Minus, Zap, Image as ImageIcon, Loader2, Sparkles,
4
+ Command, Activity, FileText, ArrowRight, Award, BrainCircuit,
5
+ ScanEye, SplitSquareHorizontal, Info, Sliders, Gauge,
6
+ X, Globe, Check, Link as LinkIcon, Lightbulb, BookOpen, Layers,
7
+ Network, GraduationCap, ChevronLeft, MonitorPlay, Database, Wand2,
8
+ AlignLeft, Share2, Infinity as InfinityIcon, MousePointerClick, Maximize2, Flame,
9
+ ArrowDown, Play, Youtube, ExternalLink
10
+ } from 'lucide-react';
11
+ import { motion, AnimatePresence, useScroll, useTransform, useSpring } from 'framer-motion';
12
+ import { clsx } from 'clsx';
13
+ import { twMerge } from 'tailwind-merge';
14
+
15
+ // --- CONFIGURATION ---
16
+ const API_URL = ""; //https://migueldiaz1-mirage-backend.hf.space
17
+ const PROJECT_URL = "https://arxiv.org/abs/2400.00000";
18
+ const KAGGLE_URL = "https://www.kaggle.com/code/migueldazbenito/mirage";
19
+ const YOUTUBE_VIDEO_ID = "GlPMrBWtZoQ";
20
+
21
+ // --- DATA (ENGLISH) ---
22
+ const MEDICAL_PROMPTS = [
23
+ { title: "Glioblastoma Multiforme", text: "MRI of the brain showing a large necrotic glioblastoma in the frontal lobe with edema." },
24
+ { title: "Liver Metastasis", text: "CT scan of the abdomen showing multiple hypodense metastatic lesions in the liver." },
25
+ { title: "Pleural Effusion", text: "Chest radiograph showing blunting of the costophrenic angle indicating pleural effusion." }
26
+ ];
27
+
28
+ const DUAL_ARITHMETIC_EXAMPLES = [
29
+ { title: "Bone Suppression", query: "Chest X-Ray", add: "Soft Tissue", sub: "Bones", desc: "Isolates lung tissue by subtracting bone structures." },
30
+ { title: "Infection Highlight", query: "Lungs X-Ray", add: "Pneumonia", sub: "Clear Lungs", desc: "Visualizes the difference between healthy and infected tissue." },
31
+ { title: "Cardiac Pacemaker", query: "Chest X-ray", add: "Pacemaker generator and leads", sub: "No foreign bodies", desc: "Add a pacemaker to the chest X-ray." }
32
+ ];
33
+
34
+ // --- UTILS ---
35
+ function cn(...inputs) {
36
+ return twMerge(clsx(inputs));
37
+ }
38
+
39
+ const RichText = ({ text, className }) => {
40
+ if (!text) return null;
41
+ const parts = text.split(/(\*\*.*?\*\*)/g);
42
+ return (
43
+ <span className={className}>
44
+ {parts.map((part, i) => {
45
+ if (part.startsWith('**') && part.endsWith('**')) {
46
+ return <strong key={i} className="font-bold text-orange-400">{part.slice(2, -2)}</strong>;
47
+ }
48
+ return part;
49
+ })}
50
+ </span>
51
+ );
52
+ };
53
+
54
+ // --- ANIMATION VARIANTS ---
55
+ const pageTransition = {
56
+ initial: { opacity: 0, y: 20 },
57
+ animate: { opacity: 1, y: 0 },
58
+ exit: { opacity: 0, y: -20 },
59
+ transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] }
60
+ };
61
+
62
+ const staggerContainer = {
63
+ hidden: { opacity: 0 },
64
+ show: { opacity: 1, transition: { staggerChildren: 0.1 } }
65
+ };
66
+
67
+ const cardVariant = {
68
+ hidden: { opacity: 0, y: 20, scale: 0.98 },
69
+ show: { opacity: 1, y: 0, scale: 1, transition: { type: "spring", stiffness: 40, damping: 10 } }
70
+ };
71
+
72
+ // --- VISUAL COMPONENTS ---
73
+
74
+ const OpticalGradient = () => {
75
+ return (
76
+ <div className="absolute inset-0 overflow-hidden bg-[#050505] pointer-events-none">
77
+ <motion.div
78
+ animate={{ x: ["-30%", "20%", "-30%"], y: ["-30%", "20%", "-30%"], scale: [1, 1.5, 1] }}
79
+ transition={{ duration: 8, repeat: Infinity, ease: "easeInOut" }}
80
+ className="absolute top-1/4 left-1/4 w-[50vw] h-[50vw] bg-orange-600/30 rounded-full blur-[100px] mix-blend-screen"
81
+ />
82
+ <motion.div
83
+ animate={{ x: ["20%", "-10%", "20%"], y: ["10%", "-30%", "10%"], scale: [1.2, 0.8, 1.2] }}
84
+ transition={{ duration: 10, repeat: Infinity, ease: "easeInOut" }}
85
+ className="absolute top-1/3 right-1/4 w-[40vw] h-[40vw] bg-red-600/20 rounded-full blur-[90px] mix-blend-screen"
86
+ />
87
+ <div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-[0.07]" />
88
+ </div>
89
+ );
90
+ };
91
+
92
+ const BreathingTitle = () => {
93
+ const phrase = [
94
+ { char: "M", type: "standard" }, { char: "I", type: "standard" }, { char: "R", type: "standard" },
95
+ { char: "A", type: "standard" }, { char: "G", type: "standard" }, { char: "E", type: "standard" },
96
+ { char: "\u00A0", type: "space" },
97
+ { char: "O", type: "highlight" }, { char: "S", type: "highlight" },
98
+ ];
99
+
100
+ return (
101
+ <h1 className="text-5xl md:text-8xl font-bold tracking-tight leading-none drop-shadow-2xl flex justify-center cursor-default group">
102
+ {phrase.map((item, index) => (
103
+ <motion.span
104
+ key={index}
105
+ initial={{ y: 0, scale: 1, filter: "brightness(1)" }}
106
+ animate={{
107
+ y: [0, -8, 0], scale: [1, 1.05, 1], filter: ["brightness(1)", "brightness(1.3)", "brightness(1)"],
108
+ textShadow: ["0 0 0px rgba(249, 115, 22, 0)", "0 0 20px rgba(249, 115, 22, 0.4)", "0 0 0px rgba(249, 115, 22, 0)"]
109
+ }}
110
+ transition={{ duration: 4, repeat: Infinity, ease: "easeInOut", delay: index * 0.15 }}
111
+ className={cn("inline-block origin-bottom", item.type === "highlight" ? "text-transparent bg-clip-text bg-gradient-to-r from-orange-400 to-red-600" : "text-white")}
112
+ >
113
+ {item.char}
114
+ </motion.span>
115
+ ))}
116
+ </h1>
117
+ );
118
+ };
119
+
120
+ const BrainstormingBackground = () => {
121
+ const MEDICAL_TERMS = [
122
+ "Pneumonia", "Atelectasis", "Cardiomegaly", "Effusion", "Infiltration", "Mass", "Nodule",
123
+ "Pneumothorax", "Consolidation", "Edema", "Emphysema", "Fibrosis", "Hernia", "Fracture"
124
+ ];
125
+ const colors = ["bg-orange-500/20 text-orange-200 border-orange-500/30", "bg-red-500/20 text-red-200 border-red-500/30"];
126
+
127
+ const floatingWords = useMemo(() => MEDICAL_TERMS.map((term, i) => ({
128
+ text: term,
129
+ x: Math.random() * 80 - 40,
130
+ y: Math.random() * 80 - 40,
131
+ scale: Math.random() * 0.4 + 0.8,
132
+ delay: Math.random() * 20,
133
+ colorClass: colors[i % colors.length]
134
+ })), []);
135
+
136
+ return (
137
+ <div className="absolute inset-0 overflow-hidden pointer-events-none flex items-center justify-center">
138
+ {floatingWords.map((word, i) => (
139
+ <motion.div
140
+ key={i}
141
+ className={`absolute px-4 py-1.5 rounded-full border backdrop-blur-sm text-xs font-bold uppercase tracking-widest whitespace-nowrap shadow-lg ${word.colorClass}`}
142
+ initial={{ x: `${word.x}vw`, y: `${word.y}vh`, scale: 0, opacity: 0 }}
143
+ animate={{ scale: [0, word.scale, 0], opacity: [0, 0.9, 0], y: [`${word.y}vh`, `${word.y - 15}vh`] }}
144
+ transition={{ duration: 5 + Math.random() * 5, repeat: Infinity, repeatDelay: Math.random() * 4, delay: word.delay, ease: "easeInOut" }}
145
+ >
146
+ {word.text}
147
+ </motion.div>
148
+ ))}
149
+ </div>
150
+ );
151
+ };
152
+
153
+ const KaggleLogo = ({ className }) => (
154
+ <svg viewBox="0 0 24 24" fill="currentColor" className={className}>
155
+ <path d="M18.825 23.859c-.022.092-.117.141-.283.141h-3.139c-.187 0-.351-.082-.492-.248l-5.16-6.59-1.398 1.434v5.204c0 .165-.116.299-.348.401-.06.027-.123.04-.188.04H5.206c-.165 0-.3-.135-.4-.403l-.004-.055V.453c0-.165.116-.3.348-.401.06-.027.123-.04.188-.04h2.611c.165 0 .3.135.4.403l.004.055v15.702l6.23-7.797c.143-.179.313-.268.511-.268h3.333c.174 0 .274.053.3.16.025.105-.01.21-.106.314l-6.31 7.027 6.643 7.824c.104.127.142.235.114.341z"/>
156
+ </svg>
157
+ );
158
+
159
+ // ==========================================
160
+ // [PREMIUM COMPONENT] Paper Explanation Page
161
+ // ==========================================
162
+ const PaperExplanation = ({ onBack }) => {
163
+ const containerRef = useRef(null);
164
+ const { scrollYProgress } = useScroll({ container: containerRef });
165
+ const scaleX = useSpring(scrollYProgress, { stiffness: 100, damping: 30, restDelta: 0.001 });
166
+
167
+ const FigurePlaceholder = ({ label, className, src, imageClassName }) => (
168
+ <div className={cn("relative group w-full aspect-video rounded-3xl overflow-hidden bg-zinc-950 border border-zinc-800 flex items-center justify-center transition-all duration-500 hover:border-orange-500/30 hover:shadow-[0_0_50px_-12px_rgba(249,115,22,0.3)]", className)}>
169
+ <div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-[0.05]" />
170
+
171
+ {src ? (
172
+ <img
173
+ src={src}
174
+ alt={label}
175
+ className={cn("w-full h-full object-cover opacity-90 group-hover:opacity-100 transition-all duration-700 group-hover:scale-105", imageClassName)}
176
+ />
177
+ ) : (
178
+ <div className="text-center p-6 transition-transform duration-500 group-hover:scale-105 relative z-10">
179
+ <div className="w-16 h-16 mx-auto mb-4 rounded-full bg-zinc-900 border border-zinc-800 flex items-center justify-center group-hover:bg-orange-500/10 group-hover:border-orange-500/30 transition-all">
180
+ <ImageIcon className="w-6 h-6 text-zinc-500 group-hover:text-orange-400 transition-colors" />
181
+ </div>
182
+ <p className="text-zinc-500 text-sm font-mono uppercase tracking-widest group-hover:text-zinc-300 transition-colors">{label}</p>
183
+ <p className="text-zinc-600 text-xs mt-2 group-hover:text-orange-500/70 transition-colors">Drag or insert figure here</p>
184
+ </div>
185
+ )}
186
+
187
+ <div className="absolute inset-0 bg-gradient-to-tr from-orange-500/0 via-orange-500/0 to-orange-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" />
188
+ </div>
189
+ );
190
+
191
+ const AppleSection = ({ children, className, gradient = "none" }) => (
192
+ <section className={cn("min-h-screen w-full flex flex-col items-center justify-center p-6 py-24 relative overflow-hidden bg-black", className)}>
193
+ {gradient === "hero" && (
194
+ <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-orange-900/20 via-black to-black opacity-60" />
195
+ )}
196
+ {gradient === "subtle" && (
197
+ <div className="absolute inset-0 bg-gradient-to-b from-black via-red-950/10 to-black" />
198
+ )}
199
+ {gradient === "amber" && (
200
+ <div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-amber-900/20 via-black to-black" />
201
+ )}
202
+ <div className="max-w-7xl w-full z-10">{children}</div>
203
+ </section>
204
+ );
205
+
206
+ return (
207
+ <motion.div
208
+ initial={{ opacity: 0 }}
209
+ animate={{ opacity: 1 }}
210
+ exit={{ opacity: 0, transition: { duration: 0.5 } }}
211
+ className="fixed inset-0 z-50 bg-black font-sans"
212
+ >
213
+ <motion.div style={{ scaleX }} className="fixed top-0 left-0 right-0 h-1 bg-gradient-to-r from-orange-500 via-red-500 to-amber-500 origin-left z-50 shadow-[0_0_20px_rgba(249,115,22,0.5)]" />
214
+
215
+ <button
216
+ onClick={onBack}
217
+ className="fixed top-8 left-8 z-50 p-4 bg-black/40 backdrop-blur-xl border border-white/10 rounded-full hover:bg-orange-500/10 hover:border-orange-500/30 hover:text-orange-400 transition-all group"
218
+ >
219
+ <ChevronLeft className="w-6 h-6 text-zinc-400 group-hover:text-orange-400 transition-colors" />
220
+ </button>
221
+
222
+ <div
223
+ ref={containerRef}
224
+ className="h-full overflow-y-scroll scroll-smooth custom-scrollbar"
225
+ >
226
+ {/* 01. HERO */}
227
+ <AppleSection gradient="hero">
228
+ <motion.div
229
+ initial={{ opacity: 0, scale: 0.9 }}
230
+ whileInView={{ opacity: 1, scale: 1 }}
231
+ transition={{ duration: 1, ease: [0.16, 1, 0.3, 1] }}
232
+ className="text-center space-y-8 relative w-full flex flex-col items-center justify-center min-h-[60vh]"
233
+ >
234
+ <div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full border border-orange-500/30 bg-orange-500/10 text-orange-400 text-xs font-medium uppercase tracking-[0.2em] backdrop-blur-md shadow-[0_0_30px_-10px_rgba(249,115,22,0.3)]">
235
+ <Award className="w-3 h-3" /> MICCAI 2025 Paper
236
+ </div>
237
+ <h1 className="text-5xl md:text-8xl font-bold tracking-tighter text-transparent bg-clip-text bg-gradient-to-r from-orange-400 via-red-500 to-orange-400 drop-shadow-2xl leading-tight">
238
+ MIRAGE
239
+ </h1>
240
+ <p className="text-lg md:text-2xl text-zinc-400 font-light max-w-2xl mx-auto leading-relaxed mb-8">
241
+ Redefining medical education through <br/> <span className="text-orange-200 font-normal">Multimodal Artificial Intelligence</span>.
242
+ </p>
243
+
244
+ <div className="pt-24 flex flex-col items-center gap-3 opacity-60 hover:opacity-100 transition-opacity">
245
+ <span className="text-[10px] uppercase tracking-[0.3em] text-zinc-500">Scroll to Explore</span>
246
+ <ArrowDown className="w-6 h-6 text-orange-500 animate-bounce" />
247
+ </div>
248
+ </motion.div>
249
+ </AppleSection>
250
+
251
+ {/* 02. PROBLEM: ATLASES */}
252
+ <AppleSection gradient="subtle">
253
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
254
+ <motion.div
255
+ initial={{ opacity: 0, x: -50 }}
256
+ whileInView={{ opacity: 1, x: 0 }}
257
+ transition={{ duration: 0.8 }}
258
+ className="space-y-8"
259
+ >
260
+ <h2 className="text-4xl md:text-5xl font-bold text-white tracking-tight">
261
+ The <span className="text-red-500">Static</span> Trap.
262
+ </h2>
263
+ <p className="text-lg text-zinc-400 leading-relaxed">
264
+ Traditional medical atlases are the gold standard for accuracy, yet they fail in the modern era. They are prohibitively expensive, cumbersome to update, and present only a single, idealized view of pathologies.
265
+ </p>
266
+ <ul className="space-y-4 pt-4">
267
+ <li className="flex items-center gap-3 text-zinc-300">
268
+ <X className="w-5 h-5 text-red-500" /> Fixed viewpoints, no 3D rotation.
269
+ </li>
270
+ <li className="flex items-center gap-3 text-zinc-300">
271
+ <X className="w-5 h-5 text-red-500" /> Limited rare case examples.
272
+ </li>
273
+ <li className="flex items-center gap-3 text-zinc-300">
274
+ <X className="w-5 h-5 text-red-500" /> Zero interactivity for students.
275
+ </li>
276
+ </ul>
277
+ </motion.div>
278
+ <motion.div
279
+ initial={{ opacity: 0, scale: 0.95 }}
280
+ whileInView={{ opacity: 1, scale: 1 }}
281
+ transition={{ duration: 0.8, delay: 0.2 }}
282
+ >
283
+ <FigurePlaceholder
284
+ label="Figure 1a: Static Atlas Limitations"
285
+ src="images/atlases.png"
286
+ className="w-full aspect-auto h-auto bg-transparent border-none shadow-none"
287
+ imageClassName="object-contain h-auto relative"
288
+ />
289
+ </motion.div>
290
+ </div>
291
+ </AppleSection>
292
+
293
+ {/* 03. PROBLEM: INTERNET */}
294
+ <AppleSection>
295
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
296
+ <motion.div
297
+ initial={{ opacity: 0, scale: 0.95 }}
298
+ whileInView={{ opacity: 1, scale: 1 }}
299
+ transition={{ duration: 0.8, delay: 0.2 }}
300
+ className="order-2 lg:order-1"
301
+ >
302
+ <FigurePlaceholder
303
+ label="Figure 1b: Search Engine Noise"
304
+ src="images/internet.png"
305
+ className="w-full aspect-auto h-auto bg-transparent border-none shadow-none"
306
+ imageClassName="object-contain h-auto relative"
307
+ />
308
+ </motion.div>
309
+ <motion.div
310
+ initial={{ opacity: 0, x: 50 }}
311
+ whileInView={{ opacity: 1, x: 0 }}
312
+ transition={{ duration: 0.8 }}
313
+ className="space-y-8 order-1 lg:order-2"
314
+ >
315
+ <h2 className="text-4xl md:text-5xl font-bold text-white tracking-tight">
316
+ The <span className="text-orange-500">Reliability</span> Gap.
317
+ </h2>
318
+ <p className="text-lg text-zinc-400 leading-relaxed">
319
+ Turning to Google Images provides speed but sacrifices trust. Results are often mislabeled, lack clinical context, or originate from unverified sources, posing a risk for medical education.
320
+ </p>
321
+ <div className="grid grid-cols-2 gap-4 pt-4">
322
+ <div className="p-4 rounded-xl bg-zinc-900 border border-zinc-800">
323
+ <Database className="w-6 h-6 text-orange-500 mb-2" />
324
+ <div className="text-sm font-bold text-white">Uncurated Data</div>
325
+ </div>
326
+ <div className="p-4 rounded-xl bg-zinc-900 border border-zinc-800">
327
+ <Globe className="w-6 h-6 text-orange-500 mb-2" />
328
+ <div className="text-sm font-bold text-white">Context Loss</div>
329
+ </div>
330
+ </div>
331
+ </motion.div>
332
+ </div>
333
+ </AppleSection>
334
+
335
+ {/* 04. THE ARCHITECTURE */}
336
+ <AppleSection gradient="amber">
337
+ <div className="flex flex-col h-full justify-center w-full">
338
+ <motion.div
339
+ initial={{ opacity: 0, y: 30 }}
340
+ whileInView={{ opacity: 1, y: 0 }}
341
+ transition={{ duration: 0.8 }}
342
+ className="text-center mb-16"
343
+ >
344
+ <h2 className="text-4xl md:text-6xl font-bold text-white mb-6">Unified Architecture</h2>
345
+ <p className="text-lg text-zinc-400 max-w-2xl mx-auto">
346
+ CLIP + ROCO + Stable Diffusion. Orchestrated in a shared latent space.
347
+ </p>
348
+ </motion.div>
349
+
350
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
351
+ {[
352
+ { title: "Retrieval", sub: "CLIP MedICaT", icon: Search, color: "orange" },
353
+ { title: "Generation", sub: "Prompt2MedImage", icon: Wand2, color: "red" },
354
+ { title: "Description", sub: "Dolly-v2 LLM", icon: AlignLeft, color: "amber" }
355
+ ].map((item, i) => (
356
+ <motion.div
357
+ key={i}
358
+ initial={{ opacity: 0, y: 20 }}
359
+ whileInView={{ opacity: 1, y: 0 }}
360
+ transition={{ duration: 0.6, delay: i * 0.1 }}
361
+ className="group p-8 rounded-3xl bg-zinc-900/40 border border-zinc-800 backdrop-blur-lg hover:border-orange-500/30 hover:bg-orange-950/10 transition-colors"
362
+ >
363
+ <div className={`w-12 h-12 rounded-xl bg-${item.color}-500/10 border border-${item.color}-500/20 flex items-center justify-center mb-6`}>
364
+ <item.icon className={`w-6 h-6 text-${item.color}-500`} />
365
+ </div>
366
+ <h3 className="text-xl font-bold text-white mb-2">{item.title}</h3>
367
+ <p className="text-zinc-500 font-mono text-sm group-hover:text-zinc-400 transition-colors">{item.sub}</p>
368
+ </motion.div>
369
+ ))}
370
+ </div>
371
+
372
+ <div className="mt-20 w-full max-w-4xl mx-auto">
373
+ <FigurePlaceholder
374
+ label="Figure 2: Pipeline Architecture Diagram"
375
+ src="images/PIPELINE.png"
376
+ className="w-full aspect-auto h-auto bg-transparent border-none shadow-none"
377
+ imageClassName="object-contain h-auto relative"
378
+ />
379
+ </div>
380
+ </div>
381
+ </AppleSection>
382
+
383
+ {/* 05. DUAL SEARCH */}
384
+ <AppleSection>
385
+ <div className="max-w-5xl w-full flex flex-col items-center">
386
+ <div className="mb-12 text-center">
387
+ <span className="text-orange-500 font-mono text-sm uppercase tracking-widest mb-4 block flex items-center justify-center gap-2">
388
+ <Flame className="w-4 h-4" /> Key Feature
389
+ </span>
390
+ <h2 className="text-4xl md:text-7xl font-bold text-white tracking-tighter">
391
+ Latent Arithmetic
392
+ </h2>
393
+ </div>
394
+
395
+ <div className="w-full grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
396
+ <div className="space-y-6">
397
+ <p className="text-xl text-zinc-300 font-light leading-normal">
398
+ We enable users to <span className="text-red-400 font-medium">"subtract"</span> concepts (like bones) and <span className="text-amber-400 font-medium">"add"</span> others (like soft tissue) mathematically.
399
+ </p>
400
+ <div className="p-6 rounded-2xl bg-zinc-900/80 border border-zinc-800 font-mono text-lg text-zinc-400 shadow-2xl">
401
+ Query<span className="text-red-500 font-bold">-Bones</span><span className="text-amber-500 font-bold">+Tissue</span> = Result
402
+ </div>
403
+ </div>
404
+ <div className="relative">
405
+ <div className="absolute inset-0 bg-orange-600/10 blur-[120px] rounded-full" />
406
+ <FigurePlaceholder
407
+ label="Figure 3: Dual Search Visual Example"
408
+ src="images/dual_search.png"
409
+ className="relative z-10 w-full aspect-auto h-auto shadow-2xl border-none bg-transparent"
410
+ imageClassName="object-contain h-auto relative"
411
+ />
412
+ </div>
413
+ </div>
414
+ </div>
415
+ </AppleSection>
416
+
417
+ {/* 06. CALL TO ACTION */}
418
+ <AppleSection gradient="hero">
419
+ <div className="w-full max-w-7xl flex flex-col items-center gap-12">
420
+
421
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-12 w-full items-center">
422
+
423
+ <motion.div
424
+ initial={{ opacity: 0, x: -20 }}
425
+ whileInView={{ opacity: 1, x: 0 }}
426
+ transition={{ duration: 0.8 }}
427
+ className="relative w-full aspect-video rounded-2xl overflow-hidden border border-zinc-800 shadow-[0_0_50px_-10px_rgba(249,115,22,0.15)] group bg-black"
428
+ >
429
+ <iframe
430
+ className="w-full h-full opacity-80 group-hover:opacity-100 transition-opacity duration-500"
431
+ src={`https://www.youtube.com/embed/${YOUTUBE_VIDEO_ID}`}
432
+ title="MIRAGE Explanation"
433
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
434
+ allowFullScreen
435
+ ></iframe>
436
+ </motion.div>
437
+
438
+ <motion.div
439
+ initial={{ opacity: 0, x: 20 }}
440
+ whileInView={{ opacity: 1, x: 0 }}
441
+ transition={{ duration: 0.8, delay: 0.2 }}
442
+ className="text-center lg:text-left space-y-8"
443
+ >
444
+ <div className="flex flex-col lg:flex-row items-center lg:items-start gap-6">
445
+ <div className="w-20 h-20 bg-white/5 rounded-2xl flex items-center justify-center border border-white/10 backdrop-blur shadow-2xl shrink-0">
446
+ <KaggleLogo className="w-10 h-10 text-blue-400" />
447
+ </div>
448
+ <div>
449
+ <h2 className="text-4xl md:text-6xl font-bold text-white tracking-tight">Run on Kaggle.</h2>
450
+ <div className="h-1 w-20 bg-gradient-to-r from-blue-500 to-cyan-400 rounded-full mt-4 mx-auto lg:mx-0" />
451
+ </div>
452
+ </div>
453
+
454
+ <p className="text-lg text-zinc-400 max-w-lg mx-auto lg:mx-0 leading-relaxed">
455
+ Experience the full MIRAGE pipeline with zero setup cost using Kaggle's free GPU infrastructure. Reproduce our MICCAI 2025 results instantly.
456
+ </p>
457
+ </motion.div>
458
+ </div>
459
+
460
+ <motion.div
461
+ initial={{ opacity: 0, y: 20 }}
462
+ whileInView={{ opacity: 1, y: 0 }}
463
+ transition={{ duration: 0.8, delay: 0.4 }}
464
+ className="flex flex-col md:flex-row justify-center gap-6 items-center pt-8 border-t border-zinc-800/50 w-full"
465
+ >
466
+ <a
467
+ href={KAGGLE_URL}
468
+ target="_blank"
469
+ className="group relative px-10 py-5 bg-white text-black rounded-full font-bold text-lg overflow-hidden transition-transform hover:scale-105 shadow-xl"
470
+ >
471
+ <span className="relative z-10 flex items-center gap-2">
472
+ <KaggleLogo className="w-5 h-5" /> Open Notebook
473
+ </span>
474
+ <div className="absolute inset-0 bg-gradient-to-r from-blue-400 to-cyan-400 opacity-0 group-hover:opacity-20 transition-opacity" />
475
+ </a>
476
+
477
+ <button onClick={onBack} className="px-10 py-5 text-zinc-400 hover:text-white font-medium text-lg transition-colors flex items-center gap-2">
478
+ <ChevronLeft className="w-4 h-4" /> Return to App
479
+ </button>
480
+ </motion.div>
481
+ </div>
482
+ </AppleSection>
483
+
484
+ </div>
485
+ </motion.div>
486
+ );
487
+ };
488
+
489
+
490
+ // --- WELCOME SCREEN (MODIFIED) ---
491
+ const WelcomeScreen = ({ onStart, onOpenPaper }) => {
492
+ return (
493
+ <motion.div
494
+ className="min-h-screen flex flex-col items-center justify-center relative overflow-hidden text-white selection:bg-orange-500/30 py-10 font-sans"
495
+ initial="initial" animate="animate" exit="exit"
496
+ >
497
+ <OpticalGradient />
498
+ <div className="container max-w-5xl px-6 relative z-10 flex flex-col items-center text-center space-y-12">
499
+ <motion.div variants={pageTransition} className="flex items-center gap-3 bg-zinc-900/80 border border-zinc-700/50 rounded-full px-5 py-2 backdrop-blur-md shadow-lg">
500
+ <Award className="w-4 h-4 text-orange-400" />
501
+ <span className="text-xs font-semibold text-zinc-200 tracking-wide uppercase">
502
+ The Fourth Workshop on Applications of Medical AI (AMAI) <span className="text-zinc-500 mx-1">|</span> MICCAI 2025
503
+ </span>
504
+ </motion.div>
505
+ <motion.div variants={pageTransition} className="space-y-4">
506
+ <BreathingTitle />
507
+ <p className="text-xl md:text-2xl text-zinc-300 max-w-2xl mx-auto leading-relaxed font-light">
508
+ Retrieval and Generation of Multimodal Images <br/> & Texts for Medical Education
509
+ </p>
510
+ </motion.div>
511
+ <motion.div variants={pageTransition} className="grid grid-cols-1 md:grid-cols-2 gap-6 w-full max-w-3xl">
512
+ <div className="bg-zinc-900/40 border border-zinc-800 hover:border-zinc-600 p-6 rounded-2xl flex flex-col items-center gap-4 transition-colors duration-300 backdrop-blur-sm">
513
+ <div className="h-12 flex items-center gap-3 opacity-90">
514
+ <BrainCircuit className="w-8 h-8 text-zinc-300" />
515
+ <div className="text-left">
516
+ <div className="text-sm font-bold text-zinc-100">FAST INFERENCE</div>
517
+ <div className="text-[10px] font-bold text-zinc-400 uppercase tracking-wider">Visual Only Retrieval</div>
518
+ </div>
519
+ </div>
520
+ <p className="text-xs text-zinc-400 leading-relaxed font-medium">
521
+ Optimized for speed. Retrieves medical imagery using pure visual vector similarity (CLIP).
522
+ </p>
523
+ </div>
524
+ <div className="bg-zinc-900/40 border border-zinc-800 hover:border-zinc-600 p-6 rounded-2xl flex flex-col items-center gap-4 transition-colors duration-300 backdrop-blur-sm">
525
+ <div className="h-12 flex items-center gap-3 opacity-90">
526
+ <ScanEye className="w-8 h-8 text-zinc-300" />
527
+ <div className="text-left">
528
+ <div className="text-sm font-bold text-zinc-100">ROCO DATASET</div>
529
+ <div className="text-[10px] font-bold text-zinc-400 uppercase tracking-wider">Radiology Optimized</div>
530
+ </div>
531
+ </div>
532
+ <p className="text-xs text-zinc-400 leading-relaxed font-medium">
533
+ Built on a foundation of thousands of annotated radiology images form PubMed.
534
+ </p>
535
+ </div>
536
+ </motion.div>
537
+
538
+ <div className="flex items-center gap-4">
539
+ <motion.button
540
+ onClick={onOpenPaper}
541
+ variants={pageTransition}
542
+ whileHover={{ scale: 1.05 }}
543
+ whileTap={{ scale: 0.95 }}
544
+ className="px-8 py-4 bg-zinc-900 border border-zinc-700 hover:border-zinc-500 text-zinc-300 rounded-full font-bold tracking-wide uppercase text-xs flex items-center gap-2 transition-all"
545
+ >
546
+ <BookOpen className="w-4 h-4" /> Explain Paper
547
+ </motion.button>
548
+
549
+ <motion.button
550
+ variants={pageTransition}
551
+ whileHover={{ scale: 1.05 }}
552
+ whileTap={{ scale: 0.95 }}
553
+ onClick={onStart}
554
+ className="group relative px-10 py-4 bg-white text-black rounded-full font-bold tracking-wide uppercase overflow-hidden shadow-[0_0_40px_-10px_rgba(255,255,255,0.3)]"
555
+ >
556
+ <span className="relative z-10 flex items-center gap-2 text-sm">
557
+ Enter System <ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
558
+ </span>
559
+ <div className="absolute inset-0 bg-gradient-to-r from-orange-400 to-orange-600 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
560
+ </motion.button>
561
+ </div>
562
+ </div>
563
+ </motion.div>
564
+ );
565
+ };
566
+
567
+ // UI HELPERS (Sliders, Counters, Checkboxes, Inputs)
568
+ const ParameterSlider = ({ icon: Icon, label, value, onChange, min, max, step, description, formatValue }) => {
569
+ return (
570
+ <div className="space-y-2">
571
+ <div className="flex justify-between items-center text-[10px] uppercase font-bold tracking-wider text-zinc-400">
572
+ <div className="flex items-center gap-1.5">
573
+ <Icon className="w-3 h-3 text-amber-500" /> {label}
574
+ </div>
575
+ <span className="text-amber-400">{formatValue ? formatValue(value) : value}</span>
576
+ </div>
577
+ <div className="relative h-4 flex items-center group">
578
+ <div className="absolute inset-x-0 h-1 bg-zinc-800 rounded-lg overflow-hidden">
579
+ <div
580
+ className="h-full bg-gradient-to-r from-yellow-600 to-amber-500"
581
+ style={{ width: `${((value - min) / (max - min)) * 100}%` }}
582
+ />
583
+ </div>
584
+ <input
585
+ type="range" min={min} max={max} step={step} value={value}
586
+ onChange={(e) => onChange(parseFloat(e.target.value))}
587
+ className="absolute w-full h-full opacity-0 cursor-pointer z-10"
588
+ />
589
+ <motion.div
590
+ className="absolute h-3 w-3 bg-amber-400 rounded-full shadow-[0_0_10px_rgba(251,191,36,0.5)] border border-yellow-200 pointer-events-none"
591
+ style={{ left: `calc(${((value - min) / (max - min)) * 100}% - 6px)` }}
592
+ />
593
+ </div>
594
+ {description && <p className="text-[9px] text-zinc-600">{description}</p>}
595
+ </div>
596
+ );
597
+ };
598
+
599
+ const TopKCounter = ({ value, onChange, disabled }) => {
600
+ return (
601
+ <div className="flex flex-col gap-1 w-full">
602
+ <div className={cn("flex items-center gap-3 bg-zinc-950/40 border border-zinc-700 rounded-lg p-1 w-full justify-between", disabled && "opacity-60 cursor-not-allowed")}>
603
+ <button
604
+ onClick={() => !disabled && onChange(Math.max(1, value - 1))}
605
+ disabled={disabled}
606
+ className="w-8 h-8 flex items-center justify-center rounded-md bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-white transition-colors disabled:pointer-events-none"
607
+ >
608
+ <Minus className="w-3 h-3" />
609
+ </button>
610
+ <div className="flex items-center gap-2">
611
+ <span className="text-[10px] font-bold text-zinc-500 uppercase">Retrieval Count</span>
612
+ <span className="font-mono text-sm font-bold text-zinc-200">K={value}</span>
613
+ </div>
614
+ <button
615
+ onClick={() => !disabled && onChange(Math.min(10, value + 1))}
616
+ disabled={disabled}
617
+ className="w-8 h-8 flex items-center justify-center rounded-md bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-white transition-colors disabled:pointer-events-none"
618
+ >
619
+ <Plus className="w-3 h-3" />
620
+ </button>
621
+ </div>
622
+ </div>
623
+ );
624
+ };
625
+
626
+ const CheckboxTile = ({ icon: Icon, label, checked, onChange, color = "orange", description, infoTitle, infoDesc }) => {
627
+ const [showInfo, setShowInfo] = useState(false);
628
+
629
+ const styles = {
630
+ rose: { active: "bg-rose-500/10 border-rose-500/50", icon: "text-rose-500", checkbox: "bg-rose-500 border-rose-500", info: "border-rose-500/30 text-rose-400" },
631
+ orange: { active: "bg-orange-500/10 border-orange-500/50", icon: "text-orange-500", checkbox: "bg-orange-500 border-orange-500", info: "border-orange-500/30 text-orange-400" },
632
+ amber: { active: "bg-amber-500/10 border-amber-500/50", icon: "text-amber-500", checkbox: "bg-amber-500 border-amber-500", info: "border-amber-500/30 text-amber-400" },
633
+ default: { active: "bg-zinc-800 border-zinc-600", icon: "text-zinc-500", checkbox: "bg-zinc-500 border-zinc-500", info: "border-zinc-700 text-zinc-400" }
634
+ };
635
+ const currentStyle = styles[color] || styles.default;
636
+
637
+ return (
638
+ <div className="flex flex-col">
639
+ <div className="flex items-center gap-2">
640
+ <motion.div
641
+ layout
642
+ onClick={() => onChange(!checked)}
643
+ className={cn("relative flex-1 rounded-xl border p-3 cursor-pointer transition-all duration-300", checked ? currentStyle.active : "bg-zinc-900/60 border-zinc-800 hover:border-zinc-600")}
644
+ >
645
+ <div className="flex items-center gap-3">
646
+ <div className={cn("w-4 h-4 rounded border flex items-center justify-center transition-colors shrink-0", checked ? `${currentStyle.checkbox} text-black` : "border-zinc-600 bg-transparent")}>
647
+ {checked && <div className="w-2 h-2 bg-white rounded-full" />}
648
+ </div>
649
+ <div className="flex-1">
650
+ <h4 className={cn("text-xs font-bold uppercase tracking-wider", checked ? "text-white" : "text-zinc-400")}>{label}</h4>
651
+ </div>
652
+ <Icon className={cn("w-4 h-4", checked ? currentStyle.icon : "text-zinc-500")} />
653
+ </div>
654
+ </motion.div>
655
+
656
+ {infoTitle && (
657
+ <button
658
+ onClick={() => setShowInfo(!showInfo)}
659
+ className={cn(
660
+ "p-2 rounded-lg border bg-zinc-900/40 hover:bg-zinc-900 transition-colors",
661
+ showInfo ? currentStyle.info + " bg-zinc-900" : "border-zinc-800 text-zinc-600 hover:text-zinc-400"
662
+ )}
663
+ >
664
+ <Info className="w-4 h-4" />
665
+ </button>
666
+ )}
667
+ </div>
668
+
669
+ <AnimatePresence>
670
+ {showInfo && infoTitle && (
671
+ <motion.div
672
+ initial={{ height: 0, opacity: 0 }}
673
+ animate={{ height: 'auto', opacity: 1 }}
674
+ exit={{ height: 0, opacity: 0 }}
675
+ className="overflow-hidden"
676
+ >
677
+ <div className={cn("mt-2 bg-zinc-900/80 border p-3 rounded-lg text-[10px] text-zinc-400 leading-relaxed shadow-inner", currentStyle.info?.split(' ')[0] || "border-zinc-800")}>
678
+ <strong className={cn("block mb-1 font-bold uppercase tracking-wide", currentStyle.info?.split(' ')[1] || "text-zinc-300")}>{infoTitle}</strong>
679
+ {infoDesc}
680
+ </div>
681
+ </motion.div>
682
+ )}
683
+ </AnimatePresence>
684
+ </div>
685
+ );
686
+ };
687
+
688
+ const InputField = ({ icon: Icon, value, onChange, placeholder, color = "orange", label }) => {
689
+ const textareaRef = useRef(null);
690
+ useEffect(() => {
691
+ if (textareaRef.current) {
692
+ textareaRef.current.style.height = 'auto';
693
+ textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
694
+ }
695
+ }, [value]);
696
+
697
+ // Gradient definitions for borders
698
+ const gradients = {
699
+ red: "bg-[conic-gradient(transparent,rgba(239,68,68,1),transparent,rgba(239,68,68,1),transparent)]",
700
+ rose: "bg-[conic-gradient(transparent,rgba(244,63,94,1),transparent,rgba(244,63,94,1),transparent)]",
701
+ orange: "bg-[conic-gradient(transparent,rgba(249,115,22,1),transparent,rgba(249,115,22,1),transparent)]",
702
+ amber: "bg-[conic-gradient(transparent,rgba(245,158,11,1),transparent,rgba(245,158,11,1),transparent)]",
703
+ }
704
+
705
+ return (
706
+ <div className="space-y-1.5">
707
+ {label && <label className="text-[10px] font-bold text-zinc-400 uppercase tracking-wider ml-1">{label}</label>}
708
+
709
+ <div className="relative group rounded-lg p-[1px] overflow-hidden bg-zinc-800/50">
710
+ <div className="absolute inset-[-100%] animate-[spin_4s_linear_infinite] opacity-0 group-focus-within:opacity-100 transition-opacity duration-500">
711
+ <div className={cn("w-full h-full", gradients[color] || gradients.orange)} />
712
+ </div>
713
+
714
+ <div className="relative bg-zinc-950 rounded-lg flex items-start">
715
+ <div className="absolute top-3 left-3 flex items-center pointer-events-none z-10">
716
+ <Icon className={cn("w-4 h-4 transition-colors", value ? "text-white" : "text-zinc-500")} />
717
+ </div>
718
+ <textarea
719
+ ref={textareaRef}
720
+ value={value}
721
+ onChange={(e) => onChange(e.target.value)}
722
+ placeholder={placeholder}
723
+ rows={1}
724
+ className="w-full pl-10 pr-3 py-3 bg-zinc-950/90 rounded-lg text-sm text-zinc-100 placeholder-zinc-500 outline-none transition-all focus:bg-zinc-900/50 relative z-10 resize-none overflow-hidden min-h-[44px]"
725
+ />
726
+ </div>
727
+ </div>
728
+ </div>
729
+ );
730
+ };
731
+
732
+ // --- DEFINICIONES FALTANTES (MOVIDAS ANTES DE MainInterface) ---
733
+
734
+ const Column = ({ title, query, color, children }) => (
735
+ <div className="space-y-6">
736
+ <div className={`pb-4 border-b border-zinc-800 border-l-4 pl-4 border-l-${color}-500`}>
737
+ <h2 className="text-xl font-bold text-white uppercase tracking-tight">{title}</h2>
738
+ <p className="text-zinc-400 font-mono text-xs mt-1 truncate">{query}</p>
739
+ </div>
740
+ {children}
741
+ </div>
742
+ );
743
+
744
+ const ResultCard = ({ item, isSynth, label }) => {
745
+ if(!item) return null;
746
+ const url = isSynth ? item.url : (item.url?.startsWith('http') ? item.url : `${API_URL}${item.url}`);
747
+
748
+ return (
749
+ <motion.div variants={cardVariant} className="group relative bg-zinc-900 rounded-2xl overflow-hidden border border-zinc-800 hover:border-zinc-600 transition-colors shadow-2xl flex flex-col h-full">
750
+ <div className="absolute top-4 left-4 z-20 flex gap-2">
751
+ <div className="px-3 py-1 bg-black/80 backdrop-blur border border-white/10 rounded-full text-[10px] font-bold uppercase tracking-wider text-white">
752
+ {label}
753
+ </div>
754
+ </div>
755
+
756
+ <div className="aspect-[4/5] w-full bg-[#020202] relative p-4 flex items-center justify-center">
757
+ <img
758
+ src={url || "https://placehold.co/400x500/18181b/3f3f46?text=No+Image"}
759
+ className={cn(
760
+ "max-w-full max-h-full object-contain shadow-2xl transition-all duration-500",
761
+ isSynth ? "opacity-90" : "opacity-90 group-hover:scale-105 group-hover:opacity-100"
762
+ )}
763
+ />
764
+ <div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-[0.05] pointer-events-none" />
765
+ <div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-black/80 pointer-events-none" />
766
+ </div>
767
+
768
+ <div className="p-5 border-t border-zinc-800 bg-zinc-900 flex-1 flex flex-col">
769
+ {isSynth ? (
770
+ <div className="flex items-center gap-3">
771
+ <div className="p-2 bg-amber-500/10 rounded-lg">
772
+ <Sparkles className="w-4 h-4 text-amber-500" />
773
+ </div>
774
+ <div>
775
+ <h4 className="text-xs font-bold text-zinc-200 uppercase tracking-wide">Synthetic Generation</h4>
776
+ <p className="text-[10px] text-zinc-500">Created by Diffusion Model</p>
777
+ </div>
778
+ </div>
779
+ ) : (
780
+ <div className="space-y-3 flex-1 flex flex-col">
781
+ <div className="flex items-start justify-between">
782
+ <span className="text-orange-500 text-[9px] font-bold uppercase tracking-widest">Findings</span>
783
+ {item.score && (
784
+ <div className="text-right">
785
+ <span className="text-white font-mono text-xs font-bold block">Score: {(item.score * 100).toFixed(1)}%</span>
786
+ </div>
787
+ )}
788
+ </div>
789
+ <p className="text-xs text-zinc-300 leading-relaxed font-sans">
790
+ {item.caption || "No caption available for this image."}
791
+ </p>
792
+ </div>
793
+ )}
794
+ </div>
795
+ </motion.div>
796
+ );
797
+ };
798
+
799
+ const DescriptionBox = ({ text, title }) => {
800
+ if(!text || text === "LLM generation skipped.") return null;
801
+ return (
802
+ <motion.div variants={cardVariant} className="bg-zinc-900/50 border-l-4 border-orange-500 border-y border-r border-zinc-800 p-6 rounded-2xl relative overflow-hidden">
803
+ <div className="flex items-center gap-3 mb-4">
804
+ <div className="p-2 bg-zinc-800 rounded-lg">
805
+ <FileText className="w-4 h-4 text-orange-400" />
806
+ </div>
807
+ <h4 className="text-xs font-bold text-zinc-200 uppercase tracking-widest">{title}</h4>
808
+ </div>
809
+ <RichText text={text} className="text-sm text-zinc-300 leading-relaxed font-sans block" />
810
+ </motion.div>
811
+ );
812
+ };
813
+
814
+ // 3. MAIN APP INTERFACE
815
+ const MainInterface = ({ onBack }) => {
816
+ const [query, setQuery] = useState('');
817
+ const [topK, setTopK] = useState(1);
818
+ const [isDualSearch, setIsDualSearch] = useState(false);
819
+ const [loadDescription, setLoadDescription] = useState(false);
820
+ const [generateSynthetic, setGenerateSynthetic] = useState(false);
821
+
822
+ const isHeavyComputation = isDualSearch || loadDescription || generateSynthetic;
823
+
824
+ useEffect(() => {
825
+ if (isHeavyComputation) {
826
+ setTopK(1);
827
+ }
828
+ }, [isHeavyComputation]);
829
+
830
+ const [showMedicalInspiration, setShowMedicalInspiration] = useState(false);
831
+ const [showDualInspiration, setShowDualInspiration] = useState(false);
832
+
833
+ const [guidanceScale, setGuidanceScale] = useState(1);
834
+ const [inferenceSteps, setInferenceSteps] = useState(5);
835
+
836
+ const [addConcept, setAddConcept] = useState('');
837
+ const [subConcept, setSubConcept] = useState('');
838
+
839
+ const [loading, setLoading] = useState(false);
840
+ const [results, setResults] = useState(null);
841
+ const [error, setError] = useState(null);
842
+
843
+ const isDualSearchValid = !isDualSearch || (addConcept.trim() !== '' && subConcept.trim() !== '');
844
+ const canExecute = !loading && query.trim() !== '' && isDualSearchValid;
845
+
846
+ const handleExecute = async () => {
847
+ if (!canExecute) return;
848
+ setLoading(true);
849
+ setResults(null);
850
+ setError(null);
851
+
852
+ try {
853
+ const payload = {
854
+ original_text: query,
855
+ sub_concept: isDualSearch ? subConcept : null,
856
+ add_concept: isDualSearch ? addConcept : null,
857
+ top_k: topK,
858
+ gen_text: loadDescription,
859
+ gen_image: generateSynthetic,
860
+ guidance_scale: generateSynthetic ? guidanceScale : undefined,
861
+ num_inference_steps: generateSynthetic ? inferenceSteps : undefined,
862
+ };
863
+ const res = await fetch(`${API_URL}/generate_comparison`, {
864
+ method: 'POST',
865
+ headers: { 'Content-Type': 'application/json' },
866
+ body: JSON.stringify(payload),
867
+ });
868
+ if (!res.ok) throw new Error("API Error");
869
+ const data = await res.json();
870
+ setResults(data);
871
+ } catch (e) {
872
+ console.error(e);
873
+ setError("Connection Error. Please try again.");
874
+ } finally {
875
+ setLoading(false);
876
+ }
877
+ };
878
+
879
+ const handleSetMedicalExample = (text) => {
880
+ setQuery(text);
881
+ setShowMedicalInspiration(false);
882
+ };
883
+
884
+ const handleSetDualExample = (ex) => {
885
+ setQuery(ex.query);
886
+ setAddConcept(ex.add);
887
+ setSubConcept(ex.sub);
888
+ setIsDualSearch(true);
889
+ setShowDualInspiration(false);
890
+ };
891
+
892
+ return (
893
+ <div className="h-screen bg-[#09090b] text-zinc-100 flex flex-col md:flex-row overflow-hidden font-sans">
894
+
895
+ {/* SIDEBAR */}
896
+ <motion.aside
897
+ initial={{ x: -50, opacity: 0 }}
898
+ animate={{ x: 0, opacity: 1 }}
899
+ className="w-full md:w-[420px] bg-zinc-950/90 border-r border-zinc-800/50 flex flex-col z-20 shadow-2xl backdrop-blur-xl flex-shrink-0"
900
+ >
901
+ <div className="p-6 border-b border-zinc-800/50 flex items-center justify-between">
902
+ <div className="flex items-center gap-3 cursor-pointer group" onClick={onBack}>
903
+ <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-orange-500 to-red-600 flex items-center justify-center shadow-lg shadow-orange-900/20">
904
+ <Zap className="w-4 h-4 text-white fill-current" />
905
+ </div>
906
+ <div>
907
+ <h1 className="font-bold tracking-tight text-zinc-100">MIRAGE <span className="text-zinc-500 font-normal">OS</span></h1>
908
+ <p className="text-[9px] text-zinc-600 uppercase tracking-widest">v1</p>
909
+ </div>
910
+ </div>
911
+ </div>
912
+
913
+ <div className="flex-1 overflow-y-auto p-6 space-y-8 custom-scrollbar">
914
+ {/* INPUT SECTION (RED SPECTRUM) */}
915
+ <section className="space-y-4">
916
+ <div className="flex items-center justify-between">
917
+ <label className="text-[10px] font-bold text-red-400 uppercase tracking-wider ml-1">Medical Query</label>
918
+ <button
919
+ onClick={() => setShowMedicalInspiration(!showMedicalInspiration)}
920
+ className="text-[9px] text-red-400/80 hover:text-red-300 transition-colors flex items-center gap-1 uppercase tracking-wide font-medium"
921
+ >
922
+ <Lightbulb className="w-2.5 h-2.5" /> Need inspiration?
923
+ </button>
924
+ </div>
925
+
926
+ {/* Inspiration Panel (Medical) */}
927
+ <AnimatePresence>
928
+ {showMedicalInspiration && (
929
+ <motion.div
930
+ initial={{ height: 0, opacity: 0 }}
931
+ animate={{ height: 'auto', opacity: 1 }}
932
+ exit={{ height: 0, opacity: 0 }}
933
+ className="overflow-hidden"
934
+ >
935
+ <div className="bg-zinc-900/50 border border-red-500/20 rounded-lg p-2 mb-3 space-y-2">
936
+ <p className="text-[9px] text-zinc-400 uppercase tracking-wider px-1">Select a query:</p>
937
+ {MEDICAL_PROMPTS.map((item, idx) => (
938
+ <button
939
+ key={idx}
940
+ onClick={() => handleSetMedicalExample(item.text)}
941
+ className="w-full text-left p-2 rounded hover:bg-red-500/10 border border-transparent hover:border-red-500/30 transition-all group"
942
+ >
943
+ <div className="text-[10px] font-bold text-zinc-300 group-hover:text-red-200">{item.title}</div>
944
+ <div className="text-[9px] text-zinc-500 group-hover:text-zinc-400 whitespace-normal leading-relaxed">{item.text}</div>
945
+ </button>
946
+ ))}
947
+ </div>
948
+ </motion.div>
949
+ )}
950
+ </AnimatePresence>
951
+
952
+ <InputField
953
+ icon={Search}
954
+ placeholder={`Enter medical query...`}
955
+ value={query}
956
+ onChange={setQuery}
957
+ color="red"
958
+ />
959
+
960
+ <div className="flex items-start gap-3 pt-2">
961
+ <TopKCounter
962
+ value={topK}
963
+ onChange={setTopK}
964
+ disabled={isHeavyComputation}
965
+ />
966
+ </div>
967
+ </section>
968
+
969
+ <div className="h-px bg-zinc-800/50" />
970
+
971
+ {/* CONTROLS */}
972
+ <section className="space-y-6">
973
+ <div className="space-y-3">
974
+ {/* DUAL SEARCH (ROSE/PINK SPECTRUM) */}
975
+ <div className="flex items-center justify-between">
976
+ <div className="flex-1">
977
+ <CheckboxTile
978
+ icon={SplitSquareHorizontal}
979
+ label="Dual Search (Arithmetic)"
980
+ checked={isDualSearch}
981
+ onChange={setIsDualSearch}
982
+ color="rose"
983
+ infoTitle="Dual Search Logic"
984
+ infoDesc="Performs vector arithmetic in the latent space. Adding a concept pushes the search vector towards a specific feature (e.g., 'Pneumonia'), while subtracting moves it away (e.g., 'Bones'). Adjusting weights fine-tunes this balance."
985
+ />
986
+ </div>
987
+ {isDualSearch && (
988
+ <button
989
+ onClick={() => setShowDualInspiration(!showDualInspiration)}
990
+ className="ml-2 text-[9px] text-rose-400/80 hover:text-rose-300 transition-colors flex items-center gap-1 uppercase tracking-wide font-medium whitespace-nowrap"
991
+ >
992
+ <Lightbulb className="w-2.5 h-2.5" /> Examples
993
+ </button>
994
+ )}
995
+ </div>
996
+
997
+ <AnimatePresence>
998
+ {isDualSearch && showDualInspiration && (
999
+ <motion.div
1000
+ initial={{ height: 0, opacity: 0 }}
1001
+ animate={{ height: 'auto', opacity: 1 }}
1002
+ exit={{ height: 0, opacity: 0 }}
1003
+ className="overflow-hidden pl-2 ml-2 border-l-2 border-rose-500/30"
1004
+ >
1005
+ <div className="bg-zinc-900/50 border border-rose-500/20 rounded-lg p-2 mb-3 space-y-2">
1006
+ <p className="text-[9px] text-zinc-400 uppercase tracking-wider px-1">Try vector arithmetic:</p>
1007
+ {DUAL_ARITHMETIC_EXAMPLES.map((item, idx) => (
1008
+ <button
1009
+ key={idx}
1010
+ onClick={() => handleSetDualExample(item)}
1011
+ className="w-full text-left p-2 rounded hover:bg-rose-500/10 border border-transparent hover:border-rose-500/30 transition-all group"
1012
+ >
1013
+ <div className="text-[10px] font-bold text-zinc-300 group-hover:text-rose-200">{item.title}</div>
1014
+ <div className="text-[9px] text-zinc-500 flex gap-2 font-mono mt-1">
1015
+ <span className="text-zinc-400">{item.query}</span>
1016
+ <span className="text-rose-400">+ {item.add}</span>
1017
+ <span className="text-rose-400">- {item.sub}</span>
1018
+ </div>
1019
+ </button>
1020
+ ))}
1021
+ </div>
1022
+ </motion.div>
1023
+ )}
1024
+ </AnimatePresence>
1025
+
1026
+ <AnimatePresence>
1027
+ {isDualSearch && (
1028
+ <motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }} className="overflow-hidden space-y-3 pl-2 border-l-2 border-rose-500/30 ml-2 pt-2">
1029
+ <InputField icon={Plus} label="Add Concept (+)" placeholder="e.g. Fracture" value={addConcept} onChange={setAddConcept} color="rose" />
1030
+ <InputField icon={Minus} label="Subtract Concept (-)" placeholder="e.g. Bones" value={subConcept} onChange={setSubConcept} color="rose" />
1031
+ {(!addConcept || !subConcept) && (
1032
+ <p className="text-[9px] text-red-500 font-bold uppercase tracking-wide animate-pulse px-1">
1033
+ * Both concepts required for calculation
1034
+ </p>
1035
+ )}
1036
+ </motion.div>
1037
+ )}
1038
+ </AnimatePresence>
1039
+
1040
+ {/* LLM CONTEXT (ORANGE SPECTRUM) */}
1041
+ <CheckboxTile
1042
+ icon={FileText}
1043
+ label="Generate Context (LLM)"
1044
+ checked={loadDescription}
1045
+ onChange={setLoadDescription}
1046
+ color="orange"
1047
+ infoTitle="LLM Radiology Report"
1048
+ infoDesc="Uses a Large Language Model to synthesize findings from the top retrieved images into a coherent radiology report. It translates raw visual retrieval data into natural language descriptions."
1049
+ />
1050
+
1051
+ {/* SYNTHETIC (AMBER SPECTRUM) */}
1052
+ <CheckboxTile
1053
+ icon={ImageIcon}
1054
+ label="Synthetic Imaging (SD)"
1055
+ checked={generateSynthetic}
1056
+ onChange={setGenerateSynthetic}
1057
+ color="amber"
1058
+ infoTitle="Generative Imaging"
1059
+ infoDesc="Creates a new, artificial medical image based on the search context using Stable Diffusion. 'Guidance Scale' controls how strictly the AI follows the prompt (higher = stricter). 'Steps' controls image quality/refinement."
1060
+ />
1061
+ <AnimatePresence>
1062
+ {generateSynthetic && (
1063
+ <motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }} className="overflow-hidden space-y-4 pl-3 border-l-2 border-amber-500/30 ml-2 py-2">
1064
+ <ParameterSlider icon={Sliders} label="Guidance Scale" value={guidanceScale} min={1.0} max={3.0} step={0.1} onChange={setGuidanceScale} description="Controls prompt adherence strictness." />
1065
+ <ParameterSlider icon={Gauge} label="Inference Steps" value={inferenceSteps} min={1} max={10} step={1} onChange={setInferenceSteps} description="Higher steps = better quality, slower speed." />
1066
+ </motion.div>
1067
+ )}
1068
+ </AnimatePresence>
1069
+ </div>
1070
+ </section>
1071
+ </div>
1072
+
1073
+ <div className="p-6 border-t border-zinc-800 bg-zinc-950 z-20 flex-shrink-0">
1074
+ <button
1075
+ onClick={handleExecute}
1076
+ disabled={!canExecute}
1077
+ className={cn(
1078
+ "w-full py-4 rounded-xl font-bold uppercase tracking-widest text-xs transition-all duration-300 flex items-center justify-center gap-2",
1079
+ canExecute
1080
+ ? "bg-white text-black hover:bg-orange-500 hover:text-white shadow-lg shadow-orange-900/20"
1081
+ : "bg-zinc-800 text-zinc-500 cursor-not-allowed border border-zinc-700/50"
1082
+ )}
1083
+ >
1084
+ {loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Activity className="w-4 h-4" />}
1085
+ {loading ? "Processing Vectors..." : "Execute Operation"}
1086
+ </button>
1087
+ </div>
1088
+ </motion.aside>
1089
+
1090
+ {/* MAIN CONTENT */}
1091
+ <main className="flex-1 relative bg-[#050505] overflow-y-auto custom-scrollbar">
1092
+ {/* Background Elements */}
1093
+ <div className="fixed inset-0 pointer-events-none">
1094
+ <div className="absolute top-0 right-0 w-[500px] h-[500px] bg-blue-900/5 rounded-full blur-[100px]" />
1095
+ <div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-orange-900/5 rounded-full blur-[100px]" />
1096
+ <div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-[0.03]" />
1097
+ </div>
1098
+
1099
+ {/* CONTENT WRAPPER */}
1100
+ <motion.div
1101
+ className="relative z-10 p-8 min-h-full flex flex-col"
1102
+ animate={{ marginRight: "0px" }}
1103
+ transition={{ duration: 0.3, ease: "easeInOut" }}
1104
+ >
1105
+ <header className="flex justify-between items-center mb-10 flex-shrink-0">
1106
+ <div className="flex items-center gap-2 text-zinc-500 text-xs font-mono uppercase tracking-widest">
1107
+ <div className={cn("w-2 h-2 rounded-full", loading ? "bg-orange-500 animate-pulse" : "bg-emerald-500")} />
1108
+ Status: {loading ? "Computing" : "Idle"}
1109
+ </div>
1110
+ </header>
1111
+
1112
+ <AnimatePresence mode="wait">
1113
+ {loading && (
1114
+ <motion.div key="loader" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="flex-1 flex flex-col items-center justify-center space-y-6 relative h-full">
1115
+ {/* STANDARD LOADER */}
1116
+ <div className="text-center space-y-4">
1117
+ <div className="relative w-32 h-32 mx-auto">
1118
+ <div className="absolute inset-0 border-t-2 border-orange-500 rounded-full animate-spin" />
1119
+ <div className="absolute inset-4 border-t-2 border-blue-500 rounded-full animate-spin direction-reverse" />
1120
+ <div className="absolute inset-0 flex items-center justify-center">
1121
+ <BrainCircuit className="w-10 h-10 text-zinc-700" />
1122
+ </div>
1123
+ </div>
1124
+ <p className="text-zinc-500 text-xs font-mono uppercase tracking-widest animate-pulse">Navigating Latent Space...</p>
1125
+ </div>
1126
+ </motion.div>
1127
+ )}
1128
+
1129
+ {!loading && !results && (
1130
+ <motion.div key="empty" initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="flex-1 flex flex-col items-center justify-center text-center opacity-50 relative overflow-hidden">
1131
+ <BrainstormingBackground />
1132
+
1133
+ <div className="z-10 flex flex-col items-center bg-zinc-950/70 backdrop-blur-md p-10 rounded-3xl border border-zinc-800 shadow-2xl">
1134
+ <Command className="w-24 h-24 mb-6 text-zinc-800" />
1135
+ <h2 className="text-4xl font-bold text-zinc-800 tracking-tighter">SYSTEM IDLE</h2>
1136
+ <p className="text-zinc-600 mt-2 text-sm max-w-sm">Ready to navigate high-dimensional medical latent spaces.</p>
1137
+ </div>
1138
+ </motion.div>
1139
+ )}
1140
+
1141
+ {!loading && results && (
1142
+ <motion.div key="results" className="w-full max-w-7xl mx-auto space-y-12" initial="hidden" animate="show" variants={staggerContainer}>
1143
+
1144
+ <div className={cn("grid gap-8", results.modified ? "grid-cols-1 xl:grid-cols-2" : "grid-cols-1 max-w-3xl mx-auto")}>
1145
+ <Column title={results.modified ? "Original Vector" : "Query Result"} query={results.original_text} color="blue">
1146
+ <div className="space-y-8">
1147
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
1148
+ {Array.isArray(results.original.real_match)
1149
+ ? results.original.real_match.map((item, i) => <ResultCard key={i} item={item} label={`Retrieval #${i+1}`} query={results.original_text} />)
1150
+ : <ResultCard item={results.original.real_match} label="Best Match" query={results.original_text} />
1151
+ }
1152
+ {results.original.synthetic.image_base64 && (
1153
+ <ResultCard item={{ url: results.original.synthetic.image_base64, score: null }} isSynth label="Synthesis" />
1154
+ )}
1155
+ </div>
1156
+ <DescriptionBox text={results.original.synthetic.generated_prompt} title="Radiology Context" />
1157
+ </div>
1158
+ </Column>
1159
+
1160
+ {results.modified && (
1161
+ <Column title="Modified Vector" query={results.modified_text} color="orange">
1162
+ <div className="space-y-8">
1163
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
1164
+ {Array.isArray(results.modified.real_match)
1165
+ ? results.modified.real_match.map((item, i) => <ResultCard key={i} item={item} label={`Retrieval #${i+1}`} query={results.modified_text} />)
1166
+ : <ResultCard item={results.modified.real_match} label="Best Match" query={results.modified_text} />
1167
+ }
1168
+ {results.modified.synthetic.image_base64 && (
1169
+ <ResultCard item={{ url: results.modified.synthetic.image_base64, score: null }} isSynth label="Synthesis" />
1170
+ )}
1171
+ </div>
1172
+ <DescriptionBox text={results.modified.synthetic.generated_prompt} title="Radiology Context" />
1173
+ </div>
1174
+ </Column>
1175
+ )}
1176
+ </div>
1177
+ </motion.div>
1178
+ )}
1179
+ </AnimatePresence>
1180
+ </motion.div>
1181
+ </main>
1182
+ </div>
1183
+ );
1184
+ };
1185
+
1186
+ // --- EXPORTACI脫N FINAL DE LA APP ---
1187
+ export default function App() {
1188
+ const [mode, setMode] = useState('welcome'); // Estados: 'welcome', 'app', 'paper'
1189
+
1190
+ return (
1191
+ <AnimatePresence mode="wait">
1192
+ {mode === 'welcome' && (
1193
+ <WelcomeScreen
1194
+ key="welcome"
1195
+ onStart={() => setMode('app')}
1196
+ onOpenPaper={() => setMode('paper')}
1197
+ />
1198
+ )}
1199
+
1200
+ {mode === 'app' && (
1201
+ <motion.div key="app" initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="h-screen w-full">
1202
+ <MainInterface onBack={() => setMode('welcome')} />
1203
+ </motion.div>
1204
+ )}
1205
+
1206
+ {/* RENDERIZADO DE P脕GINA DEL PAPER (EN ESPA脩OL) */}
1207
+ {mode === 'paper' && (
1208
+ <PaperExplanation key="paper" onBack={() => setMode('welcome')} />
1209
+ )}
1210
+ </AnimatePresence>
1211
+ );
1212
+ }
frontend/src/assets/react.svg ADDED
frontend/src/index.css ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /* --- BASE DARK MODE PREMIUM --- */
6
+ body {
7
+ margin: 0;
8
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
9
+ background-color: #09090b; /* Zinc 950 */
10
+ color: #f4f4f5; /* Zinc 100 */
11
+ -webkit-font-smoothing: antialiased;
12
+ -moz-osx-font-smoothing: grayscale;
13
+ overflow-x: hidden;
14
+ }
15
+
16
+ /* Color de selecci贸n de texto (Naranja el茅ctrico) */
17
+ ::selection {
18
+ background-color: #f97316;
19
+ color: white;
20
+ }
21
+
22
+ /* --- SCROLLBAR PERSONALIZADA --- */
23
+ ::-webkit-scrollbar {
24
+ width: 8px;
25
+ }
26
+
27
+ ::-webkit-scrollbar-track {
28
+ background: #18181b; /* Zinc 900 */
29
+ }
30
+
31
+ ::-webkit-scrollbar-thumb {
32
+ background: #3f3f46; /* Zinc 700 */
33
+ border-radius: 4px;
34
+ border: 2px solid #18181b;
35
+ }
36
+
37
+ ::-webkit-scrollbar-thumb:hover {
38
+ background: #f97316; /* Orange 500 */
39
+ }
40
+
41
+ /* Animaci贸n de brillo para botones */
42
+ @keyframes shimmer {
43
+ 0% { transform: translateX(-100%); }
44
+ 100% { transform: translateX(100%); }
45
+ }
frontend/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import './index.css'
4
+ import App from './App.jsx'
5
+
6
+ createRoot(document.getElementById('root')).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
frontend/tailwind.config.cjs ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{js,ts,jsx,tsx}",
6
+ ],
7
+ theme: {
8
+ extend: {},
9
+ },
10
+ plugins: [],
11
+ }
frontend/vite.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ })