Spaces:
Sleeping
Sleeping
Upload 11 files
Browse files- Dockerfile +27 -0
- README.md +35 -12
- app.py +331 -0
- index.html +142 -0
- llm_client.py +603 -0
- notebook_builder.py +85 -0
- pdf_processor.py +25 -0
- requirements.txt +12 -0
- script.js +346 -0
- skills.md +32 -0
- style.css +609 -0
Dockerfile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use official Python 3.9 image
|
| 2 |
+
FROM python:3.9-slim
|
| 3 |
+
|
| 4 |
+
# Set working directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Install system dependencies for PyMuPDF
|
| 8 |
+
RUN apt-get update && apt-get install -y \
|
| 9 |
+
build-essential \
|
| 10 |
+
libgl1-mesa-glx \
|
| 11 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 12 |
+
|
| 13 |
+
# Copy requirements and install
|
| 14 |
+
COPY requirements.txt .
|
| 15 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 16 |
+
|
| 17 |
+
# Copy everything else
|
| 18 |
+
COPY . .
|
| 19 |
+
|
| 20 |
+
# Create jobs directory for temporary storage
|
| 21 |
+
RUN mkdir -p /app/jobs && chmod 777 /app/jobs
|
| 22 |
+
|
| 23 |
+
# Expose port (HF Spaces uses 7860)
|
| 24 |
+
EXPOSE 7860
|
| 25 |
+
|
| 26 |
+
# Command to run uvicorn
|
| 27 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,12 +1,35 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Pundit Feynman
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
license:
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Pundit Feynman
|
| 3 |
+
emoji: π
|
| 4 |
+
colorFrom: yellow
|
| 5 |
+
colorTo: gray
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Pundit Feynman π§
|
| 12 |
+
|
| 13 |
+
**Pundit Feynman** is a powerful research paper analyzer that converts complex PDFs into educational, executable PyTorch codeβfollowing the world-renowned **Feynman Technique**.
|
| 14 |
+
|
| 15 |
+
## π Deployment Instructions for Hugging Face Spaces
|
| 16 |
+
|
| 17 |
+
1. **Create a New Space**: Choose **Docker** as the SDK.
|
| 18 |
+
2. **Upload Files**: Upload the following:
|
| 19 |
+
- `app.py`
|
| 20 |
+
- `requirements.txt`
|
| 21 |
+
- `Dockerfile`
|
| 22 |
+
- `README.md`
|
| 23 |
+
- `static/`
|
| 24 |
+
- `utils/`
|
| 25 |
+
3. **Add Secrets**: Go to **Settings > Variables and Secrets** and add your API keys:
|
| 26 |
+
- `NVIDIA_API_KEY`: Your NVIDIA NIM API key.
|
| 27 |
+
- `NVIDIA_FLUX_API_KEY`: (Optional) For visual illustrations.
|
| 28 |
+
4. **Wait for Build**: Hugging Face will automatically build and deploy the Docker image.
|
| 29 |
+
|
| 30 |
+
## π οΈ Tech Stack
|
| 31 |
+
- **Backend**: FastAPI
|
| 32 |
+
- **LLM**: NVIDIA NIM (Qwen-VL-Instruct / Qwen2-72B)
|
| 33 |
+
- **OCR**: NVIDIA NeMo Retriever
|
| 34 |
+
- **Image Gen**: FLUX.1-schnell
|
| 35 |
+
- **Frontend**: Vanilla HTML/CSS/JS (Beige & Serif Aesthetic)
|
app.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Pundit Feynman β Research Paper to Executable Notebook
|
| 3 |
+
FastAPI backend with 3-stage AI pipeline, arXiv support, and SSE streaming.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import re
|
| 8 |
+
import uuid
|
| 9 |
+
import json
|
| 10 |
+
import time
|
| 11 |
+
import shutil
|
| 12 |
+
import asyncio
|
| 13 |
+
from fastapi import FastAPI, UploadFile, File, BackgroundTasks, HTTPException
|
| 14 |
+
from fastapi.responses import FileResponse, StreamingResponse
|
| 15 |
+
from fastapi.staticfiles import StaticFiles
|
| 16 |
+
from dotenv import load_dotenv
|
| 17 |
+
|
| 18 |
+
from utils.pdf_processor import process_pdf_to_base64
|
| 19 |
+
from utils.llm_client import extract_text_from_images, run_full_pipeline_stream, generate_concept_image
|
| 20 |
+
from utils.notebook_builder import build_notebook_from_cells
|
| 21 |
+
|
| 22 |
+
load_dotenv()
|
| 23 |
+
|
| 24 |
+
app = FastAPI(title="Pundit Feynman API", version="2.0")
|
| 25 |
+
os.makedirs("jobs", exist_ok=True)
|
| 26 |
+
|
| 27 |
+
# ββ Concurrency limiter β max 3 simultaneous generations ββ
|
| 28 |
+
_generation_semaphore = asyncio.Semaphore(3)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _safe_remove(path, retries=3, delay=0.5):
|
| 32 |
+
"""Remove a file with retry for Windows file locking."""
|
| 33 |
+
import time
|
| 34 |
+
for i in range(retries):
|
| 35 |
+
try:
|
| 36 |
+
if os.path.exists(path):
|
| 37 |
+
os.remove(path)
|
| 38 |
+
return
|
| 39 |
+
except PermissionError:
|
| 40 |
+
if i < retries - 1:
|
| 41 |
+
time.sleep(delay)
|
| 42 |
+
else:
|
| 43 |
+
print(f" β Could not delete {path} (file locked)")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
# ββ Endpoint 1: Extract methodology from PDF upload ββββββββββββββββββββββ
|
| 47 |
+
|
| 48 |
+
@app.post("/api/extract")
|
| 49 |
+
async def extract(file: UploadFile = File(...)):
|
| 50 |
+
if not file.filename.endswith(".pdf"):
|
| 51 |
+
raise HTTPException(status_code=400, detail="Only PDF files are allowed")
|
| 52 |
+
|
| 53 |
+
job_id = str(uuid.uuid4())
|
| 54 |
+
pdf_path = f"jobs/{job_id}.pdf"
|
| 55 |
+
|
| 56 |
+
# Save uploaded PDF
|
| 57 |
+
with open(pdf_path, "wb") as buf:
|
| 58 |
+
shutil.copyfileobj(file.file, buf)
|
| 59 |
+
|
| 60 |
+
try:
|
| 61 |
+
# Phase 1a: PDF β base64 images
|
| 62 |
+
base64_images = process_pdf_to_base64(pdf_path)
|
| 63 |
+
|
| 64 |
+
# Phase 1b: Vision extraction (batched)
|
| 65 |
+
raw_text = extract_text_from_images(base64_images)
|
| 66 |
+
|
| 67 |
+
# Save extracted text for Phase 2
|
| 68 |
+
txt_path = f"jobs/{job_id}.txt"
|
| 69 |
+
with open(txt_path, "w", encoding="utf-8") as f:
|
| 70 |
+
f.write(raw_text)
|
| 71 |
+
|
| 72 |
+
# Clean up PDF
|
| 73 |
+
_safe_remove(pdf_path)
|
| 74 |
+
|
| 75 |
+
return {"job_id": job_id, "status": "extraction_complete", "pages": len(base64_images)}
|
| 76 |
+
|
| 77 |
+
except Exception as e:
|
| 78 |
+
print(f" \u274c Extract error: {e}")
|
| 79 |
+
import traceback
|
| 80 |
+
traceback.print_exc()
|
| 81 |
+
if os.path.exists(pdf_path):
|
| 82 |
+
_safe_remove(pdf_path)
|
| 83 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
# ββ Endpoint 1b: Extract from arXiv URL ββββββββββββββββββββββββββββββββββ
|
| 87 |
+
|
| 88 |
+
@app.post("/api/extract-arxiv")
|
| 89 |
+
async def extract_arxiv(payload: dict):
|
| 90 |
+
"""Accept an arXiv URL, download the PDF, and run extraction."""
|
| 91 |
+
import httpx
|
| 92 |
+
|
| 93 |
+
arxiv_url = payload.get("url", "").strip()
|
| 94 |
+
if not arxiv_url:
|
| 95 |
+
raise HTTPException(status_code=400, detail="Missing 'url' field")
|
| 96 |
+
|
| 97 |
+
# Extract paper ID from URL
|
| 98 |
+
match = re.search(r'arxiv\.org/(?:abs|pdf)/([0-9]+\.[0-9]+)', arxiv_url)
|
| 99 |
+
if not match:
|
| 100 |
+
raise HTTPException(
|
| 101 |
+
status_code=400,
|
| 102 |
+
detail="Invalid arXiv URL. Expected format: https://arxiv.org/abs/XXXX.XXXXX"
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
paper_id = match.group(1)
|
| 106 |
+
pdf_url = f"https://arxiv.org/pdf/{paper_id}.pdf"
|
| 107 |
+
job_id = str(uuid.uuid4())
|
| 108 |
+
pdf_path = f"jobs/{job_id}.pdf"
|
| 109 |
+
|
| 110 |
+
try:
|
| 111 |
+
# Download PDF from arXiv
|
| 112 |
+
async with httpx.AsyncClient(follow_redirects=True) as http_client:
|
| 113 |
+
print(f" β¬ Downloading PDF from arXiv: {pdf_url}")
|
| 114 |
+
response = await http_client.get(pdf_url, timeout=30.0)
|
| 115 |
+
|
| 116 |
+
if response.status_code != 200:
|
| 117 |
+
raise HTTPException(
|
| 118 |
+
status_code=500,
|
| 119 |
+
detail=f"Failed to download PDF from arXiv: HTTP {response.status_code}"
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
# Save to disk
|
| 123 |
+
with open(pdf_path, "wb") as f:
|
| 124 |
+
f.write(response.content)
|
| 125 |
+
|
| 126 |
+
size_mb = len(response.content) / (1024 * 1024)
|
| 127 |
+
print(f" β
Downloaded: {size_mb:.1f} MB")
|
| 128 |
+
|
| 129 |
+
# Same pipeline as PDF upload
|
| 130 |
+
base64_images = process_pdf_to_base64(pdf_path)
|
| 131 |
+
raw_text = extract_text_from_images(base64_images)
|
| 132 |
+
|
| 133 |
+
txt_path = f"jobs/{job_id}.txt"
|
| 134 |
+
with open(txt_path, "w", encoding="utf-8") as f:
|
| 135 |
+
f.write(raw_text)
|
| 136 |
+
|
| 137 |
+
_safe_remove(pdf_path)
|
| 138 |
+
|
| 139 |
+
return {
|
| 140 |
+
"job_id": job_id,
|
| 141 |
+
"status": "extraction_complete",
|
| 142 |
+
"pages": len(base64_images),
|
| 143 |
+
"arxiv_id": paper_id,
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
except HTTPException:
|
| 147 |
+
raise
|
| 148 |
+
except Exception as e:
|
| 149 |
+
print(f" \u274c ArXiv extract error: {e}")
|
| 150 |
+
import traceback
|
| 151 |
+
traceback.print_exc()
|
| 152 |
+
if os.path.exists(pdf_path):
|
| 153 |
+
_safe_remove(pdf_path)
|
| 154 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
# ββ Endpoint 2: Stream code generation (SSE) β 3-stage pipeline ββββββββββ
|
| 158 |
+
|
| 159 |
+
@app.get("/api/generate_stream/{job_id}")
|
| 160 |
+
async def generate_stream(job_id: str):
|
| 161 |
+
txt_path = f"jobs/{job_id}.txt"
|
| 162 |
+
if not os.path.exists(txt_path):
|
| 163 |
+
raise HTTPException(status_code=404, detail="Extraction not found. Run /api/extract first.")
|
| 164 |
+
|
| 165 |
+
with open(txt_path, "r", encoding="utf-8") as f:
|
| 166 |
+
raw_text = f.read()
|
| 167 |
+
|
| 168 |
+
print(f"\n{'='*60}")
|
| 169 |
+
print(f" Starting 3-stage pipeline for job: {job_id}")
|
| 170 |
+
print(f" Text length: {len(raw_text)} chars")
|
| 171 |
+
print(f"{'='*60}\n")
|
| 172 |
+
|
| 173 |
+
def event_generator():
|
| 174 |
+
notebook_path = f"jobs/{job_id}.ipynb"
|
| 175 |
+
final_cells = None
|
| 176 |
+
pipeline_success = False
|
| 177 |
+
|
| 178 |
+
try:
|
| 179 |
+
for event_type, data in run_full_pipeline_stream(raw_text):
|
| 180 |
+
if event_type == "text":
|
| 181 |
+
payload = json.dumps({"text": data})
|
| 182 |
+
yield f"data: {payload}\n\n"
|
| 183 |
+
|
| 184 |
+
elif event_type == "cells":
|
| 185 |
+
final_cells = data
|
| 186 |
+
print(f" β
Pipeline produced {len(data)} cells")
|
| 187 |
+
|
| 188 |
+
elif event_type == "analysis":
|
| 189 |
+
# Save analysis to disk for the /api/visualize endpoint
|
| 190 |
+
analysis_path = f"jobs/{job_id}_analysis.json"
|
| 191 |
+
try:
|
| 192 |
+
with open(analysis_path, "w", encoding="utf-8") as af:
|
| 193 |
+
json.dump(data, af)
|
| 194 |
+
except Exception:
|
| 195 |
+
pass
|
| 196 |
+
# Signal frontend that visualization is ready
|
| 197 |
+
yield f"data: {json.dumps({'analysis_done': True})}\n\n"
|
| 198 |
+
|
| 199 |
+
elif event_type == "error":
|
| 200 |
+
err_msg = f"\nβ Pipeline Error: {data}\n"
|
| 201 |
+
print(f" β Pipeline error: {data}")
|
| 202 |
+
payload = json.dumps({"text": err_msg})
|
| 203 |
+
yield f"data: {payload}\n\n"
|
| 204 |
+
|
| 205 |
+
except Exception as e:
|
| 206 |
+
err_msg = f"\nβ Unexpected Error: {str(e)}\n"
|
| 207 |
+
print(f" β Unexpected pipeline error: {e}")
|
| 208 |
+
import traceback
|
| 209 |
+
traceback.print_exc()
|
| 210 |
+
err_payload = json.dumps({"text": err_msg})
|
| 211 |
+
yield f"data: {err_payload}\n\n"
|
| 212 |
+
|
| 213 |
+
# Build notebook from cells if we got them
|
| 214 |
+
if final_cells and len(final_cells) > 0:
|
| 215 |
+
try:
|
| 216 |
+
build_notebook_from_cells(final_cells, notebook_path)
|
| 217 |
+
pipeline_success = True
|
| 218 |
+
print(f" π Notebook saved: {notebook_path}")
|
| 219 |
+
except Exception as e:
|
| 220 |
+
print(f" β Failed to build notebook: {e}")
|
| 221 |
+
err_payload = json.dumps({"text": f"\nβ Failed to save notebook: {str(e)}\n"})
|
| 222 |
+
yield f"data: {err_payload}\n\n"
|
| 223 |
+
else:
|
| 224 |
+
no_cells_msg = json.dumps({"text": "\nβ Pipeline completed but no cells were produced. Check server logs for details.\n"})
|
| 225 |
+
yield f"data: {no_cells_msg}\n\n"
|
| 226 |
+
print(f" β No cells produced β notebook not saved")
|
| 227 |
+
|
| 228 |
+
# Always send done event with status
|
| 229 |
+
done_payload = json.dumps({"done": True, "success": pipeline_success})
|
| 230 |
+
yield f"data: {done_payload}\n\n"
|
| 231 |
+
|
| 232 |
+
# Only clean up extraction text on success
|
| 233 |
+
if pipeline_success and os.path.exists(txt_path):
|
| 234 |
+
os.remove(txt_path)
|
| 235 |
+
|
| 236 |
+
return StreamingResponse(
|
| 237 |
+
event_generator(),
|
| 238 |
+
media_type="text/event-stream",
|
| 239 |
+
headers={
|
| 240 |
+
"Cache-Control": "no-cache",
|
| 241 |
+
"Connection": "keep-alive",
|
| 242 |
+
"X-Accel-Buffering": "no",
|
| 243 |
+
}
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
# ββ Endpoint 3: Download notebook ββββββββββββββββββββββββββββββββββββββββ
|
| 248 |
+
|
| 249 |
+
def cleanup_job_files(job_id: str):
|
| 250 |
+
"""Remove all job artifacts after download."""
|
| 251 |
+
for ext in [".pdf", ".txt", ".ipynb", "_analysis.json"]:
|
| 252 |
+
path = f"jobs/{job_id}{ext}"
|
| 253 |
+
if os.path.exists(path):
|
| 254 |
+
os.remove(path)
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
@app.get("/api/download/{job_id}")
|
| 258 |
+
async def download_notebook(job_id: str, background_tasks: BackgroundTasks):
|
| 259 |
+
notebook_path = f"jobs/{job_id}.ipynb"
|
| 260 |
+
if not os.path.exists(notebook_path):
|
| 261 |
+
raise HTTPException(status_code=404, detail="Notebook not found")
|
| 262 |
+
|
| 263 |
+
background_tasks.add_task(cleanup_job_files, job_id)
|
| 264 |
+
|
| 265 |
+
return FileResponse(
|
| 266 |
+
notebook_path,
|
| 267 |
+
filename="pundit_feynman_notebook.ipynb",
|
| 268 |
+
media_type="application/x-ipynb+json",
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
# ββ Health check ββββββββββββββββββββββββββββββοΏ½οΏ½οΏ½ββββββββββββββββββββββββββ
|
| 273 |
+
|
| 274 |
+
@app.get("/health")
|
| 275 |
+
async def health():
|
| 276 |
+
return {"status": "ok", "version": "2.0", "pipeline": "3-stage"}
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
# ββ Endpoint 5: Generate visual illustration βββββββββββββββββββββββββββββ
|
| 280 |
+
|
| 281 |
+
@app.post("/api/visualize/{job_id}")
|
| 282 |
+
async def visualize_concept(job_id: str):
|
| 283 |
+
"""Generate a visual illustration of the paper's core concept."""
|
| 284 |
+
print(f"\n[DEBUG] {time.strftime('%H:%M:%S')} π¨ ROUTE HIT: /api/visualize/{job_id}")
|
| 285 |
+
|
| 286 |
+
# Verify job id is sane
|
| 287 |
+
if not job_id or job_id == "null" or job_id == "undefined":
|
| 288 |
+
print(f"[DEBUG] β ERROR: Received invalid Job ID: '{job_id}'")
|
| 289 |
+
raise HTTPException(status_code=400, detail="Invalid Job ID received")
|
| 290 |
+
|
| 291 |
+
analysis_path = f"jobs/{job_id}_analysis.json"
|
| 292 |
+
if not os.path.exists(analysis_path):
|
| 293 |
+
print(f"[DEBUG] β ERROR: Analysis file does not exist: {analysis_path}")
|
| 294 |
+
# List files in jobs to help debug
|
| 295 |
+
print(f"[DEBUG] Files in jobs/: {os.listdir('jobs')}")
|
| 296 |
+
raise HTTPException(status_code=404, detail=f"Analysis not found for job {job_id}")
|
| 297 |
+
|
| 298 |
+
print(f"[DEBUG] π Loading analysis JSON...")
|
| 299 |
+
try:
|
| 300 |
+
with open(analysis_path, "r", encoding="utf-8") as f:
|
| 301 |
+
analysis = json.load(f)
|
| 302 |
+
except Exception as e:
|
| 303 |
+
print(f"[DEBUG] β JSON ERROR: Could not parse {analysis_path}: {e}")
|
| 304 |
+
raise HTTPException(status_code=500, detail="Corrupted analysis file")
|
| 305 |
+
|
| 306 |
+
try:
|
| 307 |
+
print(f"[DEBUG] ποΈ Dispatching generation to threadpool for Job: {job_id}...")
|
| 308 |
+
loop = asyncio.get_event_loop()
|
| 309 |
+
image_b64 = await loop.run_in_executor(None, generate_concept_image, analysis)
|
| 310 |
+
|
| 311 |
+
print(f"[DEBUG] β
SUCCESS: Generation finished for Job: {job_id}")
|
| 312 |
+
return {"image": f"data:image/png;base64,{image_b64}"}
|
| 313 |
+
except Exception as e:
|
| 314 |
+
print(f"[DEBUG] β GENERATION ERROR for Job {job_id}: {e}")
|
| 315 |
+
import traceback
|
| 316 |
+
traceback.print_exc()
|
| 317 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 318 |
+
|
| 319 |
+
@app.get("/api/ping")
|
| 320 |
+
async def ping():
|
| 321 |
+
print("[DEBUG] π Ping received")
|
| 322 |
+
return {"status": "ok", "message": "Pundit Feynman Backend is ALIVE"}
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
# ββ Static files (MUST be last β catch-all) ββββββββββββββββββββββββββββββ
|
| 326 |
+
|
| 327 |
+
app.mount("/", StaticFiles(directory="static", html=True), name="static")
|
| 328 |
+
|
| 329 |
+
if __name__ == "__main__":
|
| 330 |
+
import uvicorn
|
| 331 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
index.html
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Pundit Feynman β Research Paper to Code</title>
|
| 8 |
+
<link rel="stylesheet" href="/style.css">
|
| 9 |
+
<link
|
| 10 |
+
href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
|
| 11 |
+
rel="stylesheet">
|
| 12 |
+
</head>
|
| 13 |
+
|
| 14 |
+
<body>
|
| 15 |
+
<!-- Left Panel: Upload & Status -->
|
| 16 |
+
<aside class="left-panel" id="left-panel">
|
| 17 |
+
<div class="panel-inner">
|
| 18 |
+
<header>
|
| 19 |
+
<h1>Pundit Feynman</h1>
|
| 20 |
+
<p class="tagline">Upload a research paper.<br>Learn it the Feynman way.</p>
|
| 21 |
+
<button id="visualize-btn" class="header-visualize hidden" style="display: none !important;">π¨
|
| 22 |
+
Visualize Concept</button>
|
| 23 |
+
</header>
|
| 24 |
+
|
| 25 |
+
<!-- Upload State -->
|
| 26 |
+
<div id="upload-section">
|
| 27 |
+
<div id="drop-zone" class="drop-zone">
|
| 28 |
+
<svg class="upload-icon" width="32" height="32" viewBox="0 0 24 24" fill="none"
|
| 29 |
+
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
| 30 |
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
| 31 |
+
<polyline points="17 8 12 3 7 8"></polyline>
|
| 32 |
+
<line x1="12" y1="3" x2="12" y2="15"></line>
|
| 33 |
+
</svg>
|
| 34 |
+
<p class="drop-text">Drop your PDF here</p>
|
| 35 |
+
<span class="drop-hint">or click to browse</span>
|
| 36 |
+
<input type="file" id="file-input" accept="application/pdf" hidden>
|
| 37 |
+
</div>
|
| 38 |
+
|
| 39 |
+
<!-- Divider -->
|
| 40 |
+
<div class="divider">
|
| 41 |
+
<span>or paste arXiv link</span>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
<!-- arXiv URL Input -->
|
| 45 |
+
<div class="arxiv-input-row">
|
| 46 |
+
<input type="text" id="arxiv-input" class="arxiv-input"
|
| 47 |
+
placeholder="https://arxiv.org/abs/2401.12345">
|
| 48 |
+
<button id="arxiv-btn" class="btn btn-primary arxiv-btn">Go β</button>
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
|
| 52 |
+
<!-- Extraction Progress -->
|
| 53 |
+
<div id="extract-status" class="status-box hidden">
|
| 54 |
+
<div class="spinner"></div>
|
| 55 |
+
<p class="status-label" id="extract-label">Analyzing paperβ¦</p>
|
| 56 |
+
<p class="status-sub">This may take a few minutes for long papers.</p>
|
| 57 |
+
</div>
|
| 58 |
+
|
| 59 |
+
<!-- Stream Active Indicator -->
|
| 60 |
+
<div id="stream-status" class="status-box hidden">
|
| 61 |
+
<div class="pulse-dot"></div>
|
| 62 |
+
<p class="status-label">Generating code liveβ¦</p>
|
| 63 |
+
<p class="status-sub">Watch the output in the code viewer β</p>
|
| 64 |
+
</div>
|
| 65 |
+
|
| 66 |
+
<!-- Done -->
|
| 67 |
+
<div id="done-section" class="status-box hidden">
|
| 68 |
+
<p class="done-check">β</p>
|
| 69 |
+
<p class="status-label">Generation complete</p>
|
| 70 |
+
<div class="btn-row">
|
| 71 |
+
<a id="download-btn" class="btn btn-primary">β¬ Download .ipynb</a>
|
| 72 |
+
<button id="reset-btn" class="btn btn-secondary">β» New Paper</button>
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
<!-- Error -->
|
| 77 |
+
<div id="error-section" class="status-box hidden">
|
| 78 |
+
<p class="error-x">β</p>
|
| 79 |
+
<p class="status-label">Something went wrong</p>
|
| 80 |
+
<p class="status-sub" id="error-text"></p>
|
| 81 |
+
<button id="error-reset-btn" class="btn btn-secondary">β» Try Again</button>
|
| 82 |
+
</div>
|
| 83 |
+
|
| 84 |
+
<footer>
|
| 85 |
+
<p>Powered by <strong>NVIDIA NIM</strong></p>
|
| 86 |
+
<div class="feedback-footer">
|
| 87 |
+
<p>please give feedback, so that i can make it better</p>
|
| 88 |
+
<a href="https://mail.google.com/mail/?view=cm&to=Avijitshil52460@gmail.com&su=Pundit%20Feynman%20Feedback"
|
| 89 |
+
target="_blank" class="feedback-link">Avijitshil52460@gmail.com</a>
|
| 90 |
+
</div>
|
| 91 |
+
</footer>
|
| 92 |
+
</div>
|
| 93 |
+
</aside>
|
| 94 |
+
|
| 95 |
+
<!-- Right Panel: Live Code Viewer -->
|
| 96 |
+
<main class="right-panel" id="right-panel">
|
| 97 |
+
<div class="code-header">
|
| 98 |
+
<span class="code-title">Code Output</span>
|
| 99 |
+
<span class="code-badge" id="code-badge">waiting</span>
|
| 100 |
+
</div>
|
| 101 |
+
<pre class="code-viewer"
|
| 102 |
+
id="code-viewer"><code id="code-output">// Upload a paper to see the generated code hereβ¦</code></pre>
|
| 103 |
+
</main>
|
| 104 |
+
|
| 105 |
+
<script src="/script.js"></script>
|
| 106 |
+
|
| 107 |
+
<!-- Floating Image Window (Hidden) -->
|
| 108 |
+
<div id="image-float" class="float-window hidden" style="display: none !important;">
|
| 109 |
+
<div class="float-header" id="float-header">
|
| 110 |
+
<span class="float-title">π¨ Concept Illustration</span>
|
| 111 |
+
<div class="float-actions">
|
| 112 |
+
<button id="float-download" class="float-btn" title="Download PNG">
|
| 113 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
| 114 |
+
stroke-linecap="round" stroke-linejoin="round">
|
| 115 |
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
| 116 |
+
<polyline points="7 10 12 15 17 10" />
|
| 117 |
+
<line x1="12" y1="15" x2="12" y2="3" />
|
| 118 |
+
</svg>
|
| 119 |
+
</button>
|
| 120 |
+
<button id="float-minimize" class="float-btn" title="Minimize">β</button>
|
| 121 |
+
<button id="float-close" class="float-btn" title="Close">β</button>
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
<div class="float-body" id="float-body">
|
| 125 |
+
<div class="float-spinner" id="float-spinner">
|
| 126 |
+
<div class="paint-brush-container">
|
| 127 |
+
<div class="brush">ποΈ</div>
|
| 128 |
+
<div class="shimmer-line"></div>
|
| 129 |
+
</div>
|
| 130 |
+
<p id="visualize-status">FLUX is painting your conceptβ¦</p>
|
| 131 |
+
</div>
|
| 132 |
+
<img id="float-image" class="float-image hidden" alt="Concept Illustration" />
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
|
| 136 |
+
<!-- Minimized Pill (Hidden) -->
|
| 137 |
+
<div id="image-pill" class="float-pill hidden" style="display: none !important;">
|
| 138 |
+
<span>π¨ Illustration</span>
|
| 139 |
+
</div>
|
| 140 |
+
</body>
|
| 141 |
+
|
| 142 |
+
</html>
|
llm_client.py
ADDED
|
@@ -0,0 +1,603 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Pundit Feynman LLM Client β 3-Stage Pipeline
|
| 3 |
+
Stage 1: Analyze (images β structured JSON analysis)
|
| 4 |
+
Stage 2: Design (analysis β implementation plan JSON)
|
| 5 |
+
Stage 3: Generate (analysis + design β notebook cells JSON)
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import json
|
| 10 |
+
import time
|
| 11 |
+
import re
|
| 12 |
+
import requests
|
| 13 |
+
from openai import OpenAI
|
| 14 |
+
from dotenv import load_dotenv
|
| 15 |
+
|
| 16 |
+
load_dotenv()
|
| 17 |
+
|
| 18 |
+
# ββ Configuration ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 19 |
+
API_KEY = os.getenv("NVIDIA_API_KEY", "")
|
| 20 |
+
BASE_URL = os.getenv("NVIDIA_BASE_URL", "https://integrate.api.nvidia.com/v1")
|
| 21 |
+
MODEL = os.getenv("LLM_MODEL", "qwen/qwen3.5-397b-a17b")
|
| 22 |
+
MAX_IMAGES_PER_REQUEST = int(os.getenv("MAX_IMAGES_PER_REQUEST", "8"))
|
| 23 |
+
|
| 24 |
+
# OCR Configuration
|
| 25 |
+
OCR_API_KEY = os.getenv("NVIDIA_OCR_API_KEY", "")
|
| 26 |
+
OCR_API_URL = "https://ai.api.nvidia.com/v1/cv/nvidia/nemoretriever-ocr-v1"
|
| 27 |
+
|
| 28 |
+
# FLUX.1-schnell Image Generation
|
| 29 |
+
FLUX_API_KEY = os.getenv("NVIDIA_FLUX_API_KEY", "")
|
| 30 |
+
FLUX_API_URL = "https://ai.api.nvidia.com/v1/genai/black-forest-labs/flux.1-schnell"
|
| 31 |
+
|
| 32 |
+
MAX_RETRIES = 3
|
| 33 |
+
RETRY_DELAYS = [5, 15, 30]
|
| 34 |
+
|
| 35 |
+
client = OpenAI(base_url=BASE_URL, api_key=API_KEY)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# ββ Prompts ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 39 |
+
|
| 40 |
+
SYSTEM_PROMPT = (
|
| 41 |
+
"You are an expert research engineer and educator who converts academic papers into "
|
| 42 |
+
"clear, educational, executable Python code. You produce structured JSON output for "
|
| 43 |
+
"each stage of the pipeline. When building toy implementations, you create REAL working code "
|
| 44 |
+
"(PyTorch, Transformer layers, actual training loops) at reduced scale that "
|
| 45 |
+
"runs on CPU. You prioritize faithful replication of the paper's architecture "
|
| 46 |
+
"and algorithms while making the code deeply educational with clear explanations, "
|
| 47 |
+
"using the Feynman technique to break down complex math into simple analogies, "
|
| 48 |
+
"verbose logging, and insightful visualizations."
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
ANALYSIS_PROMPT = """Analyze this research paper text and return a JSON object with:
|
| 52 |
+
{
|
| 53 |
+
"title": "exact paper title",
|
| 54 |
+
"authors": ["author names"],
|
| 55 |
+
"research_field": "e.g. NLP, Computer Vision, RL",
|
| 56 |
+
"abstract_summary": "2-3 sentence plain English summary of the paper",
|
| 57 |
+
"feynman_analogy": "A brilliant, everyday analogy that maps perfectly to the paper's core key_insight (e.g., comparing attention mechanisms to a cocktail party)",
|
| 58 |
+
"feynman_core_concept": "Explain the paper's main idea as if teaching a bright 12-year-old, using the analogy above, in 3-5 sentences",
|
| 59 |
+
"key_insight": "the core novel contribution in one sentence",
|
| 60 |
+
"algorithms": [
|
| 61 |
+
{
|
| 62 |
+
"name": "algorithm name",
|
| 63 |
+
"purpose": "what it does",
|
| 64 |
+
"key_equations": ["important formulas in LaTeX notation"],
|
| 65 |
+
"pseudocode_steps": ["step1", "step2"]
|
| 66 |
+
}
|
| 67 |
+
],
|
| 68 |
+
"architecture": {
|
| 69 |
+
"type": "e.g. Transformer, CNN, GAN",
|
| 70 |
+
"components": ["list of main components"],
|
| 71 |
+
"data_flow": "description of how data flows through the model"
|
| 72 |
+
},
|
| 73 |
+
"datasets_mentioned": ["dataset names"],
|
| 74 |
+
"implementation_requirements": {
|
| 75 |
+
"frameworks": ["PyTorch"],
|
| 76 |
+
"key_hyperparameters": {"param": "value"},
|
| 77 |
+
"estimated_complexity": "low/medium/high for toy version"
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
Return ONLY valid JSON, no markdown, no extra text."""
|
| 82 |
+
|
| 83 |
+
DESIGN_PROMPT = """Based on this paper analysis, create a toy implementation design that runs on CPU.
|
| 84 |
+
Return a JSON object with:
|
| 85 |
+
{
|
| 86 |
+
"model_architecture": {
|
| 87 |
+
"type": "architecture type",
|
| 88 |
+
"embed_dim": 64,
|
| 89 |
+
"num_layers": 2,
|
| 90 |
+
"num_heads": 4,
|
| 91 |
+
"vocab_size": 1000,
|
| 92 |
+
"max_seq_len": 64,
|
| 93 |
+
"components": [
|
| 94 |
+
{
|
| 95 |
+
"name": "component name",
|
| 96 |
+
"class_name": "PythonClassName",
|
| 97 |
+
"description": "what this component does",
|
| 98 |
+
"key_params": {"param": "value"}
|
| 99 |
+
}
|
| 100 |
+
]
|
| 101 |
+
},
|
| 102 |
+
"training_config": {
|
| 103 |
+
"optimizer": "Adam",
|
| 104 |
+
"learning_rate": 0.001,
|
| 105 |
+
"num_epochs": 5,
|
| 106 |
+
"batch_size": 16,
|
| 107 |
+
"loss_function": "CrossEntropyLoss",
|
| 108 |
+
"dataset_strategy": "synthetic generation approach"
|
| 109 |
+
},
|
| 110 |
+
"visualization_plan": [
|
| 111 |
+
"loss curve",
|
| 112 |
+
"attention heatmap",
|
| 113 |
+
"sample predictions"
|
| 114 |
+
],
|
| 115 |
+
"estimated_cells": 15,
|
| 116 |
+
"code_structure": [
|
| 117 |
+
{"section": "imports", "description": "required libraries"},
|
| 118 |
+
{"section": "model", "description": "model architecture classes"},
|
| 119 |
+
{"section": "data", "description": "synthetic data generation"},
|
| 120 |
+
{"section": "training", "description": "training loop"},
|
| 121 |
+
{"section": "evaluation", "description": "testing and visualization"}
|
| 122 |
+
]
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
Return ONLY valid JSON, no markdown, no extra text."""
|
| 126 |
+
|
| 127 |
+
GENERATE_PROMPT_TEMPLATE = """You are generating a Jupyter notebook from a paper analysis and implementation design.
|
| 128 |
+
Analysis: {analysis}
|
| 129 |
+
Design: {design}
|
| 130 |
+
|
| 131 |
+
Note: You are a 397B parameter model (Qwen 3.5) with 17B actively used parameters (MoE architecture).
|
| 132 |
+
This means you have deep expertise and vast knowledge. Use it to produce genuinely educational content.
|
| 133 |
+
|
| 134 |
+
Return a JSON array of notebook cells following this **exact 13-section structure**:
|
| 135 |
+
|
| 136 |
+
1. **Title & Overview** (markdown) β Paper title, authors, a one-paragraph summary of the paper.
|
| 137 |
+
|
| 138 |
+
2. **Table of Contents** (markdown) β Numbered list of all 13 sections. Each section name should be a clickable anchor link.
|
| 139 |
+
|
| 140 |
+
3. **The Feynman Explanation** (markdown) β A step-by-step explanation of the WHOLE paper using the Feynman technique. Break down the core algorithms, math, and architecture into the absolute simplest terms possible. Expand heavily on the `feynman_analogy` and `feynman_core_concept` from the analysis. Use relatable, everyday analogies for each major step so a beginner can intuitively grasp how the system works before seeing the code.
|
| 141 |
+
|
| 142 |
+
4. **Environment Setup** (code) β pip installs and imports. Include `torch`, `numpy`, `matplotlib`, and any other needed libraries.
|
| 143 |
+
|
| 144 |
+
5. **Configuration & Hyperparameters** (code) β A single config dict or dataclass with all hyperparameters. Add comments explaining each.
|
| 145 |
+
|
| 146 |
+
6. **Data Preparation** (code) β Synthetic dataset generation or loading. Must produce realistic dummy data matching the paper's domain.
|
| 147 |
+
|
| 148 |
+
7. **Model Architecture** (code) β Full PyTorch model implementation. Use `nn.Module` subclasses with detailed docstrings about each component. Include shape comments.
|
| 149 |
+
|
| 150 |
+
8. **Training Loop** (code) β Complete training loop with loss tracking, progress printing, and gradient clipping.
|
| 151 |
+
|
| 152 |
+
9. **Training Execution** (code) β Run the training and display results.
|
| 153 |
+
|
| 154 |
+
10. **Evaluation & Metrics** (code) β Run inference on test data and compute relevant metrics.
|
| 155 |
+
|
| 156 |
+
11. **Visualizations** (code) β Matplotlib charts: loss curves, attention heatmaps or feature maps, sample predictions.
|
| 157 |
+
|
| 158 |
+
12. **Key Takeaways** (markdown) β Bullet-point summary of what was learned, what would change at full scale, potential improvements.
|
| 159 |
+
|
| 160 |
+
13. **References** (markdown) β Paper citation, related work links, library documentation links.
|
| 161 |
+
|
| 162 |
+
Each cell in the JSON array must have:
|
| 163 |
+
{{"cell_type": "code" or "markdown", "source": "cell content as a string"}}
|
| 164 |
+
|
| 165 |
+
RULES:
|
| 166 |
+
- All code must be executable on CPU
|
| 167 |
+
- Use educational variable names and heavy commenting
|
| 168 |
+
- Include print() statements showing tensor shapes and intermediate results
|
| 169 |
+
- Follow the 13-section structure exactly
|
| 170 |
+
- Minimum 15 cells total
|
| 171 |
+
- The Feynman Explanation should be at least 300 words
|
| 172 |
+
- Return ONLY the JSON array, no markdown fences"""
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
# ββ OCR extraction (NVIDIA NeMo Retriever OCR v1) βββββββββββββββββββββββββ
|
| 176 |
+
|
| 177 |
+
def extract_text_from_images(base64_images):
|
| 178 |
+
"""Extract text from paper page images using NVIDIA NeMo Retriever OCR API.
|
| 179 |
+
Sends page images to the dedicated OCR model for fast, accurate extraction.
|
| 180 |
+
Falls back to page-by-page if a batch request fails.
|
| 181 |
+
"""
|
| 182 |
+
all_text = []
|
| 183 |
+
headers = {
|
| 184 |
+
"Authorization": f"Bearer {OCR_API_KEY}",
|
| 185 |
+
"Accept": "application/json",
|
| 186 |
+
"Content-Type": "application/json",
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
total = len(base64_images)
|
| 190 |
+
print(f" OCR: Processing {total} pages via NVIDIA NeMo Retriever...")
|
| 191 |
+
|
| 192 |
+
for page_idx, img_b64 in enumerate(base64_images):
|
| 193 |
+
print(f" Page {page_idx + 1}/{total}...")
|
| 194 |
+
|
| 195 |
+
payload = {
|
| 196 |
+
"input": [
|
| 197 |
+
{
|
| 198 |
+
"type": "image_url",
|
| 199 |
+
"url": f"data:image/jpeg;base64,{img_b64}"
|
| 200 |
+
}
|
| 201 |
+
],
|
| 202 |
+
"merge_levels": ["paragraph"]
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
try:
|
| 206 |
+
resp = requests.post(
|
| 207 |
+
OCR_API_URL,
|
| 208 |
+
headers=headers,
|
| 209 |
+
json=payload,
|
| 210 |
+
timeout=60,
|
| 211 |
+
)
|
| 212 |
+
resp.raise_for_status()
|
| 213 |
+
result = resp.json()
|
| 214 |
+
|
| 215 |
+
# Extract text from OCR response
|
| 216 |
+
page_text = _parse_ocr_response(result, page_idx + 1)
|
| 217 |
+
if page_text:
|
| 218 |
+
all_text.append(page_text)
|
| 219 |
+
|
| 220 |
+
except Exception as e:
|
| 221 |
+
print(f" \u26a0 OCR failed for page {page_idx + 1}: {e}")
|
| 222 |
+
# Continue with remaining pages
|
| 223 |
+
continue
|
| 224 |
+
|
| 225 |
+
if not all_text:
|
| 226 |
+
raise RuntimeError("OCR failed: No text extracted from any page")
|
| 227 |
+
|
| 228 |
+
combined = "\n\n".join(all_text)
|
| 229 |
+
print(f" OCR complete: {len(combined)} chars from {len(all_text)}/{total} pages")
|
| 230 |
+
return combined
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def _parse_ocr_response(response_json, page_num):
|
| 234 |
+
"""Parse the NVIDIA OCR API response into clean text.
|
| 235 |
+
Response format: {"data": [{"text_detections": [{"text_prediction": {"text": ..., "confidence": ...}}]}]}
|
| 236 |
+
"""
|
| 237 |
+
texts = []
|
| 238 |
+
try:
|
| 239 |
+
for item in response_json.get("data", []):
|
| 240 |
+
for detection in item.get("text_detections", []):
|
| 241 |
+
pred = detection.get("text_prediction", {})
|
| 242 |
+
text = pred.get("text", "").strip()
|
| 243 |
+
confidence = pred.get("confidence", 0)
|
| 244 |
+
# Only include text with reasonable confidence
|
| 245 |
+
if text and confidence > 0.3:
|
| 246 |
+
texts.append(text)
|
| 247 |
+
except Exception as e:
|
| 248 |
+
print(f" \u26a0 Error parsing OCR response for page {page_num}: {e}")
|
| 249 |
+
return ""
|
| 250 |
+
|
| 251 |
+
return "\n".join(texts)
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
# ββ LLM Call with Retry βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 255 |
+
|
| 256 |
+
def call_with_retry(messages, max_tokens=4096, temperature=0.3, stream=False):
|
| 257 |
+
"""Call the LLM API with retry logic for transient errors."""
|
| 258 |
+
last_error = None
|
| 259 |
+
|
| 260 |
+
for attempt in range(MAX_RETRIES):
|
| 261 |
+
try:
|
| 262 |
+
kwargs = dict(
|
| 263 |
+
model=MODEL,
|
| 264 |
+
messages=messages,
|
| 265 |
+
max_tokens=max_tokens,
|
| 266 |
+
temperature=temperature,
|
| 267 |
+
timeout=300,
|
| 268 |
+
)
|
| 269 |
+
if stream:
|
| 270 |
+
kwargs["stream"] = True
|
| 271 |
+
return client.chat.completions.create(**kwargs)
|
| 272 |
+
else:
|
| 273 |
+
response = client.chat.completions.create(**kwargs)
|
| 274 |
+
return response.choices[0].message.content
|
| 275 |
+
|
| 276 |
+
except Exception as e:
|
| 277 |
+
error_str = str(e).lower()
|
| 278 |
+
if any(kw in error_str for kw in ["429", "rate", "500", "503", "overloaded", "unavailable"]):
|
| 279 |
+
last_error = e
|
| 280 |
+
wait = RETRY_DELAYS[min(attempt, len(RETRY_DELAYS) - 1)]
|
| 281 |
+
print(f" β Transient error. Waiting {wait}s before retry {attempt + 1}/{MAX_RETRIES}...")
|
| 282 |
+
time.sleep(wait)
|
| 283 |
+
else:
|
| 284 |
+
raise
|
| 285 |
+
|
| 286 |
+
raise RuntimeError(f"Failed after {MAX_RETRIES} retries. Last error: {last_error}")
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
# ββ JSON Parsing ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 290 |
+
|
| 291 |
+
def parse_llm_json(raw_text, step_name):
|
| 292 |
+
"""Parse JSON from LLM response, with cleanup and one repair attempt."""
|
| 293 |
+
if raw_text is None:
|
| 294 |
+
print(f" β LLM returned None for {step_name}")
|
| 295 |
+
return {}
|
| 296 |
+
text = raw_text.strip()
|
| 297 |
+
|
| 298 |
+
# Strip markdown code fences if present
|
| 299 |
+
if text.startswith("```"):
|
| 300 |
+
first_newline = text.index("\n")
|
| 301 |
+
text = text[first_newline + 1:]
|
| 302 |
+
if text.endswith("```"):
|
| 303 |
+
text = text[:-3]
|
| 304 |
+
text = text.strip()
|
| 305 |
+
|
| 306 |
+
# Try direct parse
|
| 307 |
+
try:
|
| 308 |
+
return json.loads(text)
|
| 309 |
+
except json.JSONDecodeError as e:
|
| 310 |
+
print(f" β JSON parse failed in {step_name}. Attempting repair...")
|
| 311 |
+
|
| 312 |
+
# Attempt auto-repair via LLM
|
| 313 |
+
repair_prompt = (
|
| 314 |
+
f"The following text was supposed to be valid JSON but has a syntax error:\n\n"
|
| 315 |
+
f"{text[:6000]}\n\n"
|
| 316 |
+
f"Error: {e}\n\n"
|
| 317 |
+
f"Return ONLY the corrected valid JSON, nothing else."
|
| 318 |
+
)
|
| 319 |
+
repaired = call_with_retry(
|
| 320 |
+
messages=[
|
| 321 |
+
{"role": "system", "content": "You are a JSON repair tool. Return only valid JSON."},
|
| 322 |
+
{"role": "user", "content": repair_prompt},
|
| 323 |
+
],
|
| 324 |
+
max_tokens=max(len(text) // 2, 4096),
|
| 325 |
+
temperature=0.1,
|
| 326 |
+
)
|
| 327 |
+
if repaired is None:
|
| 328 |
+
raise ValueError(f"Could not repair JSON from {step_name} β LLM returned None")
|
| 329 |
+
repaired = repaired.strip()
|
| 330 |
+
if repaired.startswith("```"):
|
| 331 |
+
repaired = repaired.split("\n", 1)[1]
|
| 332 |
+
if repaired.endswith("```"):
|
| 333 |
+
repaired = repaired[:-3]
|
| 334 |
+
|
| 335 |
+
try:
|
| 336 |
+
return json.loads(repaired.strip())
|
| 337 |
+
except json.JSONDecodeError:
|
| 338 |
+
# Last resort: try to extract JSON from the text
|
| 339 |
+
json_match = re.search(r'[\[{].*[\]}]', repaired.strip(), re.DOTALL)
|
| 340 |
+
if json_match:
|
| 341 |
+
return json.loads(json_match.group())
|
| 342 |
+
raise ValueError(f"Could not parse JSON from {step_name} even after repair.")
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
# ββ Pipeline Stages βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 346 |
+
|
| 347 |
+
def analyze_paper(raw_text):
|
| 348 |
+
"""Stage 1: Analyze extracted text into structured JSON."""
|
| 349 |
+
messages = [
|
| 350 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 351 |
+
{"role": "user", "content": f"{ANALYSIS_PROMPT}\n\n--- EXTRACTED PAPER TEXT ---\n\n{raw_text}"},
|
| 352 |
+
]
|
| 353 |
+
raw = call_with_retry(messages, max_tokens=6144, temperature=0.2)
|
| 354 |
+
return parse_llm_json(raw, "paper_analysis")
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def design_implementation(analysis):
|
| 358 |
+
"""Stage 2: Create implementation design from analysis."""
|
| 359 |
+
messages = [
|
| 360 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 361 |
+
{"role": "user", "content": f"{DESIGN_PROMPT}\n\n--- PAPER ANALYSIS ---\n\n{json.dumps(analysis, indent=2)}"},
|
| 362 |
+
]
|
| 363 |
+
raw = call_with_retry(messages, max_tokens=6144, temperature=0.2)
|
| 364 |
+
return parse_llm_json(raw, "implementation_design")
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
def generate_notebook_cells_stream(analysis, design):
|
| 368 |
+
"""
|
| 369 |
+
Stage 3: Generate notebook cells from analysis and design.
|
| 370 |
+
Yields tokens from the LLM for live streaming in the UI.
|
| 371 |
+
Finally yields the parsed cells list.
|
| 372 |
+
"""
|
| 373 |
+
prompt = GENERATE_PROMPT_TEMPLATE.format(
|
| 374 |
+
analysis=json.dumps(analysis, indent=2),
|
| 375 |
+
design=json.dumps(design, indent=2),
|
| 376 |
+
)
|
| 377 |
+
messages = [
|
| 378 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 379 |
+
{"role": "user", "content": prompt},
|
| 380 |
+
]
|
| 381 |
+
|
| 382 |
+
# Use streaming mode
|
| 383 |
+
stream = call_with_retry(messages, max_tokens=65536, temperature=0.3, stream=True)
|
| 384 |
+
full_response = []
|
| 385 |
+
|
| 386 |
+
for chunk in stream:
|
| 387 |
+
if chunk.choices and chunk.choices[0].delta.content:
|
| 388 |
+
token = chunk.choices[0].delta.content
|
| 389 |
+
full_response.append(token)
|
| 390 |
+
yield ("token", token)
|
| 391 |
+
|
| 392 |
+
raw_text = "".join(full_response)
|
| 393 |
+
result = parse_llm_json(raw_text, "notebook_cells")
|
| 394 |
+
|
| 395 |
+
# Final logic to ensure we return a list of cells
|
| 396 |
+
cells = []
|
| 397 |
+
if isinstance(result, dict):
|
| 398 |
+
cells = result.get("cells", [{"cell_type": "markdown", "source": json.dumps(result, indent=2)}])
|
| 399 |
+
elif isinstance(result, list):
|
| 400 |
+
cells = result
|
| 401 |
+
else:
|
| 402 |
+
cells = [{"cell_type": "markdown", "source": raw_text}]
|
| 403 |
+
|
| 404 |
+
yield ("cells_final", cells)
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
# ββ Streaming Pipeline βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 408 |
+
|
| 409 |
+
def run_full_pipeline_stream(raw_text):
|
| 410 |
+
"""
|
| 411 |
+
Orchestrates the full 3-stage pipeline.
|
| 412 |
+
Yields SSE-formatted text events for the frontend code viewer.
|
| 413 |
+
Returns final cells via the 'cells' key in the last event.
|
| 414 |
+
|
| 415 |
+
Yields tuples of (event_type, data):
|
| 416 |
+
("text", str) β display text for the code viewer
|
| 417 |
+
("cells", list) β final cells (only yielded once at end)
|
| 418 |
+
("analysis", dict) β analysis metadata
|
| 419 |
+
("error", str) β error message
|
| 420 |
+
"""
|
| 421 |
+
try:
|
| 422 |
+
# ββ Stage 1: Analyze ββ
|
| 423 |
+
yield ("text", "\n Analyzing Paper\n")
|
| 424 |
+
yield ("text", " " + "β" * 40 + "\n\n")
|
| 425 |
+
|
| 426 |
+
analysis = analyze_paper(raw_text)
|
| 427 |
+
|
| 428 |
+
if not analysis:
|
| 429 |
+
yield ("text", " Analysis returned empty. The LLM may have failed.\n\n")
|
| 430 |
+
yield ("error", "Analysis returned empty result")
|
| 431 |
+
return
|
| 432 |
+
|
| 433 |
+
title = analysis.get("title", "Unknown Paper")
|
| 434 |
+
field = analysis.get("research_field", "")
|
| 435 |
+
insight = analysis.get("key_insight", "")
|
| 436 |
+
algos = [a.get("name", "") for a in analysis.get("algorithms", [])]
|
| 437 |
+
feynman_analogy = analysis.get("feynman_analogy", "")
|
| 438 |
+
feynman_concept = analysis.get("feynman_core_concept", "")
|
| 439 |
+
|
| 440 |
+
# Clean, minimal analysis output
|
| 441 |
+
yield ("text", f" {title}\n")
|
| 442 |
+
yield ("text", f" {field}\n\n")
|
| 443 |
+
|
| 444 |
+
# The Feynman Explanation β the star of the show
|
| 445 |
+
if feynman_analogy or feynman_concept:
|
| 446 |
+
yield ("text", " βββ The Feynman Explanation βββ\n\n")
|
| 447 |
+
if feynman_analogy:
|
| 448 |
+
yield ("text", f" {feynman_analogy}\n\n")
|
| 449 |
+
if feynman_concept:
|
| 450 |
+
yield ("text", f" {feynman_concept}\n\n")
|
| 451 |
+
|
| 452 |
+
if insight:
|
| 453 |
+
yield ("text", f" Key Insight: {insight}\n\n")
|
| 454 |
+
|
| 455 |
+
yield ("text", " Analysis complete.\n\n")
|
| 456 |
+
|
| 457 |
+
yield ("analysis", {
|
| 458 |
+
"title": title,
|
| 459 |
+
"field": field,
|
| 460 |
+
"insight": insight,
|
| 461 |
+
"algorithms": algos,
|
| 462 |
+
"feynman_analogy": feynman_analogy,
|
| 463 |
+
})
|
| 464 |
+
|
| 465 |
+
# ββ Stage 2: Design ββ
|
| 466 |
+
yield ("text", "\n Designing Implementation\n")
|
| 467 |
+
yield ("text", " " + "β" * 40 + "\n\n")
|
| 468 |
+
|
| 469 |
+
design = design_implementation(analysis)
|
| 470 |
+
if not design:
|
| 471 |
+
design = {}
|
| 472 |
+
|
| 473 |
+
arch = design.get("model_architecture", {})
|
| 474 |
+
tc = design.get("training_config", {})
|
| 475 |
+
yield ("text", f" Architecture: {arch.get('type', 'N/A')}\n")
|
| 476 |
+
yield ("text", f" Training: {tc.get('optimizer', 'Adam')}, lr={tc.get('learning_rate', 0.001)}, {tc.get('num_epochs', 10)} epochs\n")
|
| 477 |
+
yield ("text", " Design complete.\n\n")
|
| 478 |
+
|
| 479 |
+
# ββ Stage 3: Generate (Now with LIVE STREAMING) ββ
|
| 480 |
+
yield ("text", "\n Generating Notebook (Live Streaming)\n")
|
| 481 |
+
yield ("text", " " + "β" * 40 + "\n\n")
|
| 482 |
+
|
| 483 |
+
cells = []
|
| 484 |
+
for event_type, data in generate_notebook_cells_stream(analysis, design):
|
| 485 |
+
if event_type == "token":
|
| 486 |
+
# Yield raw tokens to the code viewer for "ghost-writing" effect
|
| 487 |
+
yield ("text", data)
|
| 488 |
+
elif event_type == "cells_final":
|
| 489 |
+
cells = data
|
| 490 |
+
|
| 491 |
+
code_cells = sum(1 for c in cells if c.get("cell_type") == "code")
|
| 492 |
+
md_cells = sum(1 for c in cells if c.get("cell_type") == "markdown")
|
| 493 |
+
yield ("text", f"\n\n β
Generation complete: {len(cells)} cells ({code_cells} code, {md_cells} markdown)\n")
|
| 494 |
+
yield ("text", " Notebook ready for download.\n")
|
| 495 |
+
|
| 496 |
+
yield ("cells", cells)
|
| 497 |
+
|
| 498 |
+
except Exception as e:
|
| 499 |
+
yield ("error", str(e))
|
| 500 |
+
|
| 501 |
+
|
| 502 |
+
# ββ Legacy compatibility βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 503 |
+
# Keep old function signatures working for backward compatibility
|
| 504 |
+
|
| 505 |
+
def extract_methodology(base64_images):
|
| 506 |
+
"""Legacy wrapper: extracts text from images."""
|
| 507 |
+
return extract_text_from_images(base64_images)
|
| 508 |
+
|
| 509 |
+
|
| 510 |
+
# ββ Visual Illustration (FLUX.1-schnell) βββββββββββββββββββββββββββββββββββ
|
| 511 |
+
|
| 512 |
+
# System prompt for Qwen to craft image generation prompts
|
| 513 |
+
IMAGE_PROMPT_SYSTEM = """You are a world-class scientific illustrator and prompt engineer.
|
| 514 |
+
Your job: given a structured analysis of a research paper, write ONE prompt for an
|
| 515 |
+
AI image generator (FLUX) that will produce a clear, beautiful, academic-quality
|
| 516 |
+
visual illustration of the paper's CORE CONCEPT.
|
| 517 |
+
|
| 518 |
+
Rules:
|
| 519 |
+
1. Focus on the MAIN IDEA β the central algorithm, architecture, or mechanism.
|
| 520 |
+
2. Describe the visual layout precisely: shapes, arrows, labels, flow direction.
|
| 521 |
+
3. Use academic illustration style: clean lines, labeled components, white background.
|
| 522 |
+
4. Include spatial relationships: "on the left", "flowing into", "surrounded by".
|
| 523 |
+
5. Mention color coding for different components.
|
| 524 |
+
6. Do NOT include text/equations in the image β focus on visual metaphors.
|
| 525 |
+
7. Keep it to ONE paragraph, 80-120 words.
|
| 526 |
+
8. End with style keywords: "scientific diagram, educational poster, vector style,
|
| 527 |
+
clean layout, professional, high resolution"
|
| 528 |
+
|
| 529 |
+
Return ONLY the prompt text, nothing else."""
|
| 530 |
+
|
| 531 |
+
def generate_concept_image(analysis):
|
| 532 |
+
"""
|
| 533 |
+
Generate a visual illustration of a paper's core concept.
|
| 534 |
+
Step 1: Qwen crafts a detailed, structured prompt from the analysis.
|
| 535 |
+
Step 2: FLUX.1-schnell generates the image.
|
| 536 |
+
Returns base64-encoded PNG string or None on failure.
|
| 537 |
+
"""
|
| 538 |
+
if not FLUX_API_KEY:
|
| 539 |
+
raise RuntimeError("NVIDIA_FLUX_API_KEY not set")
|
| 540 |
+
|
| 541 |
+
# ββ Step 1: Qwen β Image Prompt ββ
|
| 542 |
+
analysis_summary = json.dumps({
|
| 543 |
+
"title": analysis.get("title", ""),
|
| 544 |
+
"research_field": analysis.get("research_field") or analysis.get("field", ""),
|
| 545 |
+
"key_insight": analysis.get("key_insight") or analysis.get("insight", ""),
|
| 546 |
+
"algorithms": analysis.get("algorithms", []),
|
| 547 |
+
"feynman_analogy": analysis.get("feynman_analogy", ""),
|
| 548 |
+
"feynman_core_concept": analysis.get("feynman_core_concept", ""),
|
| 549 |
+
}, indent=2)
|
| 550 |
+
|
| 551 |
+
prompt_messages = [
|
| 552 |
+
{"role": "system", "content": IMAGE_PROMPT_SYSTEM},
|
| 553 |
+
{"role": "user", "content": f"Create an image generation prompt for this paper:\n\n{analysis_summary}"},
|
| 554 |
+
]
|
| 555 |
+
|
| 556 |
+
print(" π¨ Generating image prompt via Qwen...")
|
| 557 |
+
image_prompt = call_with_retry(prompt_messages, max_tokens=300, temperature=0.7)
|
| 558 |
+
if not image_prompt:
|
| 559 |
+
raise RuntimeError("Qwen returned empty image prompt")
|
| 560 |
+
|
| 561 |
+
# Add preamble for FLUX to ensure academic quality
|
| 562 |
+
full_prompt = (
|
| 563 |
+
"A detailed, clean scientific illustration for an academic paper. "
|
| 564 |
+
"Style: professional educational diagram, labeled components, "
|
| 565 |
+
"modern flat vector design, white background, high contrast, "
|
| 566 |
+
"color-coded sections, no text. "
|
| 567 |
+
f"{image_prompt.strip()}"
|
| 568 |
+
)
|
| 569 |
+
print(f" π FLUX prompt ({len(full_prompt)} chars): {full_prompt[:100]}...")
|
| 570 |
+
|
| 571 |
+
# ββ Step 2: FLUX.1-schnell β Image ββ
|
| 572 |
+
print(" πΌοΈ Calling FLUX.1-schnell...")
|
| 573 |
+
headers = {
|
| 574 |
+
"Authorization": f"Bearer {FLUX_API_KEY}",
|
| 575 |
+
"Content-Type": "application/json",
|
| 576 |
+
"Accept": "application/json",
|
| 577 |
+
}
|
| 578 |
+
payload = {
|
| 579 |
+
"prompt": full_prompt,
|
| 580 |
+
"height": 1024,
|
| 581 |
+
"width": 1024,
|
| 582 |
+
"num_inference_steps": 4,
|
| 583 |
+
"guidance_scale": 0.0,
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
response = requests.post(FLUX_API_URL, headers=headers, json=payload, timeout=60)
|
| 587 |
+
|
| 588 |
+
if response.status_code != 200:
|
| 589 |
+
raise RuntimeError(f"FLUX API error {response.status_code}: {response.text[:200]}")
|
| 590 |
+
|
| 591 |
+
result = response.json()
|
| 592 |
+
# FLUX returns {"image": "base64..."} or {"artifacts": [{"base64": "..."}]}
|
| 593 |
+
image_b64 = None
|
| 594 |
+
if "image" in result:
|
| 595 |
+
image_b64 = result["image"]
|
| 596 |
+
elif "artifacts" in result and len(result["artifacts"]) > 0:
|
| 597 |
+
image_b64 = result["artifacts"][0].get("base64", "")
|
| 598 |
+
|
| 599 |
+
if not image_b64:
|
| 600 |
+
raise RuntimeError("FLUX returned no image data")
|
| 601 |
+
|
| 602 |
+
print(f" β
Image generated ({len(image_b64)} chars base64)")
|
| 603 |
+
return image_b64
|
notebook_builder.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Pundit Feynman Notebook Builder
|
| 3 |
+
Supports both structured JSON cells and legacy free-text β regex approach.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import re
|
| 7 |
+
import nbformat
|
| 8 |
+
from nbformat.v4 import new_notebook, new_code_cell, new_markdown_cell
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def build_notebook_from_cells(cells_json, output_path):
|
| 12 |
+
"""
|
| 13 |
+
Build a .ipynb from a list of structured cell dicts.
|
| 14 |
+
Each cell: {"cell_type": "code"|"markdown", "source": "..."}
|
| 15 |
+
"""
|
| 16 |
+
nb = new_notebook()
|
| 17 |
+
nb.metadata["kernelspec"] = {
|
| 18 |
+
"display_name": "Python 3",
|
| 19 |
+
"language": "python",
|
| 20 |
+
"name": "python3",
|
| 21 |
+
}
|
| 22 |
+
nb.metadata["language_info"] = {
|
| 23 |
+
"name": "python",
|
| 24 |
+
"version": "3.9",
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
for cell_data in cells_json:
|
| 28 |
+
cell_type = cell_data.get("cell_type", "code")
|
| 29 |
+
source = cell_data.get("source", "")
|
| 30 |
+
|
| 31 |
+
if cell_type == "markdown":
|
| 32 |
+
nb.cells.append(new_markdown_cell(source))
|
| 33 |
+
elif cell_type == "code":
|
| 34 |
+
nb.cells.append(new_code_cell(source))
|
| 35 |
+
else:
|
| 36 |
+
# Default to code for unknown types
|
| 37 |
+
nb.cells.append(new_code_cell(source))
|
| 38 |
+
|
| 39 |
+
# Fallback: if no cells, add a placeholder
|
| 40 |
+
if not nb.cells:
|
| 41 |
+
nb.cells.append(new_markdown_cell("# No cells were generated"))
|
| 42 |
+
|
| 43 |
+
with open(output_path, "w", encoding="utf-8") as f:
|
| 44 |
+
nbformat.write(nb, f)
|
| 45 |
+
|
| 46 |
+
code_cells = sum(1 for c in nb.cells if c.cell_type == "code")
|
| 47 |
+
md_cells = sum(1 for c in nb.cells if c.cell_type == "markdown")
|
| 48 |
+
print(f" π Notebook saved: {output_path} ({len(nb.cells)} cells: {code_cells} code, {md_cells} markdown)")
|
| 49 |
+
return output_path
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def build_notebook(full_text, output_path):
|
| 53 |
+
"""
|
| 54 |
+
Legacy: Parses mixed markdown/code text into a Jupyter Notebook.
|
| 55 |
+
Separates ```python code blocks into Code cells, everything else into Markdown cells.
|
| 56 |
+
"""
|
| 57 |
+
nb = new_notebook()
|
| 58 |
+
nb.metadata["kernelspec"] = {
|
| 59 |
+
"display_name": "Python 3",
|
| 60 |
+
"language": "python",
|
| 61 |
+
"name": "python3",
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
# Split on ```python ... ``` blocks
|
| 65 |
+
pattern = r"```python\s*\n(.*?)```"
|
| 66 |
+
parts = re.split(pattern, full_text, flags=re.DOTALL)
|
| 67 |
+
|
| 68 |
+
for i, part in enumerate(parts):
|
| 69 |
+
content = part.strip()
|
| 70 |
+
if not content:
|
| 71 |
+
continue
|
| 72 |
+
|
| 73 |
+
if i % 2 == 0:
|
| 74 |
+
nb.cells.append(new_markdown_cell(content))
|
| 75 |
+
else:
|
| 76 |
+
nb.cells.append(new_code_cell(content))
|
| 77 |
+
|
| 78 |
+
if not nb.cells:
|
| 79 |
+
nb.cells.append(new_markdown_cell(full_text))
|
| 80 |
+
|
| 81 |
+
with open(output_path, "w", encoding="utf-8") as f:
|
| 82 |
+
nbformat.write(nb, f)
|
| 83 |
+
|
| 84 |
+
print(f" π Notebook saved: {output_path} ({len(nb.cells)} cells)")
|
| 85 |
+
return output_path
|
pdf_processor.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import base64
|
| 2 |
+
import fitz # PyMuPDF
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def process_pdf_to_base64(pdf_path: str, dpi: int = 150) -> list[str]:
|
| 6 |
+
"""
|
| 7 |
+
Converts each page of a PDF into a base64-encoded JPEG string.
|
| 8 |
+
Preserves full RGB color (important for color-coded graphs in papers).
|
| 9 |
+
"""
|
| 10 |
+
try:
|
| 11 |
+
doc = fitz.open(pdf_path)
|
| 12 |
+
base64_images = []
|
| 13 |
+
|
| 14 |
+
for page in doc:
|
| 15 |
+
pix = page.get_pixmap(dpi=dpi)
|
| 16 |
+
img_bytes = pix.tobytes("jpeg")
|
| 17 |
+
img_b64 = base64.b64encode(img_bytes).decode("utf-8")
|
| 18 |
+
base64_images.append(img_b64)
|
| 19 |
+
|
| 20 |
+
doc.close()
|
| 21 |
+
print(f"Extracted {len(base64_images)} pages at {dpi} DPI (color preserved)")
|
| 22 |
+
return base64_images
|
| 23 |
+
except Exception as e:
|
| 24 |
+
print(f"Error processing PDF: {e}")
|
| 25 |
+
raise e
|
requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Pundit Feynman: Paper-to-Notebook Pipeline
|
| 2 |
+
# Created for Hugging Face Spaces Deployment
|
| 3 |
+
|
| 4 |
+
fastapi
|
| 5 |
+
uvicorn
|
| 6 |
+
python-dotenv
|
| 7 |
+
requests
|
| 8 |
+
openai
|
| 9 |
+
httpx
|
| 10 |
+
pymupdf
|
| 11 |
+
nbformat
|
| 12 |
+
python-multipart
|
script.js
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ββ DOM Refs ββ
|
| 2 |
+
const dropZone = document.getElementById('drop-zone');
|
| 3 |
+
const fileInput = document.getElementById('file-input');
|
| 4 |
+
const uploadSection = document.getElementById('upload-section');
|
| 5 |
+
const extractStatus = document.getElementById('extract-status');
|
| 6 |
+
const extractLabel = document.getElementById('extract-label');
|
| 7 |
+
const streamStatus = document.getElementById('stream-status');
|
| 8 |
+
const doneSection = document.getElementById('done-section');
|
| 9 |
+
const errorSection = document.getElementById('error-section');
|
| 10 |
+
const errorText = document.getElementById('error-text');
|
| 11 |
+
const downloadBtn = document.getElementById('download-btn');
|
| 12 |
+
const resetBtn = document.getElementById('reset-btn');
|
| 13 |
+
const errorResetBtn = document.getElementById('error-reset-btn');
|
| 14 |
+
const codeOutput = document.getElementById('code-output');
|
| 15 |
+
const codeViewer = document.getElementById('code-viewer');
|
| 16 |
+
const codeBadge = document.getElementById('code-badge');
|
| 17 |
+
const arxivInput = document.getElementById('arxiv-input');
|
| 18 |
+
const arxivBtn = document.getElementById('arxiv-btn');
|
| 19 |
+
const visualizeBtn = document.getElementById('visualize-btn');
|
| 20 |
+
const imageFloat = document.getElementById('image-float');
|
| 21 |
+
const imagePill = document.getElementById('image-pill');
|
| 22 |
+
const floatHeader = document.getElementById('float-header');
|
| 23 |
+
const floatImage = document.getElementById('float-image');
|
| 24 |
+
const floatSpinner = document.getElementById('float-spinner');
|
| 25 |
+
const floatDownload = document.getElementById('float-download');
|
| 26 |
+
const floatMinimize = document.getElementById('float-minimize');
|
| 27 |
+
console.log('π Pundit Feynman Script Loaded [v2.1]');
|
| 28 |
+
console.log('π¨ Visualize Button found:', !!visualizeBtn);
|
| 29 |
+
|
| 30 |
+
window.onerror = function (msg, url, lineNo, columnNo, error) {
|
| 31 |
+
alert(`JS Error: ${msg}\nLine: ${lineNo}\nCheck browser console!`);
|
| 32 |
+
return false;
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
// Test backend connectivity
|
| 36 |
+
fetch('/api/ping').then(r => r.json()).then(d => console.log('π Backend connectivity:', d.status)).catch(e => console.error('β Backend UNREACHABLE:', e));
|
| 37 |
+
|
| 38 |
+
// ββ Visual Illustration State ββ
|
| 39 |
+
let currentJobId = null;
|
| 40 |
+
window._debugJobId = () => currentJobId; // Access via console: window._debugJobId()
|
| 41 |
+
|
| 42 |
+
// ββ State Manager ββ
|
| 43 |
+
function showSection(section) {
|
| 44 |
+
[uploadSection, extractStatus, streamStatus, doneSection, errorSection]
|
| 45 |
+
.forEach(el => el.classList.add('hidden'));
|
| 46 |
+
if (section) section.classList.remove('hidden');
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// ββ Drag & Drop ββ
|
| 50 |
+
dropZone.addEventListener('click', () => fileInput.click());
|
| 51 |
+
|
| 52 |
+
dropZone.addEventListener('dragover', (e) => {
|
| 53 |
+
e.preventDefault();
|
| 54 |
+
dropZone.classList.add('drag-over');
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
|
| 58 |
+
|
| 59 |
+
dropZone.addEventListener('drop', (e) => {
|
| 60 |
+
e.preventDefault();
|
| 61 |
+
dropZone.classList.remove('drag-over');
|
| 62 |
+
if (e.dataTransfer.files.length > 0) handleUpload(e.dataTransfer.files[0]);
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
fileInput.addEventListener('change', (e) => {
|
| 66 |
+
if (e.target.files.length > 0) handleUpload(e.target.files[0]);
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
// ββ arXiv URL Handler ββ
|
| 70 |
+
arxivBtn.addEventListener('click', () => handleArxiv());
|
| 71 |
+
arxivInput.addEventListener('keydown', (e) => {
|
| 72 |
+
if (e.key === 'Enter') handleArxiv();
|
| 73 |
+
});
|
| 74 |
+
|
| 75 |
+
async function handleArxiv() {
|
| 76 |
+
const url = arxivInput.value.trim();
|
| 77 |
+
if (!url) return;
|
| 78 |
+
if (!url.includes('arxiv.org')) {
|
| 79 |
+
alert('Please enter a valid arXiv URL (e.g. https://arxiv.org/abs/2401.12345)');
|
| 80 |
+
return;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
showSection(extractStatus);
|
| 84 |
+
extractLabel.textContent = 'Downloading & analyzing arXiv paperβ¦';
|
| 85 |
+
codeOutput.textContent = '// Downloading PDF from arXivβ¦';
|
| 86 |
+
codeBadge.textContent = 'extracting';
|
| 87 |
+
codeBadge.className = 'code-badge';
|
| 88 |
+
|
| 89 |
+
try {
|
| 90 |
+
const res = await fetch('/api/extract-arxiv', {
|
| 91 |
+
method: 'POST',
|
| 92 |
+
headers: { 'Content-Type': 'application/json' },
|
| 93 |
+
body: JSON.stringify({ url })
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
if (!res.ok) {
|
| 97 |
+
const err = await res.json().catch(() => ({ detail: 'arXiv extraction failed' }));
|
| 98 |
+
throw new Error(err.detail || 'arXiv extraction failed');
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
const data = await res.json();
|
| 102 |
+
console.log('arXiv extraction complete:', data);
|
| 103 |
+
startStream(data.job_id);
|
| 104 |
+
|
| 105 |
+
} catch (err) {
|
| 106 |
+
showError(err.message);
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// ββ Upload & Extract (Step 1) ββ
|
| 111 |
+
async function handleUpload(file) {
|
| 112 |
+
if (!file.name.toLowerCase().endsWith('.pdf')) {
|
| 113 |
+
alert('Please upload a PDF file.');
|
| 114 |
+
return;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
// Show extraction spinner
|
| 118 |
+
showSection(extractStatus);
|
| 119 |
+
extractLabel.textContent = 'Uploading & analyzing paperβ¦';
|
| 120 |
+
codeOutput.textContent = '// Waiting for paper analysis to completeβ¦';
|
| 121 |
+
codeBadge.textContent = 'extracting';
|
| 122 |
+
codeBadge.className = 'code-badge';
|
| 123 |
+
|
| 124 |
+
const formData = new FormData();
|
| 125 |
+
formData.append('file', file);
|
| 126 |
+
|
| 127 |
+
try {
|
| 128 |
+
const res = await fetch('/api/extract', {
|
| 129 |
+
method: 'POST',
|
| 130 |
+
body: formData
|
| 131 |
+
});
|
| 132 |
+
|
| 133 |
+
if (!res.ok) {
|
| 134 |
+
const err = await res.json().catch(() => ({ detail: 'Extraction failed' }));
|
| 135 |
+
throw new Error(err.detail || 'Extraction failed');
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
const data = await res.json();
|
| 139 |
+
console.log('Extraction complete:', data);
|
| 140 |
+
|
| 141 |
+
// Hide visualize button from previous run if any
|
| 142 |
+
visualizeBtn.classList.add('hidden');
|
| 143 |
+
|
| 144 |
+
// Start streaming (Step 2)
|
| 145 |
+
startStream(data.job_id);
|
| 146 |
+
|
| 147 |
+
} catch (err) {
|
| 148 |
+
showError(err.message);
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
// ββ Live Streaming (Step 2) ββ
|
| 153 |
+
function startStream(jobId) {
|
| 154 |
+
currentJobId = jobId; // Store immediately
|
| 155 |
+
showSection(streamStatus);
|
| 156 |
+
codeOutput.textContent = '';
|
| 157 |
+
codeBadge.textContent = 'streaming';
|
| 158 |
+
codeBadge.className = 'code-badge streaming';
|
| 159 |
+
|
| 160 |
+
const source = new EventSource(`/api/generate_stream/${jobId}`);
|
| 161 |
+
let hasError = false;
|
| 162 |
+
|
| 163 |
+
source.onmessage = (event) => {
|
| 164 |
+
try {
|
| 165 |
+
const payload = JSON.parse(event.data);
|
| 166 |
+
|
| 167 |
+
if (payload.done) {
|
| 168 |
+
source.close();
|
| 169 |
+
if (payload.success) {
|
| 170 |
+
onStreamComplete(jobId);
|
| 171 |
+
} else {
|
| 172 |
+
// Pipeline finished but failed β show error state
|
| 173 |
+
showError('Pipeline failed to generate notebook. Check the code output panel for details.');
|
| 174 |
+
codeBadge.textContent = 'failed';
|
| 175 |
+
codeBadge.className = 'code-badge';
|
| 176 |
+
}
|
| 177 |
+
return;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
if (payload.analysis_done) {
|
| 181 |
+
// Show visualize button early!
|
| 182 |
+
visualizeBtn.classList.remove('hidden');
|
| 183 |
+
return;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
if (payload.text) {
|
| 187 |
+
// Check if it's an error message
|
| 188 |
+
if (payload.text.includes('β')) {
|
| 189 |
+
hasError = true;
|
| 190 |
+
}
|
| 191 |
+
codeOutput.textContent += payload.text;
|
| 192 |
+
// Auto-scroll to bottom
|
| 193 |
+
codeViewer.scrollTop = codeViewer.scrollHeight;
|
| 194 |
+
}
|
| 195 |
+
} catch (e) {
|
| 196 |
+
console.error('Parse error:', e);
|
| 197 |
+
}
|
| 198 |
+
};
|
| 199 |
+
|
| 200 |
+
source.onerror = (err) => {
|
| 201 |
+
console.error('SSE error:', err);
|
| 202 |
+
source.close();
|
| 203 |
+
showError('Stream connection lost. Please try again.');
|
| 204 |
+
};
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
function onStreamComplete(jobId) {
|
| 208 |
+
showSection(doneSection);
|
| 209 |
+
downloadBtn.href = `/api/download/${jobId}`;
|
| 210 |
+
currentJobId = jobId; // Store for visualization
|
| 211 |
+
codeBadge.textContent = 'complete';
|
| 212 |
+
codeBadge.className = 'code-badge done';
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
// ββ Visual Illustration Logic ββ
|
| 216 |
+
|
| 217 |
+
visualizeBtn.addEventListener('click', async (e) => {
|
| 218 |
+
console.log('π±οΈ Visualize button CLICKED. Event object:', e);
|
| 219 |
+
|
| 220 |
+
if (!currentJobId) {
|
| 221 |
+
console.error('β Cannot visualize: currentJobId is null');
|
| 222 |
+
alert('Software Error: Job ID not captured yet. Please wait for analysis or refresh.');
|
| 223 |
+
return;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
console.log('π¨ Requesting visualization for Job:', currentJobId);
|
| 227 |
+
|
| 228 |
+
// Disable button to prevent double-clicks
|
| 229 |
+
visualizeBtn.disabled = true;
|
| 230 |
+
const originalText = visualizeBtn.textContent;
|
| 231 |
+
visualizeBtn.textContent = 'π¨ Painting...';
|
| 232 |
+
|
| 233 |
+
// Show float UI
|
| 234 |
+
imageFloat.classList.remove('hidden');
|
| 235 |
+
imagePill.classList.add('hidden');
|
| 236 |
+
floatImage.classList.add('hidden');
|
| 237 |
+
floatSpinner.classList.remove('hidden');
|
| 238 |
+
|
| 239 |
+
try {
|
| 240 |
+
const url = `/api/visualize/${currentJobId}`;
|
| 241 |
+
console.log('π Fetching:', url);
|
| 242 |
+
|
| 243 |
+
const res = await fetch(url, { method: 'POST' });
|
| 244 |
+
console.log('π₯ Response status:', res.status);
|
| 245 |
+
|
| 246 |
+
if (!res.ok) {
|
| 247 |
+
const errDetail = await res.json().catch(() => ({ detail: 'Network error' }));
|
| 248 |
+
throw new Error(errDetail.detail || `Server error ${res.status}`);
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
const data = await res.json();
|
| 252 |
+
console.log('πΌοΈ Image received! Length:', data.image.length);
|
| 253 |
+
|
| 254 |
+
floatImage.src = data.image;
|
| 255 |
+
floatImage.classList.remove('hidden');
|
| 256 |
+
floatSpinner.classList.add('hidden');
|
| 257 |
+
} catch (err) {
|
| 258 |
+
console.error('β Visualization flow error:', err);
|
| 259 |
+
alert(`Painting failed: ${err.message}`);
|
| 260 |
+
imageFloat.classList.add('hidden');
|
| 261 |
+
} finally {
|
| 262 |
+
visualizeBtn.disabled = false;
|
| 263 |
+
visualizeBtn.textContent = originalText;
|
| 264 |
+
console.log('π Visualize flow completed.');
|
| 265 |
+
}
|
| 266 |
+
});
|
| 267 |
+
|
| 268 |
+
// Drag Logic
|
| 269 |
+
let isDragging = false;
|
| 270 |
+
let startX, startY, initialX, initialY;
|
| 271 |
+
|
| 272 |
+
floatHeader.addEventListener('mousedown', (e) => {
|
| 273 |
+
isDragging = true;
|
| 274 |
+
startX = e.clientX;
|
| 275 |
+
startY = e.clientY;
|
| 276 |
+
initialX = imageFloat.offsetLeft;
|
| 277 |
+
initialY = imageFloat.offsetTop;
|
| 278 |
+
imageFloat.style.transition = 'none';
|
| 279 |
+
});
|
| 280 |
+
|
| 281 |
+
document.addEventListener('mousemove', (e) => {
|
| 282 |
+
if (!isDragging) return;
|
| 283 |
+
const dx = e.clientX - startX;
|
| 284 |
+
const dy = e.clientY - startY;
|
| 285 |
+
imageFloat.style.left = (initialX + dx) + 'px';
|
| 286 |
+
imageFloat.style.top = (initialY + dy) + 'px';
|
| 287 |
+
imageFloat.style.bottom = 'auto'; // Remove fixed positioning
|
| 288 |
+
imageFloat.style.right = 'auto';
|
| 289 |
+
});
|
| 290 |
+
|
| 291 |
+
document.addEventListener('mouseup', () => {
|
| 292 |
+
isDragging = false;
|
| 293 |
+
imageFloat.style.transition = '';
|
| 294 |
+
});
|
| 295 |
+
|
| 296 |
+
// Minimize/Close/Download
|
| 297 |
+
floatMinimize.addEventListener('click', () => {
|
| 298 |
+
imageFloat.classList.add('hidden');
|
| 299 |
+
imagePill.classList.remove('hidden');
|
| 300 |
+
});
|
| 301 |
+
|
| 302 |
+
imagePill.addEventListener('click', () => {
|
| 303 |
+
imageFloat.classList.remove('hidden');
|
| 304 |
+
imagePill.classList.add('hidden');
|
| 305 |
+
});
|
| 306 |
+
|
| 307 |
+
floatClose.addEventListener('click', () => {
|
| 308 |
+
imageFloat.classList.add('hidden');
|
| 309 |
+
imagePill.classList.add('hidden');
|
| 310 |
+
});
|
| 311 |
+
|
| 312 |
+
floatDownload.addEventListener('click', () => {
|
| 313 |
+
if (!floatImage.src) return;
|
| 314 |
+
const link = document.createElement('a');
|
| 315 |
+
link.href = floatImage.src;
|
| 316 |
+
link.download = `pundit_feynman_illustration_${currentJobId}.png`;
|
| 317 |
+
link.click();
|
| 318 |
+
});
|
| 319 |
+
|
| 320 |
+
// ββ Error & Reset ββ
|
| 321 |
+
function showError(msg) {
|
| 322 |
+
showSection(errorSection);
|
| 323 |
+
errorText.textContent = msg;
|
| 324 |
+
codeBadge.textContent = 'error';
|
| 325 |
+
codeBadge.className = 'code-badge';
|
| 326 |
+
// Cleanup float on error
|
| 327 |
+
imageFloat.classList.add('hidden');
|
| 328 |
+
imagePill.classList.add('hidden');
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
function resetUI() {
|
| 332 |
+
showSection(uploadSection);
|
| 333 |
+
fileInput.value = '';
|
| 334 |
+
arxivInput.value = '';
|
| 335 |
+
codeOutput.textContent = '// Upload a paper to see the generated code hereβ¦';
|
| 336 |
+
codeBadge.textContent = 'waiting';
|
| 337 |
+
codeBadge.className = 'code-badge';
|
| 338 |
+
currentJobId = null;
|
| 339 |
+
visualizeBtn.classList.add('hidden');
|
| 340 |
+
// Cleanup float on reset
|
| 341 |
+
imageFloat.classList.add('hidden');
|
| 342 |
+
imagePill.classList.add('hidden');
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
resetBtn.addEventListener('click', resetUI);
|
| 346 |
+
errorResetBtn.addEventListener('click', resetUI);
|
skills.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SYSTEM INITIATION: Yana COMPUTATIONAL ENGINE
|
| 2 |
+
# ROLE: World-Class Research-to-Code Synthesizer
|
| 3 |
+
|
| 4 |
+
You are an elite, hyper-optimized computational engine. Your objective is to seamlessly and deterministically translate uploaded documents into world-class, production-ready code. You operate with absolute precision and strict fidelity to the source material.
|
| 5 |
+
|
| 6 |
+
# CORE DIRECTIVE: ZERO HALLUCINATION
|
| 7 |
+
You must NEVER invent, assume, or hallucinate architectures, frameworks, or complexities that are not explicitly present in the provided document. Your output must strictly mirror the domain, scope, and instructions of the text.
|
| 8 |
+
- If the document describes a simple terminal-based Python utility, write a simple terminal-based Python utility.
|
| 9 |
+
- If the document describes a deep learning Transformer model, write PyTorch code for a Transformer.
|
| 10 |
+
- If the document lacks code-able logic entirely, output a markdown explanation stating that no executable architecture was found.
|
| 11 |
+
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# PHASE 1: HIGH-FIDELITY EXTRACTION
|
| 15 |
+
Analyze the provided document images and extract a precise technical blueprint:
|
| 16 |
+
1. **Domain & Scope:** Identify the exact nature of the project (e.g., CLI application, machine learning model, data pipeline).
|
| 17 |
+
2. **Logic & Architecture:** Extract the exact step-by-step logic, operational loops, equations, or neural network layers described.
|
| 18 |
+
3. **Constraints:** Note any explicit technical constraints (e.g., "no external libraries," "requires terminal I/O," "use AdamW optimizer").
|
| 19 |
+
|
| 20 |
+
# PHASE 2: SEAMLESS CODE GENERATION (JUPYTER NOTEBOOK FORMAT)
|
| 21 |
+
Translate the Phase 1 blueprint into flawless, executable Python code. You are a world-class developer; your code must be elegant, modular, and heavily commented.
|
| 22 |
+
|
| 23 |
+
## Execution Standards:
|
| 24 |
+
1. **Absolute Grounding:** Implement ONLY what was extracted in Phase 1.
|
| 25 |
+
2. **Best-in-Class Syntax:** Use strict type hinting, modular functions/classes, and robust error handling as appropriate for the domain.
|
| 26 |
+
3. **Self-Contained Execution:** The code must be immediately runnable. If data is required but not provided, generate a lightweight, synthetic mock data function.
|
| 27 |
+
|
| 28 |
+
## Output Formatting (Strict Parser Rules):
|
| 29 |
+
You are streaming directly into an automated parser that compiles a `.ipynb` file. You must strictly alternate between explanatory text and code.
|
| 30 |
+
- Wrap ALL explanatory text in standard Markdown.
|
| 31 |
+
- Wrap ALL Python code strictly inside standard markdown code blocks (```python ... ```).
|
| 32 |
+
- Separate the notebook into logical sequence blocks based on the document's structure.
|
style.css
ADDED
|
@@ -0,0 +1,609 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ββ Reset & Base ββ */
|
| 2 |
+
*,
|
| 3 |
+
*::before,
|
| 4 |
+
*::after {
|
| 5 |
+
margin: 0;
|
| 6 |
+
padding: 0;
|
| 7 |
+
box-sizing: border-box;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
:root {
|
| 11 |
+
--bg: #f5f0e8;
|
| 12 |
+
--bg-deep: #ebe4d6;
|
| 13 |
+
--text: #2c2417;
|
| 14 |
+
--text-muted: #7a6e5d;
|
| 15 |
+
--accent: #8b6914;
|
| 16 |
+
--accent-soft: #c9a84c;
|
| 17 |
+
--border: #d4cbb8;
|
| 18 |
+
--code-bg: #1e1e2e;
|
| 19 |
+
--code-text: #cdd6f4;
|
| 20 |
+
--code-accent: #89b4fa;
|
| 21 |
+
--panel-shadow: 0 0 40px rgba(0, 0, 0, 0.06);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
html,
|
| 25 |
+
body {
|
| 26 |
+
height: 100%;
|
| 27 |
+
overflow: hidden;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
body {
|
| 31 |
+
font-family: 'Times New Roman', 'Playfair Display', Georgia, serif;
|
| 32 |
+
background: var(--bg);
|
| 33 |
+
color: var(--text);
|
| 34 |
+
display: flex;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/* ββ Left Panel ββ */
|
| 38 |
+
.left-panel {
|
| 39 |
+
width: 380px;
|
| 40 |
+
min-width: 380px;
|
| 41 |
+
height: 100vh;
|
| 42 |
+
background: var(--bg);
|
| 43 |
+
border-right: 1px solid var(--border);
|
| 44 |
+
display: flex;
|
| 45 |
+
flex-direction: column;
|
| 46 |
+
overflow-y: auto;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.panel-inner {
|
| 50 |
+
padding: 40px 32px 24px;
|
| 51 |
+
flex: 1;
|
| 52 |
+
display: flex;
|
| 53 |
+
flex-direction: column;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* ββ Header ββ */
|
| 57 |
+
header {
|
| 58 |
+
margin-bottom: 32px;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
header h1 {
|
| 62 |
+
font-family: 'Playfair Display', Georgia, serif;
|
| 63 |
+
font-size: 2rem;
|
| 64 |
+
font-weight: 700;
|
| 65 |
+
color: var(--accent);
|
| 66 |
+
margin-bottom: 8px;
|
| 67 |
+
letter-spacing: -0.02em;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.header-visualize {
|
| 71 |
+
display: inline-block;
|
| 72 |
+
margin-top: 16px;
|
| 73 |
+
background: transparent;
|
| 74 |
+
border: 1px solid #6b4226;
|
| 75 |
+
color: #6b4226;
|
| 76 |
+
padding: 8px 16px;
|
| 77 |
+
border-radius: 20px;
|
| 78 |
+
font-family: 'Times New Roman', serif;
|
| 79 |
+
font-size: 1rem;
|
| 80 |
+
font-weight: 600;
|
| 81 |
+
cursor: pointer;
|
| 82 |
+
transition: all 0.2s;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
.header-visualize:hover {
|
| 86 |
+
background: #6b4226;
|
| 87 |
+
color: #fff;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.tagline {
|
| 91 |
+
font-size: 0.95rem;
|
| 92 |
+
color: var(--text-muted);
|
| 93 |
+
line-height: 1.5;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/* ββ Drop Zone ββ */
|
| 97 |
+
.drop-zone {
|
| 98 |
+
border: 2px dashed var(--border);
|
| 99 |
+
border-radius: 12px;
|
| 100 |
+
padding: 36px 24px;
|
| 101 |
+
text-align: center;
|
| 102 |
+
cursor: pointer;
|
| 103 |
+
transition: all 0.25s ease;
|
| 104 |
+
background: var(--bg-deep);
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.drop-zone:hover,
|
| 108 |
+
.drop-zone.drag-over {
|
| 109 |
+
border-color: var(--accent);
|
| 110 |
+
background: rgba(139, 105, 20, 0.06);
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.upload-icon {
|
| 114 |
+
color: var(--accent-soft);
|
| 115 |
+
margin-bottom: 12px;
|
| 116 |
+
opacity: 0.8;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.drop-text {
|
| 120 |
+
font-size: 1rem;
|
| 121 |
+
font-weight: 600;
|
| 122 |
+
margin-bottom: 4px;
|
| 123 |
+
color: var(--text);
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.drop-hint {
|
| 127 |
+
font-size: 0.85rem;
|
| 128 |
+
color: var(--text-muted);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
/* ββ Divider & arXiv Input ββ */
|
| 132 |
+
.divider {
|
| 133 |
+
display: flex;
|
| 134 |
+
align-items: center;
|
| 135 |
+
gap: 12px;
|
| 136 |
+
margin: 16px 0;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.divider::before,
|
| 140 |
+
.divider::after {
|
| 141 |
+
content: '';
|
| 142 |
+
flex: 1;
|
| 143 |
+
height: 1px;
|
| 144 |
+
background: var(--border);
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.divider span {
|
| 148 |
+
font-size: 0.78rem;
|
| 149 |
+
color: var(--text-muted);
|
| 150 |
+
white-space: nowrap;
|
| 151 |
+
letter-spacing: 0.02em;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.arxiv-input-row {
|
| 155 |
+
display: flex;
|
| 156 |
+
gap: 8px;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.arxiv-input {
|
| 160 |
+
flex: 1;
|
| 161 |
+
padding: 14px 16px;
|
| 162 |
+
border: 1.5px solid var(--border);
|
| 163 |
+
border-radius: 8px;
|
| 164 |
+
background: var(--bg);
|
| 165 |
+
font-family: 'Times New Roman', Georgia, serif;
|
| 166 |
+
font-size: 0.9rem;
|
| 167 |
+
color: var(--text);
|
| 168 |
+
outline: none;
|
| 169 |
+
transition: border-color 0.2s ease;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.arxiv-input:focus {
|
| 173 |
+
border-color: var(--accent);
|
| 174 |
+
background: #fff;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.arxiv-input::placeholder {
|
| 178 |
+
color: var(--text-muted);
|
| 179 |
+
opacity: 0.6;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.arxiv-btn {
|
| 183 |
+
padding: 14px 20px;
|
| 184 |
+
font-size: 0.85rem;
|
| 185 |
+
white-space: nowrap;
|
| 186 |
+
font-family: 'Times New Roman', Georgia, serif;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
/* ββ Status Boxes ββ */
|
| 190 |
+
.status-box {
|
| 191 |
+
text-align: center;
|
| 192 |
+
padding: 32px 0;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
.spinner {
|
| 196 |
+
width: 28px;
|
| 197 |
+
height: 28px;
|
| 198 |
+
border: 2.5px solid var(--border);
|
| 199 |
+
border-top-color: var(--accent);
|
| 200 |
+
border-radius: 50%;
|
| 201 |
+
margin: 0 auto 16px;
|
| 202 |
+
animation: spin 0.7s linear infinite;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
@keyframes spin {
|
| 206 |
+
to {
|
| 207 |
+
transform: rotate(360deg);
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
.pulse-dot {
|
| 212 |
+
width: 12px;
|
| 213 |
+
height: 12px;
|
| 214 |
+
background: #22c55e;
|
| 215 |
+
border-radius: 50%;
|
| 216 |
+
margin: 0 auto 16px;
|
| 217 |
+
animation: pulse 1.5s ease-in-out infinite;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
@keyframes pulse {
|
| 221 |
+
|
| 222 |
+
0%,
|
| 223 |
+
100% {
|
| 224 |
+
opacity: 1;
|
| 225 |
+
transform: scale(1);
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
50% {
|
| 229 |
+
opacity: 0.5;
|
| 230 |
+
transform: scale(1.3);
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
.status-label {
|
| 235 |
+
font-family: 'Times New Roman', Georgia, serif;
|
| 236 |
+
font-size: 0.9rem;
|
| 237 |
+
font-weight: 600;
|
| 238 |
+
color: var(--text);
|
| 239 |
+
margin-bottom: 6px;
|
| 240 |
+
text-transform: uppercase;
|
| 241 |
+
letter-spacing: 0.05em;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.status-sub {
|
| 245 |
+
font-family: 'Times New Roman', Georgia, serif;
|
| 246 |
+
font-size: 0.82rem;
|
| 247 |
+
color: var(--text-muted);
|
| 248 |
+
line-height: 1.4;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.done-check {
|
| 252 |
+
font-size: 2rem;
|
| 253 |
+
color: #22c55e;
|
| 254 |
+
margin-bottom: 8px;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.error-x {
|
| 258 |
+
font-size: 2rem;
|
| 259 |
+
color: #ef4444;
|
| 260 |
+
margin-bottom: 8px;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
/* ββ Buttons ββ */
|
| 264 |
+
.btn-row {
|
| 265 |
+
display: flex;
|
| 266 |
+
gap: 10px;
|
| 267 |
+
justify-content: center;
|
| 268 |
+
margin-top: 16px;
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
.btn {
|
| 272 |
+
display: inline-flex;
|
| 273 |
+
align-items: center;
|
| 274 |
+
gap: 6px;
|
| 275 |
+
padding: 10px 20px;
|
| 276 |
+
border-radius: 8px;
|
| 277 |
+
font-weight: 600;
|
| 278 |
+
font-size: 0.82rem;
|
| 279 |
+
cursor: pointer;
|
| 280 |
+
border: none;
|
| 281 |
+
text-decoration: none;
|
| 282 |
+
transition: all 0.2s ease;
|
| 283 |
+
font-family: 'Times New Roman', Georgia, serif;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.btn-primary {
|
| 287 |
+
background: var(--accent);
|
| 288 |
+
color: #fff;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.btn-primary:hover {
|
| 292 |
+
background: #6f5410;
|
| 293 |
+
transform: translateY(-1px);
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
.btn-secondary {
|
| 297 |
+
background: transparent;
|
| 298 |
+
color: var(--text);
|
| 299 |
+
border: 1px solid var(--border);
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
.btn-secondary:hover {
|
| 303 |
+
background: var(--bg-deep);
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
/* ββ Footer ββ */
|
| 307 |
+
footer {
|
| 308 |
+
margin-top: auto;
|
| 309 |
+
padding-top: 24px;
|
| 310 |
+
text-align: center;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
footer p {
|
| 314 |
+
font-size: 0.72rem;
|
| 315 |
+
color: var(--text-muted);
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
footer strong {
|
| 319 |
+
color: var(--accent);
|
| 320 |
+
font-weight: 600;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
/* ββ Right Panel: Code Viewer ββ */
|
| 324 |
+
.right-panel {
|
| 325 |
+
flex: 1;
|
| 326 |
+
height: 100vh;
|
| 327 |
+
background: var(--code-bg);
|
| 328 |
+
display: flex;
|
| 329 |
+
flex-direction: column;
|
| 330 |
+
overflow: hidden;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.code-header {
|
| 334 |
+
display: flex;
|
| 335 |
+
align-items: center;
|
| 336 |
+
justify-content: space-between;
|
| 337 |
+
padding: 14px 24px;
|
| 338 |
+
background: #181825;
|
| 339 |
+
border-bottom: 1px solid #313244;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.code-title {
|
| 343 |
+
font-family: 'JetBrains Mono', monospace;
|
| 344 |
+
font-size: 0.78rem;
|
| 345 |
+
color: #6c7086;
|
| 346 |
+
text-transform: uppercase;
|
| 347 |
+
letter-spacing: 0.08em;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.code-badge {
|
| 351 |
+
font-family: 'JetBrains Mono', monospace;
|
| 352 |
+
font-size: 0.68rem;
|
| 353 |
+
padding: 3px 10px;
|
| 354 |
+
border-radius: 20px;
|
| 355 |
+
background: #313244;
|
| 356 |
+
color: #6c7086;
|
| 357 |
+
text-transform: uppercase;
|
| 358 |
+
letter-spacing: 0.05em;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.code-badge.streaming {
|
| 362 |
+
background: rgba(34, 197, 94, 0.15);
|
| 363 |
+
color: #22c55e;
|
| 364 |
+
animation: pulse 1.5s ease-in-out infinite;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.code-badge.done {
|
| 368 |
+
background: rgba(34, 197, 94, 0.15);
|
| 369 |
+
color: #22c55e;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
.code-viewer {
|
| 373 |
+
flex: 1;
|
| 374 |
+
overflow-y: auto;
|
| 375 |
+
padding: 24px;
|
| 376 |
+
margin: 0;
|
| 377 |
+
font-family: 'Times New Roman', Georgia, serif;
|
| 378 |
+
font-size: 0.95rem;
|
| 379 |
+
line-height: 1.8;
|
| 380 |
+
color: var(--code-text);
|
| 381 |
+
white-space: pre-wrap;
|
| 382 |
+
word-wrap: break-word;
|
| 383 |
+
scrollbar-width: thin;
|
| 384 |
+
scrollbar-color: #313244 transparent;
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
.code-viewer::-webkit-scrollbar {
|
| 388 |
+
width: 6px;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
.code-viewer::-webkit-scrollbar-thumb {
|
| 392 |
+
background: #313244;
|
| 393 |
+
border-radius: 3px;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
.code-viewer code {
|
| 397 |
+
font-family: inherit;
|
| 398 |
+
color: inherit;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
/* Feedback Footer */
|
| 402 |
+
.feedback-footer {
|
| 403 |
+
margin-top: 16px;
|
| 404 |
+
padding-top: 16px;
|
| 405 |
+
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
| 406 |
+
font-size: 0.95rem;
|
| 407 |
+
color: #6b4226;
|
| 408 |
+
line-height: 1.5;
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
.feedback-link {
|
| 412 |
+
display: inline-block;
|
| 413 |
+
margin-top: 6px;
|
| 414 |
+
color: #5a3318;
|
| 415 |
+
text-decoration: none;
|
| 416 |
+
font-size: 1.05rem;
|
| 417 |
+
font-weight: 700;
|
| 418 |
+
transition: opacity 0.2s;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.feedback-link:hover {
|
| 422 |
+
text-decoration: underline;
|
| 423 |
+
opacity: 0.8;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
/* ββ Floating Window ββ */
|
| 427 |
+
.float-window {
|
| 428 |
+
position: fixed;
|
| 429 |
+
bottom: 24px;
|
| 430 |
+
right: 24px;
|
| 431 |
+
width: 400px;
|
| 432 |
+
background: #fff;
|
| 433 |
+
border-radius: 12px;
|
| 434 |
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
| 435 |
+
z-index: 1000;
|
| 436 |
+
overflow: hidden;
|
| 437 |
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
| 438 |
+
display: flex;
|
| 439 |
+
flex-direction: column;
|
| 440 |
+
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s;
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
.float-header {
|
| 444 |
+
background: #fdfaf6;
|
| 445 |
+
/* Beige header */
|
| 446 |
+
padding: 12px 16px;
|
| 447 |
+
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
| 448 |
+
display: flex;
|
| 449 |
+
justify-content: space-between;
|
| 450 |
+
align-items: center;
|
| 451 |
+
cursor: move;
|
| 452 |
+
/* Indicate draggable */
|
| 453 |
+
user-select: none;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
.float-title {
|
| 457 |
+
font-family: 'Playfair Display', serif;
|
| 458 |
+
font-weight: 700;
|
| 459 |
+
font-size: 0.9rem;
|
| 460 |
+
color: #6b4226;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
.float-actions {
|
| 464 |
+
display: flex;
|
| 465 |
+
gap: 8px;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
.float-btn {
|
| 469 |
+
background: transparent;
|
| 470 |
+
border: none;
|
| 471 |
+
color: #8b8b8b;
|
| 472 |
+
font-size: 1rem;
|
| 473 |
+
cursor: pointer;
|
| 474 |
+
width: 28px;
|
| 475 |
+
height: 28px;
|
| 476 |
+
display: flex;
|
| 477 |
+
align-items: center;
|
| 478 |
+
justify-content: center;
|
| 479 |
+
border-radius: 6px;
|
| 480 |
+
transition: all 0.2s;
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
.float-btn:hover {
|
| 484 |
+
background: rgba(0, 0, 0, 0.05);
|
| 485 |
+
color: #6b4226;
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
.float-body {
|
| 489 |
+
position: relative;
|
| 490 |
+
min-height: 200px;
|
| 491 |
+
max-height: 400px;
|
| 492 |
+
display: flex;
|
| 493 |
+
align-items: center;
|
| 494 |
+
justify-content: center;
|
| 495 |
+
background: #fafafa;
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
.float-image {
|
| 499 |
+
width: 100%;
|
| 500 |
+
height: auto;
|
| 501 |
+
display: block;
|
| 502 |
+
max-height: 400px;
|
| 503 |
+
object-fit: contain;
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
.float-spinner {
|
| 507 |
+
padding: 40px;
|
| 508 |
+
text-align: center;
|
| 509 |
+
color: #8b8b8b;
|
| 510 |
+
font-size: 0.85rem;
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
/* ββ Paint Brush Loading ββ */
|
| 514 |
+
.paint-brush-container {
|
| 515 |
+
position: relative;
|
| 516 |
+
width: 60px;
|
| 517 |
+
height: 60px;
|
| 518 |
+
margin: 0 auto 16px;
|
| 519 |
+
display: flex;
|
| 520 |
+
align-items: center;
|
| 521 |
+
justify-content: center;
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
.brush {
|
| 525 |
+
font-size: 32px;
|
| 526 |
+
z-index: 2;
|
| 527 |
+
animation: sweep 1.5s infinite ease-in-out;
|
| 528 |
+
transform-origin: bottom center;
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
@keyframes sweep {
|
| 532 |
+
|
| 533 |
+
0%,
|
| 534 |
+
100% {
|
| 535 |
+
transform: rotate(-15deg) translateX(-10px);
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
50% {
|
| 539 |
+
transform: rotate(15deg) translateX(10px);
|
| 540 |
+
}
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
.shimmer-line {
|
| 544 |
+
position: absolute;
|
| 545 |
+
bottom: 10px;
|
| 546 |
+
width: 40px;
|
| 547 |
+
height: 4px;
|
| 548 |
+
background: var(--accent-soft);
|
| 549 |
+
border-radius: 2px;
|
| 550 |
+
opacity: 0.3;
|
| 551 |
+
animation: paint-shimmer 1.5s infinite ease-in-out;
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
@keyframes paint-shimmer {
|
| 555 |
+
|
| 556 |
+
0%,
|
| 557 |
+
100% {
|
| 558 |
+
width: 0;
|
| 559 |
+
left: 10px;
|
| 560 |
+
opacity: 0;
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
50% {
|
| 564 |
+
width: 40px;
|
| 565 |
+
left: 10px;
|
| 566 |
+
opacity: 0.6;
|
| 567 |
+
}
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
.float-spinner p {
|
| 571 |
+
font-family: 'Times New Roman', serif;
|
| 572 |
+
font-style: italic;
|
| 573 |
+
color: var(--text-muted);
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
.header-visualize:disabled {
|
| 577 |
+
opacity: 0.5;
|
| 578 |
+
cursor: not-allowed;
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
/* Minimized Pill */
|
| 582 |
+
.float-pill {
|
| 583 |
+
position: fixed;
|
| 584 |
+
bottom: 24px;
|
| 585 |
+
right: 24px;
|
| 586 |
+
background: #6b4226;
|
| 587 |
+
color: #fff;
|
| 588 |
+
padding: 10px 20px;
|
| 589 |
+
border-radius: 30px;
|
| 590 |
+
font-family: 'Playfair Display', serif;
|
| 591 |
+
font-size: 0.9rem;
|
| 592 |
+
font-weight: 600;
|
| 593 |
+
cursor: pointer;
|
| 594 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
| 595 |
+
z-index: 1001;
|
| 596 |
+
display: flex;
|
| 597 |
+
align-items: center;
|
| 598 |
+
gap: 8px;
|
| 599 |
+
transition: transform 0.2s;
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
.float-pill:hover {
|
| 603 |
+
transform: translateY(-2px);
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
/* ββ Utility ββ */
|
| 607 |
+
.hidden {
|
| 608 |
+
display: none !important;
|
| 609 |
+
}
|