|
|
import { useEffect, useState } from "react"; |
|
|
import { useVLMContext } from "../context/useVLMContext"; |
|
|
import { THEME } from "../constants"; |
|
|
|
|
|
interface LoadingScreenProps { |
|
|
onComplete: () => void; |
|
|
} |
|
|
|
|
|
export default function LoadingScreen({ onComplete }: LoadingScreenProps) { |
|
|
const [progress, setProgress] = useState(0); |
|
|
const [currentStep, setCurrentStep] = useState("Initializing environment..."); |
|
|
const [isError, setIsError] = useState(false); |
|
|
const [hasStartedLoading, setHasStartedLoading] = useState(false); |
|
|
const [mounted, setMounted] = useState(false); |
|
|
|
|
|
const { loadModel, isLoaded, isLoading } = useVLMContext(); |
|
|
|
|
|
useEffect(() => { |
|
|
setMounted(true); |
|
|
}, []); |
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
if (hasStartedLoading || isLoading || isLoaded) return; |
|
|
|
|
|
const loadModelAndProgress = async () => { |
|
|
setHasStartedLoading(true); |
|
|
|
|
|
try { |
|
|
setCurrentStep("Checking WebGPU compatibility..."); |
|
|
|
|
|
|
|
|
if (!navigator.gpu) { |
|
|
setCurrentStep("WebGPU is not available in this browser context."); |
|
|
setIsError(true); |
|
|
return; |
|
|
} |
|
|
|
|
|
await loadModel((message, percentage) => { |
|
|
const cleanMsg = message.replace("...", ""); |
|
|
setCurrentStep(cleanMsg); |
|
|
|
|
|
if (percentage !== undefined) { |
|
|
setProgress(percentage); |
|
|
} |
|
|
}); |
|
|
|
|
|
setCurrentStep("System ready."); |
|
|
setProgress(100); |
|
|
onComplete(); |
|
|
} catch (error) { |
|
|
console.error("Error loading model:", error); |
|
|
setCurrentStep( |
|
|
`ERR: ${error instanceof Error ? error.message : String(error)}`, |
|
|
); |
|
|
setIsError(true); |
|
|
} |
|
|
}; |
|
|
|
|
|
loadModelAndProgress(); |
|
|
}, [hasStartedLoading, isLoading, isLoaded, loadModel, onComplete]); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
if (isLoaded && !hasStartedLoading) { |
|
|
setProgress(100); |
|
|
setCurrentStep("Model cached and ready."); |
|
|
setTimeout(onComplete, 500); |
|
|
} |
|
|
}, [isLoaded, hasStartedLoading, onComplete]); |
|
|
|
|
|
return ( |
|
|
<> |
|
|
<div |
|
|
className="absolute inset-0 flex items-center justify-center p-8 z-50" |
|
|
style={{ |
|
|
backgroundColor: THEME.beigeLight, |
|
|
backgroundImage: ` |
|
|
linear-gradient(${THEME.beigeDark} 1px, transparent 1px), |
|
|
linear-gradient(90deg, ${THEME.beigeDark} 1px, transparent 1px) |
|
|
`, |
|
|
backgroundSize: "40px 40px", |
|
|
}} |
|
|
> |
|
|
<div |
|
|
className={`max-w-md w-full backdrop-blur-sm rounded-sm border shadow-xl transition-all duration-700 transform ${mounted ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"}`} |
|
|
style={{ |
|
|
backgroundColor: `${THEME.beigeLight}F2`, |
|
|
borderColor: THEME.beigeDark, |
|
|
}} |
|
|
> |
|
|
{/* Header */} |
|
|
<div |
|
|
className={`h-1 w-full transition-colors duration-300 ${isError ? "bg-[var(--mistral-red)]" : "bg-[var(--mistral-orange)]"}`} |
|
|
></div> |
|
|
<div className="p-8 space-y-8"> |
|
|
{/* Status Icon Area */} |
|
|
<div className="flex justify-center"> |
|
|
{isError ? ( |
|
|
<div |
|
|
className="w-20 h-20 rounded-full flex items-center justify-center border" |
|
|
style={{ |
|
|
backgroundColor: `${THEME.errorRed}1A`, |
|
|
borderColor: `${THEME.errorRed}33`, |
|
|
}} |
|
|
> |
|
|
<svg |
|
|
className="w-10 h-10" |
|
|
style={{ color: THEME.errorRed }} |
|
|
fill="none" |
|
|
viewBox="0 0 24 24" |
|
|
stroke="currentColor" |
|
|
strokeWidth={2} |
|
|
> |
|
|
<path |
|
|
strokeLinecap="round" |
|
|
strokeLinejoin="round" |
|
|
d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" |
|
|
/> |
|
|
</svg> |
|
|
</div> |
|
|
) : ( |
|
|
<div className="relative"> |
|
|
{/* Spinning Ring */} |
|
|
<div |
|
|
className="w-20 h-20 border-4 border-t-[var(--mistral-orange)] rounded-full animate-spin" |
|
|
style={{ |
|
|
borderColor: THEME.beigeDark, |
|
|
borderTopColor: THEME.mistralOrange, |
|
|
}} |
|
|
></div> |
|
|
{/* Center Dot */} |
|
|
<div className="absolute inset-0 flex items-center justify-center"> |
|
|
<div |
|
|
className="w-2 h-2 rounded-full animate-pulse" |
|
|
style={{ backgroundColor: THEME.mistralOrange }} |
|
|
></div> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
<div className="text-center space-y-2"> |
|
|
<h2 |
|
|
className="text-2xl font-bold tracking-tight" |
|
|
style={{ color: THEME.textBlack }} |
|
|
> |
|
|
{isError ? "Initialization Failed" : "Loading Model"} |
|
|
</h2> |
|
|
<p className="text-sm text-gray-500 font-mono uppercase tracking-widest"> |
|
|
Ministral-3B-Instruct |
|
|
</p> |
|
|
</div> |
|
|
{/* Progress Section */} |
|
|
{!isError && ( |
|
|
<div className="space-y-4"> |
|
|
<div className="flex justify-between text-xs font-mono font-bold text-gray-500"> |
|
|
<span>PROGRESS</span> |
|
|
<span>{Math.round(progress)}%</span> |
|
|
</div> |
|
|
<div |
|
|
className="w-full rounded-full h-4 overflow-hidden border" |
|
|
style={{ |
|
|
backgroundColor: `${THEME.beigeDark}80`, |
|
|
borderColor: THEME.beigeDark, |
|
|
}} |
|
|
> |
|
|
<div |
|
|
className="h-full progress-stripe transition-all duration-500 ease-out" |
|
|
style={{ |
|
|
width: `${progress}%`, |
|
|
backgroundColor: THEME.mistralOrange, |
|
|
}} |
|
|
/> |
|
|
</div> |
|
|
{/* "Terminal" Log Output */} |
|
|
<div |
|
|
className="bg-white border p-3 rounded-sm" |
|
|
style={{ borderColor: THEME.beigeDark }} |
|
|
> |
|
|
<div className="flex items-center space-x-2"> |
|
|
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div> |
|
|
<p className="font-mono text-xs text-gray-600 truncate"> |
|
|
{`> ${currentStep}`} |
|
|
</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
{/* Error Actions */} |
|
|
{isError && ( |
|
|
<div className="space-y-4"> |
|
|
<div |
|
|
className="border p-4 rounded text-left" |
|
|
style={{ |
|
|
backgroundColor: `${THEME.errorRed}0D`, |
|
|
borderColor: `${THEME.errorRed}33`, |
|
|
}} |
|
|
> |
|
|
<p |
|
|
className="font-mono text-xs break-words" |
|
|
style={{ color: THEME.errorRed }} |
|
|
> |
|
|
{`> Error: ${currentStep}`} |
|
|
</p> |
|
|
</div> |
|
|
<button |
|
|
onClick={() => window.location.reload()} |
|
|
className="w-full py-3 text-white font-bold transition-colors shadow-lg hover:bg-black" |
|
|
style={{ backgroundColor: THEME.textBlack }} |
|
|
> |
|
|
RELOAD APPLICATION |
|
|
</button> |
|
|
</div> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</> |
|
|
); |
|
|
} |