|
|
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"; |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
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; |
|
|
const dotSpeed = 0.3; |
|
|
|
|
|
const setup = () => { |
|
|
canvas.width = window.innerWidth; |
|
|
canvas.height = window.innerHeight; |
|
|
dots = []; |
|
|
|
|
|
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, |
|
|
vy: (Math.random() - 0.5) * dotSpeed, |
|
|
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); |
|
|
|
|
|
|
|
|
dots.forEach((dot) => { |
|
|
|
|
|
dot.x += dot.vx; |
|
|
dot.y += dot.vy; |
|
|
|
|
|
|
|
|
if (dot.x <= 0 || dot.x >= canvas.width) dot.vx *= -1; |
|
|
if (dot.y <= 0 || dot.y >= canvas.height) dot.vy *= -1; |
|
|
|
|
|
|
|
|
ctx.beginPath(); |
|
|
ctx.arc(dot.x, dot.y, dot.radius, 0, Math.PI * 2); |
|
|
ctx.fillStyle = `rgba(255, 255, 255, ${dot.opacity})`; |
|
|
ctx.fill(); |
|
|
}); |
|
|
|
|
|
|
|
|
ctx.lineWidth = 0.5; |
|
|
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 (distance < maxConnectionDistance) { |
|
|
|
|
|
const opacity = 1 - distance / maxConnectionDistance; |
|
|
ctx.strokeStyle = `rgba(255, 255, 255, ${opacity * 0.3})`; |
|
|
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> |
|
|
); |
|
|
}; |
|
|
|