ibibrahim's picture
Create demo (#1)
c675f75 verified
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>
);
};