Spaces:
Runtime error
Runtime error
| import { useState, useEffect } from "react"; | |
| import { Header } from "./components/Header"; | |
| import { Hero } from "./components/Hero"; | |
| import { AuthStatus } from "./components/AuthStatus"; | |
| import { ExamAnalyzer } from "./components/ExamAnalyzer"; | |
| import { Features } from "./components/Features"; | |
| import { SimpleChatPanel } from "./components/SimpleChatPanel"; | |
| import { CLASSLENS_ICON } from "./lib/icon"; | |
| const VALID_INVITE_CODE = "taboola-npo-cz"; | |
| function InviteCodeGate({ onSuccess }: { onSuccess: () => void }) { | |
| const [code, setCode] = useState(""); | |
| const [error, setError] = useState(false); | |
| const [isShaking, setIsShaking] = useState(false); | |
| const handleSubmit = (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| if (code.trim().toLowerCase() === VALID_INVITE_CODE) { | |
| // Save to localStorage so user doesn't need to enter again | |
| localStorage.setItem("classlens_access", "granted"); | |
| onSuccess(); | |
| } else { | |
| setError(true); | |
| setIsShaking(true); | |
| setTimeout(() => setIsShaking(false), 500); | |
| } | |
| }; | |
| return ( | |
| <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-[#0f1419] via-[#1a2332] to-[#0f1419]"> | |
| <div className="relative"> | |
| {/* Decorative background elements */} | |
| <div className="absolute -top-20 -left-20 w-40 h-40 bg-[var(--color-primary)]/20 rounded-full blur-3xl" /> | |
| <div className="absolute -bottom-20 -right-20 w-40 h-40 bg-[var(--color-accent)]/20 rounded-full blur-3xl" /> | |
| <div | |
| className={`relative bg-[var(--color-surface)] border border-[var(--color-border)] rounded-2xl p-8 max-w-md w-full shadow-2xl ${ | |
| isShaking ? "animate-shake" : "" | |
| }`} | |
| > | |
| {/* Logo */} | |
| <div className="text-center mb-8"> | |
| <img | |
| src={CLASSLENS_ICON} | |
| alt="ClassLens" | |
| className="w-24 h-24 mx-auto mb-4 rounded-2xl shadow-lg" | |
| /> | |
| <h1 className="text-2xl font-bold text-[var(--color-text)] font-display"> | |
| ClassLens | |
| </h1> | |
| <p className="text-[var(--color-text-muted)] mt-2"> | |
| AI 驅動的考試分析 | |
| </p> | |
| </div> | |
| {/* Invite code form */} | |
| <form onSubmit={handleSubmit} className="space-y-4"> | |
| <div> | |
| <label | |
| htmlFor="invite-code" | |
| className="block text-sm font-medium text-[var(--color-text-muted)] mb-2" | |
| > | |
| 輸入邀請碼 | |
| </label> | |
| <input | |
| id="invite-code" | |
| type="text" | |
| value={code} | |
| onChange={(e) => { | |
| setCode(e.target.value); | |
| setError(false); | |
| }} | |
| placeholder="請輸入您的邀請碼..." | |
| className={`w-full px-4 py-3 bg-[var(--color-background)] border rounded-xl text-[var(--color-text)] placeholder-[var(--color-text-muted)] focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] transition-all ${ | |
| error | |
| ? "border-red-500 focus:ring-red-500" | |
| : "border-[var(--color-border)]" | |
| }`} | |
| autoFocus | |
| /> | |
| {error && ( | |
| <p className="mt-2 text-sm text-red-400 flex items-center gap-1"> | |
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
| </svg> | |
| 邀請碼無效,請重試。 | |
| </p> | |
| )} | |
| </div> | |
| <button | |
| type="submit" | |
| className="w-full py-3 px-4 bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-accent)] text-white font-semibold rounded-xl hover:opacity-90 transition-opacity focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] focus:ring-offset-2 focus:ring-offset-[var(--color-surface)]" | |
| > | |
| 進入應用程式 → | |
| </button> | |
| </form> | |
| {/* Footer */} | |
| <p className="text-center text-xs text-[var(--color-text-muted)] mt-6"> | |
| Don't have an invite code?{" "} | |
| <a | |
| href="mailto:kuanz1991@gmail.com" | |
| className="text-[var(--color-primary)] hover:underline" | |
| > | |
| Contact us | |
| </a> | |
| </p> | |
| </div> | |
| </div> | |
| {/* Add shake animation */} | |
| <style>{` | |
| @keyframes shake { | |
| 0%, 100% { transform: translateX(0); } | |
| 10%, 30%, 50%, 70%, 90% { transform: translateX(-4px); } | |
| 20%, 40%, 60%, 80% { transform: translateX(4px); } | |
| } | |
| .animate-shake { | |
| animation: shake 0.5s ease-in-out; | |
| } | |
| `}</style> | |
| </div> | |
| ); | |
| } | |
| export default function App() { | |
| const [hasAccess, setHasAccess] = useState<boolean | null>(null); | |
| const [teacherEmail, setTeacherEmail] = useState<string>(""); | |
| const [isAuthenticated, setIsAuthenticated] = useState(false); | |
| const [showAnalyzer, setShowAnalyzer] = useState(false); | |
| const [testMode, setTestMode] = useState(false); | |
| // Check for existing access on mount | |
| useEffect(() => { | |
| const access = localStorage.getItem("classlens_access"); | |
| setHasAccess(access === "granted"); | |
| }, []); | |
| // Check URL params for auth callback and test mode | |
| useEffect(() => { | |
| const params = new URLSearchParams(window.location.search); | |
| const authSuccess = params.get("auth_success"); | |
| const email = params.get("email"); | |
| const authError = params.get("auth_error"); | |
| const test = params.get("test"); | |
| if (test === "chat") { | |
| setTestMode(true); | |
| return; | |
| } | |
| if (authSuccess === "true" && email) { | |
| setTeacherEmail(email); | |
| setIsAuthenticated(true); | |
| setShowAnalyzer(true); | |
| // Clean URL | |
| window.history.replaceState({}, "", window.location.pathname); | |
| } else if (authError) { | |
| console.error("Auth error:", authError); | |
| window.history.replaceState({}, "", window.location.pathname); | |
| } | |
| }, []); | |
| const handleStartAnalysis = () => { | |
| if (isAuthenticated) { | |
| setShowAnalyzer(true); | |
| } | |
| }; | |
| // Loading state | |
| if (hasAccess === null) { | |
| return ( | |
| <div className="min-h-screen flex items-center justify-center bg-[var(--color-background)]"> | |
| <div className="w-8 h-8 border-2 border-[var(--color-primary)] border-t-transparent rounded-full animate-spin" /> | |
| </div> | |
| ); | |
| } | |
| // Invite code gate | |
| if (!hasAccess) { | |
| return <InviteCodeGate onSuccess={() => setHasAccess(true)} />; | |
| } | |
| // Test mode - show simple chat panel | |
| if (testMode) { | |
| return ( | |
| <main className="flex min-h-screen flex-col items-center justify-center bg-slate-100 dark:bg-slate-950 p-4"> | |
| <div className="mx-auto w-full max-w-3xl"> | |
| <div className="mb-4 text-center"> | |
| <h1 className="text-2xl font-bold mb-2">ChatKit Test Mode</h1> | |
| <p className="text-gray-600">Testing basic ChatKit functionality</p> | |
| <a href="/" className="text-blue-500 underline text-sm">← Back to app</a> | |
| </div> | |
| <SimpleChatPanel /> | |
| </div> | |
| </main> | |
| ); | |
| } | |
| return ( | |
| <div className="min-h-screen"> | |
| <Header | |
| isAuthenticated={isAuthenticated} | |
| teacherEmail={teacherEmail} | |
| /> | |
| <main> | |
| {!showAnalyzer ? ( | |
| <> | |
| <Hero | |
| isAuthenticated={isAuthenticated} | |
| onStartAnalysis={handleStartAnalysis} | |
| /> | |
| <AuthStatus | |
| teacherEmail={teacherEmail} | |
| setTeacherEmail={setTeacherEmail} | |
| isAuthenticated={isAuthenticated} | |
| setIsAuthenticated={setIsAuthenticated} | |
| /> | |
| <Features /> | |
| </> | |
| ) : ( | |
| <ExamAnalyzer | |
| teacherEmail={teacherEmail} | |
| onBack={() => setShowAnalyzer(false)} | |
| /> | |
| )} | |
| </main> | |
| <footer className="py-8 text-center text-sm text-[var(--color-text-muted)]"> | |
| <p>© 2026 ClassLens • AI-Powered Teaching Assistant</p> | |
| </footer> | |
| </div> | |
| ); | |
| } | |