shreyask commited on
Commit
a4d1302
·
verified ·
1 Parent(s): d650163
src/components/LoadingScreen.tsx CHANGED
@@ -1,11 +1,21 @@
1
  import { ChevronDown } from "lucide-react";
2
-
3
  import { MODEL_OPTIONS } from "../constants/models";
4
  import LiquidAILogo from "./icons/LiquidAILogo";
5
  import HfLogo from "./icons/HfLogo";
 
 
 
6
 
7
- import { useEffect, useRef } from "react";
8
-
 
 
 
 
 
 
 
 
9
  export const LoadingScreen = ({
10
  isLoading,
11
  progress,
@@ -25,10 +35,26 @@ export const LoadingScreen = ({
25
  setIsModelDropdownOpen: (isOpen: boolean) => void;
26
  handleModelSelect: (modelId: string) => void;
27
  }) => {
28
- const model = MODEL_OPTIONS.find((opt) => opt.id === selectedModelId);
 
 
 
 
 
29
  const canvasRef = useRef<HTMLCanvasElement>(null);
 
 
 
30
 
31
- // Background Animation Effect
 
 
 
 
 
 
 
 
32
  useEffect(() => {
33
  const canvas = canvasRef.current;
34
  if (!canvas) return;
@@ -36,54 +62,64 @@ export const LoadingScreen = ({
36
  const ctx = canvas.getContext("2d");
37
  if (!ctx) return;
38
 
 
 
 
 
 
39
  let animationFrameId: number;
40
- let dots: {
41
- x: number;
42
- y: number;
43
- radius: number;
44
- speed: number;
45
- opacity: number;
46
- blur: number;
47
- }[] = [];
48
 
49
  const setup = () => {
50
- canvas.width = window.innerWidth;
51
- canvas.height = window.innerHeight;
 
 
 
 
 
 
52
  dots = [];
53
- const numDots = Math.floor((canvas.width * canvas.height) / 15000);
54
  for (let i = 0; i < numDots; ++i) {
55
  dots.push({
56
- x: Math.random() * canvas.width,
57
- y: Math.random() * canvas.height,
58
- radius: Math.random() * 1.5 + 0.5,
59
- speed: Math.random() * 0.5 + 0.1,
60
- opacity: Math.random() * 0.5 + 0.2,
61
- blur: Math.random() > 0.7 ? Math.random() * 2 + 1 : 0,
 
 
62
  });
63
  }
64
  };
65
 
66
  const draw = () => {
67
  if (!ctx) return;
68
- ctx.clearRect(0, 0, canvas.width, canvas.height);
 
 
69
 
70
  dots.forEach((dot) => {
71
- // Update dot position
72
  dot.y += dot.speed;
73
- if (dot.y > canvas.height) {
74
- dot.y = 0 - dot.radius;
75
- dot.x = Math.random() * canvas.width;
 
 
76
  }
77
 
78
- // Draw dot
 
 
 
79
  ctx.beginPath();
80
- ctx.arc(dot.x, dot.y, dot.radius, 0, Math.PI * 2);
81
- ctx.fillStyle = `rgba(255, 255, 255, ${dot.opacity})`;
82
- if (dot.blur > 0) {
83
- ctx.filter = `blur(${dot.blur}px)`;
84
- }
85
  ctx.fill();
86
- ctx.filter = "none"; // Reset filter
87
  });
88
 
89
  animationFrameId = requestAnimationFrame(draw);
@@ -97,7 +133,6 @@ export const LoadingScreen = ({
97
 
98
  setup();
99
  draw();
100
-
101
  window.addEventListener("resize", handleResize);
102
 
103
  return () => {
@@ -106,148 +141,408 @@ export const LoadingScreen = ({
106
  };
107
  }, []);
108
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  return (
110
- <div className="relative flex flex-col items-center justify-center h-screen bg-gray-900 text-white p-4 overflow-hidden">
111
- {/* Background Canvas for Animation */}
112
  <canvas
113
  ref={canvasRef}
114
  className="absolute top-0 left-0 w-full h-full z-0"
115
  />
116
 
117
  {/* Vignette Overlay */}
118
- <div className="absolute top-0 left-0 w-full h-full z-10 bg-[radial-gradient(ellipse_at_center,_rgba(17,24,39,0)_30%,_#111827_95%)]"></div>
 
 
 
119
 
120
  {/* Main Content */}
121
- <div className="relative z-20 max-w-2xl w-full flex flex-col items-center">
122
- <div className="flex items-center justify-center mb-6 gap-6 text-5xl md:text-6xl">
 
123
  <a
124
  href="https://www.liquid.ai/"
125
  target="_blank"
126
  rel="noopener noreferrer"
127
  title="Liquid AI"
 
128
  >
129
- <LiquidAILogo className="h-20 md:h-24 text-gray-300 hover:text-white transition-colors" />
130
  </a>
131
- <span className="text-gray-600">×</span>
132
  <a
133
  href="https://huggingface.co/docs/transformers.js"
134
  target="_blank"
135
  rel="noopener noreferrer"
136
  title="Transformers.js"
 
 
 
 
 
 
 
 
 
 
 
137
  >
138
- <HfLogo className="h-24 md:h-28 text-gray-300 hover:text-white transition-colors" />
139
  </a>
140
  </div>
141
 
142
- <div className="w-full text-center mb-6">
143
- <h1 className="text-5xl font-bold mb-2 text-gray-100 tracking-tight">
 
144
  LFM2 WebGPU
145
  </h1>
146
- <p className="text-md md:text-lg text-gray-400">
147
- In-browser tool calling, powered by Transformers.js
148
- </p>
149
- </div>
150
-
151
- <div className="w-full text-left text-gray-300 space-y-4 mb-6 text-base max-w-xl">
152
- <p>
153
- This demo showcases in-browser tool calling with LFM2, a new
154
- generation of hybrid models by{" "}
155
  <a
156
- href="https://www.liquid.ai/"
157
  target="_blank"
158
  rel="noopener noreferrer"
159
- className="text-indigo-400 hover:underline font-medium"
160
  >
161
- Liquid AI
162
- </a>{" "}
163
- designed for edge AI and on-device deployment.
164
- </p>
165
- <p>
166
- Everything runs entirely in your browser with{" "}
167
- <a
168
- href="https://huggingface.co/docs/transformers.js"
169
- target="_blank"
170
- rel="noopener noreferrer"
171
- className="text-indigo-400 hover:underline font-medium"
172
- >
173
- Transformers.js
174
- </a>{" "}
175
- and ONNX Runtime Web, meaning no data is sent to a server. It can
176
- even run offline!
177
  </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  </div>
179
 
180
- <p className="text-gray-400 mb-6">
181
- Select a model and click load to get started.
 
 
 
 
 
 
 
 
 
182
  </p>
183
 
184
- <div className="relative">
185
- <div className="flex rounded-lg shadow-lg bg-indigo-600">
186
- <button
187
- onClick={isLoading ? undefined : loadSelectedModel}
188
- disabled={isLoading}
189
- className={`flex items-center justify-center rounded-l-lg font-bold transition-all text-lg ${isLoading ? "bg-gray-700 text-gray-400 cursor-not-allowed" : "bg-indigo-600 hover:bg-indigo-700"}`}
 
 
 
 
 
190
  >
191
- <div className="px-6 py-3">
192
- {isLoading ? (
193
- <div className="flex items-center">
194
- <span className="inline-block w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin"></span>
195
- <span className="ml-3">Loading... ({progress}%)</span>
196
- </div>
197
- ) : (
198
- `Load ${model?.label}`
199
- )}
200
- </div>
201
- </button>
202
- <button
203
- onClick={(e) => {
204
- if (!isLoading) {
205
- e.stopPropagation();
206
- setIsModelDropdownOpen(!isModelDropdownOpen);
207
  }
208
- }}
209
- aria-label="Select model"
210
- className="px-3 py-3 border-l border-indigo-800 hover:bg-indigo-700 transition-colors rounded-r-lg disabled:cursor-not-allowed disabled:bg-gray-700"
211
- disabled={isLoading}
212
- >
213
- <ChevronDown size={24} />
214
- </button>
215
- </div>
 
 
 
 
 
 
216
 
217
- {isModelDropdownOpen && (
218
- <div className="absolute left-0 right-0 top-full mt-2 bg-gray-800 border border-gray-700 rounded-lg shadow-lg z-10 w-full overflow-hidden">
219
- {MODEL_OPTIONS.map((option) => (
220
- <button
221
- key={option.id}
222
- onClick={() => handleModelSelect(option.id)}
223
- className={`w-full px-4 py-2 text-left hover:bg-gray-700 transition-colors ${selectedModelId === option.id ? "bg-indigo-600 text-white" : "text-gray-200"}`}
224
- >
225
- <div className="font-medium">{option.label}</div>
226
- <div className="text-sm text-gray-400">{option.size}</div>
227
- </button>
228
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  </div>
230
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  </div>
232
 
 
233
  {error && (
234
- <div className="bg-red-900/50 border border-red-700/60 rounded-lg p-4 mt-6 max-w-md text-center">
235
- <p className="text-sm text-red-200">Error: {error}</p>
 
 
 
236
  <button
237
  onClick={loadSelectedModel}
238
- className="mt-3 text-sm bg-red-600 hover:bg-red-700 px-4 py-1.5 rounded-md font-semibold transition-colors"
239
  >
240
- Retry
241
  </button>
242
  </div>
243
  )}
244
  </div>
245
 
246
- {/* Click-away listener for dropdown */}
247
  {isModelDropdownOpen && (
248
  <div
249
- className="fixed inset-0 z-5"
250
- onClick={() => setIsModelDropdownOpen(false)}
 
 
 
 
 
 
 
 
 
251
  />
252
  )}
253
  </div>
 
1
  import { ChevronDown } from "lucide-react";
 
2
  import { MODEL_OPTIONS } from "../constants/models";
3
  import LiquidAILogo from "./icons/LiquidAILogo";
4
  import HfLogo from "./icons/HfLogo";
5
+ import MCPLogo from "./icons/MCPLogo";
6
+ import { useEffect, useMemo, useRef, useState } from "react";
7
+ import ReactDOM from "react-dom";
8
 
9
+ type 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
+ };
19
  export const LoadingScreen = ({
20
  isLoading,
21
  progress,
 
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
+ // Refs
44
  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 centering
48
 
49
+ // For keyboard navigation
50
+ 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;
 
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);
 
133
 
134
  setup();
135
  draw();
 
136
  window.addEventListener("resize", handleResize);
137
 
138
  return () => {
 
141
  };
142
  }, []);
143
 
144
+ // Close dropdown on Escape / click outside
145
+ 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 + resize
194
+ 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
+ // Center
214
+ let left = Math.round(rect.left + rect.width / 2 - dropdownWidth / 2);
215
+ // Clamp to viewport
216
+ left = Math.min(
217
+ Math.max(margin, left),
218
+ window.innerWidth - dropdownWidth - margin
219
+ );
220
+
221
+ // Flip up if not enough space below
222
+ const spaceBelow = window.innerHeight - rect.bottom;
223
+ const spaceAbove = rect.top;
224
+ const estimatedItemH = 56; // rough item height
225
+ 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
  <canvas
245
  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
  <a
260
  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
  <a
270
  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
+ <a
280
+ 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 WebGPU
294
  </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
  <a
298
+ 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 Protocol
317
+ </h3>
318
+ <p className="text-sm leading-relaxed">
319
+ Connect seamlessly to remote{" "}
320
+ <a
321
+ href="https://modelcontextprotocol.io/"
322
+ target="_blank"
323
+ rel="noopener noreferrer"
324
+ className="text-indigo-400 hover:underline"
325
+ >
326
+ MCP servers
327
+ </a>{" "}
328
+ using streaming or SSE protocols with support for no-auth, basic
329
+ 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 Technology
337
+ </h3>
338
+ <p className="text-sm leading-relaxed">
339
+ Powered by{" "}
340
+ <a
341
+ href="https://www.liquid.ai/"
342
+ target="_blank"
343
+ rel="noopener noreferrer"
344
+ className="text-indigo-400 hover:underline"
345
+ >
346
+ Liquid AI’s
347
+ </a>{" "}
348
+ LFM2 hybrid models, optimized for on-device deployment and edge AI
349
+ 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
+ <a
357
+ 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.js
363
+ </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 server
371
+ to get started.
372
+ </p>
373
+
374
+ <div className="relative">
375
+ <div
376
+ ref={wrapperRef} // anchor for dropdown centering
377
+ className="flex rounded-2xl shadow-2xl overflow-hidden"
378
  >
379
+ <button
380
+ onClick={isLoading ? undefined : loadSelectedModel}
381
+ disabled={isLoading}
382
+ className={`flex items-center justify-center font-bold transition-all text-lg flex-1 ${
383
+ isLoading
384
+ ? "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
+ isLoading
391
+ ? `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
+ <button
410
+ 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
+ isLoading
440
+ ? "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
+ <ChevronDown
446
+ 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
+ <div
459
+ 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
+ <button
471
+ 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
+ selected
482
+ ? "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 - 1
486
+ ? "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.body
511
+ )}
512
+ </div>
513
  </div>
514
 
515
+ {/* Error */}
516
  {error && (
517
+ <div
518
+ 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
  <button
523
  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 Again
527
  </button>
528
  </div>
529
  )}
530
  </div>
531
 
532
+ {/* Click-away fallback for touch devices */}
533
  {isModelDropdownOpen && (
534
  <div
535
+ 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>
src/components/icons/MCPLogo.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+
3
+ const MCPLogo = ({
4
+ className = "",
5
+ ...props
6
+ }: React.SVGProps<SVGSVGElement>) => (
7
+ <svg
8
+ viewBox="0 0 180 180"
9
+ fill="none"
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ className={className}
12
+ {...props}
13
+ >
14
+ <path
15
+ d="M23.5996 85.2532L86.2021 22.6507C94.8457 14.0071 108.86 14.0071 117.503 22.6507C126.147 31.2942 126.147 45.3083 117.503 53.9519L70.2254 101.23"
16
+ stroke="currentColor"
17
+ strokeWidth="11.0667"
18
+ strokeLinecap="round"
19
+ />
20
+ <path
21
+ d="M70.8789 100.578L117.504 53.952C126.148 45.3083 140.163 45.3083 148.806 53.952L149.132 54.278C157.776 62.9216 157.776 76.9357 149.132 85.5792L92.5139 142.198C89.6327 145.079 89.6327 149.75 92.5139 152.631L104.14 164.257"
22
+ stroke="currentColor"
23
+ strokeWidth="11.0667"
24
+ strokeLinecap="round"
25
+ />
26
+ <path
27
+ d="M101.853 38.3013L55.553 84.6011C46.9094 93.2447 46.9094 107.258 55.553 115.902C64.1966 124.546 78.2106 124.546 86.8543 115.902L133.154 69.6025"
28
+ stroke="currentColor"
29
+ strokeWidth="11.0667"
30
+ strokeLinecap="round"
31
+ />
32
+ </svg>
33
+ );
34
+
35
+ export default MCPLogo;
src/services/mcpClient.ts CHANGED
@@ -3,7 +3,7 @@ import { WebSocketClientTransport } from "@modelcontextprotocol/sdk/client/webso
3
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
5
  import type { Tool } from "@modelcontextprotocol/sdk/types.js";
6
-
7
  import type {
8
  MCPServerConfig,
9
  MCPServerConnection,
@@ -20,6 +20,13 @@ export class MCPClientService {
20
  constructor() {
21
  // Load saved server configurations from localStorage
22
  this.loadServerConfigs();
 
 
 
 
 
 
 
23
  }
24
 
25
  // Add state change listener
@@ -72,6 +79,7 @@ export class MCPClientService {
72
  this.connections.set(config.id, connection);
73
  });
74
  }
 
75
  } catch (error) {
76
  // Silently handle missing or corrupted config
77
  }
@@ -86,7 +94,11 @@ export class MCPClientService {
86
  localStorage.setItem(STORAGE_KEYS.MCP_SERVERS, JSON.stringify(configs));
87
  } catch (error) {
88
  // Handle storage errors gracefully
89
- throw new Error(`Failed to save server configuration: ${error instanceof Error ? error.message : 'Unknown error'}`);
 
 
 
 
90
  }
91
  }
92
 
@@ -255,6 +267,7 @@ export class MCPClientService {
255
  if (client) {
256
  try {
257
  await client.close();
 
258
  } catch (error) {
259
  // Handle disconnect error silently
260
  }
@@ -305,7 +318,11 @@ export class MCPClientService {
305
  isError: Boolean(result.isError),
306
  };
307
  } catch (error) {
308
- throw new Error(`Tool execution failed (${toolName}): ${error instanceof Error ? error.message : 'Unknown error'}`);
 
 
 
 
309
  }
310
  }
311
 
@@ -351,7 +368,11 @@ export class MCPClientService {
351
  await client.close();
352
  return true;
353
  } catch (error) {
354
- throw new Error(`Connection test failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
 
 
 
 
355
  }
356
  }
357
 
 
3
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
5
  import type { Tool } from "@modelcontextprotocol/sdk/types.js";
6
+ import { MCP_SERVERS } from "../tools/mcp_servers";
7
  import type {
8
  MCPServerConfig,
9
  MCPServerConnection,
 
20
  constructor() {
21
  // Load saved server configurations from localStorage
22
  this.loadServerConfigs();
23
+
24
+ // If no servers are present, load initial list from MCP_SERVERS (imported)
25
+ if (this.connections.size === 0) {
26
+ MCP_SERVERS.forEach((config) => {
27
+ this.addServer(config);
28
+ });
29
+ }
30
  }
31
 
32
  // Add state change listener
 
79
  this.connections.set(config.id, connection);
80
  });
81
  }
82
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
83
  } catch (error) {
84
  // Silently handle missing or corrupted config
85
  }
 
94
  localStorage.setItem(STORAGE_KEYS.MCP_SERVERS, JSON.stringify(configs));
95
  } catch (error) {
96
  // Handle storage errors gracefully
97
+ throw new Error(
98
+ `Failed to save server configuration: ${
99
+ error instanceof Error ? error.message : "Unknown error"
100
+ }`
101
+ );
102
  }
103
  }
104
 
 
267
  if (client) {
268
  try {
269
  await client.close();
270
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
271
  } catch (error) {
272
  // Handle disconnect error silently
273
  }
 
318
  isError: Boolean(result.isError),
319
  };
320
  } catch (error) {
321
+ throw new Error(
322
+ `Tool execution failed (${toolName}): ${
323
+ error instanceof Error ? error.message : "Unknown error"
324
+ }`
325
+ );
326
  }
327
  }
328
 
 
368
  await client.close();
369
  return true;
370
  } catch (error) {
371
+ throw new Error(
372
+ `Connection test failed: ${
373
+ error instanceof Error ? error.message : "Unknown error"
374
+ }`
375
+ );
376
  }
377
  }
378
 
src/tools/mcp_servers.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { MCPServerConfig } from "../types/mcp";
2
+
3
+ export const MCP_SERVERS: MCPServerConfig[] = [
4
+ {
5
+ id: "hf-transformers-demo-gitmcp",
6
+ name: "HuggingFace Transformers.js Documentation",
7
+ url: "https://gitmcp.io/huggingface/transformers.js",
8
+ enabled: true,
9
+ transport: "streamable-http",
10
+ },
11
+ {
12
+ id: "mcp-servers-docs",
13
+ name: "MCP Documentation",
14
+ url: "https://gitmcp.io/modelcontextprotocol/modelcontextprotocol",
15
+ enabled: true,
16
+ transport: "streamable-http",
17
+ },
18
+ ];