Add demo source code

#1
by Xenova HF Staff - opened
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
README.md CHANGED
@@ -1,10 +1,14 @@
1
  ---
2
- title: WebGPU
3
- emoji: 😻
4
- colorFrom: green
5
  colorTo: red
6
- sdk: docker
7
  pinned: false
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: LFM2 WebGPU – In-browser tool calling
3
+ emoji: 🛠️
4
+ colorFrom: yellow
5
  colorTo: red
6
+ sdk: static
7
  pinned: false
8
+ license: apache-2.0
9
+ short_description: In-browser tool calling, powered by Transformers.js
10
+ app_build_command: npm run build
11
+ app_file: dist/index.html
12
  ---
13
 
14
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
eslint.config.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import reactHooks from "eslint-plugin-react-hooks";
4
+ import reactRefresh from "eslint-plugin-react-refresh";
5
+ import tseslint from "typescript-eslint";
6
+ import { globalIgnores } from "eslint/config";
7
+
8
+ export default tseslint.config([
9
+ globalIgnores(["dist"]),
10
+ {
11
+ files: ["**/*.{ts,tsx}"],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs["recommended-latest"],
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ ecmaVersion: 2020,
20
+ globals: globals.browser,
21
+ },
22
+ },
23
+ ]);
index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/liquidai-logo.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>LFM2 WebGPU - In-Browser Tool Calling</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
package.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "lfm-tool-calling",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@huggingface/transformers": "^3.7.1",
14
+ "@monaco-editor/react": "^4.7.0",
15
+ "@tailwindcss/vite": "^4.1.11",
16
+ "idb": "^8.0.3",
17
+ "lucide-react": "^0.535.0",
18
+ "react": "^19.1.0",
19
+ "react-dom": "^19.1.0",
20
+ "tailwindcss": "^4.1.11"
21
+ },
22
+ "devDependencies": {
23
+ "@eslint/js": "^9.30.1",
24
+ "@types/react": "^19.1.8",
25
+ "@types/react-dom": "^19.1.6",
26
+ "@vitejs/plugin-react": "^4.6.0",
27
+ "eslint": "^9.30.1",
28
+ "eslint-plugin-react-hooks": "^5.2.0",
29
+ "eslint-plugin-react-refresh": "^0.4.20",
30
+ "globals": "^16.3.0",
31
+ "typescript": "~5.8.3",
32
+ "typescript-eslint": "^8.35.1",
33
+ "vite": "^7.0.4"
34
+ }
35
+ }
public/liquidai-logo.svg ADDED
src/App.tsx ADDED
@@ -0,0 +1,826 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, {
2
+ useState,
3
+ useEffect,
4
+ useCallback,
5
+ useRef,
6
+ useMemo,
7
+ } from "react";
8
+ import { openDB, type IDBPDatabase } from "idb";
9
+ import { Play, Plus, Zap, RotateCcw, Settings, X } from "lucide-react";
10
+ import { useLLM } from "./hooks/useLLM";
11
+
12
+ import type { Tool } from "./components/ToolItem";
13
+
14
+ import {
15
+ parsePythonicCalls,
16
+ extractPythonicCalls,
17
+ extractFunctionAndRenderer,
18
+ generateSchemaFromCode,
19
+ extractToolCallContent,
20
+ mapArgsToNamedParams,
21
+ getErrorMessage,
22
+ isMobileOrTablet,
23
+ } from "./utils";
24
+
25
+ import { DEFAULT_SYSTEM_PROMPT } from "./constants/systemPrompt";
26
+ import { DB_NAME, STORE_NAME, SETTINGS_STORE_NAME } from "./constants/db";
27
+
28
+ import { DEFAULT_TOOLS, TEMPLATE } from "./tools";
29
+ import ToolResultRenderer from "./components/ToolResultRenderer";
30
+ import ToolCallIndicator from "./components/ToolCallIndicator";
31
+ import ToolItem from "./components/ToolItem";
32
+ import ResultBlock from "./components/ResultBlock";
33
+ import ExamplePrompts from "./components/ExamplePrompts";
34
+
35
+ import { LoadingScreen } from "./components/LoadingScreen";
36
+
37
+ interface RenderInfo {
38
+ call: string;
39
+ result?: any;
40
+ renderer?: string;
41
+ input?: Record<string, any>;
42
+ error?: string;
43
+ }
44
+
45
+ interface BaseMessage {
46
+ role: "system" | "user" | "assistant";
47
+ content: string;
48
+ }
49
+ interface ToolMessage {
50
+ role: "tool";
51
+ content: string;
52
+ renderInfo: RenderInfo[]; // Rich data for the UI
53
+ }
54
+ type Message = BaseMessage | ToolMessage;
55
+
56
+ async function getDB(): Promise<IDBPDatabase> {
57
+ return openDB(DB_NAME, 1, {
58
+ upgrade(db) {
59
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
60
+ db.createObjectStore(STORE_NAME, {
61
+ keyPath: "id",
62
+ autoIncrement: true,
63
+ });
64
+ }
65
+ if (!db.objectStoreNames.contains(SETTINGS_STORE_NAME)) {
66
+ db.createObjectStore(SETTINGS_STORE_NAME, { keyPath: "key" });
67
+ }
68
+ },
69
+ });
70
+ }
71
+
72
+ const App: React.FC = () => {
73
+ const [systemPrompt, setSystemPrompt] = useState<string>(
74
+ DEFAULT_SYSTEM_PROMPT,
75
+ );
76
+ const [isSystemPromptModalOpen, setIsSystemPromptModalOpen] =
77
+ useState<boolean>(false);
78
+ const [tempSystemPrompt, setTempSystemPrompt] = useState<string>("");
79
+ const [messages, setMessages] = useState<Message[]>([]);
80
+ const [tools, setTools] = useState<Tool[]>([]);
81
+ const [input, setInput] = useState<string>("");
82
+ const [isGenerating, setIsGenerating] = useState<boolean>(false);
83
+ const isMobile = useMemo(isMobileOrTablet, []);
84
+ const [selectedModelId, setSelectedModelId] = useState<string>(
85
+ isMobile ? "350M" : "1.2B",
86
+ );
87
+ const [isModelDropdownOpen, setIsModelDropdownOpen] =
88
+ useState<boolean>(false);
89
+ const chatContainerRef = useRef<HTMLDivElement>(null);
90
+ const debounceTimers = useRef<Record<number, NodeJS.Timeout>>({});
91
+ const toolsContainerRef = useRef<HTMLDivElement>(null);
92
+ const inputRef = useRef<HTMLInputElement>(null);
93
+ const {
94
+ isLoading,
95
+ isReady,
96
+ error,
97
+ progress,
98
+ loadModel,
99
+ generateResponse,
100
+ clearPastKeyValues,
101
+ } = useLLM(selectedModelId);
102
+
103
+ const loadTools = useCallback(async (): Promise<void> => {
104
+ const db = await getDB();
105
+ const allTools: Tool[] = await db.getAll(STORE_NAME);
106
+ if (allTools.length === 0) {
107
+ const defaultTools: Tool[] = Object.entries(DEFAULT_TOOLS).map(
108
+ ([name, code], id) => ({
109
+ id,
110
+ name,
111
+ code,
112
+ enabled: true,
113
+ isCollapsed: false,
114
+ }),
115
+ );
116
+ const tx = db.transaction(STORE_NAME, "readwrite");
117
+ await Promise.all(defaultTools.map((tool) => tx.store.put(tool)));
118
+ await tx.done;
119
+ setTools(defaultTools);
120
+ } else {
121
+ setTools(allTools.map((t) => ({ ...t, isCollapsed: false })));
122
+ }
123
+ }, []);
124
+
125
+ useEffect(() => {
126
+ loadTools();
127
+ }, [loadTools]);
128
+
129
+ useEffect(() => {
130
+ if (chatContainerRef.current) {
131
+ chatContainerRef.current.scrollTop =
132
+ chatContainerRef.current.scrollHeight;
133
+ }
134
+ }, [messages]);
135
+
136
+ const updateToolInDB = async (tool: Tool): Promise<void> => {
137
+ const db = await getDB();
138
+ await db.put(STORE_NAME, tool);
139
+ };
140
+
141
+ const saveToolDebounced = (tool: Tool): void => {
142
+ if (tool.id !== undefined && debounceTimers.current[tool.id]) {
143
+ clearTimeout(debounceTimers.current[tool.id]);
144
+ }
145
+ if (tool.id !== undefined) {
146
+ debounceTimers.current[tool.id] = setTimeout(() => {
147
+ updateToolInDB(tool);
148
+ }, 300);
149
+ }
150
+ };
151
+
152
+ const clearChat = useCallback(() => {
153
+ setMessages([]);
154
+ clearPastKeyValues();
155
+ }, [clearPastKeyValues]);
156
+
157
+ const addTool = async (): Promise<void> => {
158
+ const newTool: Omit<Tool, "id"> = {
159
+ name: "new_tool",
160
+ code: TEMPLATE,
161
+ enabled: true,
162
+ isCollapsed: false,
163
+ };
164
+ const db = await getDB();
165
+ const id = await db.add(STORE_NAME, newTool);
166
+ setTools((prev) => {
167
+ const updated = [...prev, { ...newTool, id: id as number }];
168
+ setTimeout(() => {
169
+ if (toolsContainerRef.current) {
170
+ toolsContainerRef.current.scrollTop =
171
+ toolsContainerRef.current.scrollHeight;
172
+ }
173
+ }, 0);
174
+ return updated;
175
+ });
176
+ clearChat();
177
+ };
178
+
179
+ const deleteTool = async (id: number): Promise<void> => {
180
+ if (debounceTimers.current[id]) {
181
+ clearTimeout(debounceTimers.current[id]);
182
+ }
183
+ const db = await getDB();
184
+ await db.delete(STORE_NAME, id);
185
+ setTools(tools.filter((tool) => tool.id !== id));
186
+ clearChat();
187
+ };
188
+
189
+ const toggleToolEnabled = (id: number): void => {
190
+ let changedTool: Tool | undefined;
191
+ const newTools = tools.map((tool) => {
192
+ if (tool.id === id) {
193
+ changedTool = { ...tool, enabled: !tool.enabled };
194
+ return changedTool;
195
+ }
196
+ return tool;
197
+ });
198
+ setTools(newTools);
199
+ if (changedTool) saveToolDebounced(changedTool);
200
+ };
201
+
202
+ const toggleToolCollapsed = (id: number): void => {
203
+ setTools(
204
+ tools.map((tool) =>
205
+ tool.id === id ? { ...tool, isCollapsed: !tool.isCollapsed } : tool,
206
+ ),
207
+ );
208
+ };
209
+
210
+ const expandTool = (id: number): void => {
211
+ setTools(
212
+ tools.map((tool) =>
213
+ tool.id === id ? { ...tool, isCollapsed: false } : tool,
214
+ ),
215
+ );
216
+ };
217
+
218
+ const handleToolCodeChange = (id: number, newCode: string): void => {
219
+ let changedTool: Tool | undefined;
220
+ const newTools = tools.map((tool) => {
221
+ if (tool.id === id) {
222
+ const { functionCode } = extractFunctionAndRenderer(newCode);
223
+ const schema = generateSchemaFromCode(functionCode);
224
+ changedTool = { ...tool, code: newCode, name: schema.name };
225
+ return changedTool;
226
+ }
227
+ return tool;
228
+ });
229
+ setTools(newTools);
230
+ if (changedTool) saveToolDebounced(changedTool);
231
+ };
232
+
233
+ const executeToolCall = async (callString: string): Promise<string> => {
234
+ const parsedCall = parsePythonicCalls(callString);
235
+ if (!parsedCall) throw new Error(`Invalid tool call format: ${callString}`);
236
+
237
+ const { name, positionalArgs, keywordArgs } = parsedCall;
238
+ const toolToUse = tools.find((t) => t.name === name && t.enabled);
239
+ if (!toolToUse) throw new Error(`Tool '${name}' not found or is disabled.`);
240
+
241
+ const { functionCode } = extractFunctionAndRenderer(toolToUse.code);
242
+ const schema = generateSchemaFromCode(functionCode);
243
+ const paramNames = Object.keys(schema.parameters.properties);
244
+
245
+ const finalArgs: any[] = [];
246
+ const requiredParams = schema.parameters.required || [];
247
+
248
+ for (let i = 0; i < paramNames.length; ++i) {
249
+ const paramName = paramNames[i];
250
+ if (i < positionalArgs.length) {
251
+ finalArgs.push(positionalArgs[i]);
252
+ } else if (keywordArgs.hasOwnProperty(paramName)) {
253
+ finalArgs.push(keywordArgs[paramName]);
254
+ } else if (
255
+ schema.parameters.properties[paramName].hasOwnProperty("default")
256
+ ) {
257
+ finalArgs.push(schema.parameters.properties[paramName].default);
258
+ } else if (!requiredParams.includes(paramName)) {
259
+ finalArgs.push(undefined);
260
+ } else {
261
+ throw new Error(`Missing required argument: ${paramName}`);
262
+ }
263
+ }
264
+
265
+ const bodyMatch = functionCode.match(/function[^{]+\{([\s\S]*)\}/);
266
+ if (!bodyMatch) {
267
+ throw new Error(
268
+ "Could not parse function body. Ensure it's a standard `function` declaration.",
269
+ );
270
+ }
271
+ const body = bodyMatch[1];
272
+ const AsyncFunction = Object.getPrototypeOf(
273
+ async function () {},
274
+ ).constructor;
275
+ const func = new AsyncFunction(...paramNames, body);
276
+ const result = await func(...finalArgs);
277
+ return JSON.stringify(result);
278
+ };
279
+
280
+ const executeToolCalls = async (
281
+ toolCallContent: string,
282
+ ): Promise<RenderInfo[]> => {
283
+ const toolCalls = extractPythonicCalls(toolCallContent);
284
+ if (toolCalls.length === 0)
285
+ return [{ call: "", error: "No valid tool calls found." }];
286
+
287
+ const results: RenderInfo[] = [];
288
+ for (const call of toolCalls) {
289
+ try {
290
+ const result = await executeToolCall(call);
291
+ const parsedCall = parsePythonicCalls(call);
292
+ const toolUsed = parsedCall
293
+ ? tools.find((t) => t.name === parsedCall.name && t.enabled)
294
+ : null;
295
+ const { rendererCode } = toolUsed
296
+ ? extractFunctionAndRenderer(toolUsed.code)
297
+ : { rendererCode: undefined };
298
+
299
+ let parsedResult;
300
+ try {
301
+ parsedResult = JSON.parse(result);
302
+ } catch {
303
+ parsedResult = result;
304
+ }
305
+
306
+ let namedParams: Record<string, any> = Object.create(null);
307
+ if (parsedCall && toolUsed) {
308
+ const schema = generateSchemaFromCode(
309
+ extractFunctionAndRenderer(toolUsed.code).functionCode,
310
+ );
311
+ const paramNames = Object.keys(schema.parameters.properties);
312
+ namedParams = mapArgsToNamedParams(
313
+ paramNames,
314
+ parsedCall.positionalArgs,
315
+ parsedCall.keywordArgs,
316
+ );
317
+ }
318
+
319
+ results.push({
320
+ call,
321
+ result: parsedResult,
322
+ renderer: rendererCode,
323
+ input: namedParams,
324
+ });
325
+ } catch (error) {
326
+ const errorMessage = getErrorMessage(error);
327
+ results.push({ call, error: errorMessage });
328
+ }
329
+ }
330
+ return results;
331
+ };
332
+
333
+ const handleSendMessage = async (): Promise<void> => {
334
+ if (!input.trim() || !isReady) return;
335
+
336
+ const userMessage: Message = { role: "user", content: input };
337
+ let currentMessages: Message[] = [...messages, userMessage];
338
+ setMessages(currentMessages);
339
+ setInput("");
340
+ setIsGenerating(true);
341
+
342
+ try {
343
+ const toolSchemas = tools
344
+ .filter((tool) => tool.enabled)
345
+ .map((tool) => generateSchemaFromCode(tool.code));
346
+
347
+ while (true) {
348
+ const messagesForGeneration = [
349
+ { role: "system" as const, content: systemPrompt },
350
+ ...currentMessages,
351
+ ];
352
+
353
+ setMessages([...currentMessages, { role: "assistant", content: "" }]);
354
+
355
+ let accumulatedContent = "";
356
+ const response = await generateResponse(
357
+ messagesForGeneration,
358
+ toolSchemas,
359
+ (token: string) => {
360
+ accumulatedContent += token;
361
+ setMessages((current) => {
362
+ const updated = [...current];
363
+ updated[updated.length - 1] = {
364
+ role: "assistant",
365
+ content: accumulatedContent,
366
+ };
367
+ return updated;
368
+ });
369
+ },
370
+ );
371
+
372
+ currentMessages.push({ role: "assistant", content: response });
373
+ const toolCallContent = extractToolCallContent(response);
374
+
375
+ if (toolCallContent) {
376
+ const toolResults = await executeToolCalls(toolCallContent);
377
+
378
+ const toolMessage: ToolMessage = {
379
+ role: "tool",
380
+ content: JSON.stringify(toolResults.map((r) => r.result ?? null)),
381
+ renderInfo: toolResults,
382
+ };
383
+ currentMessages.push(toolMessage);
384
+ setMessages([...currentMessages]);
385
+ continue;
386
+ } else {
387
+ setMessages(currentMessages);
388
+ break;
389
+ }
390
+ }
391
+ } catch (error) {
392
+ const errorMessage = getErrorMessage(error);
393
+ setMessages([
394
+ ...currentMessages,
395
+ {
396
+ role: "assistant",
397
+ content: `Error generating response: ${errorMessage}`,
398
+ },
399
+ ]);
400
+ } finally {
401
+ setIsGenerating(false);
402
+ setTimeout(() => inputRef.current?.focus(), 0);
403
+ }
404
+ };
405
+
406
+ const loadSystemPrompt = useCallback(async (): Promise<void> => {
407
+ try {
408
+ const db = await getDB();
409
+ const stored = await db.get(SETTINGS_STORE_NAME, "systemPrompt");
410
+ if (stored && stored.value) setSystemPrompt(stored.value);
411
+ } catch (error) {
412
+ console.error("Failed to load system prompt:", error);
413
+ }
414
+ }, []);
415
+
416
+ const saveSystemPrompt = useCallback(
417
+ async (prompt: string): Promise<void> => {
418
+ try {
419
+ const db = await getDB();
420
+ await db.put(SETTINGS_STORE_NAME, {
421
+ key: "systemPrompt",
422
+ value: prompt,
423
+ });
424
+ } catch (error) {
425
+ console.error("Failed to save system prompt:", error);
426
+ }
427
+ },
428
+ [],
429
+ );
430
+
431
+ const loadSelectedModel = useCallback(async (): Promise<void> => {
432
+ try {
433
+ await loadModel();
434
+ } catch (error) {
435
+ console.error("Failed to load model:", error);
436
+ }
437
+ }, [selectedModelId, loadModel]);
438
+
439
+ const loadSelectedModelId = useCallback(async (): Promise<void> => {
440
+ try {
441
+ const db = await getDB();
442
+ const stored = await db.get(SETTINGS_STORE_NAME, "selectedModelId");
443
+ if (stored && stored.value) {
444
+ setSelectedModelId(stored.value);
445
+ }
446
+ } catch (error) {
447
+ console.error("Failed to load selected model ID:", error);
448
+ }
449
+ }, []);
450
+
451
+ useEffect(() => {
452
+ loadSystemPrompt();
453
+ }, [loadSystemPrompt]);
454
+
455
+ const handleOpenSystemPromptModal = (): void => {
456
+ setTempSystemPrompt(systemPrompt);
457
+ setIsSystemPromptModalOpen(true);
458
+ };
459
+
460
+ const handleSaveSystemPrompt = (): void => {
461
+ setSystemPrompt(tempSystemPrompt);
462
+ saveSystemPrompt(tempSystemPrompt);
463
+ setIsSystemPromptModalOpen(false);
464
+ };
465
+
466
+ const handleCancelSystemPrompt = (): void => {
467
+ setTempSystemPrompt("");
468
+ setIsSystemPromptModalOpen(false);
469
+ };
470
+
471
+ const handleResetSystemPrompt = (): void => {
472
+ setTempSystemPrompt(DEFAULT_SYSTEM_PROMPT);
473
+ };
474
+
475
+ const saveSelectedModel = useCallback(
476
+ async (modelId: string): Promise<void> => {
477
+ try {
478
+ const db = await getDB();
479
+ await db.put(SETTINGS_STORE_NAME, {
480
+ key: "selectedModelId",
481
+ value: modelId,
482
+ });
483
+ } catch (error) {
484
+ console.error("Failed to save selected model ID:", error);
485
+ }
486
+ },
487
+ [],
488
+ );
489
+
490
+ useEffect(() => {
491
+ loadSystemPrompt();
492
+ loadSelectedModelId();
493
+ }, [loadSystemPrompt, loadSelectedModelId]);
494
+
495
+ const handleModelSelect = async (modelId: string) => {
496
+ setSelectedModelId(modelId);
497
+ setIsModelDropdownOpen(false);
498
+ await saveSelectedModel(modelId);
499
+ };
500
+
501
+ const handleExampleClick = async (messageText: string): Promise<void> => {
502
+ if (!isReady || isGenerating) return;
503
+ setInput(messageText);
504
+
505
+ const userMessage: Message = { role: "user", content: messageText };
506
+ const currentMessages: Message[] = [...messages, userMessage];
507
+ setMessages(currentMessages);
508
+ setInput("");
509
+ setIsGenerating(true);
510
+
511
+ try {
512
+ const toolSchemas = tools
513
+ .filter((tool) => tool.enabled)
514
+ .map((tool) => generateSchemaFromCode(tool.code));
515
+
516
+ while (true) {
517
+ const messagesForGeneration = [
518
+ { role: "system" as const, content: systemPrompt },
519
+ ...currentMessages,
520
+ ];
521
+
522
+ setMessages([...currentMessages, { role: "assistant", content: "" }]);
523
+
524
+ let accumulatedContent = "";
525
+ const response = await generateResponse(
526
+ messagesForGeneration,
527
+ toolSchemas,
528
+ (token: string) => {
529
+ accumulatedContent += token;
530
+ setMessages((current) => {
531
+ const updated = [...current];
532
+ updated[updated.length - 1] = {
533
+ role: "assistant",
534
+ content: accumulatedContent,
535
+ };
536
+ return updated;
537
+ });
538
+ },
539
+ );
540
+
541
+ currentMessages.push({ role: "assistant", content: response });
542
+ const toolCallContent = extractToolCallContent(response);
543
+
544
+ if (toolCallContent) {
545
+ const toolResults = await executeToolCalls(toolCallContent);
546
+
547
+ const toolMessage: ToolMessage = {
548
+ role: "tool",
549
+ content: JSON.stringify(toolResults.map((r) => r.result ?? null)),
550
+ renderInfo: toolResults,
551
+ };
552
+ currentMessages.push(toolMessage);
553
+ setMessages([...currentMessages]);
554
+ continue;
555
+ } else {
556
+ setMessages(currentMessages);
557
+ break;
558
+ }
559
+ }
560
+ } catch (error) {
561
+ const errorMessage = getErrorMessage(error);
562
+ setMessages([
563
+ ...currentMessages,
564
+ {
565
+ role: "assistant",
566
+ content: `Error generating response: ${errorMessage}`,
567
+ },
568
+ ]);
569
+ } finally {
570
+ setIsGenerating(false);
571
+ setTimeout(() => inputRef.current?.focus(), 0);
572
+ }
573
+ };
574
+
575
+ return (
576
+ <div className="font-sans bg-gray-900">
577
+ {!isReady ? (
578
+ <LoadingScreen
579
+ isLoading={isLoading}
580
+ progress={progress}
581
+ error={error}
582
+ loadSelectedModel={loadSelectedModel}
583
+ selectedModelId={selectedModelId}
584
+ isModelDropdownOpen={isModelDropdownOpen}
585
+ setIsModelDropdownOpen={setIsModelDropdownOpen}
586
+ handleModelSelect={handleModelSelect}
587
+ />
588
+ ) : (
589
+ <div className="flex h-screen text-white">
590
+ <div className="w-1/2 flex flex-col p-4">
591
+ <div className="flex items-center justify-between mb-4">
592
+ <div className="flex items-center gap-3">
593
+ <h1 className="text-3xl font-bold text-gray-200">
594
+ LFM2 WebGPU
595
+ </h1>
596
+ </div>
597
+ <div className="flex items-center gap-3">
598
+ <div className="flex items-center text-green-400">
599
+ <Zap size={16} className="mr-2" />
600
+ Ready
601
+ </div>
602
+ <button
603
+ disabled={isGenerating}
604
+ onClick={clearChat}
605
+ className={`h-10 flex items-center px-3 py-2 rounded-lg font-bold transition-colors text-sm ${
606
+ isGenerating
607
+ ? "bg-gray-600 cursor-not-allowed opacity-50"
608
+ : "bg-gray-600 hover:bg-gray-700"
609
+ }`}
610
+ title="Clear chat"
611
+ >
612
+ <RotateCcw size={14} className="mr-2" /> Clear
613
+ </button>
614
+ <button
615
+ onClick={handleOpenSystemPromptModal}
616
+ className="h-10 flex items-center px-3 py-2 rounded-lg font-bold transition-colors bg-gray-600 hover:bg-gray-700 text-sm"
617
+ title="Edit system prompt"
618
+ >
619
+ <Settings size={16} />
620
+ </button>
621
+ </div>
622
+ </div>
623
+
624
+ <div
625
+ ref={chatContainerRef}
626
+ className="flex-grow bg-gray-800 rounded-lg p-4 overflow-y-auto mb-4 space-y-4"
627
+ >
628
+ {messages.length === 0 && isReady ? (
629
+ <ExamplePrompts onExampleClick={handleExampleClick} />
630
+ ) : (
631
+ messages.map((msg, index) => {
632
+ const key = `${msg.role}-${index}`;
633
+
634
+ if (msg.role === "user") {
635
+ return (
636
+ <div key={key} className="flex justify-end">
637
+ <div className="p-3 rounded-lg max-w-md bg-indigo-600">
638
+ <p className="text-sm whitespace-pre-wrap">
639
+ {msg.content}
640
+ </p>
641
+ </div>
642
+ </div>
643
+ );
644
+ } else if (msg.role === "assistant") {
645
+ const isToolCall = msg.content.includes(
646
+ "<|tool_call_start|>",
647
+ );
648
+
649
+ if (isToolCall) {
650
+ const nextMessage = messages[index + 1];
651
+ const isCompleted = nextMessage?.role === "tool";
652
+ const hasError =
653
+ isCompleted &&
654
+ (nextMessage as ToolMessage).renderInfo.some(
655
+ (info) => !!info.error,
656
+ );
657
+
658
+ return (
659
+ <div key={key} className="flex justify-start">
660
+ <div className="p-3 rounded-lg bg-gray-700">
661
+ <ToolCallIndicator
662
+ content={msg.content}
663
+ isRunning={!isCompleted}
664
+ hasError={hasError}
665
+ />
666
+ </div>
667
+ </div>
668
+ );
669
+ }
670
+
671
+ return (
672
+ <div key={key} className="flex justify-start">
673
+ <div className="p-3 rounded-lg max-w-md bg-gray-700">
674
+ <p className="text-sm whitespace-pre-wrap">
675
+ {msg.content}
676
+ </p>
677
+ </div>
678
+ </div>
679
+ );
680
+ } else if (msg.role === "tool") {
681
+ const visibleToolResults = msg.renderInfo.filter(
682
+ (info) =>
683
+ info.error || (info.result != null && info.renderer),
684
+ );
685
+
686
+ if (visibleToolResults.length === 0) return null;
687
+
688
+ return (
689
+ <div key={key} className="flex justify-start">
690
+ <div className="p-3 rounded-lg bg-gray-700 max-w-lg">
691
+ <div className="space-y-3">
692
+ {visibleToolResults.map((info, idx) => (
693
+ <div className="flex flex-col gap-2" key={idx}>
694
+ <div className="text-xs text-gray-400 font-mono">
695
+ {info.call}
696
+ </div>
697
+ {info.error ? (
698
+ <ResultBlock error={info.error} />
699
+ ) : (
700
+ <ToolResultRenderer
701
+ result={info.result}
702
+ rendererCode={info.renderer}
703
+ input={info.input}
704
+ />
705
+ )}
706
+ </div>
707
+ ))}
708
+ </div>
709
+ </div>
710
+ </div>
711
+ );
712
+ }
713
+ return null;
714
+ })
715
+ )}
716
+ </div>
717
+
718
+ <div className="flex">
719
+ <input
720
+ ref={inputRef}
721
+ type="text"
722
+ value={input}
723
+ onChange={(e) => setInput(e.target.value)}
724
+ onKeyDown={(e) =>
725
+ e.key === "Enter" &&
726
+ !isGenerating &&
727
+ isReady &&
728
+ handleSendMessage()
729
+ }
730
+ disabled={isGenerating || !isReady}
731
+ className="flex-grow bg-gray-700 rounded-l-lg p-3 focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50"
732
+ placeholder={
733
+ isReady
734
+ ? "Type your message here..."
735
+ : "Load model first to enable chat"
736
+ }
737
+ />
738
+ <button
739
+ onClick={handleSendMessage}
740
+ disabled={isGenerating || !isReady}
741
+ className="bg-indigo-600 hover:bg-indigo-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white font-bold p-3 rounded-r-lg transition-colors"
742
+ >
743
+ <Play size={20} />
744
+ </button>
745
+ </div>
746
+ </div>
747
+
748
+ <div className="w-1/2 flex flex-col p-4 border-l border-gray-700">
749
+ <div className="flex justify-between items-center mb-4">
750
+ <h2 className="text-2xl font-bold text-teal-400">Tools</h2>
751
+ <button
752
+ onClick={addTool}
753
+ className="flex items-center bg-teal-600 hover:bg-teal-700 text-white font-bold py-2 px-4 rounded-lg transition-colors"
754
+ >
755
+ <Plus size={16} className="mr-2" /> Add Tool
756
+ </button>
757
+ </div>
758
+ <div
759
+ ref={toolsContainerRef}
760
+ className="flex-grow bg-gray-800 rounded-lg p-4 overflow-y-auto space-y-3"
761
+ >
762
+ {tools.map((tool) => (
763
+ <ToolItem
764
+ key={tool.id}
765
+ tool={tool}
766
+ onToggleEnabled={() => toggleToolEnabled(tool.id)}
767
+ onToggleCollapsed={() => toggleToolCollapsed(tool.id)}
768
+ onExpand={() => expandTool(tool.id)}
769
+ onDelete={() => deleteTool(tool.id)}
770
+ onCodeChange={(newCode) =>
771
+ handleToolCodeChange(tool.id, newCode)
772
+ }
773
+ />
774
+ ))}
775
+ </div>
776
+ </div>
777
+ </div>
778
+ )}
779
+
780
+ {isSystemPromptModalOpen && (
781
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
782
+ <div className="bg-gray-800 rounded-lg p-6 w-3/4 max-w-4xl max-h-3/4 flex flex-col text-gray-100">
783
+ <div className="flex justify-between items-center mb-4">
784
+ <h2 className="text-xl font-bold text-indigo-400">
785
+ Edit System Prompt
786
+ </h2>
787
+ <button
788
+ onClick={handleCancelSystemPrompt}
789
+ className="text-gray-400 hover:text-white"
790
+ >
791
+ <X size={20} />
792
+ </button>
793
+ </div>
794
+ <div className="flex-grow mb-4">
795
+ <textarea
796
+ value={tempSystemPrompt}
797
+ onChange={(e) => setTempSystemPrompt(e.target.value)}
798
+ className="w-full h-full bg-gray-700 text-white p-4 rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-indigo-500"
799
+ placeholder="Enter your system prompt here..."
800
+ style={{ minHeight: "300px" }}
801
+ />
802
+ </div>
803
+ <div className="flex justify-between">
804
+ <button
805
+ onClick={handleResetSystemPrompt}
806
+ className="px-4 py-2 bg-teal-600 hover:bg-teal-700 rounded-lg transition-colors"
807
+ >
808
+ Reset
809
+ </button>
810
+ <div className="flex gap-3">
811
+ <button
812
+ onClick={handleSaveSystemPrompt}
813
+ className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 rounded-lg transition-colors"
814
+ >
815
+ Save
816
+ </button>
817
+ </div>
818
+ </div>
819
+ </div>
820
+ </div>
821
+ )}
822
+ </div>
823
+ );
824
+ };
825
+
826
+ export default App;
src/components/ExamplePrompts.tsx ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type React from "react";
2
+ import { DEFAULT_EXAMPLES, type Example } from "../constants/examples";
3
+
4
+ interface ExamplePromptsProps {
5
+ examples?: Example[];
6
+ onExampleClick: (messageText: string) => void;
7
+ }
8
+
9
+ const ExamplePrompts: React.FC<ExamplePromptsProps> = ({
10
+ examples,
11
+ onExampleClick,
12
+ }) => (
13
+ <div className="flex flex-col items-center justify-center h-full space-y-6">
14
+ <div className="text-center mb-6">
15
+ <h2 className="text-2xl font-semibold text-gray-300 mb-1">
16
+ Try an example
17
+ </h2>
18
+ <p className="text-sm text-gray-500">Click one to get started</p>
19
+ </div>
20
+
21
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 max-w-2xl w-full px-4">
22
+ {(examples || DEFAULT_EXAMPLES).map((example, index) => (
23
+ <button
24
+ key={index}
25
+ onClick={() => onExampleClick(example.messageText)}
26
+ className="flex items-center gap-3 p-4 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors text-left group cursor-pointer"
27
+ >
28
+ <span className="text-xl flex-shrink-0 group-hover:scale-110 transition-transform">
29
+ {example.icon}
30
+ </span>
31
+ <span className="text-sm text-gray-200 group-hover:text-white transition-colors">
32
+ {example.displayText}
33
+ </span>
34
+ </button>
35
+ ))}
36
+ </div>
37
+ </div>
38
+ );
39
+
40
+ export default ExamplePrompts;
src/components/LoadingScreen.tsx ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,
12
+ error,
13
+ loadSelectedModel,
14
+ selectedModelId,
15
+ isModelDropdownOpen,
16
+ setIsModelDropdownOpen,
17
+ handleModelSelect,
18
+ }: {
19
+ isLoading: boolean;
20
+ progress: number;
21
+ error: string | null;
22
+ loadSelectedModel: () => void;
23
+ selectedModelId: string;
24
+ isModelDropdownOpen: boolean;
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;
35
+
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);
90
+ };
91
+
92
+ const handleResize = () => {
93
+ cancelAnimationFrame(animationFrameId);
94
+ setup();
95
+ draw();
96
+ };
97
+
98
+ setup();
99
+ draw();
100
+
101
+ window.addEventListener("resize", handleResize);
102
+
103
+ return () => {
104
+ window.removeEventListener("resize", handleResize);
105
+ cancelAnimationFrame(animationFrameId);
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>
254
+ );
255
+ };
src/components/ResultBlock.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type React from "react";
2
+
3
+ const ResultBlock: React.FC<{ error?: string; result?: any }> = ({
4
+ error,
5
+ result,
6
+ }) => (
7
+ <div
8
+ className={
9
+ error
10
+ ? "bg-red-900 border border-red-600 rounded p-3"
11
+ : "bg-gray-700 border border-gray-600 rounded p-3"
12
+ }
13
+ >
14
+ {error ? <p className="text-red-300 text-sm">Error: {error}</p> : null}
15
+ <pre className="text-sm text-gray-300 whitespace-pre-wrap overflow-auto mt-2">
16
+ {typeof result === "object" ? JSON.stringify(result, null, 2) : result}
17
+ </pre>
18
+ </div>
19
+ );
20
+
21
+ export default ResultBlock;
src/components/ToolCallIndicator.tsx ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type React from "react";
2
+ import { extractToolCallContent } from "../utils";
3
+
4
+ const ToolCallIndicator: React.FC<{
5
+ content: string;
6
+ isRunning: boolean;
7
+ hasError: boolean;
8
+ }> = ({ content, isRunning, hasError }) => (
9
+ <div
10
+ className={`transition-all duration-500 ease-in-out rounded-lg p-4 ${
11
+ isRunning
12
+ ? "bg-gradient-to-r from-yellow-900/30 to-orange-900/30 border border-yellow-600/50"
13
+ : hasError
14
+ ? "bg-gradient-to-r from-red-900/30 to-rose-900/30 border border-red-600/50"
15
+ : "bg-gradient-to-r from-green-900/30 to-emerald-900/30 border border-green-600/50"
16
+ }`}
17
+ >
18
+ <div className="flex items-start space-x-3">
19
+ <div className="flex-shrink-0">
20
+ <div className="relative w-6 h-6">
21
+ {/* Spinner for running */}
22
+ <div
23
+ className={`absolute inset-0 flex items-center justify-center transition-opacity duration-500 ${
24
+ isRunning ? "opacity-100" : "opacity-0 pointer-events-none"
25
+ }`}
26
+ >
27
+ <div className="w-6 h-6 bg-green-400/0 border-2 border-yellow-400 border-t-transparent rounded-full animate-spin"></div>
28
+ </div>
29
+
30
+ {/* Cross for error */}
31
+ <div
32
+ className={`absolute inset-0 flex items-center justify-center transition-opacity duration-500 ${
33
+ hasError ? "opacity-100" : "opacity-0 pointer-events-none"
34
+ }`}
35
+ >
36
+ <div className="w-6 h-6 bg-red-400/100 rounded-full flex items-center justify-center transition-colors duration-500 ease-in-out">
37
+ <span className="text-xs text-gray-900 font-bold">✗</span>
38
+ </div>
39
+ </div>
40
+
41
+ {/* Tick for success */}
42
+ <div
43
+ className={`absolute inset-0 flex items-center justify-center transition-opacity duration-500 ${
44
+ !isRunning && !hasError
45
+ ? "opacity-100"
46
+ : "opacity-0 pointer-events-none"
47
+ }`}
48
+ >
49
+ <div className="w-6 h-6 bg-green-400/100 rounded-full flex items-center justify-center transition-colors duration-500 ease-in-out">
50
+ <span className="text-xs text-gray-900 font-bold">✓</span>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ <div className="flex-grow min-w-0">
56
+ <div className="flex items-center space-x-2 mb-2">
57
+ <span
58
+ className={`font-semibold text-sm transition-colors duration-500 ease-in-out ${
59
+ isRunning
60
+ ? "text-yellow-400"
61
+ : hasError
62
+ ? "text-red-400"
63
+ : "text-green-400"
64
+ }`}
65
+ >
66
+ 🔧 Tool Call
67
+ </span>
68
+ {isRunning && (
69
+ <span className="text-yellow-300 text-xs animate-pulse">
70
+ Running...
71
+ </span>
72
+ )}
73
+ </div>
74
+ <div className="bg-gray-800/50 rounded p-2 mb-2">
75
+ <code className="text-xs text-gray-300 font-mono break-all">
76
+ {extractToolCallContent(content) ?? "..."}
77
+ </code>
78
+ </div>
79
+ <p
80
+ className={`text-xs transition-colors duration-500 ease-in-out ${
81
+ isRunning
82
+ ? "text-yellow-200"
83
+ : hasError
84
+ ? "text-red-200"
85
+ : "text-green-200"
86
+ }`}
87
+ >
88
+ {isRunning
89
+ ? "Executing tool call..."
90
+ : hasError
91
+ ? "Tool call failed"
92
+ : "Tool call completed"}
93
+ </p>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ );
98
+ export default ToolCallIndicator;
src/components/ToolItem.tsx ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Editor from "@monaco-editor/react";
2
+ import { ChevronUp, ChevronDown, Trash2, Power } from "lucide-react";
3
+ import { useMemo } from "react";
4
+
5
+ import { extractFunctionAndRenderer, generateSchemaFromCode } from "../utils";
6
+
7
+ export interface Tool {
8
+ id: number;
9
+ name: string;
10
+ code: string;
11
+ enabled: boolean;
12
+ isCollapsed?: boolean;
13
+ renderer?: string;
14
+ }
15
+
16
+ interface ToolItemProps {
17
+ tool: Tool;
18
+ onToggleEnabled: () => void;
19
+ onToggleCollapsed: () => void;
20
+ onExpand: () => void;
21
+ onDelete: () => void;
22
+ onCodeChange: (newCode: string) => void;
23
+ }
24
+
25
+ const ToolItem: React.FC<ToolItemProps> = ({
26
+ tool,
27
+ onToggleEnabled,
28
+ onToggleCollapsed,
29
+ onDelete,
30
+ onCodeChange,
31
+ }) => {
32
+ const { functionCode } = extractFunctionAndRenderer(tool.code);
33
+ const schema = useMemo(
34
+ () => generateSchemaFromCode(functionCode),
35
+ [functionCode],
36
+ );
37
+
38
+ return (
39
+ <div
40
+ className={`bg-gray-700 rounded-lg p-4 transition-all ${!tool.enabled ? "opacity-50 grayscale" : ""}`}
41
+ >
42
+ <div
43
+ className="flex justify-between items-center cursor-pointer"
44
+ onClick={onToggleCollapsed}
45
+ >
46
+ <div>
47
+ <h3 className="text-lg font-bold text-teal-300 font-mono">
48
+ {schema.name}
49
+ </h3>
50
+ <div className="text-xs text-gray-300 mt-1">{schema.description}</div>
51
+ </div>
52
+ <div className="flex items-center space-x-3">
53
+ <button
54
+ onClick={(e) => {
55
+ e.stopPropagation();
56
+ onToggleEnabled();
57
+ }}
58
+ className={`p-1 rounded-full ${tool.enabled ? "text-green-400 hover:bg-green-900" : "text-red-400 hover:bg-red-900"}`}
59
+ >
60
+ <Power size={18} />
61
+ </button>
62
+ <button
63
+ onClick={(e) => {
64
+ e.stopPropagation();
65
+ onDelete();
66
+ }}
67
+ className="p-2 text-gray-400 hover:text-red-500 hover:bg-gray-600 rounded-lg"
68
+ >
69
+ <Trash2 size={18} />
70
+ </button>
71
+ <button
72
+ onClick={(e) => {
73
+ e.stopPropagation();
74
+ onToggleCollapsed();
75
+ }}
76
+ className="p-2 text-gray-400 hover:text-white"
77
+ >
78
+ {tool.isCollapsed ? (
79
+ <ChevronDown size={20} />
80
+ ) : (
81
+ <ChevronUp size={20} />
82
+ )}
83
+ </button>
84
+ </div>
85
+ </div>
86
+ {!tool.isCollapsed && (
87
+ <div className="mt-4 grid grid-cols-1 md:grid-cols-3 gap-4">
88
+ <div className="md:col-span-2">
89
+ <label className="text-sm font-bold text-gray-400">
90
+ Implementation & Renderer
91
+ </label>
92
+ <div
93
+ className="mt-1 rounded-md overflow-visible border border-gray-600"
94
+ style={{ overflow: "visible" }}
95
+ >
96
+ <Editor
97
+ height="300px"
98
+ language="javascript"
99
+ theme="vs-dark"
100
+ value={tool.code}
101
+ onChange={(value) => onCodeChange(value || "")}
102
+ options={{
103
+ minimap: { enabled: false },
104
+ scrollbar: { verticalScrollbarSize: 10 },
105
+ fontSize: 14,
106
+ lineDecorationsWidth: 0,
107
+ lineNumbersMinChars: 3,
108
+ scrollBeyondLastLine: false,
109
+ }}
110
+ />
111
+ </div>
112
+ </div>
113
+ <div className="flex flex-col">
114
+ <label className="text-sm font-bold text-gray-400">
115
+ Generated Schema
116
+ </label>
117
+ <div className="mt-1 rounded-md flex-grow overflow-visible border border-gray-600">
118
+ <Editor
119
+ height="300px"
120
+ language="json"
121
+ theme="vs-dark"
122
+ value={JSON.stringify(schema, null, 2)}
123
+ options={{
124
+ readOnly: true,
125
+ minimap: { enabled: false },
126
+ scrollbar: { verticalScrollbarSize: 10 },
127
+ lineNumbers: "off",
128
+ glyphMargin: false,
129
+ folding: false,
130
+ lineDecorationsWidth: 0,
131
+ lineNumbersMinChars: 0,
132
+ scrollBeyondLastLine: false,
133
+ fontSize: 12,
134
+ }}
135
+ />
136
+ </div>
137
+ </div>
138
+ </div>
139
+ )}
140
+ </div>
141
+ );
142
+ };
143
+
144
+ export default ToolItem;
src/components/ToolResultRenderer.tsx ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import ResultBlock from "./ResultBlock";
3
+
4
+ const ToolResultRenderer: React.FC<{
5
+ result: any;
6
+ rendererCode?: string;
7
+ input?: any;
8
+ }> = ({ result, rendererCode, input }) => {
9
+ if (!rendererCode) {
10
+ return <ResultBlock result={result} />;
11
+ }
12
+
13
+ try {
14
+ const exportMatch = rendererCode.match(/export\s+default\s+(.*)/s);
15
+ if (!exportMatch) {
16
+ throw new Error("Invalid renderer format - no export default found");
17
+ }
18
+
19
+ const componentCode = exportMatch[1].trim();
20
+ const componentFunction = new Function(
21
+ "React",
22
+ "input",
23
+ "output",
24
+ `
25
+ const { createElement: h, Fragment } = React;
26
+ const JSXComponent = ${componentCode};
27
+ return JSXComponent(input, output);
28
+ `,
29
+ );
30
+
31
+ const element = componentFunction(React, input || {}, result);
32
+ return element;
33
+ } catch (error) {
34
+ return (
35
+ <ResultBlock
36
+ error={error instanceof Error ? error.message : "Unknown error"}
37
+ result={result}
38
+ />
39
+ );
40
+ }
41
+ };
42
+ export default ToolResultRenderer;
src/components/icons/HfLogo.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type React from "react";
2
+
3
+ export default (props: React.SVGProps<SVGSVGElement>) => (
4
+ <svg
5
+ {...props}
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ viewBox="0 0 24 24"
8
+ fill="currentColor"
9
+ >
10
+ <path
11
+ d="M2.25 11.535c0-3.407 1.847-6.554 4.844-8.258a9.822 9.822 0 019.687 0c2.997 1.704 4.844 4.851 4.844 8.258 0 5.266-4.337 9.535-9.687 9.535S2.25 16.8 2.25 11.535z"
12
+ fill="#FF9D0B"
13
+ ></path>
14
+ <path
15
+ d="M11.938 20.086c4.797 0 8.687-3.829 8.687-8.551 0-4.722-3.89-8.55-8.687-8.55-4.798 0-8.688 3.828-8.688 8.55 0 4.722 3.89 8.55 8.688 8.55z"
16
+ fill="#FFD21E"
17
+ ></path>
18
+ <path
19
+ d="M11.875 15.113c2.457 0 3.25-2.156 3.25-3.263 0-.576-.393-.394-1.023-.089-.582.283-1.365.675-2.224.675-1.798 0-3.25-1.693-3.25-.586 0 1.107.79 3.263 3.25 3.263h-.003z"
20
+ fill="#FF323D"
21
+ ></path>
22
+ <path
23
+ d="M14.76 9.21c.32.108.445.753.767.585.447-.233.707-.708.659-1.204a1.235 1.235 0 00-.879-1.059 1.262 1.262 0 00-1.33.394c-.322.384-.377.92-.14 1.36.153.283.638-.177.925-.079l-.002.003zm-5.887 0c-.32.108-.448.753-.768.585a1.226 1.226 0 01-.658-1.204c.048-.495.395-.913.878-1.059a1.262 1.262 0 011.33.394c.322.384.377.92.14 1.36-.152.283-.64-.177-.925-.079l.003.003zm1.12 5.34a2.166 2.166 0 011.325-1.106c.07-.02.144.06.219.171l.192.306c.069.1.139.175.209.175.074 0 .15-.074.223-.172l.205-.302c.08-.11.157-.188.234-.165.537.168.986.536 1.25 1.026.932-.724 1.275-1.905 1.275-2.633 0-.508-.306-.426-.81-.19l-.616.296c-.52.24-1.148.48-1.824.48-.676 0-1.302-.24-1.823-.48l-.589-.283c-.52-.248-.838-.342-.838.177 0 .703.32 1.831 1.187 2.56l.18.14z"
24
+ fill="#3A3B45"
25
+ ></path>
26
+ <path
27
+ d="M17.812 10.366a.806.806 0 00.813-.8c0-.441-.364-.8-.813-.8a.806.806 0 00-.812.8c0 .442.364.8.812.8zm-11.624 0a.806.806 0 00.812-.8c0-.441-.364-.8-.812-.8a.806.806 0 00-.813.8c0 .442.364.8.813.8zM4.515 13.073c-.405 0-.765.162-1.017.46a1.455 1.455 0 00-.333.925 1.801 1.801 0 00-.485-.074c-.387 0-.737.146-.985.409a1.41 1.41 0 00-.2 1.722 1.302 1.302 0 00-.447.694c-.06.222-.12.69.2 1.166a1.267 1.267 0 00-.093 1.236c.238.533.81.958 1.89 1.405l.24.096c.768.3 1.473.492 1.478.494.89.243 1.808.375 2.732.394 1.465 0 2.513-.443 3.115-1.314.93-1.342.842-2.575-.274-3.763l-.151-.154c-.692-.684-1.155-1.69-1.25-1.912-.195-.655-.71-1.383-1.562-1.383-.46.007-.889.233-1.15.605-.25-.31-.495-.553-.715-.694a1.87 1.87 0 00-.993-.312zm14.97 0c.405 0 .767.162 1.017.46.216.262.333.588.333.925.158-.047.322-.071.487-.074.388 0 .738.146.985.409a1.41 1.41 0 01.2 1.722c.22.178.377.422.445.694.06.222.12.69-.2 1.166.244.37.279.836.093 1.236-.238.533-.81.958-1.889 1.405l-.239.096c-.77.3-1.475.492-1.48.494-.89.243-1.808.375-2.732.394-1.465 0-2.513-.443-3.115-1.314-.93-1.342-.842-2.575.274-3.763l.151-.154c.695-.684 1.157-1.69 1.252-1.912.195-.655.708-1.383 1.56-1.383.46.007.889.233 1.15.605.25-.31.495-.553.718-.694.244-.162.523-.265.814-.3l.176-.012z"
28
+ fill="#FF9D0B"
29
+ ></path>
30
+ <path
31
+ d="M9.785 20.132c.688-.994.638-1.74-.305-2.667-.945-.928-1.495-2.288-1.495-2.288s-.205-.788-.672-.714c-.468.074-.81 1.25.17 1.971.977.721-.195 1.21-.573.534-.375-.677-1.405-2.416-1.94-2.751-.532-.332-.907-.148-.782.541.125.687 2.357 2.35 2.14 2.707-.218.362-.983-.42-.983-.42S2.953 14.9 2.43 15.46c-.52.558.398 1.026 1.7 1.803 1.308.778 1.41.985 1.225 1.28-.187.295-3.07-2.1-3.34-1.083-.27 1.011 2.943 1.304 2.745 2.006-.2.7-2.265-1.324-2.685-.537-.425.79 2.913 1.718 2.94 1.725 1.075.276 3.813.859 4.77-.522zm4.432 0c-.687-.994-.64-1.74.305-2.667.943-.928 1.493-2.288 1.493-2.288s.205-.788.675-.714c.465.074.807 1.25-.17 1.971-.98.721.195 1.21.57.534.377-.677 1.407-2.416 1.94-2.751.532-.332.91-.148.782.541-.125.687-2.355 2.35-2.137 2.707.215.362.98-.42.98-.42S21.05 14.9 21.57 15.46c.52.558-.395 1.026-1.7 1.803-1.308.778-1.408.985-1.225 1.28.187.295 3.07-2.1 3.34-1.083.27 1.011-2.94 1.304-2.743 2.006.2.7 2.263-1.324 2.685-.537.423.79-2.912 1.718-2.94 1.725-1.077.276-3.815.859-4.77-.522z"
32
+ fill="#FFD21E"
33
+ ></path>
34
+ </svg>
35
+ );
src/components/icons/LiquidAILogo.tsx ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type React from "react";
2
+
3
+ export default (props: React.SVGProps<SVGSVGElement>) => (
4
+ <svg
5
+ {...props}
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ viewBox="0 0 24 24"
8
+ fill="currentColor"
9
+ >
10
+ <path d="M12.028 8.546l-.008.005 3.03 5.25a3.94 3.94 0 01.643 2.162c0 .754-.212 1.46-.58 2.062l6.173-1.991L11.63 0 9.304 3.872l2.724 4.674zM6.837 24l4.85-4.053h-.013c-2.219 0-4.017-1.784-4.017-3.984 0-.794.235-1.534.64-2.156l2.865-4.976-2.381-4.087L2 16.034 6.83 24h.007zM13.737 19.382h-.001L8.222 24h8.182l4.148-6.769-6.815 2.151z"></path>
11
+ </svg>
12
+ );
src/constants/db.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export const DB_NAME = "tool-caller-db";
2
+ export const STORE_NAME = "tools";
3
+ export const SETTINGS_STORE_NAME = "settings";
src/constants/examples.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface Example {
2
+ icon: string;
3
+ displayText: string;
4
+ messageText: string;
5
+ }
6
+
7
+ export const DEFAULT_EXAMPLES: Example[] = [
8
+ {
9
+ icon: "🌍",
10
+ displayText: "Where am I and what time is it?",
11
+ messageText: "Where am I and what time is it?",
12
+ },
13
+ {
14
+ icon: "👋",
15
+ displayText: "Say hello",
16
+ messageText: "Say hello",
17
+ },
18
+ {
19
+ icon: "🔢",
20
+ displayText: "Solve a math problem",
21
+ messageText: "What is 123 plus 15% of 200 all divided by 7?",
22
+ },
23
+ {
24
+ icon: "😴",
25
+ displayText: "Sleep for 3 seconds",
26
+ messageText: "Sleep for 3 seconds",
27
+ },
28
+ {
29
+ icon: "🎲",
30
+ displayText: "Generate a random number",
31
+ messageText: "Generate a random number between 1 and 100.",
32
+ },
33
+ {
34
+ icon: "📹",
35
+ displayText: "Play a video",
36
+ messageText:
37
+ 'Open the following webpage: "https://www.youtube.com/embed/dQw4w9WgXcQ?autoplay=1".',
38
+ },
39
+ ];
src/constants/models.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export const MODEL_OPTIONS = [
2
+ { id: "350M", label: "LFM2-350M", size: "350M parameters (312 MB)" },
3
+ { id: "700M", label: "LFM2-700M", size: "700M parameters (579 MB)" },
4
+ { id: "1.2B", label: "LFM2-1.2B", size: "1.2B parameters (868 MB)" },
5
+ ];
src/constants/systemPrompt.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const DEFAULT_SYSTEM_PROMPT = [
2
+ "You are an AI assistant with access to a set of tools.",
3
+ "When a user asks a question, determine if a tool should be called to help answer.",
4
+ "If a tool is needed, respond with a tool call using the following format: ",
5
+ "<|tool_call_start|>[tool_function_call_1, tool_function_call_2, ...]<|tool_call_end|>.",
6
+ 'Each tool function call should use Python-like syntax, e.g., speak("Hello"), random_number(min=1, max=10).',
7
+ "If no tool is needed, you should answer the user directly without calling any tools.",
8
+ "Always use the most relevant tool(s) for the user's request.",
9
+ "If a tool returns an error, explain the error to the user.",
10
+ "Be concise and helpful.",
11
+ ].join(" ");
src/hooks/useLLM.ts ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useRef, useCallback } from "react";
2
+ import {
3
+ AutoModelForCausalLM,
4
+ AutoTokenizer,
5
+ TextStreamer,
6
+ } from "@huggingface/transformers";
7
+
8
+ interface LLMState {
9
+ isLoading: boolean;
10
+ isReady: boolean;
11
+ error: string | null;
12
+ progress: number;
13
+ }
14
+
15
+ interface LLMInstance {
16
+ model: any;
17
+ tokenizer: any;
18
+ }
19
+
20
+ let moduleCache: {
21
+ [modelId: string]: {
22
+ instance: LLMInstance | null;
23
+ loadingPromise: Promise<LLMInstance> | null;
24
+ };
25
+ } = {};
26
+
27
+ export const useLLM = (modelId?: string) => {
28
+ const [state, setState] = useState<LLMState>({
29
+ isLoading: false,
30
+ isReady: false,
31
+ error: null,
32
+ progress: 0,
33
+ });
34
+
35
+ const instanceRef = useRef<LLMInstance | null>(null);
36
+ const loadingPromiseRef = useRef<Promise<LLMInstance> | null>(null);
37
+
38
+ const abortControllerRef = useRef<AbortController | null>(null);
39
+ const pastKeyValuesRef = useRef<any>(null);
40
+
41
+ const loadModel = useCallback(async () => {
42
+ if (!modelId) {
43
+ throw new Error("Model ID is required");
44
+ }
45
+
46
+ const MODEL_ID = `onnx-community/LFM2-${modelId}-ONNX`;
47
+
48
+ if (!moduleCache[modelId]) {
49
+ moduleCache[modelId] = {
50
+ instance: null,
51
+ loadingPromise: null,
52
+ };
53
+ }
54
+
55
+ const cache = moduleCache[modelId];
56
+
57
+ const existingInstance = instanceRef.current || cache.instance;
58
+ if (existingInstance) {
59
+ instanceRef.current = existingInstance;
60
+ cache.instance = existingInstance;
61
+ setState((prev) => ({ ...prev, isReady: true, isLoading: false }));
62
+ return existingInstance;
63
+ }
64
+
65
+ const existingPromise = loadingPromiseRef.current || cache.loadingPromise;
66
+ if (existingPromise) {
67
+ try {
68
+ const instance = await existingPromise;
69
+ instanceRef.current = instance;
70
+ cache.instance = instance;
71
+ setState((prev) => ({ ...prev, isReady: true, isLoading: false }));
72
+ return instance;
73
+ } catch (error) {
74
+ setState((prev) => ({
75
+ ...prev,
76
+ isLoading: false,
77
+ error:
78
+ error instanceof Error ? error.message : "Failed to load model",
79
+ }));
80
+ throw error;
81
+ }
82
+ }
83
+
84
+ setState((prev) => ({
85
+ ...prev,
86
+ isLoading: true,
87
+ error: null,
88
+ progress: 0,
89
+ }));
90
+
91
+ abortControllerRef.current = new AbortController();
92
+
93
+ const loadingPromise = (async () => {
94
+ try {
95
+ const progressCallback = (progress: any) => {
96
+ // Only update progress for weights
97
+ if (
98
+ progress.status === "progress" &&
99
+ progress.file.endsWith(".onnx_data")
100
+ ) {
101
+ const percentage = Math.round(
102
+ (progress.loaded / progress.total) * 100,
103
+ );
104
+ setState((prev) => ({ ...prev, progress: percentage }));
105
+ }
106
+ };
107
+
108
+ const tokenizer = await AutoTokenizer.from_pretrained(MODEL_ID, {
109
+ progress_callback: progressCallback,
110
+ });
111
+
112
+ const model = await AutoModelForCausalLM.from_pretrained(MODEL_ID, {
113
+ dtype: "q4f16",
114
+ device: "webgpu",
115
+ progress_callback: progressCallback,
116
+ });
117
+
118
+ const instance = { model, tokenizer };
119
+ instanceRef.current = instance;
120
+ cache.instance = instance;
121
+ loadingPromiseRef.current = null;
122
+ cache.loadingPromise = null;
123
+
124
+ setState((prev) => ({
125
+ ...prev,
126
+ isLoading: false,
127
+ isReady: true,
128
+ progress: 100,
129
+ }));
130
+ return instance;
131
+ } catch (error) {
132
+ loadingPromiseRef.current = null;
133
+ cache.loadingPromise = null;
134
+ setState((prev) => ({
135
+ ...prev,
136
+ isLoading: false,
137
+ error:
138
+ error instanceof Error ? error.message : "Failed to load model",
139
+ }));
140
+ throw error;
141
+ }
142
+ })();
143
+
144
+ loadingPromiseRef.current = loadingPromise;
145
+ cache.loadingPromise = loadingPromise;
146
+ return loadingPromise;
147
+ }, [modelId]);
148
+
149
+ const generateResponse = useCallback(
150
+ async (
151
+ messages: Array<{ role: string; content: string }>,
152
+ tools: Array<any>,
153
+ onToken?: (token: string) => void,
154
+ ): Promise<string> => {
155
+ const instance = instanceRef.current;
156
+ if (!instance) {
157
+ throw new Error("Model not loaded. Call loadModel() first.");
158
+ }
159
+
160
+ const { model, tokenizer } = instance;
161
+
162
+ // Apply chat template with tools
163
+ const input = tokenizer.apply_chat_template(messages, {
164
+ tools,
165
+ add_generation_prompt: true,
166
+ return_dict: true,
167
+ });
168
+
169
+ const streamer = onToken
170
+ ? new TextStreamer(tokenizer, {
171
+ skip_prompt: true,
172
+ skip_special_tokens: false,
173
+ callback_function: (token: string) => {
174
+ onToken(token);
175
+ },
176
+ })
177
+ : undefined;
178
+
179
+ // Generate the response
180
+ const { sequences, past_key_values } = await model.generate({
181
+ ...input,
182
+ past_key_values: pastKeyValuesRef.current,
183
+ max_new_tokens: 512,
184
+ do_sample: false,
185
+ streamer,
186
+ return_dict_in_generate: true,
187
+ });
188
+ pastKeyValuesRef.current = past_key_values;
189
+
190
+ // Decode the generated text with special tokens preserved (except final <|im_end|>) for tool call detection
191
+ const response = tokenizer
192
+ .batch_decode(sequences.slice(null, [input.input_ids.dims[1], null]), {
193
+ skip_special_tokens: false,
194
+ })[0]
195
+ .replace(/<\|im_end\|>$/, "");
196
+
197
+ return response;
198
+ },
199
+ [],
200
+ );
201
+
202
+ const clearPastKeyValues = useCallback(() => {
203
+ pastKeyValuesRef.current = null;
204
+ }, []);
205
+
206
+ const cleanup = useCallback(() => {
207
+ if (abortControllerRef.current) {
208
+ abortControllerRef.current.abort();
209
+ }
210
+ }, []);
211
+
212
+ useEffect(() => {
213
+ return cleanup;
214
+ }, [cleanup]);
215
+
216
+ useEffect(() => {
217
+ if (modelId && moduleCache[modelId]) {
218
+ const existingInstance =
219
+ instanceRef.current || moduleCache[modelId].instance;
220
+ if (existingInstance) {
221
+ instanceRef.current = existingInstance;
222
+ setState((prev) => ({ ...prev, isReady: true }));
223
+ }
224
+ }
225
+ }, [modelId]);
226
+
227
+ return {
228
+ ...state,
229
+ loadModel,
230
+ generateResponse,
231
+ clearPastKeyValues,
232
+ cleanup,
233
+ };
234
+ };
src/index.css ADDED
@@ -0,0 +1 @@
 
 
1
+ @import "tailwindcss";
src/main.tsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import "./index.css";
4
+ import App from "./App.tsx";
5
+
6
+ createRoot(document.getElementById("root")!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ );
src/tools/get_location.js ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Get the user's current location using the browser's geolocation API.
3
+ * @returns {Promise<{ latitude: number, longitude: number }>} The current position { latitude, longitude }.
4
+ */
5
+ export async function get_location() {
6
+ return new Promise((resolve, reject) => {
7
+ if (!navigator.geolocation) {
8
+ reject("Geolocation not supported.");
9
+ return;
10
+ }
11
+ navigator.geolocation.getCurrentPosition(
12
+ (pos) =>
13
+ resolve({
14
+ latitude: pos.coords.latitude,
15
+ longitude: pos.coords.longitude,
16
+ }),
17
+ (err) => reject(err.message || "Geolocation error"),
18
+ );
19
+ });
20
+ }
21
+
22
+ export default (input, output) =>
23
+ React.createElement(
24
+ "div",
25
+ { className: "bg-green-50 border border-green-200 rounded-lg p-4" },
26
+ React.createElement(
27
+ "div",
28
+ { className: "flex items-center mb-2" },
29
+ React.createElement(
30
+ "div",
31
+ {
32
+ className:
33
+ "w-8 h-8 bg-green-100 rounded-full flex items-center justify-center mr-3",
34
+ },
35
+ "📍",
36
+ ),
37
+ React.createElement(
38
+ "h3",
39
+ { className: "text-green-900 font-semibold" },
40
+ "Location",
41
+ ),
42
+ ),
43
+ output?.latitude && output?.longitude
44
+ ? React.createElement(
45
+ "div",
46
+ { className: "space-y-1 text-sm" },
47
+ React.createElement(
48
+ "p",
49
+ { className: "text-green-700" },
50
+ React.createElement(
51
+ "span",
52
+ { className: "font-medium" },
53
+ "Latitude: ",
54
+ ),
55
+ output.latitude.toFixed(6),
56
+ ),
57
+ React.createElement(
58
+ "p",
59
+ { className: "text-green-700" },
60
+ React.createElement(
61
+ "span",
62
+ { className: "font-medium" },
63
+ "Longitude: ",
64
+ ),
65
+ output.longitude.toFixed(6),
66
+ ),
67
+ React.createElement(
68
+ "a",
69
+ {
70
+ href: `https://maps.google.com?q=${output.latitude},${output.longitude}`,
71
+ target: "_blank",
72
+ rel: "noopener noreferrer",
73
+ className:
74
+ "inline-block mt-2 text-green-600 hover:text-green-800 underline text-xs",
75
+ },
76
+ "View on Google Maps",
77
+ ),
78
+ )
79
+ : React.createElement(
80
+ "p",
81
+ { className: "text-green-700 text-sm" },
82
+ JSON.stringify(output),
83
+ ),
84
+ );
src/tools/get_time.js ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Get the current date and time.
3
+ * @returns {{ iso: string, local: string }} The current date and time as ISO and local time strings.
4
+ */
5
+ export function get_time() {
6
+ const now = new Date();
7
+ return {
8
+ iso: now.toISOString(),
9
+ local: now.toLocaleString(undefined, {
10
+ dateStyle: "full",
11
+ timeStyle: "long",
12
+ }),
13
+ };
14
+ }
15
+
16
+ export default (input, output) =>
17
+ React.createElement(
18
+ "div",
19
+ { className: "bg-amber-50 border border-amber-200 rounded-lg p-4" },
20
+ React.createElement(
21
+ "div",
22
+ { className: "flex items-center mb-2" },
23
+ React.createElement(
24
+ "div",
25
+ {
26
+ className:
27
+ "w-8 h-8 bg-amber-100 rounded-full flex items-center justify-center mr-3",
28
+ },
29
+ "🕐",
30
+ ),
31
+ React.createElement(
32
+ "h3",
33
+ { className: "text-amber-900 font-semibold" },
34
+ "Current Time",
35
+ ),
36
+ ),
37
+ React.createElement(
38
+ "div",
39
+ { className: "text-sm space-y-1" },
40
+ React.createElement(
41
+ "p",
42
+ { className: "text-amber-700 font-mono" },
43
+ output.local,
44
+ ),
45
+ React.createElement(
46
+ "p",
47
+ { className: "text-amber-600 text-xs" },
48
+ new Date(output.iso).toLocaleString(),
49
+ ),
50
+ ),
51
+ );
src/tools/index.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import SPEAK_TOOL from "./speak.js?raw";
2
+ import GET_LOCATION_TOOL from "./get_location.js?raw";
3
+ import SLEEP_TOOL from "./sleep.js?raw";
4
+ import GET_TIME_TOOL from "./get_time.js?raw";
5
+ import RANDOM_NUMBER_TOOL from "./random_number.js?raw";
6
+ import MATH_EVAL_TOOL from "./math_eval.js?raw";
7
+ import TEMPLATE_TOOL from "./template.js?raw";
8
+ import OPEN_WEBPAGE_TOOL from "./open_webpage.js?raw";
9
+
10
+ export const DEFAULT_TOOLS = {
11
+ speak: SPEAK_TOOL,
12
+ get_location: GET_LOCATION_TOOL,
13
+ sleep: SLEEP_TOOL,
14
+ get_time: GET_TIME_TOOL,
15
+ random_number: RANDOM_NUMBER_TOOL,
16
+ math_eval: MATH_EVAL_TOOL,
17
+ open_webpage: OPEN_WEBPAGE_TOOL,
18
+ };
19
+ export const TEMPLATE = TEMPLATE_TOOL;
src/tools/math_eval.js ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Evaluate a math expression.
3
+ * @param {string} expression - The math expression (e.g., "2 + 2 * (3 - 1)").
4
+ * @returns {number} The result of the expression.
5
+ */
6
+ export function math_eval(expression) {
7
+ // Only allow numbers, spaces, and math symbols: + - * / % ( ) .
8
+ if (!/^[\d\s+\-*/%.()]+$/.test(expression)) {
9
+ throw new Error("Invalid characters in expression.");
10
+ }
11
+ return Function('"use strict";return (' + expression + ")")();
12
+ }
13
+
14
+ export default (input, output) =>
15
+ React.createElement(
16
+ "div",
17
+ { className: "bg-emerald-50 border border-emerald-200 rounded-lg p-4" },
18
+ React.createElement(
19
+ "div",
20
+ { className: "flex items-center mb-2" },
21
+ React.createElement(
22
+ "div",
23
+ {
24
+ className:
25
+ "w-8 h-8 bg-emerald-100 rounded-full flex items-center justify-center mr-3",
26
+ },
27
+ "🧮",
28
+ ),
29
+ React.createElement(
30
+ "h3",
31
+ { className: "text-emerald-900 font-semibold" },
32
+ "Math Evaluation",
33
+ ),
34
+ ),
35
+ React.createElement(
36
+ "div",
37
+ { className: "text-center" },
38
+ React.createElement(
39
+ "div",
40
+ { className: "text-lg font-mono text-emerald-700 mb-1" },
41
+ input.expression || "Unknown expression",
42
+ ),
43
+ React.createElement(
44
+ "div",
45
+ { className: "text-2xl font-bold text-emerald-600 mb-1" },
46
+ `= ${output}`,
47
+ ),
48
+ React.createElement(
49
+ "p",
50
+ { className: "text-emerald-500 text-xs" },
51
+ "Calculation result",
52
+ ),
53
+ ),
54
+ );
src/tools/open_webpage.js ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Open a webpage
3
+ * @param {string} src - The URL of the webpage.
4
+ * @returns {string} The validated URL.
5
+ */
6
+ export function open_webpage(src) {
7
+ try {
8
+ const urlObj = new URL(src);
9
+ if (!["http:", "https:"].includes(urlObj.protocol)) {
10
+ throw new Error("Only HTTP and HTTPS URLs are allowed.");
11
+ }
12
+ return urlObj.href;
13
+ } catch (error) {
14
+ throw new Error("Invalid URL provided.");
15
+ }
16
+ }
17
+
18
+ export default (input, output) => {
19
+ return React.createElement(
20
+ "div",
21
+ { className: "bg-blue-50 border border-blue-200 rounded-lg p-4" },
22
+ React.createElement(
23
+ "div",
24
+ { className: "flex items-center mb-2" },
25
+ React.createElement(
26
+ "div",
27
+ {
28
+ className:
29
+ "w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-3",
30
+ },
31
+ "🌐",
32
+ ),
33
+ React.createElement(
34
+ "h3",
35
+ { className: "text-blue-900 font-semibold" },
36
+ "Web Page",
37
+ ),
38
+ ),
39
+ React.createElement("iframe", {
40
+ src: output,
41
+ className: "w-full border border-blue-300 rounded",
42
+ width: 480,
43
+ height: 360,
44
+ title: "Embedded content",
45
+ allow: "autoplay",
46
+ frameBorder: "0",
47
+ }),
48
+ );
49
+ };
src/tools/random_number.js ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Generate a random integer between min and max (inclusive).
3
+ * @param {number} min - Minimum value (inclusive).
4
+ * @param {number} max - Maximum value (inclusive).
5
+ * @returns {number} A random integer.
6
+ */
7
+ export function random_number(min, max) {
8
+ min = Math.ceil(Number(min));
9
+ max = Math.floor(Number(max));
10
+ if (isNaN(min) || isNaN(max) || min > max) {
11
+ throw new Error("Invalid min or max value.");
12
+ }
13
+ return Math.floor(Math.random() * (max - min + 1)) + min;
14
+ }
15
+
16
+ export default (input, output) =>
17
+ React.createElement(
18
+ "div",
19
+ { className: "bg-indigo-50 border border-indigo-200 rounded-lg p-4" },
20
+ React.createElement(
21
+ "div",
22
+ { className: "flex items-center mb-2" },
23
+ React.createElement(
24
+ "div",
25
+ {
26
+ className:
27
+ "w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center mr-3",
28
+ },
29
+ "🎲",
30
+ ),
31
+ React.createElement(
32
+ "h3",
33
+ { className: "text-indigo-900 font-semibold" },
34
+ "Random Number",
35
+ ),
36
+ ),
37
+ React.createElement(
38
+ "div",
39
+ { className: "text-center" },
40
+ React.createElement(
41
+ "div",
42
+ { className: "text-3xl font-bold text-indigo-600 mb-1" },
43
+ output,
44
+ ),
45
+ React.createElement(
46
+ "p",
47
+ { className: "text-indigo-500 text-xs" },
48
+ `Range: ${input.min || "?"} - ${input.max || "?"}`,
49
+ ),
50
+ ),
51
+ );
src/tools/sleep.js ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Sleep for a given number of seconds.
3
+ * @param {number} seconds - The number of seconds to sleep.
4
+ * @return {void}
5
+ */
6
+ export async function sleep(seconds) {
7
+ return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
8
+ }
9
+
10
+ export default (input, output) =>
11
+ React.createElement(
12
+ "div",
13
+ { className: "bg-purple-50 border border-purple-200 rounded-lg p-4" },
14
+ React.createElement(
15
+ "div",
16
+ { className: "flex items-center mb-2" },
17
+ React.createElement(
18
+ "div",
19
+ {
20
+ className:
21
+ "w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center mr-3",
22
+ },
23
+ "😴",
24
+ ),
25
+ React.createElement(
26
+ "h3",
27
+ { className: "text-purple-900 font-semibold" },
28
+ "Sleep",
29
+ ),
30
+ ),
31
+ React.createElement(
32
+ "div",
33
+ { className: "text-sm space-y-1" },
34
+ React.createElement(
35
+ "p",
36
+ { className: "text-purple-700 font-medium" },
37
+ `Slept for ${input.seconds || "unknown"} seconds`,
38
+ ),
39
+ React.createElement(
40
+ "p",
41
+ { className: "text-purple-600 text-xs" },
42
+ output,
43
+ ),
44
+ ),
45
+ );
src/tools/speak.js ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Speak text using the browser's speech synthesis API.
3
+ * @param {string} text - The text to speak.
4
+ * @param {string} [voice] - The name of the voice to use (optional).
5
+ * @return {void}
6
+ */
7
+ export function speak(text, voice = undefined) {
8
+ const utter = new window.SpeechSynthesisUtterance(text);
9
+ if (voice) {
10
+ const voices = window.speechSynthesis.getVoices();
11
+ const match = voices.find((v) => v.name === voice);
12
+ if (match) utter.voice = match;
13
+ }
14
+ window.speechSynthesis.speak(utter);
15
+ }
16
+
17
+ export default (input, output) =>
18
+ React.createElement(
19
+ "div",
20
+ { className: "bg-blue-50 border border-blue-200 rounded-lg p-4" },
21
+ React.createElement(
22
+ "div",
23
+ { className: "flex items-center mb-2" },
24
+ React.createElement(
25
+ "div",
26
+ {
27
+ className:
28
+ "w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-3",
29
+ },
30
+ "🔊",
31
+ ),
32
+ React.createElement(
33
+ "h3",
34
+ { className: "text-blue-900 font-semibold" },
35
+ "Speech Synthesis",
36
+ ),
37
+ ),
38
+ React.createElement(
39
+ "div",
40
+ { className: "text-sm space-y-1" },
41
+ React.createElement(
42
+ "p",
43
+ { className: "text-blue-700 font-medium" },
44
+ `Speaking: "${input.text || "Unknown text"}"`,
45
+ ),
46
+ input.voice &&
47
+ React.createElement(
48
+ "p",
49
+ { className: "text-blue-600 text-xs" },
50
+ `Voice: ${input.voice}`,
51
+ ),
52
+ React.createElement(
53
+ "p",
54
+ { className: "text-blue-600 text-xs" },
55
+ typeof output === "string" ? output : "Speech completed successfully",
56
+ ),
57
+ ),
58
+ );
src/tools/template.js ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Description of the tool.
3
+ * @param {any} parameter1 - Description of the first parameter.
4
+ * @param {any} parameter2 - Description of the second parameter.
5
+ * @returns {any} Description of the return value.
6
+ */
7
+ export function new_tool(parameter1, parameter2) {
8
+ // TODO: Implement the tool logic here
9
+ return true; // Placeholder return value
10
+ }
11
+
12
+ export default (input, output) =>
13
+ React.createElement(
14
+ "div",
15
+ { className: "bg-amber-50 border border-amber-200 rounded-lg p-4" },
16
+ React.createElement(
17
+ "div",
18
+ { className: "flex items-center mb-2" },
19
+ React.createElement(
20
+ "div",
21
+ {
22
+ className:
23
+ "w-8 h-8 bg-amber-100 rounded-full flex items-center justify-center mr-3",
24
+ },
25
+ "🛠️",
26
+ ),
27
+ React.createElement(
28
+ "h3",
29
+ { className: "text-amber-900 font-semibold" },
30
+ "Tool Name",
31
+ ),
32
+ ),
33
+ React.createElement(
34
+ "div",
35
+ { className: "text-sm space-y-1" },
36
+ React.createElement(
37
+ "p",
38
+ { className: "text-amber-700 font-medium" },
39
+ `Input: ${JSON.stringify(input)}`,
40
+ ),
41
+ React.createElement(
42
+ "p",
43
+ { className: "text-amber-600 text-xs" },
44
+ `Output: ${output}`,
45
+ ),
46
+ ),
47
+ );
src/utils.ts ADDED
@@ -0,0 +1,370 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ interface ParsedCall {
2
+ name: string;
3
+ positionalArgs: any[];
4
+ keywordArgs: Record<string, any>;
5
+ }
6
+
7
+ interface Schema {
8
+ name: string;
9
+ description: string;
10
+ parameters: {
11
+ type: string;
12
+ properties: Record<
13
+ string,
14
+ {
15
+ type: string;
16
+ description: string;
17
+ default?: any;
18
+ }
19
+ >;
20
+ required: string[];
21
+ };
22
+ }
23
+
24
+ interface JSDocParam {
25
+ type: string;
26
+ description: string;
27
+ isOptional: boolean;
28
+ defaultValue?: string;
29
+ }
30
+
31
+ const parseArguments = (argsString: string): string[] => {
32
+ const args: string[] = [];
33
+ let current = "";
34
+ let inQuotes = false;
35
+ let quoteChar = "";
36
+ let depth = 0;
37
+
38
+ for (let i = 0; i < argsString.length; i++) {
39
+ const char = argsString[i];
40
+
41
+ if (!inQuotes && (char === '"' || char === "'")) {
42
+ inQuotes = true;
43
+ quoteChar = char;
44
+ current += char;
45
+ } else if (inQuotes && char === quoteChar) {
46
+ inQuotes = false;
47
+ quoteChar = "";
48
+ current += char;
49
+ } else if (!inQuotes && char === "(") {
50
+ depth++;
51
+ current += char;
52
+ } else if (!inQuotes && char === ")") {
53
+ depth--;
54
+ current += char;
55
+ } else if (!inQuotes && char === "," && depth === 0) {
56
+ args.push(current.trim());
57
+ current = "";
58
+ } else {
59
+ current += char;
60
+ }
61
+ }
62
+
63
+ if (current.trim()) {
64
+ args.push(current.trim());
65
+ }
66
+
67
+ return args;
68
+ };
69
+
70
+ export const extractPythonicCalls = (toolCallContent: string): string[] => {
71
+ try {
72
+ const cleanContent = toolCallContent.trim();
73
+
74
+ try {
75
+ const parsed = JSON.parse(cleanContent);
76
+ if (Array.isArray(parsed)) {
77
+ return parsed;
78
+ }
79
+ } catch {
80
+ // Fallback to manual parsing
81
+ }
82
+
83
+ if (cleanContent.startsWith("[") && cleanContent.endsWith("]")) {
84
+ const inner = cleanContent.slice(1, -1).trim();
85
+ if (!inner) return [];
86
+ return parseArguments(inner).map((call) =>
87
+ call.trim().replace(/^['"]|['"]$/g, ""),
88
+ );
89
+ }
90
+
91
+ return [cleanContent];
92
+ } catch (error) {
93
+ console.error("Error parsing tool calls:", error);
94
+ return [];
95
+ }
96
+ };
97
+
98
+ export const parsePythonicCalls = (command: string): ParsedCall | null => {
99
+ const callMatch = command.match(/^([a-zA-Z0-9_]+)\((.*)\)$/);
100
+ if (!callMatch) return null;
101
+
102
+ const [, name, argsStr] = callMatch;
103
+ const args = parseArguments(argsStr);
104
+ const positionalArgs: any[] = [];
105
+ const keywordArgs: Record<string, any> = {};
106
+
107
+ for (const arg of args) {
108
+ const kwargMatch = arg.match(/^([a-zA-Z0-9_]+)\s*=\s*(.*)$/);
109
+ if (kwargMatch) {
110
+ const [, key, value] = kwargMatch;
111
+ try {
112
+ keywordArgs[key] = JSON.parse(value);
113
+ } catch {
114
+ keywordArgs[key] = value;
115
+ }
116
+ } else {
117
+ try {
118
+ positionalArgs.push(JSON.parse(arg));
119
+ } catch {
120
+ positionalArgs.push(arg);
121
+ }
122
+ }
123
+ }
124
+ return { name, positionalArgs, keywordArgs };
125
+ };
126
+
127
+ export const extractFunctionAndRenderer = (
128
+ code: string,
129
+ ): { functionCode: string; rendererCode?: string } => {
130
+ if (typeof code !== "string") {
131
+ return { functionCode: code };
132
+ }
133
+
134
+ const exportMatch = code.match(/export\s+default\s+/);
135
+ if (!exportMatch) {
136
+ return { functionCode: code };
137
+ }
138
+
139
+ const exportIndex = exportMatch.index!;
140
+ const functionCode = code.substring(0, exportIndex).trim();
141
+ const rendererCode = code.substring(exportIndex).trim();
142
+
143
+ return { functionCode, rendererCode };
144
+ };
145
+
146
+ /**
147
+ * Helper function to extract JSDoc parameters from JSDoc comments.
148
+ */
149
+ const extractJSDocParams = (
150
+ jsdoc: string,
151
+ ): Record<string, JSDocParam & { jsdocDefault?: string }> => {
152
+ const jsdocParams: Record<string, JSDocParam & { jsdocDefault?: string }> =
153
+ {};
154
+ const lines = jsdoc
155
+ .split("\n")
156
+ .map((line) => line.trim().replace(/^\*\s?/, ""));
157
+ const paramRegex =
158
+ /@param\s+\{([^}]+)\}\s+(\[?[a-zA-Z0-9_]+(?:=[^\]]+)?\]?|\S+)\s*-?\s*(.*)?/;
159
+
160
+ for (const line of lines) {
161
+ const paramMatch = line.match(paramRegex);
162
+ if (paramMatch) {
163
+ let [, type, namePart, description] = paramMatch;
164
+ description = description || "";
165
+ let isOptional = false;
166
+ let name = namePart;
167
+ let jsdocDefault: string | undefined = undefined;
168
+
169
+ if (name.startsWith("[") && name.endsWith("]")) {
170
+ isOptional = true;
171
+ name = name.slice(1, -1);
172
+ }
173
+ if (name.includes("=")) {
174
+ const [n, def] = name.split("=");
175
+ name = n.trim();
176
+ jsdocDefault = def.trim().replace(/['"]/g, "");
177
+ }
178
+
179
+ jsdocParams[name] = {
180
+ type: type.toLowerCase(),
181
+ description: description.trim(),
182
+ isOptional,
183
+ defaultValue: undefined,
184
+ jsdocDefault,
185
+ };
186
+ }
187
+ }
188
+ return jsdocParams;
189
+ };
190
+
191
+ /**
192
+ * Helper function to extract function signature information.
193
+ */
194
+ const extractFunctionSignature = (
195
+ functionCode: string,
196
+ ): {
197
+ name: string;
198
+ params: { name: string; defaultValue?: string }[];
199
+ } | null => {
200
+ const functionSignatureMatch = functionCode.match(
201
+ /function\s+([a-zA-Z0-9_]+)\s*\(([^)]*)\)/,
202
+ );
203
+ if (!functionSignatureMatch) {
204
+ return null;
205
+ }
206
+
207
+ const functionName = functionSignatureMatch[1];
208
+ const params = functionSignatureMatch[2]
209
+ .split(",")
210
+ .map((p) => p.trim())
211
+ .filter(Boolean)
212
+ .map((p) => {
213
+ const [name, defaultValue] = p.split("=").map((s) => s.trim());
214
+ return { name, defaultValue };
215
+ });
216
+
217
+ return { name: functionName, params };
218
+ };
219
+
220
+ export const generateSchemaFromCode = (code: string): Schema => {
221
+ const { functionCode } = extractFunctionAndRenderer(code);
222
+
223
+ if (typeof functionCode !== "string") {
224
+ return {
225
+ name: "invalid_code",
226
+ description: "Code is not a valid string.",
227
+ parameters: { type: "object", properties: {}, required: [] },
228
+ };
229
+ }
230
+
231
+ // 1. Extract function signature, name, and parameter names directly from the code
232
+ const signatureInfo = extractFunctionSignature(functionCode);
233
+ if (!signatureInfo) {
234
+ return {
235
+ name: "invalid_function",
236
+ description: "Could not parse function signature.",
237
+ parameters: { type: "object", properties: {}, required: [] },
238
+ };
239
+ }
240
+
241
+ const { name: functionName, params: paramsFromSignature } = signatureInfo;
242
+
243
+ const schema: Schema = {
244
+ name: functionName,
245
+ description: "",
246
+ parameters: {
247
+ type: "object",
248
+ properties: {},
249
+ required: [],
250
+ },
251
+ };
252
+
253
+ // 2. Parse JSDoc comments to get descriptions and types
254
+ const jsdocMatch = functionCode.match(/\/\*\*([\s\S]*?)\*\//);
255
+ let jsdocParams: Record<string, JSDocParam & { jsdocDefault?: string }> = {};
256
+ if (jsdocMatch) {
257
+ const jsdoc = jsdocMatch[1];
258
+ jsdocParams = extractJSDocParams(jsdoc);
259
+
260
+ const descriptionLines = jsdoc
261
+ .split("\n")
262
+ .map((line) => line.trim().replace(/^\*\s?/, ""))
263
+ .filter((line) => !line.startsWith("@") && line);
264
+
265
+ schema.description = descriptionLines.join(" ").trim();
266
+ }
267
+
268
+ // 3. Combine signature parameters with JSDoc info
269
+ for (const param of paramsFromSignature) {
270
+ const paramName = param.name;
271
+ const jsdocInfo = jsdocParams[paramName];
272
+ schema.parameters.properties[paramName] = {
273
+ type: jsdocInfo ? jsdocInfo.type : "any",
274
+ description: jsdocInfo ? jsdocInfo.description : "",
275
+ };
276
+
277
+ // Prefer default from signature, then from JSDoc
278
+ if (param.defaultValue !== undefined) {
279
+ // Try to parse as JSON, fallback to string
280
+ try {
281
+ schema.parameters.properties[paramName].default = JSON.parse(
282
+ param.defaultValue.replace(/'/g, '"'),
283
+ );
284
+ } catch {
285
+ schema.parameters.properties[paramName].default = param.defaultValue;
286
+ }
287
+ } else if (jsdocInfo && jsdocInfo.jsdocDefault !== undefined) {
288
+ schema.parameters.properties[paramName].default = jsdocInfo.jsdocDefault;
289
+ }
290
+
291
+ // A parameter is required if:
292
+ // - Not optional in JSDoc
293
+ // - No default in signature
294
+ // - No default in JSDoc
295
+ const hasDefault =
296
+ param.defaultValue !== undefined ||
297
+ (jsdocInfo && jsdocInfo.jsdocDefault !== undefined);
298
+ if (!jsdocInfo || (!jsdocInfo.isOptional && !hasDefault)) {
299
+ schema.parameters.required.push(paramName);
300
+ }
301
+ }
302
+
303
+ return schema;
304
+ };
305
+
306
+ /**
307
+ * Extracts tool call content from a string using the tool call markers.
308
+ */
309
+ export const extractToolCallContent = (content: string): string | null => {
310
+ const toolCallMatch = content.match(
311
+ /<\|tool_call_start\|>(.*?)<\|tool_call_end\|>/s,
312
+ );
313
+ return toolCallMatch ? toolCallMatch[1].trim() : null;
314
+ };
315
+
316
+ /**
317
+ * Maps positional and keyword arguments to named parameters based on schema.
318
+ */
319
+ export const mapArgsToNamedParams = (
320
+ paramNames: string[],
321
+ positionalArgs: any[],
322
+ keywordArgs: Record<string, any>,
323
+ ): Record<string, any> => {
324
+ const namedParams: Record<string, any> = Object.create(null);
325
+ positionalArgs.forEach((arg, idx) => {
326
+ if (idx < paramNames.length) {
327
+ namedParams[paramNames[idx]] = arg;
328
+ }
329
+ });
330
+ Object.assign(namedParams, keywordArgs);
331
+ return namedParams;
332
+ };
333
+
334
+ export const getErrorMessage = (error: unknown): string => {
335
+ if (error instanceof Error) {
336
+ return error.message;
337
+ }
338
+ if (typeof error === "string") {
339
+ return error;
340
+ }
341
+ if (error && typeof error === "object") {
342
+ return JSON.stringify(error);
343
+ }
344
+ return String(error);
345
+ };
346
+
347
+ /**
348
+ * Adapted from https://stackoverflow.com/questions/11381673/detecting-a-mobile-browser.
349
+ */
350
+ export function isMobileOrTablet() {
351
+ let check = false;
352
+ (function (a: string) {
353
+ if (
354
+ /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(
355
+ a,
356
+ ) ||
357
+ /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
358
+ a.slice(0, 4),
359
+ )
360
+ )
361
+ check = true;
362
+ })(
363
+ navigator.userAgent ||
364
+ navigator.vendor ||
365
+ ("opera" in window && typeof window.opera === "string"
366
+ ? window.opera
367
+ : ""),
368
+ );
369
+ return check;
370
+ }
src/vite-env.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ /// <reference types="vite/client" />
tsconfig.app.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+
18
+ /* Linting */
19
+ "strict": true,
20
+ "noUnusedLocals": true,
21
+ "noUnusedParameters": true,
22
+ "erasableSyntaxOnly": true,
23
+ "noFallthroughCasesInSwitch": true,
24
+ "noUncheckedSideEffectImports": true
25
+ },
26
+ "include": ["src"]
27
+ }
tsconfig.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
tsconfig.node.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2023",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "verbatimModuleSyntax": true,
13
+ "moduleDetection": "force",
14
+ "noEmit": true,
15
+
16
+ /* Linting */
17
+ "strict": true,
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "erasableSyntaxOnly": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "noUncheckedSideEffectImports": true
23
+ },
24
+ "include": ["vite.config.ts"]
25
+ }
vite.config.ts ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import tailwindcss from "@tailwindcss/vite";
4
+
5
+ // https://vite.dev/config/
6
+ export default defineConfig({
7
+ plugins: [react(), tailwindcss()],
8
+ });