import { ChevronDown } from "lucide-react"; import { MODEL_OPTIONS } from "../constants/models"; import LiquidAILogo from "./icons/LiquidAILogo"; import HfLogo from "./icons/HfLogo"; import MCPLogo from "./icons/MCPLogo"; import { useEffect, useMemo, useRef, useState } from "react"; import ReactDOM from "react-dom"; type Dot = { x: number; y: number; radius: number; speed: number; opacity: number; blur: number; pulse: number; pulseSpeed: 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 = useMemo( () => MODEL_OPTIONS.find((opt) => opt.id === selectedModelId), [selectedModelId] ); // Refs const canvasRef = useRef(null); const dropdownBtnRef = useRef(null); const dropdownRef = useRef(null); const wrapperRef = useRef(null); // NEW: anchor for centering // For keyboard navigation const [activeIndex, setActiveIndex] = useState( Math.max( 0, MODEL_OPTIONS.findIndex((m) => m.id === selectedModelId) ) ); // Background Animation Effect (crisper dots + reduced motion) useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; const prefersReduced = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; let animationFrameId: number; let dots: Dot[] = []; const setup = () => { const dpr = Math.max(1, Math.min(2, window.devicePixelRatio || 1)); const { innerWidth, innerHeight } = window; canvas.width = Math.floor(innerWidth * dpr); canvas.height = Math.floor(innerHeight * dpr); canvas.style.width = `${innerWidth}px`; canvas.style.height = `${innerHeight}px`; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); dots = []; const numDots = Math.floor((innerWidth * innerHeight) / 12000); for (let i = 0; i < numDots; ++i) { dots.push({ x: Math.random() * innerWidth, y: Math.random() * innerHeight, radius: Math.random() * 2 + 0.3, speed: prefersReduced ? 0 : Math.random() * 0.3 + 0.05, opacity: Math.random() * 0.4 + 0.1, blur: Math.random() > 0.8 ? Math.random() * 1.5 + 0.5 : 0, pulse: Math.random() * Math.PI * 2, pulseSpeed: prefersReduced ? 0 : Math.random() * 0.02 + 0.01, }); } }; const draw = () => { if (!ctx) return; const width = canvas.clientWidth; const height = canvas.clientHeight; ctx.clearRect(0, 0, width, height); dots.forEach((dot) => { dot.y += dot.speed; dot.pulse += dot.pulseSpeed; if (dot.y > height + dot.radius) { dot.y = -dot.radius; dot.x = Math.random() * width; } const pulseFactor = 1 + Math.sin(dot.pulse) * 0.2; const currentRadius = dot.radius * pulseFactor; const currentOpacity = dot.opacity * (0.8 + Math.sin(dot.pulse) * 0.2); ctx.beginPath(); ctx.arc(dot.x, dot.y, currentRadius, 0, Math.PI * 2); ctx.fillStyle = `rgba(255, 255, 255, ${currentOpacity})`; if (dot.blur > 0) ctx.filter = `blur(${dot.blur}px)`; ctx.fill(); ctx.filter = "none"; }); animationFrameId = requestAnimationFrame(draw); }; const handleResize = () => { cancelAnimationFrame(animationFrameId); setup(); draw(); }; setup(); draw(); window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize", handleResize); cancelAnimationFrame(animationFrameId); }; }, []); // Close dropdown on Escape / click outside useEffect(() => { if (!isModelDropdownOpen) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setIsModelDropdownOpen(false); if (e.key === "ArrowDown") { e.preventDefault(); setActiveIndex((i) => Math.min(MODEL_OPTIONS.length - 1, i + 1)); } if (e.key === "ArrowUp") { e.preventDefault(); setActiveIndex((i) => Math.max(0, i - 1)); } if (e.key === "Enter") { e.preventDefault(); const opt = MODEL_OPTIONS[activeIndex]; if (opt) { handleModelSelect(opt.id); setIsModelDropdownOpen(false); dropdownBtnRef.current?.focus(); } } }; const onClick = (e: MouseEvent) => { const target = e.target as Node; if ( dropdownRef.current && !dropdownRef.current.contains(target) && !dropdownBtnRef.current?.contains(target) ) { setIsModelDropdownOpen(false); } }; document.addEventListener("keydown", onKey); document.addEventListener("mousedown", onClick); return () => { document.removeEventListener("keydown", onKey); document.removeEventListener("mousedown", onClick); }; }, [ isModelDropdownOpen, activeIndex, setIsModelDropdownOpen, handleModelSelect, ]); // Recompute portal position on open + resize const [, forceRerender] = useState(0); useEffect(() => { const onResize = () => forceRerender((x) => x + 1); window.addEventListener("resize", onResize); return () => window.removeEventListener("resize", onResize); }, []); // Compute portal style based on the whole button group (center + clamp + optional drop-up) const portalStyle = useMemo(() => { if (typeof window === "undefined") return {}; const anchor = wrapperRef.current || dropdownBtnRef.current; if (!anchor) return {}; const rect = anchor.getBoundingClientRect(); const margin = 8; const minWidth = 320; const dropdownWidth = Math.max(rect.width, minWidth); // Center let left = Math.round(rect.left + rect.width / 2 - dropdownWidth / 2); // Clamp to viewport left = Math.min( Math.max(margin, left), window.innerWidth - dropdownWidth - margin ); // Flip up if not enough space below const spaceBelow = window.innerHeight - rect.bottom; const spaceAbove = rect.top; const estimatedItemH = 56; // rough item height const estimatedPad = 16; const estimatedHeight = estimatedItemH * Math.min(MODEL_OPTIONS.length, 6) + estimatedPad; const dropUp = spaceBelow < estimatedHeight && spaceAbove > spaceBelow; const top = dropUp ? rect.top - estimatedHeight - 8 : rect.bottom + 8; return { position: "fixed" as const, left: `${left}px`, top: `${top}px`, width: `${dropdownWidth}px`, zIndex: 100, }; }, []); return (
{/* Background Canvas */} {/* Vignette Overlay */}
{/* Grid Overlay */}
{/* Main Content */}
{/* Logos */}
× ×
{/* Hero */}

LFM2 MCP

Run next-gen hybrid models in your browser with tools powered by the{" "} Model Context Protocol (MCP) {" "} enabling secure, real-time connections to remote servers.

{/* Description Cards */}

Model Context Protocol

Connect seamlessly to remote{" "} MCP servers {" "} using streaming or SSE protocols with support for no-auth, basic auth, and OAuth.

Edge AI Technology

Powered by{" "} Liquid AI’s {" "} LFM2 hybrid models, optimized for on-device deployment and edge AI scenarios.

Everything runs entirely in your browser with{" "} Transformers.js {" "} and ONNX Runtime Web.

{/* Action */}

Select a model to load locally, and connect to a remote MCP server to get started.

{/* Dropdown (Portal) */} {isModelDropdownOpen && typeof document !== "undefined" && ReactDOM.createPortal( , document.body )}
{/* Error */} {error && (

Error: {error}

)}
{/* Click-away fallback for touch devices */} {isModelDropdownOpen && (
{ const target = e.target as Node; if ( dropdownRef.current && !dropdownRef.current.contains(target) && !dropdownBtnRef.current?.contains(target) ) { setIsModelDropdownOpen(false); } }} /> )}
); };