Spaces:
Running
Running
| import { ChevronDown } from "lucide-react"; | |
| import { MODEL_OPTIONS } from "../constants/models"; | |
| import IBMLogo from "./icons/IBMLogo"; | |
| import HfLogo from "./icons/HfLogo"; | |
| import { useEffect, useRef } from "react"; | |
| // Define the structure of our animated dots | |
| interface Dot { | |
| x: number; | |
| y: number; | |
| vx: number; | |
| vy: number; | |
| radius: number; | |
| opacity: number; | |
| } | |
| export const LoadingScreen = ({ | |
| isLoading, | |
| progress, | |
| error, | |
| loadSelectedModel, | |
| selectedModelId, | |
| isModelDropdownOpen, | |
| setIsModelDropdownOpen, | |
| handleModelSelect, | |
| }: { | |
| isLoading: boolean; | |
| progress: number; | |
| error: string | null; | |
| loadSelectedModel: () => void; | |
| selectedModelId: string; | |
| isModelDropdownOpen: boolean; | |
| setIsModelDropdownOpen: (isOpen: boolean) => void; | |
| handleModelSelect: (modelId: string) => void; | |
| }) => { | |
| const model = MODEL_OPTIONS.find((opt) => opt.id === selectedModelId); | |
| const canvasRef = useRef<HTMLCanvasElement>(null); | |
| // Background Animation Effect | |
| useEffect(() => { | |
| const canvas = canvasRef.current; | |
| if (!canvas) return; | |
| const ctx = canvas.getContext("2d"); | |
| if (!ctx) return; | |
| let animationFrameId: number; | |
| let dots: Dot[] = []; | |
| const maxConnectionDistance = 130; // Max distance to draw a line between dots | |
| const dotSpeed = 0.3; // How fast dots move | |
| const setup = () => { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| dots = []; | |
| // Adjust dot density based on screen area | |
| const numDots = Math.floor((canvas.width * canvas.height) / 20000); | |
| for (let i = 0; i < numDots; ++i) { | |
| dots.push({ | |
| x: Math.random() * canvas.width, | |
| y: Math.random() * canvas.height, | |
| vx: (Math.random() - 0.5) * dotSpeed, // Random horizontal velocity | |
| vy: (Math.random() - 0.5) * dotSpeed, // Random vertical velocity | |
| radius: Math.random() * 1.5 + 0.5, | |
| opacity: Math.random() * 0.5 + 0.2, | |
| }); | |
| } | |
| }; | |
| const draw = () => { | |
| if (!ctx) return; | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // 1. Update and draw dots | |
| dots.forEach((dot) => { | |
| // Update position | |
| dot.x += dot.vx; | |
| dot.y += dot.vy; | |
| // Bounce off edges | |
| if (dot.x <= 0 || dot.x >= canvas.width) dot.vx *= -1; | |
| if (dot.y <= 0 || dot.y >= canvas.height) dot.vy *= -1; | |
| // Draw dot | |
| ctx.beginPath(); | |
| ctx.arc(dot.x, dot.y, dot.radius, 0, Math.PI * 2); | |
| ctx.fillStyle = `rgba(255, 255, 255, ${dot.opacity})`; | |
| ctx.fill(); | |
| }); | |
| // 2. Draw connecting lines | |
| ctx.lineWidth = 0.5; // Use a thin line for connections | |
| for (let i = 0; i < dots.length; i++) { | |
| for (let j = i + 1; j < dots.length; j++) { | |
| const dot1 = dots[i]; | |
| const dot2 = dots[j]; | |
| const dx = dot1.x - dot2.x; | |
| const dy = dot1.y - dot2.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| // If dots are close enough, draw a line | |
| if (distance < maxConnectionDistance) { | |
| // Calculate opacity based on distance (closer = more opaque) | |
| const opacity = 1 - distance / maxConnectionDistance; | |
| ctx.strokeStyle = `rgba(255, 255, 255, ${opacity * 0.3})`; // Faint white lines | |
| ctx.beginPath(); | |
| ctx.moveTo(dot1.x, dot1.y); | |
| ctx.lineTo(dot2.x, dot2.y); | |
| ctx.stroke(); | |
| } | |
| } | |
| } | |
| animationFrameId = requestAnimationFrame(draw); | |
| }; | |
| const handleResize = () => { | |
| cancelAnimationFrame(animationFrameId); | |
| setup(); | |
| draw(); | |
| }; | |
| setup(); | |
| draw(); | |
| window.addEventListener("resize", handleResize); | |
| return () => { | |
| window.removeEventListener("resize", handleResize); | |
| cancelAnimationFrame(animationFrameId); | |
| }; | |
| }, []); | |
| return ( | |
| <div className="relative flex flex-col items-center justify-center h-screen bg-gradient-to-br from-[#031b4e] via-[#06183d] to-[#010409] text-gray-100 text-[16px] md:text-[17px] p-8 overflow-hidden"> | |
| {/* Background Canvas for Animation */} | |
| <canvas | |
| ref={canvasRef} | |
| className="absolute top-0 left-0 w-full h-full z-0" | |
| /> | |
| {/* Vignette Overlay */} | |
| <div className="absolute top-0 left-0 w-full h-full z-10 bg-[radial-gradient(ellipse_at_center,_rgba(3,27,78,0)_30%,_rgba(1,4,9,0.85)_95%)]"></div> | |
| {/* Main Content */} | |
| <div className="relative z-20 max-w-3xl w-full flex flex-col items-center bg-white/5 border border-white/10 backdrop-blur-xl rounded-3xl p-10 shadow-[0_35px_65px_rgba(3,27,78,0.55)] space-y-8"> | |
| <div className="flex items-center justify-center gap-6 text-5xl md:text-6xl"> | |
| <a | |
| href="https://huggingface.co/ibm-granite" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| title="IBM Granite" | |
| > | |
| <div className="size-24 md:size-28 bg-blue-500 rounded-sm p-2 flex items-center justify-center"> | |
| <IBMLogo className="text-white" /> | |
| </div> | |
| </a> | |
| <span className="text-[#78a9ff]">×</span> | |
| <a | |
| href="https://huggingface.co/docs/transformers.js" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| title="Transformers.js" | |
| > | |
| <HfLogo className="h-24 md:h-28 text-gray-300 hover:text-white transition-colors" /> | |
| </a> | |
| </div> | |
| <div className="w-full text-center"> | |
| <h1 className="text-5xl font-semibold mb-2 text-white tracking-tight"> | |
| Granite-4.0 WebGPU | |
| </h1> | |
| <p className="text-md md:text-lg text-[#a6c8ff]"> | |
| In-browser tool calling, powered by Transformers.js | |
| </p> | |
| </div> | |
| <div className="w-full text-left text-[#d0e2ff] space-y-4 text-xl"> | |
| <p> | |
| This demo showcases in-browser tool calling with Granite-4.0, a new | |
| series of models by{" "} | |
| <a | |
| href="https://huggingface.co/ibm-granite" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="text-[#78a9ff] hover:underline font-medium" | |
| > | |
| IBM Granite | |
| </a>{" "} | |
| designed for edge AI and on-device deployment. | |
| </p> | |
| <p> | |
| Everything runs entirely in your browser with{" "} | |
| <a | |
| href="https://huggingface.co/docs/transformers.js" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="text-[#78a9ff] hover:underline font-medium" | |
| > | |
| Transformers.js | |
| </a>{" "} | |
| and ONNX Runtime Web, meaning no data is sent to a server. It can | |
| even run offline! | |
| </p> | |
| </div> | |
| <p className="text-[#a6c8ff]"> | |
| Select a model and click load to get started. | |
| </p> | |
| <div className="relative w-full max-w-lg"> | |
| <div className="flex rounded-2xl border border-white/12 bg-white/10 overflow-hidden shadow-[0_18px_45px_rgba(3,27,78,0.45)]"> | |
| <button | |
| onClick={isLoading ? undefined : loadSelectedModel} | |
| disabled={isLoading} | |
| className={`flex-1 flex items-center justify-center font-semibold transition-all text-lg ${isLoading ? "bg-white/5 text-[#8da2d8] cursor-not-allowed" : "bg-[#0f62fe] hover:bg-[#0043ce] text-white"}`} | |
| > | |
| <div className="px-6 py-3"> | |
| {isLoading ? ( | |
| <div className="flex items-center"> | |
| <span className="inline-block w-5 h-5 border-2 border-white/80 border-t-transparent rounded-full animate-spin"></span> | |
| <span className="ml-3 text-md font-medium"> | |
| Loading... ({progress}%) | |
| </span> | |
| </div> | |
| ) : ( | |
| `Load ${model?.label}` | |
| )} | |
| </div> | |
| </button> | |
| <button | |
| onClick={(e) => { | |
| if (!isLoading) { | |
| e.stopPropagation(); | |
| setIsModelDropdownOpen(!isModelDropdownOpen); | |
| } | |
| }} | |
| aria-label="Select model" | |
| className="px-4 py-3 border-l border-white/15 bg-[#0f62fe] hover:bg-[#0043ce] transition-colors text-white disabled:cursor-not-allowed disabled:bg-white/5" | |
| disabled={isLoading} | |
| > | |
| <ChevronDown size={24} /> | |
| </button> | |
| </div> | |
| {isModelDropdownOpen && ( | |
| <div className="absolute left-0 right-0 bottom-full mb-3 bg-[#02102c]/98 border border-white/12 rounded-xl shadow-[0_22px_55px_rgba(3,27,78,0.55)] z-10 w-full overflow-visible backdrop-blur-2xl"> | |
| {MODEL_OPTIONS.map((option) => ( | |
| <button | |
| key={option.id} | |
| onClick={() => handleModelSelect(option.id)} | |
| className={`w-full px-5 py-3 text-left text-sm font-medium rounded-lg border transition-all ${ | |
| selectedModelId === option.id | |
| ? "border-[#78a9ff]/60 bg-[#0f62fe]/25 text-white shadow-[0_10px_25px_rgba(15,98,254,0.25)]" | |
| : "border-transparent text-[#d0e2ff] hover:border-[#78a9ff]/30 hover:bg-white/12 hover:text-white" | |
| }`} | |
| > | |
| <div className="font-medium">{option.label}</div> | |
| <div className="text-md text-[#95a8dd]">{option.size}</div> | |
| </button> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| {error && ( | |
| <div className="bg-[#2d0709]/70 border border-[#ff8389]/40 rounded-2xl p-4 w-full max-w-md text-center shadow-[0_15px_35px_rgba(45,7,9,0.4)]"> | |
| <p className="text-sm text-[#ffb3b8]">Error: {error}</p> | |
| <button | |
| onClick={loadSelectedModel} | |
| className="mt-3 text-sm px-4 py-2 rounded-full bg-white/15 hover:bg-white/25 border border-white/20 text-white font-semibold transition-all" | |
| > | |
| Retry | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| {/* Click-away listener for dropdown */} | |
| {isModelDropdownOpen && ( | |
| <div | |
| className="fixed inset-0 z-5" | |
| onClick={() => setIsModelDropdownOpen(false)} | |
| /> | |
| )} | |
| </div> | |
| ); | |
| }; | |