CoolFace
Apppublic

LiquidAI/LFM2-MCP

sourceHugging Faceapache-2.0updated 3mo agoView on Hugging Face
39likes
LoadingScreen.tsx551 linesDownload Raw Back to components
1import { ChevronDown } from "lucide-react";2import { MODEL_OPTIONS } from "../constants/models";3import LiquidAILogo from "./icons/LiquidAILogo";4import HfLogo from "./icons/HfLogo";5import MCPLogo from "./icons/MCPLogo";6import { useEffect, useMemo, useRef, useState } from "react";7import ReactDOM from "react-dom";8 9type Dot = {10  x: number;11  y: number;12  radius: number;13  speed: number;14  opacity: number;15  blur: number;16  pulse: number;17  pulseSpeed: number;18};19export const LoadingScreen = ({20  isLoading,21  progress,22  error,23  loadSelectedModel,24  selectedModelId,25  isModelDropdownOpen,26  setIsModelDropdownOpen,27  handleModelSelect,28}: {29  isLoading: boolean;30  progress: number;31  error: string | null;32  loadSelectedModel: () => void;33  selectedModelId: string;34  isModelDropdownOpen: boolean;35  setIsModelDropdownOpen: (isOpen: boolean) => void;36  handleModelSelect: (modelId: string) => void;37}) => {38  const model = useMemo(39    () => MODEL_OPTIONS.find((opt) => opt.id === selectedModelId),40    [selectedModelId]41  );42 43  // Refs44  const canvasRef = useRef<HTMLCanvasElement>(null);45  const dropdownBtnRef = useRef<HTMLButtonElement>(null);46  const dropdownRef = useRef<HTMLDivElement>(null);47  const wrapperRef = useRef<HTMLDivElement>(null); // NEW: anchor for centering48 49  // For keyboard navigation50  const [activeIndex, setActiveIndex] = useState(51    Math.max(52      0,53      MODEL_OPTIONS.findIndex((m) => m.id === selectedModelId)54    )55  );56 57  // Background Animation Effect (crisper dots + reduced motion)58  useEffect(() => {59    const canvas = canvasRef.current;60    if (!canvas) return;61 62    const ctx = canvas.getContext("2d");63    if (!ctx) return;64 65    const prefersReduced =66      typeof window !== "undefined" &&67      window.matchMedia &&68      window.matchMedia("(prefers-reduced-motion: reduce)").matches;69 70    let animationFrameId: number;71    let dots: Dot[] = [];72 73    const setup = () => {74      const dpr = Math.max(1, Math.min(2, window.devicePixelRatio || 1));75      const { innerWidth, innerHeight } = window;76      canvas.width = Math.floor(innerWidth * dpr);77      canvas.height = Math.floor(innerHeight * dpr);78      canvas.style.width = `${innerWidth}px`;79      canvas.style.height = `${innerHeight}px`;80      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);81 82      dots = [];83      const numDots = Math.floor((innerWidth * innerHeight) / 12000);84      for (let i = 0; i < numDots; ++i) {85        dots.push({86          x: Math.random() * innerWidth,87          y: Math.random() * innerHeight,88          radius: Math.random() * 2 + 0.3,89          speed: prefersReduced ? 0 : Math.random() * 0.3 + 0.05,90          opacity: Math.random() * 0.4 + 0.1,91          blur: Math.random() > 0.8 ? Math.random() * 1.5 + 0.5 : 0,92          pulse: Math.random() * Math.PI * 2,93          pulseSpeed: prefersReduced ? 0 : Math.random() * 0.02 + 0.01,94        });95      }96    };97 98    const draw = () => {99      if (!ctx) return;100      const width = canvas.clientWidth;101      const height = canvas.clientHeight;102      ctx.clearRect(0, 0, width, height);103 104      dots.forEach((dot) => {105        dot.y += dot.speed;106        dot.pulse += dot.pulseSpeed;107 108        if (dot.y > height + dot.radius) {109          dot.y = -dot.radius;110          dot.x = Math.random() * width;111        }112 113        const pulseFactor = 1 + Math.sin(dot.pulse) * 0.2;114        const currentRadius = dot.radius * pulseFactor;115        const currentOpacity = dot.opacity * (0.8 + Math.sin(dot.pulse) * 0.2);116 117        ctx.beginPath();118        ctx.arc(dot.x, dot.y, currentRadius, 0, Math.PI * 2);119        ctx.fillStyle = `rgba(255, 255, 255, ${currentOpacity})`;120        if (dot.blur > 0) ctx.filter = `blur(${dot.blur}px)`;121        ctx.fill();122        ctx.filter = "none";123      });124 125      animationFrameId = requestAnimationFrame(draw);126    };127 128    const handleResize = () => {129      cancelAnimationFrame(animationFrameId);130      setup();131      draw();132    };133 134    setup();135    draw();136    window.addEventListener("resize", handleResize);137 138    return () => {139      window.removeEventListener("resize", handleResize);140      cancelAnimationFrame(animationFrameId);141    };142  }, []);143 144  // Close dropdown on Escape / click outside145  useEffect(() => {146    if (!isModelDropdownOpen) return;147 148    const onKey = (e: KeyboardEvent) => {149      if (e.key === "Escape") setIsModelDropdownOpen(false);150      if (e.key === "ArrowDown") {151        e.preventDefault();152        setActiveIndex((i) => Math.min(MODEL_OPTIONS.length - 1, i + 1));153      }154      if (e.key === "ArrowUp") {155        e.preventDefault();156        setActiveIndex((i) => Math.max(0, i - 1));157      }158      if (e.key === "Enter") {159        e.preventDefault();160        const opt = MODEL_OPTIONS[activeIndex];161        if (opt) {162          handleModelSelect(opt.id);163          setIsModelDropdownOpen(false);164          dropdownBtnRef.current?.focus();165        }166      }167    };168 169    const onClick = (e: MouseEvent) => {170      const target = e.target as Node;171      if (172        dropdownRef.current &&173        !dropdownRef.current.contains(target) &&174        !dropdownBtnRef.current?.contains(target)175      ) {176        setIsModelDropdownOpen(false);177      }178    };179 180    document.addEventListener("keydown", onKey);181    document.addEventListener("mousedown", onClick);182    return () => {183      document.removeEventListener("keydown", onKey);184      document.removeEventListener("mousedown", onClick);185    };186  }, [187    isModelDropdownOpen,188    activeIndex,189    setIsModelDropdownOpen,190    handleModelSelect,191  ]);192 193  // Recompute portal position on open + resize194  const [, forceRerender] = useState(0);195  useEffect(() => {196    const onResize = () => forceRerender((x) => x + 1);197    window.addEventListener("resize", onResize);198    return () => window.removeEventListener("resize", onResize);199  }, []);200 201  // Compute portal style based on the whole button group (center + clamp + optional drop-up)202  const portalStyle = useMemo(() => {203    if (typeof window === "undefined") return {};204    const anchor = wrapperRef.current || dropdownBtnRef.current;205    if (!anchor) return {};206 207    const rect = anchor.getBoundingClientRect();208 209    const margin = 8;210    const minWidth = 320;211    const dropdownWidth = Math.max(rect.width, minWidth);212 213    // Center214    let left = Math.round(rect.left + rect.width / 2 - dropdownWidth / 2);215    // Clamp to viewport216    left = Math.min(217      Math.max(margin, left),218      window.innerWidth - dropdownWidth - margin219    );220 221    // Flip up if not enough space below222    const spaceBelow = window.innerHeight - rect.bottom;223    const spaceAbove = rect.top;224    const estimatedItemH = 56; // rough item height225    const estimatedPad = 16;226    const estimatedHeight =227      estimatedItemH * Math.min(MODEL_OPTIONS.length, 6) + estimatedPad;228    const dropUp = spaceBelow < estimatedHeight && spaceAbove > spaceBelow;229 230    const top = dropUp ? rect.top - estimatedHeight - 8 : rect.bottom + 8;231 232    return {233      position: "fixed" as const,234      left: `${left}px`,235      top: `${top}px`,236      width: `${dropdownWidth}px`,237      zIndex: 100,238    };239  }, []);240 241  return (242    <div className="relative flex flex-col items-center justify-center min-h-screen bg-gradient-to-br from-gray-900 via-slate-900 to-gray-900 text-white p-6 overflow-hidden">243      {/* Background Canvas */}244      <canvas245        ref={canvasRef}246        className="absolute top-0 left-0 w-full h-full z-0"247      />248 249      {/* Vignette Overlay */}250      <div className="absolute top-0 left-0 w-full h-full z-10 bg-[radial-gradient(ellipse_at_center,_rgba(15,23,42,0.1)_0%,_rgba(15,23,42,0.4)_40%,_rgba(15,23,42,0.9)_100%)]" />251 252      {/* Grid Overlay */}253      <div className="absolute inset-0 z-5 opacity-[0.02] bg-[linear-gradient(rgba(255,255,255,0.1)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.1)_1px,transparent_1px)] bg-[size:50px_50px]" />254 255      {/* Main Content */}256      <div className="relative z-20 max-w-4xl w-full flex flex-col items-center">257        {/* Logos */}258        <div className="flex items-center justify-center mb-8 gap-5">259          <a260            href="https://www.liquid.ai/"261            target="_blank"262            rel="noopener noreferrer"263            title="Liquid AI"264            className="transform transition-all duration-300 hover:scale-105 hover:-translate-y-1"265          >266            <LiquidAILogo className="h-16 md:h-20 text-gray-300 hover:text-white drop-shadow-lg" />267          </a>268          <span className="text-gray-500 text-3xl font-extralight">×</span>269          <a270            href="https://huggingface.co/docs/transformers.js"271            target="_blank"272            rel="noopener noreferrer"273            title="Transformers.js"274            className="transform transition-all duration-300 hover:scale-105 hover:-translate-y-1"275          >276            <HfLogo className="h-16 md:h-20 text-gray-300 hover:text-white drop-shadow-lg" />277          </a>278          <span className="text-gray-500 text-3xl font-extralight">×</span>279          <a280            href="https://modelcontextprotocol.io/"281            target="_blank"282            rel="noopener noreferrer"283            title="Model Context Protocol"284            className="transform transition-all duration-300 hover:scale-105 hover:-translate-y-1"285          >286            <MCPLogo className="h-16 md:h-20 text-gray-300 hover:text-white drop-shadow-lg" />287          </a>288        </div>289 290        {/* Hero */}291        <div className="text-center mb-8 space-y-4">292          <h1 className="text-5xl sm:text-6xl md:text-7xl font-black bg-gradient-to-r from-white via-gray-100 to-gray-300 bg-clip-text text-transparent tracking-tight leading-none">293            LFM2 MCP294          </h1>295          <p className="text-lg sm:text-xl md:text-2xl text-gray-300 font-light leading-relaxed">296            Run next-gen hybrid models in your browser with tools powered by the{" "}297            <a298              href="https://modelcontextprotocol.io/"299              target="_blank"300              rel="noopener noreferrer"301            >302              <span className="text-indigo-400 font-medium">303                Model Context Protocol (MCP)304              </span>{" "}305              enabling secure, real-time connections to remote servers.306            </a>307          </p>308          <div className="w-24 h-1 bg-gradient-to-r from-indigo-500 to-purple-500 rounded-full mx-auto" />309        </div>310 311        {/* Description Cards */}312        <div className="grid md:grid-cols-2 gap-6 text-gray-400 mb-10">313          <div className="bg-white/5 backdrop-blur-sm rounded-xl p-6 border border-white/10">314            <h3 className="text-white font-semibold mb-3 flex items-center">315              <div className="w-2 h-2 bg-indigo-500 rounded-full mr-3" />316              Model Context Protocol317            </h3>318            <p className="text-sm leading-relaxed">319              Connect seamlessly to remote{" "}320              <a321                href="https://modelcontextprotocol.io/"322                target="_blank"323                rel="noopener noreferrer"324                className="text-indigo-400 hover:underline"325              >326                MCP servers327              </a>{" "}328              using streaming or SSE protocols with support for no-auth, basic329              auth, and OAuth.330            </p>331          </div>332 333          <div className="bg-white/5 backdrop-blur-sm rounded-xl p-6 border border-white/10">334            <h3 className="text-white font-semibold mb-3 flex items-center">335              <div className="w-2 h-2 bg-purple-500 rounded-full mr-3" />336              Edge AI Technology337            </h3>338            <p className="text-sm leading-relaxed">339              Powered by{" "}340              <a341                href="https://www.liquid.ai/"342                target="_blank"343                rel="noopener noreferrer"344                className="text-indigo-400 hover:underline"345              >346                Liquid AI’s347              </a>{" "}348              LFM2 hybrid models, optimized for on-device deployment and edge AI349              scenarios.350            </p>351          </div>352        </div>353 354        <p className="text-gray-400 text-base sm:text-lg mb-10">355          Everything runs entirely in your browser with{" "}356          <a357            href="https://huggingface.co/docs/transformers.js"358            target="_blank"359            rel="noopener noreferrer"360            className="text-indigo-400 hover:underline font-medium"361          >362            Transformers.js363          </a>{" "}364          and ONNX Runtime Web.365        </p>366 367        {/* Action */}368        <div className="text-center space-y-6">369          <p className="text-gray-400 text-base sm:text-lg font-medium">370            Select a model to load locally, and connect to a remote MCP server371            to get started.372          </p>373 374          <div className="relative">375            <div376              ref={wrapperRef} // anchor for dropdown centering377              className="flex rounded-2xl shadow-2xl overflow-hidden"378            >379              <button380                onClick={isLoading ? undefined : loadSelectedModel}381                disabled={isLoading}382                className={`flex items-center justify-center font-bold transition-all text-lg flex-1 ${383                  isLoading384                    ? "bg-gray-700 text-gray-400 cursor-not-allowed"385                    : "bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 text-white shadow-lg hover:shadow-xl transform hover:scale-[1.01] active:scale-[0.99]"386                }`}387                aria-live="polite"388                aria-busy={isLoading}389                aria-label={390                  isLoading391                    ? `Loading ${model?.label ?? "model"} ${progress}%`392                    : `Load ${model?.label ?? "model"}`393                }394              >395                <div className="px-8 py-4">396                  {isLoading ? (397                    <div className="flex items-center">398                      <div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />399                      <span className="ml-3 font-semibold">400                        Loading... {progress}%401                      </span>402                    </div>403                  ) : (404                    <span className="font-semibold">Load {model?.label}</span>405                  )}406                </div>407              </button>408 409              <button410                ref={dropdownBtnRef}411                onClick={(e) => {412                  if (!isLoading) {413                    e.stopPropagation();414                    setIsModelDropdownOpen(!isModelDropdownOpen);415                    setActiveIndex(416                      Math.max(417                        0,418                        MODEL_OPTIONS.findIndex((m) => m.id === selectedModelId)419                      )420                    );421                  }422                }}423                onKeyDown={(e) => {424                  if (isLoading) return;425                  if (426                    e.key === " " ||427                    e.key === "Enter" ||428                    e.key === "ArrowDown"429                  ) {430                    e.preventDefault();431                    if (!isModelDropdownOpen) setIsModelDropdownOpen(true);432                  }433                }}434                aria-haspopup="menu"435                aria-expanded={isModelDropdownOpen}436                aria-controls="model-dropdown"437                aria-label="Select model"438                className={`px-4 py-4 border-l border-white/20 transition-all ${439                  isLoading440                    ? "bg-gray-700 cursor-not-allowed"441                    : "bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 hover:shadow-lg transform hover:scale-[1.01] active:scale-[0.99]"442                }`}443                disabled={isLoading}444              >445                <ChevronDown446                  size={20}447                  className={`transition-transform duration-200 ${448                    isModelDropdownOpen ? "rotate-180" : ""449                  }`}450                />451              </button>452            </div>453 454            {/* Dropdown (Portal) */}455            {isModelDropdownOpen &&456              typeof document !== "undefined" &&457              ReactDOM.createPortal(458                <div459                  id="model-dropdown"460                  ref={dropdownRef}461                  style={portalStyle}462                  role="menu"463                  aria-label="Model options"464                  className="bg-gray-800/95 border border-gray-600/50 rounded-2xl shadow-2xl overflow-hidden animate-in slide-in-from-top-2 duration-200 dropdown-z30"465                >466                  {MODEL_OPTIONS.map((option, index) => {467                    const selected = selectedModelId === option.id;468                    const isActive = activeIndex === index;469                    return (470                      <button471                        key={option.id}472                        role="menuitem"473                        aria-checked={selected}474                        onMouseEnter={() => setActiveIndex(index)}475                        onClick={() => {476                          handleModelSelect(option.id);477                          setIsModelDropdownOpen(false);478                          dropdownBtnRef.current?.focus();479                        }}480                        className={`w-full px-6 py-4 text-left transition-all duration-200 relative group outline-none ${481                          selected482                            ? "bg-gradient-to-r from-indigo-600/50 to-purple-600/50 text-white border-l-4 border-indigo-400"483                            : "text-gray-200 hover:bg-white/10 hover:text-white"484                        } ${index === 0 ? "rounded-t-2xl" : ""} ${485                          index === MODEL_OPTIONS.length - 1486                            ? "rounded-b-2xl"487                            : ""488                        } ${isActive && !selected ? "bg-white/5" : ""}`}489                      >490                        <div className="flex items-center justify-between">491                          <div>492                            <div className="font-semibold text-lg">493                              {option.label}494                            </div>495                            <div className="text-sm text-gray-400 mt-1">496                              {option.size}497                            </div>498                          </div>499                          {selected && (500                            <div className="w-2 h-2 bg-indigo-400 rounded-full" />501                          )}502                        </div>503                        {!selected && (504                          <div className="absolute inset-0 bg-gradient-to-r from-indigo-500/10 to-purple-500/10 opacity-0 group-hover:opacity-100 transition-opacity rounded-2xl" />505                        )}506                      </button>507                    );508                  })}509                </div>,510                document.body511              )}512          </div>513        </div>514 515        {/* Error */}516        {error && (517          <div518            role="alert"519            className="bg-red-900/30 backdrop-blur-sm border border-red-500/50 rounded-2xl p-6 mt-8 max-w-md text-center"520          >521            <p className="text-red-200 mb-4 font-medium">Error: {error}</p>522            <button523              onClick={loadSelectedModel}524              className="bg-gradient-to-r from-red-600 to-red-700 hover:from-red-500 hover:to-red-600 px-6 py-3 rounded-xl font-semibold transition-all transform hover:scale-105 active:scale-95 shadow-lg"525            >526              Try Again527            </button>528          </div>529        )}530      </div>531 532      {/* Click-away fallback for touch devices */}533      {isModelDropdownOpen && (534        <div535          className="fixed inset-0 z-40 bg-black/20"536          onClick={(e) => {537            const target = e.target as Node;538            if (539              dropdownRef.current &&540              !dropdownRef.current.contains(target) &&541              !dropdownBtnRef.current?.contains(target)542            ) {543              setIsModelDropdownOpen(false);544            }545          }}546        />547      )}548    </div>549  );550};551