Socratic-Lens / src /app /page.tsx
Jainish1808
Initial commit - Socratic Lens
4000a4c
'use client';
import { useState, useRef } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Upload, FileText, Image as ImageIcon, Send, Loader2, RefreshCcw, MessageCircle, BookOpen, Camera } from 'lucide-react';
import clsx from 'clsx';
import WikipediaCard from '@/components/WikipediaCard';
import AskQuestionModal from '@/components/AskQuestionModal';
import QuizModal from '@/components/QuizModal';
import CameraModal from '@/components/CameraModal';
export default function Home() {
const [file, setFile] = useState<File | null>(null);
const [preview, setPreview] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [topics, setTopics] = useState<string[]>([]);
const [rawContent, setRawContent] = useState<string>('');
const fileInputRef = useRef<HTMLInputElement>(null);
// Modal states
const [askModalOpen, setAskModalOpen] = useState(false);
const [quizModalOpen, setQuizModalOpen] = useState(false);
const [cameraModalOpen, setCameraModalOpen] = useState(false);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
const selectedFile = e.target.files[0];
setFileWithPreview(selectedFile);
}
};
const setFileWithPreview = (selectedFile: File) => {
setFile(selectedFile);
setError(null);
if (selectedFile.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
setPreview(e.target?.result as string);
};
reader.readAsDataURL(selectedFile);
} else {
setPreview(null);
}
};
const handleCameraCapture = (capturedFile: File) => {
setFileWithPreview(capturedFile);
};
const handleSubmit = async () => {
if (!file) {
setError("Please select an image or document first.");
return;
}
setLoading(true);
setError(null);
setResult(null);
setTopics([]);
setRawContent('');
const formData = new FormData();
formData.append('file', file);
try {
const res = await fetch('/api/analyze', {
method: 'POST',
body: formData,
});
if (!res.ok) {
const errData = await res.json();
throw new Error(errData.error || 'Failed to analyze');
}
const data = await res.json();
const rawText = data.result || "";
setRawContent(rawText);
let extractedTopics: string[] = [];
const multiMatch = rawText.match(/\[\[TOPICS?:\s*(.*?)\]\]/i);
if (multiMatch) {
extractedTopics = multiMatch[1].split(',').map((t: string) => t.trim()).filter((t: string) => t.length > 0);
}
const cleanText = rawText.replace(/\[\[TOPICS?:.*?\]\]/i, "").trim();
setResult(cleanText);
setTopics(extractedTopics);
} catch (err: any) {
setError(err.message || "Something went wrong.");
} finally {
setLoading(false);
}
};
const reset = () => {
setFile(null);
setPreview(null);
setResult(null);
setError(null);
setTopics([]);
setRawContent('');
};
return (
<>
<main className="min-h-screen p-6 md:p-12">
<div className="max-w-6xl mx-auto flex flex-col gap-8">
{/* Header */}
<header className="flex items-center justify-between">
<h1 className="text-3xl font-bold tracking-tight text-gray-800 dark:text-gray-100 flex items-center gap-3">
<span className="p-2 bg-sky-100 dark:bg-sky-900 rounded-xl text-sky-600 dark:text-sky-300">
<ImageIcon size={24} />
</span>
Socratic Lens
</h1>
<div className="text-sm px-3 py-1 bg-gray-200 dark:bg-gray-800 rounded-full font-medium text-gray-600 dark:text-gray-400">
Beta
</div>
</header>
{/* Main Interaction Area */}
{!result ? (
<section className="flex-1 flex flex-col items-center justify-center min-h-[50vh] transition-all">
{/* Two Upload Boxes Side by Side */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 w-full max-w-3xl">
{/* Upload Document Box */}
<div
onClick={() => fileInputRef.current?.click()}
className={clsx(
"w-full aspect-square rounded-[32px] border-4 border-dashed flex flex-col items-center justify-center cursor-pointer transition-all hover:scale-[1.02] active:scale-[0.98] overflow-hidden",
file && !preview
? "border-green-400 bg-green-50 dark:bg-green-900/20"
: "border-gray-300 dark:border-gray-700 hover:border-sky-400 hover:bg-sky-50 dark:hover:bg-gray-800"
)}
>
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
className="hidden"
accept="image/*,application/pdf"
/>
{file && !preview ? (
<>
<FileText className="w-12 h-12 text-green-600 mb-3" />
<p className="text-sm font-medium text-green-800 dark:text-green-300 text-center px-4 truncate max-w-full">{file.name}</p>
<p className="text-xs text-green-600 dark:text-green-400 mt-1">Tap to change</p>
</>
) : (
<>
<Upload className="w-12 h-12 text-gray-400 mb-3" />
<p className="text-base font-medium text-gray-600 dark:text-gray-300">Upload Document</p>
<p className="text-xs text-gray-400 mt-1">Images or PDFs</p>
</>
)}
</div>
{/* Take Photo Box - Opens Camera Modal */}
<div
onClick={() => setCameraModalOpen(true)}
className={clsx(
"w-full aspect-square rounded-[32px] border-4 border-dashed flex flex-col items-center justify-center cursor-pointer transition-all hover:scale-[1.02] active:scale-[0.98] overflow-hidden",
preview
? "border-green-400 bg-green-50 dark:bg-green-900/20 p-2"
: "border-gray-300 dark:border-gray-700 hover:border-sky-400 hover:bg-sky-50 dark:hover:bg-gray-800"
)}
>
{preview ? (
<div className="relative w-full h-full">
<img
src={preview}
alt="Preview"
className="w-full h-full object-contain rounded-2xl"
/>
<div className="absolute bottom-2 left-1/2 -translate-x-1/2 bg-black/60 text-white text-xs px-3 py-1 rounded-full">
Tap to retake
</div>
</div>
) : (
<>
<Camera className="w-12 h-12 text-gray-400 mb-3" />
<p className="text-base font-medium text-gray-600 dark:text-gray-300">Take Photo</p>
<p className="text-xs text-gray-400 mt-1">Use Camera</p>
</>
)}
</div>
</div>
{/* Analyze Button */}
<div className="mt-8">
<button
onClick={handleSubmit}
disabled={loading || !file}
className={clsx(
"flex items-center gap-3 px-8 py-4 rounded-full text-lg font-semibold shadow-xl transition-all",
loading || !file
? "bg-gray-300 text-gray-500 cursor-not-allowed grayscale"
: "bg-gray-900 dark:bg-white text-white dark:text-gray-900 hover:scale-105 active:scale-95"
)}
>
{loading ? <Loader2 className="animate-spin" /> : <Send />}
{loading ? "Analyzing..." : "Analyze with Lens"}
</button>
</div>
{error && (
<div className="mt-6 p-4 bg-red-100 text-red-700 rounded-2xl flex items-center gap-2">
<span>⚠️</span> {error}
</div>
)}
</section>
) : (
<section className="animate-fade-in-up">
<div className="flex justify-between items-center mb-6">
<button
onClick={reset}
className="flex items-center gap-2 text-sm font-medium text-gray-500 hover:text-black dark:hover:text-white transition-colors px-4 py-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800"
>
<RefreshCcw size={16} />
Analyze Another
</button>
<span className="text-xs text-gray-400 uppercase tracking-wider">AI Generated Content</span>
</div>
{/* 2-Column Layout */}
<div className="flex flex-col lg:flex-row gap-8 justify-center">
{/* Main AI Content (Center/Left) */}
<div className="flex-1 max-w-3xl">
<div className="bg-white dark:bg-neutral-900 rounded-[24px] p-6 md:p-8 shadow-xl border border-gray-200 dark:border-neutral-800">
<article className="prose prose-lg dark:prose-invert max-w-none markdown-content">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{result}
</ReactMarkdown>
</article>
</div>
{/* Action Buttons */}
<div className="flex flex-wrap gap-3 mt-6">
<button
onClick={() => setAskModalOpen(true)}
className="flex items-center gap-2 px-5 py-3 bg-white dark:bg-neutral-900 border border-gray-200 dark:border-neutral-700 rounded-xl font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-neutral-800 hover:border-sky-500 transition-all shadow-sm"
>
<MessageCircle size={18} className="text-sky-500" />
Ask a Question
</button>
<button
onClick={() => setQuizModalOpen(true)}
className="flex items-center gap-2 px-5 py-3 bg-sky-500 text-white rounded-xl font-medium hover:bg-sky-600 transition-all shadow-sm"
>
<BookOpen size={18} />
Quiz Me
</button>
</div>
</div>
{/* Sidebar (Right) - Wikipedia */}
{topics.length > 0 && (
<div className="w-full lg:w-80 flex-shrink-0">
<WikipediaCard topics={topics} />
</div>
)}
</div>
</section>
)}
</div>
</main>
{/* Modals */}
<AskQuestionModal
isOpen={askModalOpen}
onClose={() => setAskModalOpen(false)}
context={rawContent}
/>
<QuizModal
isOpen={quizModalOpen}
onClose={() => setQuizModalOpen(false)}
context={rawContent}
/>
<CameraModal
isOpen={cameraModalOpen}
onClose={() => setCameraModalOpen(false)}
onCapture={handleCameraCapture}
/>
</>
);
}