ibibrahim Xenova HF Staff commited on
Commit
c675f75
·
verified ·
1 Parent(s): 58f2f9b

Create demo (#1)

Browse files

- [WIP] do not merge yet (0fb17e108386b0d4175564b8f6d1baf9b55ab328)
- Delete index.html (2ea66b78a61812e89c3c0b39a831847eebae54ef)
- Delete style.css (2d84b4823957e674885571e16558b0e5c6b8377c)
- Upload 33 files (5ed8a7ec3ef4619a9bd8e25b887e84ce188d7200)


Co-authored-by: Joshua <Xenova@users.noreply.huggingface.co>

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 CHANGED
@@ -1,29 +1,16 @@
1
- <!DOCTYPE html>
2
  <html lang="en">
3
-
4
- <head>
5
  <meta charset="UTF-8" />
6
- <link rel="stylesheet" href="style.css" />
7
-
 
 
8
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
9
- <title>Transformers.js - Object Detection</title>
10
- </head>
11
-
12
- <body>
13
- <h1>Object Detection w/ 🤗 Transformers.js</h1>
14
- <label id="container" for="upload">
15
- <svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
16
- <path fill="#000"
17
- d="M3.5 24.3a3 3 0 0 1-1.9-.8c-.5-.5-.8-1.2-.8-1.9V2.9c0-.7.3-1.3.8-1.9.6-.5 1.2-.7 2-.7h18.6c.7 0 1.3.2 1.9.7.5.6.7 1.2.7 2v18.6c0 .7-.2 1.4-.7 1.9a3 3 0 0 1-2 .8H3.6Zm0-2.7h18.7V2.9H3.5v18.7Zm2.7-2.7h13.3c.3 0 .5 0 .6-.3v-.7l-3.7-5a.6.6 0 0 0-.6-.2c-.2 0-.4 0-.5.3l-3.5 4.6-2.4-3.3a.6.6 0 0 0-.6-.3c-.2 0-.4.1-.5.3l-2.7 3.6c-.1.2-.2.4 0 .7.1.2.3.3.6.3Z">
18
- </path>
19
- </svg>
20
- Click to upload image
21
- <label id="example">(or try example)</label>
22
- </label>
23
- <label id="status">Loading model...</label>
24
- <input id="upload" type="file" accept="image/*" />
25
-
26
- <script src="index.js" type="module"></script>
27
- </body>
28
-
29
- </html>
 
1
+ <!doctype html>
2
  <html lang="en">
3
+ <head>
 
4
  <meta charset="UTF-8" />
5
+ <link
6
+ rel="icon"
7
+ href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🛠️</text></svg>"
8
+ />
9
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10
+ <title>Granite WebGPU - In-Browser Tool Calling</title>
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+ <script type="module" src="/src/main.tsx"></script>
15
+ </body>
16
+ </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
index.js DELETED
@@ -1,76 +0,0 @@
1
- import { pipeline } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.4.1';
2
-
3
- // Reference the elements that we will need
4
- const status = document.getElementById('status');
5
- const fileUpload = document.getElementById('upload');
6
- const imageContainer = document.getElementById('container');
7
- const example = document.getElementById('example');
8
-
9
- const EXAMPLE_URL = 'https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/city-streets.jpg';
10
-
11
- // Create a new object detection pipeline
12
- status.textContent = 'Loading model...';
13
- const detector = await pipeline('object-detection', 'Xenova/detr-resnet-50');
14
- status.textContent = 'Ready';
15
-
16
- example.addEventListener('click', (e) => {
17
- e.preventDefault();
18
- detect(EXAMPLE_URL);
19
- });
20
-
21
- fileUpload.addEventListener('change', function (e) {
22
- const file = e.target.files[0];
23
- if (!file) {
24
- return;
25
- }
26
-
27
- const reader = new FileReader();
28
-
29
- // Set up a callback when the file is loaded
30
- reader.onload = e2 => detect(e2.target.result);
31
-
32
- reader.readAsDataURL(file);
33
- });
34
-
35
-
36
- // Detect objects in the image
37
- async function detect(img) {
38
- imageContainer.innerHTML = '';
39
- imageContainer.style.backgroundImage = `url(${img})`;
40
-
41
- status.textContent = 'Analysing...';
42
- const output = await detector(img, {
43
- threshold: 0.5,
44
- percentage: true,
45
- });
46
- status.textContent = '';
47
- output.forEach(renderBox);
48
- }
49
-
50
- // Render a bounding box and label on the image
51
- function renderBox({ box, label }) {
52
- const { xmax, xmin, ymax, ymin } = box;
53
-
54
- // Generate a random color for the box
55
- const color = '#' + Math.floor(Math.random() * 0xFFFFFF).toString(16).padStart(6, 0);
56
-
57
- // Draw the box
58
- const boxElement = document.createElement('div');
59
- boxElement.className = 'bounding-box';
60
- Object.assign(boxElement.style, {
61
- borderColor: color,
62
- left: 100 * xmin + '%',
63
- top: 100 * ymin + '%',
64
- width: 100 * (xmax - xmin) + '%',
65
- height: 100 * (ymax - ymin) + '%',
66
- })
67
-
68
- // Draw label
69
- const labelElement = document.createElement('span');
70
- labelElement.textContent = label;
71
- labelElement.className = 'bounding-box-label';
72
- labelElement.style.backgroundColor = color;
73
-
74
- boxElement.appendChild(labelElement);
75
- imageContainer.appendChild(boxElement);
76
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
package.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "granite-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.6",
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
+ }
src/App.tsx ADDED
@@ -0,0 +1,758 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, RotateCcw, Wrench } from "lucide-react";
10
+ import { useLLM } from "./hooks/useLLM";
11
+
12
+ import type { Tool } from "./components/ToolItem";
13
+
14
+ import {
15
+ extractFunctionAndRenderer,
16
+ generateSchemaFromCode,
17
+ getErrorMessage,
18
+ isMobileOrTablet,
19
+ } from "./utils";
20
+
21
+ import { DB_NAME, STORE_NAME, SETTINGS_STORE_NAME } from "./constants/db";
22
+
23
+ import { DEFAULT_TOOLS, TEMPLATE } from "./tools";
24
+ import ToolResultRenderer from "./components/ToolResultRenderer";
25
+ import ToolCallIndicator from "./components/ToolCallIndicator";
26
+ import ToolItem from "./components/ToolItem";
27
+ import ResultBlock from "./components/ResultBlock";
28
+ import ExamplePrompts from "./components/ExamplePrompts";
29
+
30
+ import { LoadingScreen } from "./components/LoadingScreen";
31
+
32
+ interface RenderInfo {
33
+ call: string;
34
+ result?: any;
35
+ renderer?: string;
36
+ input?: Record<string, any>;
37
+ error?: string;
38
+ }
39
+
40
+ interface BaseMessage {
41
+ role: "system" | "user" | "assistant";
42
+ content: string;
43
+ }
44
+ interface ToolMessage {
45
+ role: "tool";
46
+ content: string;
47
+ renderInfo: RenderInfo[]; // Rich data for the UI
48
+ }
49
+ type Message = BaseMessage | ToolMessage;
50
+
51
+ async function getDB(): Promise<IDBPDatabase> {
52
+ return openDB(DB_NAME, 1, {
53
+ upgrade(db) {
54
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
55
+ db.createObjectStore(STORE_NAME, {
56
+ keyPath: "id",
57
+ autoIncrement: true,
58
+ });
59
+ }
60
+ if (!db.objectStoreNames.contains(SETTINGS_STORE_NAME)) {
61
+ db.createObjectStore(SETTINGS_STORE_NAME, { keyPath: "key" });
62
+ }
63
+ },
64
+ });
65
+ }
66
+
67
+ const App: React.FC = () => {
68
+ const [messages, setMessages] = useState<Message[]>([]);
69
+ const [tools, setTools] = useState<Tool[]>([]);
70
+ const [input, setInput] = useState<string>("");
71
+ const [isGenerating, setIsGenerating] = useState<boolean>(false);
72
+ const isMobile = useMemo(isMobileOrTablet, []);
73
+ const [selectedModel, setSelectedModel] = useState<string>(
74
+ isMobile ? "350M" : "1B",
75
+ );
76
+ const [isModelDropdownOpen, setIsModelDropdownOpen] =
77
+ useState<boolean>(false);
78
+ const [isToolsPanelVisible, setIsToolsPanelVisible] =
79
+ useState<boolean>(false);
80
+ const chatContainerRef = useRef<HTMLDivElement>(null);
81
+ const debounceTimers = useRef<Record<number, NodeJS.Timeout>>({});
82
+ const toolsContainerRef = useRef<HTMLDivElement>(null);
83
+ const inputRef = useRef<HTMLInputElement>(null);
84
+ const {
85
+ isLoading,
86
+ isReady,
87
+ error,
88
+ progress,
89
+ loadModel,
90
+ generateResponse,
91
+ clearPastKeyValues,
92
+ } = useLLM(selectedModel);
93
+
94
+ const loadTools = useCallback(async (): Promise<void> => {
95
+ const db = await getDB();
96
+ const allTools: Tool[] = await db.getAll(STORE_NAME);
97
+ if (allTools.length === 0) {
98
+ const defaultTools: Tool[] = Object.entries(DEFAULT_TOOLS).map(
99
+ ([name, code], id) => ({
100
+ id,
101
+ name,
102
+ code,
103
+ enabled: true,
104
+ isCollapsed: false,
105
+ }),
106
+ );
107
+ const tx = db.transaction(STORE_NAME, "readwrite");
108
+ await Promise.all(defaultTools.map((tool) => tx.store.put(tool)));
109
+ await tx.done;
110
+ setTools(defaultTools);
111
+ } else {
112
+ setTools(allTools.map((t) => ({ ...t, isCollapsed: false })));
113
+ }
114
+ }, []);
115
+
116
+ useEffect(() => {
117
+ loadTools();
118
+ }, [loadTools]);
119
+
120
+ useEffect(() => {
121
+ if (chatContainerRef.current) {
122
+ chatContainerRef.current.scrollTop =
123
+ chatContainerRef.current.scrollHeight;
124
+ }
125
+ }, [messages]);
126
+
127
+ const updateToolInDB = async (tool: Tool): Promise<void> => {
128
+ const db = await getDB();
129
+ await db.put(STORE_NAME, tool);
130
+ };
131
+
132
+ const saveToolDebounced = (tool: Tool): void => {
133
+ if (tool.id !== undefined && debounceTimers.current[tool.id]) {
134
+ clearTimeout(debounceTimers.current[tool.id]);
135
+ }
136
+ if (tool.id !== undefined) {
137
+ debounceTimers.current[tool.id] = setTimeout(() => {
138
+ updateToolInDB(tool);
139
+ }, 300);
140
+ }
141
+ };
142
+
143
+ const clearChat = useCallback(() => {
144
+ setMessages([]);
145
+ clearPastKeyValues();
146
+ }, [clearPastKeyValues]);
147
+
148
+ const addTool = async (): Promise<void> => {
149
+ const newTool: Omit<Tool, "id"> = {
150
+ name: "new_tool",
151
+ code: TEMPLATE,
152
+ enabled: true,
153
+ isCollapsed: false,
154
+ };
155
+ const db = await getDB();
156
+ const id = await db.add(STORE_NAME, newTool);
157
+ setTools((prev) => {
158
+ const updated = [...prev, { ...newTool, id: id as number }];
159
+ setTimeout(() => {
160
+ if (toolsContainerRef.current) {
161
+ toolsContainerRef.current.scrollTop =
162
+ toolsContainerRef.current.scrollHeight;
163
+ }
164
+ }, 0);
165
+ return updated;
166
+ });
167
+ clearChat();
168
+ };
169
+
170
+ const deleteTool = async (id: number): Promise<void> => {
171
+ if (debounceTimers.current[id]) {
172
+ clearTimeout(debounceTimers.current[id]);
173
+ }
174
+ const db = await getDB();
175
+ await db.delete(STORE_NAME, id);
176
+ setTools(tools.filter((tool) => tool.id !== id));
177
+ clearChat();
178
+ };
179
+
180
+ const toggleToolEnabled = (id: number): void => {
181
+ let changedTool: Tool | undefined;
182
+ const newTools = tools.map((tool) => {
183
+ if (tool.id === id) {
184
+ changedTool = { ...tool, enabled: !tool.enabled };
185
+ return changedTool;
186
+ }
187
+ return tool;
188
+ });
189
+ setTools(newTools);
190
+ if (changedTool) saveToolDebounced(changedTool);
191
+ };
192
+
193
+ const toggleToolCollapsed = (id: number): void => {
194
+ setTools(
195
+ tools.map((tool) =>
196
+ tool.id === id ? { ...tool, isCollapsed: !tool.isCollapsed } : tool,
197
+ ),
198
+ );
199
+ };
200
+
201
+ const expandTool = (id: number): void => {
202
+ setTools(
203
+ tools.map((tool) =>
204
+ tool.id === id ? { ...tool, isCollapsed: false } : tool,
205
+ ),
206
+ );
207
+ };
208
+
209
+ const handleToolCodeChange = (id: number, newCode: string): void => {
210
+ let changedTool: Tool | undefined;
211
+ const newTools = tools.map((tool) => {
212
+ if (tool.id === id) {
213
+ const { functionCode } = extractFunctionAndRenderer(newCode);
214
+ const schema = generateSchemaFromCode(functionCode);
215
+ changedTool = { ...tool, code: newCode, name: schema.name };
216
+ return changedTool;
217
+ }
218
+ return tool;
219
+ });
220
+ setTools(newTools);
221
+ if (changedTool) saveToolDebounced(changedTool);
222
+ };
223
+
224
+ interface ToolCallPayload {
225
+ name: string;
226
+ arguments?: Record<string, any>;
227
+ }
228
+
229
+ const extractToolCalls = (text: string): ToolCallPayload[] => {
230
+ const matches = Array.from(
231
+ text.matchAll(/<tool_call>([\s\S]*?)<\/tool_call>/g),
232
+ );
233
+ const toolCalls: ToolCallPayload[] = [];
234
+
235
+ for (const match of matches) {
236
+ try {
237
+ const parsed = JSON.parse(match[1].trim());
238
+ if (parsed && typeof parsed.name === "string") {
239
+ toolCalls.push({
240
+ name: parsed.name,
241
+ arguments: parsed.arguments ?? {},
242
+ });
243
+ }
244
+ } catch {
245
+ // ignore malformed tool call payloads
246
+ }
247
+ }
248
+
249
+ return toolCalls;
250
+ };
251
+
252
+ const executeToolCall = async (
253
+ toolCall: ToolCallPayload,
254
+ ): Promise<{
255
+ serializedResult: string;
256
+ rendererCode?: string;
257
+ input: Record<string, any>;
258
+ }> => {
259
+ const toolToUse = tools.find((t) => t.name === toolCall.name && t.enabled);
260
+ if (!toolToUse)
261
+ throw new Error(`Tool '${toolCall.name}' not found or is disabled.`);
262
+
263
+ const { functionCode, rendererCode } = extractFunctionAndRenderer(
264
+ toolToUse.code,
265
+ );
266
+ const schema = generateSchemaFromCode(functionCode);
267
+ const properties = schema.parameters?.properties ?? {};
268
+ const paramNames = Object.keys(properties);
269
+ const requiredParams = schema.parameters?.required ?? [];
270
+ const callArgs = toolCall.arguments ?? {};
271
+
272
+ const finalArgs: any[] = [];
273
+ const resolvedArgs: Record<string, any> = Object.create(null);
274
+
275
+ for (const paramName of paramNames) {
276
+ const propertyConfig = properties[paramName] ?? {};
277
+ if (Object.prototype.hasOwnProperty.call(callArgs, paramName)) {
278
+ const value = callArgs[paramName];
279
+ finalArgs.push(value);
280
+ resolvedArgs[paramName] = value;
281
+ } else if (
282
+ Object.prototype.hasOwnProperty.call(propertyConfig, "default")
283
+ ) {
284
+ const value = propertyConfig.default;
285
+ finalArgs.push(value);
286
+ resolvedArgs[paramName] = value;
287
+ } else if (!requiredParams.includes(paramName)) {
288
+ finalArgs.push(undefined);
289
+ resolvedArgs[paramName] = undefined;
290
+ } else {
291
+ throw new Error(`Missing required argument: ${paramName}`);
292
+ }
293
+ }
294
+
295
+ for (const extraKey of Object.keys(callArgs)) {
296
+ if (!Object.prototype.hasOwnProperty.call(resolvedArgs, extraKey)) {
297
+ resolvedArgs[extraKey] = callArgs[extraKey];
298
+ }
299
+ }
300
+
301
+ const bodyMatch = functionCode.match(/function[^{]+\{([\s\S]*)\}/);
302
+ if (!bodyMatch) {
303
+ throw new Error(
304
+ "Could not parse function body. Ensure it's a standard `function` declaration.",
305
+ );
306
+ }
307
+ const body = bodyMatch[1];
308
+ const AsyncFunction = Object.getPrototypeOf(
309
+ async function () {},
310
+ ).constructor;
311
+ const func = new AsyncFunction(...paramNames, body);
312
+ const result = await func(...finalArgs);
313
+
314
+ return {
315
+ serializedResult: JSON.stringify(result),
316
+ rendererCode,
317
+ input: resolvedArgs,
318
+ };
319
+ };
320
+
321
+ const executeToolCalls = async (
322
+ toolCalls: ToolCallPayload[],
323
+ ): Promise<RenderInfo[]> => {
324
+ if (toolCalls.length === 0) {
325
+ return [{ call: "", error: "No valid tool calls found." }];
326
+ }
327
+
328
+ const results: RenderInfo[] = [];
329
+
330
+ for (const toolCall of toolCalls) {
331
+ const callDisplay = `<tool_call>${JSON.stringify(toolCall)}</tool_call>`;
332
+ try {
333
+ const { serializedResult, rendererCode, input } =
334
+ await executeToolCall(toolCall);
335
+
336
+ let parsedResult: unknown;
337
+ try {
338
+ parsedResult = JSON.parse(serializedResult);
339
+ } catch {
340
+ parsedResult = serializedResult;
341
+ }
342
+
343
+ results.push({
344
+ call: callDisplay,
345
+ result: parsedResult,
346
+ renderer: rendererCode,
347
+ input,
348
+ });
349
+ } catch (error) {
350
+ results.push({
351
+ call: callDisplay,
352
+ error: getErrorMessage(error),
353
+ });
354
+ }
355
+ }
356
+
357
+ return results;
358
+ };
359
+
360
+ const handleSendMessage = async (): Promise<void> => {
361
+ if (!input.trim() || !isReady) return;
362
+
363
+ const userMessage: Message = { role: "user", content: input };
364
+ let currentMessages: Message[] = [...messages, userMessage];
365
+ setMessages(currentMessages);
366
+ setInput("");
367
+ setIsGenerating(true);
368
+
369
+ try {
370
+ const toolSchemas = tools
371
+ .filter((tool) => tool.enabled)
372
+ .map((tool) => generateSchemaFromCode(tool.code));
373
+
374
+ while (true) {
375
+ const messagesForGeneration = [...currentMessages];
376
+
377
+ setMessages([...currentMessages, { role: "assistant", content: "" }]);
378
+
379
+ let accumulatedContent = "";
380
+ const response = await generateResponse(
381
+ messagesForGeneration,
382
+ toolSchemas,
383
+ (token: string) => {
384
+ accumulatedContent += token;
385
+ setMessages((current) => {
386
+ const updated = [...current];
387
+ updated[updated.length - 1] = {
388
+ role: "assistant",
389
+ content: accumulatedContent,
390
+ };
391
+ return updated;
392
+ });
393
+ },
394
+ );
395
+
396
+ currentMessages.push({ role: "assistant", content: response });
397
+ const toolCalls = extractToolCalls(response);
398
+
399
+ if (toolCalls.length > 0) {
400
+ const toolResults = await executeToolCalls(toolCalls);
401
+
402
+ const toolMessage: ToolMessage = {
403
+ role: "tool",
404
+ content: JSON.stringify(toolResults.map((r) => r.result ?? null)),
405
+ renderInfo: toolResults,
406
+ };
407
+ currentMessages.push(toolMessage);
408
+ setMessages([...currentMessages]);
409
+ continue;
410
+ } else {
411
+ setMessages(currentMessages);
412
+ break;
413
+ }
414
+ }
415
+ } catch (error) {
416
+ const errorMessage = getErrorMessage(error);
417
+ setMessages([
418
+ ...currentMessages,
419
+ {
420
+ role: "assistant",
421
+ content: `Error generating response: ${errorMessage}`,
422
+ },
423
+ ]);
424
+ } finally {
425
+ setIsGenerating(false);
426
+ setTimeout(() => inputRef.current?.focus(), 0);
427
+ }
428
+ };
429
+
430
+ const loadSelectedModel = useCallback(async (): Promise<void> => {
431
+ try {
432
+ await loadModel();
433
+ } catch (error) {
434
+ console.error("Failed to load model:", error);
435
+ }
436
+ }, [selectedModel, loadModel]);
437
+
438
+ const saveSelectedModel = useCallback(async (modelId: string) => {
439
+ try {
440
+ const db = await getDB();
441
+ await db.put(SETTINGS_STORE_NAME, {
442
+ key: "selectedModelId",
443
+ value: modelId,
444
+ });
445
+ } catch (error) {
446
+ console.error("Failed to save selected model ID:", error);
447
+ }
448
+ }, []);
449
+
450
+ const loadSelectedModelId = useCallback(async (): Promise<void> => {
451
+ try {
452
+ const db = await getDB();
453
+ const stored = await db.get(SETTINGS_STORE_NAME, "selectedModelId");
454
+ if (stored && stored.value) {
455
+ setSelectedModel(stored.value);
456
+ }
457
+ } catch (error) {
458
+ console.error("Failed to load selected model ID:", error);
459
+ }
460
+ }, []);
461
+
462
+ useEffect(() => {
463
+ loadSelectedModelId();
464
+ }, [loadSelectedModelId]);
465
+
466
+ const handleModelSelect = async (modelId: string) => {
467
+ setSelectedModel(modelId);
468
+ setIsModelDropdownOpen(false);
469
+ await saveSelectedModel(modelId);
470
+ };
471
+
472
+ const handleExampleClick = async (messageText: string): Promise<void> => {
473
+ if (!isReady || isGenerating) return;
474
+ setInput(messageText);
475
+
476
+ const userMessage: Message = { role: "user", content: messageText };
477
+ const currentMessages: Message[] = [...messages, userMessage];
478
+ setMessages(currentMessages);
479
+ setInput("");
480
+ setIsGenerating(true);
481
+
482
+ try {
483
+ const toolSchemas = tools
484
+ .filter((tool) => tool.enabled)
485
+ .map((tool) => generateSchemaFromCode(tool.code));
486
+
487
+ while (true) {
488
+ const messagesForGeneration = [...currentMessages];
489
+
490
+ setMessages([...currentMessages, { role: "assistant", content: "" }]);
491
+
492
+ let accumulatedContent = "";
493
+ const response = await generateResponse(
494
+ messagesForGeneration,
495
+ toolSchemas,
496
+ (token: string) => {
497
+ accumulatedContent += token;
498
+ setMessages((current) => {
499
+ const updated = [...current];
500
+ updated[updated.length - 1] = {
501
+ role: "assistant",
502
+ content: accumulatedContent,
503
+ };
504
+ return updated;
505
+ });
506
+ },
507
+ );
508
+
509
+ currentMessages.push({ role: "assistant", content: response });
510
+ const toolCalls = extractToolCalls(response);
511
+
512
+ if (toolCalls.length > 0) {
513
+ const toolResults = await executeToolCalls(toolCalls);
514
+
515
+ const toolMessage: ToolMessage = {
516
+ role: "tool",
517
+ content: JSON.stringify(toolResults.map((r) => r.result ?? null)),
518
+ renderInfo: toolResults,
519
+ };
520
+ currentMessages.push(toolMessage);
521
+ setMessages([...currentMessages]);
522
+ continue;
523
+ } else {
524
+ setMessages(currentMessages);
525
+ break;
526
+ }
527
+ }
528
+ } catch (error) {
529
+ const errorMessage = getErrorMessage(error);
530
+ setMessages([
531
+ ...currentMessages,
532
+ {
533
+ role: "assistant",
534
+ content: `Error generating response: ${errorMessage}`,
535
+ },
536
+ ]);
537
+ } finally {
538
+ setIsGenerating(false);
539
+ setTimeout(() => inputRef.current?.focus(), 0);
540
+ }
541
+ };
542
+
543
+ return (
544
+ <div className="font-sans min-h-screen bg-gradient-to-br from-[#031b4e] via-[#06183d] to-[#010409] text-gray-100 text-[16px] md:text-[17px]">
545
+ {!isReady ? (
546
+ <LoadingScreen
547
+ isLoading={isLoading}
548
+ progress={progress}
549
+ error={error}
550
+ loadSelectedModel={loadSelectedModel}
551
+ selectedModelId={selectedModel}
552
+ isModelDropdownOpen={isModelDropdownOpen}
553
+ setIsModelDropdownOpen={setIsModelDropdownOpen}
554
+ handleModelSelect={handleModelSelect}
555
+ />
556
+ ) : (
557
+ <div className="flex h-screen text-gray-100 w-full gap-6 py-10 px-8">
558
+ <div className="flex-1 flex flex-col p-6 bg-white/5 backdrop-blur-lg border border-white/10 rounded-3xl shadow-[0_35px_65px_rgba(3,27,78,0.55)] min-h-0">
559
+ <div className="flex items-center justify-between mb-6">
560
+ <div className="space-y-1">
561
+ <span className="inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.35em] text-[#78a9ff]">
562
+ IBM Granite
563
+ </span>
564
+ <h1 className="text-3xl font-semibold text-white">
565
+ Granite-4.0 Tool Studio
566
+ </h1>
567
+ </div>
568
+ <div className="flex items-center gap-3">
569
+ <button
570
+ disabled={isGenerating}
571
+ onClick={clearChat}
572
+ className={`h-10 flex items-center px-4 rounded-full font-semibold text-sm transition-all border ${
573
+ isGenerating
574
+ ? "border-white/15 bg-white/10 text-[#a6c8ff] opacity-50 cursor-not-allowed"
575
+ : "border-white/20 bg-white/8 text-[#d0e2ff] hover:border-[#78a9ff]/50 hover:bg-[#0f62fe]/15"
576
+ }`}
577
+ title="Clear chat"
578
+ >
579
+ <RotateCcw size={14} className="mr-2" /> Reset Thread
580
+ </button>
581
+ <button
582
+ onClick={() =>
583
+ setIsToolsPanelVisible((previous) => !previous)
584
+ }
585
+ className={`h-10 flex items-center px-4 rounded-full font-semibold text-sm transition-all border ${
586
+ isToolsPanelVisible
587
+ ? "border-[#78a9ff]/60 bg-[#0f62fe]/25 text-white shadow-[0_10px_25px_rgba(15,98,254,0.25)]"
588
+ : "border-white/20 bg-white/8 text-[#d0e2ff] hover:border-[#78a9ff]/50 hover:bg-[#0f62fe]/15"
589
+ }`}
590
+ >
591
+ <Wrench size={16} className="mr-2" />
592
+ {isToolsPanelVisible ? "Hide Tools" : "Show Tools"}
593
+ </button>
594
+ </div>
595
+ </div>
596
+ <div
597
+ ref={chatContainerRef}
598
+ className="flex-grow bg-[#0b1e3f]/80 border border-white/10 rounded-2xl p-6 overflow-y-auto mb-6 space-y-5 shadow-inner min-h-0"
599
+ >
600
+ {messages.length === 0 && isReady ? (
601
+ <ExamplePrompts onExampleClick={handleExampleClick} />
602
+ ) : (
603
+ messages.map((msg, index) => {
604
+ const key = `${msg.role}-${index}`;
605
+ if (msg.role === "user") {
606
+ return (
607
+ <div key={key} className="flex justify-end">
608
+ <div className="px-4 py-3 rounded-2xl max-w-md bg-[#0f62fe]/30 border border-[#78a9ff]/40 shadow-[0_20px_45px_rgba(10,49,140,0.25)]">
609
+ <p className="text-md text-white whitespace-pre-wrap">
610
+ {msg.content}
611
+ </p>
612
+ </div>
613
+ </div>
614
+ );
615
+ }
616
+ if (msg.role === "assistant") {
617
+ const isToolCall = msg.content.includes("<tool_call>");
618
+ if (isToolCall) {
619
+ const nextMessage = messages[index + 1];
620
+ const isCompleted = nextMessage?.role === "tool";
621
+ const hasError =
622
+ isCompleted &&
623
+ (nextMessage as ToolMessage).renderInfo.some(
624
+ (info) => !!info.error,
625
+ );
626
+ return (
627
+ <div key={key} className="flex justify-start">
628
+ <div className="px-4 py-3 rounded-2xl bg-white/8 border border-[#0f62fe]/30 shadow-[0_18px_50px_rgba(0,0,0,0.35)]">
629
+ <ToolCallIndicator
630
+ content={msg.content}
631
+ isRunning={!isCompleted}
632
+ hasError={hasError}
633
+ />
634
+ </div>
635
+ </div>
636
+ );
637
+ }
638
+ return (
639
+ <div key={key} className="flex justify-start">
640
+ <div className="px-4 py-3 rounded-2xl max-w-md bg-white/8 border border-white/15 shadow-[0_18px_50px_rgba(0,0,0,0.35)]">
641
+ <p className="text-md text-[#d0e2ff] whitespace-pre-wrap">
642
+ {msg.content}
643
+ </p>
644
+ </div>
645
+ </div>
646
+ );
647
+ }
648
+ if (msg.role === "tool") {
649
+ const visibleToolResults = msg.renderInfo.filter(
650
+ (info) =>
651
+ info.error || (info.result != null && info.renderer),
652
+ );
653
+ if (visibleToolResults.length === 0) return null;
654
+ return (
655
+ <div key={key} className="flex justify-start">
656
+ <div className="p-4 rounded-2xl bg-white/8 border border-white/15 max-w-lg shadow-[0_18px_50px_rgba(0,0,0,0.35)]">
657
+ <div className="space-y-4">
658
+ {visibleToolResults.map((info, idx) => (
659
+ <div className="flex flex-col gap-2" key={idx}>
660
+ <div className="text-xs text-[#a6c8ff] font-mono">
661
+ {info.call}
662
+ </div>
663
+ {info.error ? (
664
+ <ResultBlock error={info.error} />
665
+ ) : (
666
+ <ToolResultRenderer
667
+ result={info.result}
668
+ rendererCode={info.renderer}
669
+ input={info.input}
670
+ />
671
+ )}
672
+ </div>
673
+ ))}
674
+ </div>
675
+ </div>
676
+ </div>
677
+ );
678
+ }
679
+ return null;
680
+ })
681
+ )}
682
+ </div>
683
+ <div className="flex items-center gap-3">
684
+ <div className="flex flex-1 items-center bg-white/5 border border-white/10 rounded-2xl overflow-hidden shadow-[0_15px_45px_rgba(0,0,0,0.35)]">
685
+ <input
686
+ ref={inputRef}
687
+ type="text"
688
+ value={input}
689
+ onChange={(e) => setInput(e.target.value)}
690
+ onKeyDown={(e) =>
691
+ e.key === "Enter" &&
692
+ !isGenerating &&
693
+ isReady &&
694
+ handleSendMessage()
695
+ }
696
+ disabled={isGenerating || !isReady}
697
+ className="flex-grow bg-transparent px-5 py-3 text-lg text-white placeholder:text-[#a6c8ff]/70 focus:outline-none disabled:opacity-40"
698
+ placeholder={
699
+ isReady
700
+ ? "Type your message here..."
701
+ : "Load a Granite model to enable chat"
702
+ }
703
+ />
704
+ <button
705
+ onClick={handleSendMessage}
706
+ disabled={isGenerating || !isReady}
707
+ className="h-full px-5 py-3 bg-[#0f62fe] hover:bg-[#0043ce] disabled:bg-[#0f62fe]/40 disabled:cursor-not-allowed text-white font-semibold transition-all"
708
+ >
709
+ <Play size={28} />
710
+ </button>
711
+ </div>
712
+ </div>
713
+ </div>
714
+ {isToolsPanelVisible && (
715
+ <div className="w-full md:w-1/2 flex flex-col p-6 bg-white/5 backdrop-blur-lg border border-white/10 rounded-3xl shadow-[0_35px_65px_rgba(3,27,78,0.55)] min-h-0">
716
+ <div className="flex justify-between items-center mb-6">
717
+ <div>
718
+ <span className="text-xs font-semibold uppercase tracking-[0.25em] text-[#78a9ff]">
719
+ Tool Workspace
720
+ </span>
721
+ <h2 className="text-2xl font-semibold text-white mt-1">
722
+ Tools
723
+ </h2>
724
+ </div>
725
+ <button
726
+ onClick={addTool}
727
+ className="flex items-center bg-gradient-to-r from-[#0f62fe] to-[#4589ff] hover:brightness-110 text-white font-semibold py-2 px-4 rounded-full transition-all shadow-[0_15px_35px_rgba(15,98,254,0.35)]"
728
+ >
729
+ <Plus size={16} className="mr-2" /> Add Tool
730
+ </button>
731
+ </div>
732
+ <div
733
+ ref={toolsContainerRef}
734
+ className="flex-grow bg-[#0b1e3f]/60 border border-white/10 rounded-2xl p-4 overflow-y-auto space-y-3"
735
+ >
736
+ {tools.map((tool) => (
737
+ <ToolItem
738
+ key={tool.id}
739
+ tool={tool}
740
+ onToggleEnabled={() => toggleToolEnabled(tool.id)}
741
+ onToggleCollapsed={() => toggleToolCollapsed(tool.id)}
742
+ onExpand={() => expandTool(tool.id)}
743
+ onDelete={() => deleteTool(tool.id)}
744
+ onCodeChange={(newCode) =>
745
+ handleToolCodeChange(tool.id, newCode)
746
+ }
747
+ />
748
+ ))}
749
+ </div>
750
+ </div>
751
+ )}
752
+ </div>
753
+ )}
754
+ </div>
755
+ );
756
+ };
757
+
758
+ export default App;
src/components/ExamplePrompts.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-3xl font-semibold text-white mb-1">Try an example</h2>
16
+ <p className="text-md text-[#9bb5ff]">Click one to get started</p>
17
+ </div>
18
+
19
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 max-w-4xl w-full px-4">
20
+ {(examples || DEFAULT_EXAMPLES).map((example, index) => (
21
+ <button
22
+ key={index}
23
+ onClick={() => onExampleClick(example.messageText)}
24
+ className="group relative overflow-hidden rounded-2xl border border-white/12 bg-white/8 text-left shadow-[0_22px_55px_rgba(3,27,78,0.35)] transition-all hover:-translate-y-0.5 hover:shadow-[0_28px_65px_rgba(15,98,254,0.35)]"
25
+ >
26
+ <span className="pointer-events-none absolute inset-0 bg-gradient-to-r from-[#0f62fe]/25 via-transparent to-transparent opacity-0 transition-opacity group-hover:opacity-100" />
27
+ <div className="relative flex items-center gap-4 px-5 py-4">
28
+ <span className="flex size-12 flex-shrink-0 items-center justify-center rounded-full border border-[#78a9ff]/30 bg-[#0f62fe]/15 text-xl text-[#a6c8ff] transition-all group-hover:scale-105 group-hover:bg-[#0f62fe]/25 group-hover:text-white">
29
+ {example.icon}
30
+ </span>
31
+ <div className="flex flex-1 items-center">
32
+ <span className="text-md font-medium text-white">
33
+ {example.displayText}
34
+ </span>
35
+ </div>
36
+ <span className="flex size-10 items-center justify-center rounded-full border border-white/10 bg-white/10 text-[#9bb5ff] transition-all group-hover:border-[#78a9ff]/40 group-hover:bg-[#0f62fe]/35 group-hover:text-white">
37
+
38
+ </span>
39
+ </div>
40
+ </button>
41
+ ))}
42
+ </div>
43
+ </div>
44
+ );
45
+
46
+ export default ExamplePrompts;
src/components/LoadingScreen.tsx ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ChevronDown } from "lucide-react";
2
+
3
+ import { MODEL_OPTIONS } from "../constants/models";
4
+ import IBMLogo from "./icons/IBMLogo";
5
+ import HfLogo from "./icons/HfLogo";
6
+
7
+ import { useEffect, useRef } from "react";
8
+
9
+ // Define the structure of our animated dots
10
+ interface Dot {
11
+ x: number;
12
+ y: number;
13
+ vx: number;
14
+ vy: number;
15
+ radius: number;
16
+ opacity: number;
17
+ }
18
+
19
+ export const LoadingScreen = ({
20
+ isLoading,
21
+ progress,
22
+ error,
23
+ loadSelectedModel,
24
+ selectedModelId,
25
+ isModelDropdownOpen,
26
+ setIsModelDropdownOpen,
27
+ handleModelSelect,
28
+ }: {
29
+ isLoading: boolean;
30
+ progress: number;
31
+ error: string | null;
32
+ loadSelectedModel: () => void;
33
+ selectedModelId: string;
34
+ isModelDropdownOpen: boolean;
35
+ setIsModelDropdownOpen: (isOpen: boolean) => void;
36
+ handleModelSelect: (modelId: string) => void;
37
+ }) => {
38
+ const model = MODEL_OPTIONS.find((opt) => opt.id === selectedModelId);
39
+ const canvasRef = useRef<HTMLCanvasElement>(null);
40
+
41
+ // Background Animation Effect
42
+ useEffect(() => {
43
+ const canvas = canvasRef.current;
44
+ if (!canvas) return;
45
+
46
+ const ctx = canvas.getContext("2d");
47
+ if (!ctx) return;
48
+
49
+ let animationFrameId: number;
50
+ let dots: Dot[] = [];
51
+ const maxConnectionDistance = 130; // Max distance to draw a line between dots
52
+ const dotSpeed = 0.3; // How fast dots move
53
+
54
+ const setup = () => {
55
+ canvas.width = window.innerWidth;
56
+ canvas.height = window.innerHeight;
57
+ dots = [];
58
+ // Adjust dot density based on screen area
59
+ const numDots = Math.floor((canvas.width * canvas.height) / 20000);
60
+
61
+ for (let i = 0; i < numDots; ++i) {
62
+ dots.push({
63
+ x: Math.random() * canvas.width,
64
+ y: Math.random() * canvas.height,
65
+ vx: (Math.random() - 0.5) * dotSpeed, // Random horizontal velocity
66
+ vy: (Math.random() - 0.5) * dotSpeed, // Random vertical velocity
67
+ radius: Math.random() * 1.5 + 0.5,
68
+ opacity: Math.random() * 0.5 + 0.2,
69
+ });
70
+ }
71
+ };
72
+
73
+ const draw = () => {
74
+ if (!ctx) return;
75
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
76
+
77
+ // 1. Update and draw dots
78
+ dots.forEach((dot) => {
79
+ // Update position
80
+ dot.x += dot.vx;
81
+ dot.y += dot.vy;
82
+
83
+ // Bounce off edges
84
+ if (dot.x <= 0 || dot.x >= canvas.width) dot.vx *= -1;
85
+ if (dot.y <= 0 || dot.y >= canvas.height) dot.vy *= -1;
86
+
87
+ // Draw dot
88
+ ctx.beginPath();
89
+ ctx.arc(dot.x, dot.y, dot.radius, 0, Math.PI * 2);
90
+ ctx.fillStyle = `rgba(255, 255, 255, ${dot.opacity})`;
91
+ ctx.fill();
92
+ });
93
+
94
+ // 2. Draw connecting lines
95
+ ctx.lineWidth = 0.5; // Use a thin line for connections
96
+ for (let i = 0; i < dots.length; i++) {
97
+ for (let j = i + 1; j < dots.length; j++) {
98
+ const dot1 = dots[i];
99
+ const dot2 = dots[j];
100
+ const dx = dot1.x - dot2.x;
101
+ const dy = dot1.y - dot2.y;
102
+ const distance = Math.sqrt(dx * dx + dy * dy);
103
+
104
+ // If dots are close enough, draw a line
105
+ if (distance < maxConnectionDistance) {
106
+ // Calculate opacity based on distance (closer = more opaque)
107
+ const opacity = 1 - distance / maxConnectionDistance;
108
+ ctx.strokeStyle = `rgba(255, 255, 255, ${opacity * 0.3})`; // Faint white lines
109
+ ctx.beginPath();
110
+ ctx.moveTo(dot1.x, dot1.y);
111
+ ctx.lineTo(dot2.x, dot2.y);
112
+ ctx.stroke();
113
+ }
114
+ }
115
+ }
116
+
117
+ animationFrameId = requestAnimationFrame(draw);
118
+ };
119
+
120
+ const handleResize = () => {
121
+ cancelAnimationFrame(animationFrameId);
122
+ setup();
123
+ draw();
124
+ };
125
+
126
+ setup();
127
+ draw();
128
+
129
+ window.addEventListener("resize", handleResize);
130
+
131
+ return () => {
132
+ window.removeEventListener("resize", handleResize);
133
+ cancelAnimationFrame(animationFrameId);
134
+ };
135
+ }, []);
136
+
137
+ return (
138
+ <div className="relative flex flex-col items-center justify-center h-screen bg-gradient-to-br from-[#031b4e] via-[#06183d] to-[#010409] text-gray-100 text-[16px] md:text-[17px] p-8 overflow-hidden">
139
+ {/* Background Canvas for Animation */}
140
+ <canvas
141
+ ref={canvasRef}
142
+ className="absolute top-0 left-0 w-full h-full z-0"
143
+ />
144
+
145
+ {/* Vignette Overlay */}
146
+ <div className="absolute top-0 left-0 w-full h-full z-10 bg-[radial-gradient(ellipse_at_center,_rgba(3,27,78,0)_30%,_rgba(1,4,9,0.85)_95%)]"></div>
147
+
148
+ {/* Main Content */}
149
+ <div className="relative z-20 max-w-3xl w-full flex flex-col items-center bg-white/5 border border-white/10 backdrop-blur-xl rounded-3xl p-10 shadow-[0_35px_65px_rgba(3,27,78,0.55)] space-y-8">
150
+ <div className="flex items-center justify-center gap-6 text-5xl md:text-6xl">
151
+ <a
152
+ href="https://huggingface.co/ibm-granite"
153
+ target="_blank"
154
+ rel="noopener noreferrer"
155
+ title="IBM Granite"
156
+ >
157
+ <div className="size-24 md:size-28 bg-blue-500 rounded-sm p-2 flex items-center justify-center">
158
+ <IBMLogo className="text-white" />
159
+ </div>
160
+ </a>
161
+ <span className="text-[#78a9ff]">×</span>
162
+ <a
163
+ href="https://huggingface.co/docs/transformers.js"
164
+ target="_blank"
165
+ rel="noopener noreferrer"
166
+ title="Transformers.js"
167
+ >
168
+ <HfLogo className="h-24 md:h-28 text-gray-300 hover:text-white transition-colors" />
169
+ </a>
170
+ </div>
171
+
172
+ <div className="w-full text-center">
173
+ <h1 className="text-5xl font-semibold mb-2 text-white tracking-tight">
174
+ Granite-4.0 WebGPU
175
+ </h1>
176
+ <p className="text-md md:text-lg text-[#a6c8ff]">
177
+ In-browser tool calling, powered by Transformers.js
178
+ </p>
179
+ </div>
180
+
181
+ <div className="w-full text-left text-[#d0e2ff] space-y-4 text-xl">
182
+ <p>
183
+ This demo showcases in-browser tool calling with Granite-4.0, a new
184
+ series of models by{" "}
185
+ <a
186
+ href="https://huggingface.co/ibm-granite"
187
+ target="_blank"
188
+ rel="noopener noreferrer"
189
+ className="text-[#78a9ff] hover:underline font-medium"
190
+ >
191
+ IBM Granite
192
+ </a>{" "}
193
+ designed for edge AI and on-device deployment.
194
+ </p>
195
+ <p>
196
+ Everything runs entirely in your browser with{" "}
197
+ <a
198
+ href="https://huggingface.co/docs/transformers.js"
199
+ target="_blank"
200
+ rel="noopener noreferrer"
201
+ className="text-[#78a9ff] hover:underline font-medium"
202
+ >
203
+ Transformers.js
204
+ </a>{" "}
205
+ and ONNX Runtime Web, meaning no data is sent to a server. It can
206
+ even run offline!
207
+ </p>
208
+ </div>
209
+
210
+ <p className="text-[#a6c8ff]">
211
+ Select a model and click load to get started.
212
+ </p>
213
+
214
+ <div className="relative w-full max-w-lg">
215
+ <div className="flex rounded-2xl border border-white/12 bg-white/10 overflow-hidden shadow-[0_18px_45px_rgba(3,27,78,0.45)]">
216
+ <button
217
+ onClick={isLoading ? undefined : loadSelectedModel}
218
+ disabled={isLoading}
219
+ className={`flex-1 flex items-center justify-center font-semibold transition-all text-lg ${isLoading ? "bg-white/5 text-[#8da2d8] cursor-not-allowed" : "bg-[#0f62fe] hover:bg-[#0043ce] text-white"}`}
220
+ >
221
+ <div className="px-6 py-3">
222
+ {isLoading ? (
223
+ <div className="flex items-center">
224
+ <span className="inline-block w-5 h-5 border-2 border-white/80 border-t-transparent rounded-full animate-spin"></span>
225
+ <span className="ml-3 text-md font-medium">
226
+ Loading... ({progress}%)
227
+ </span>
228
+ </div>
229
+ ) : (
230
+ `Load ${model?.label}`
231
+ )}
232
+ </div>
233
+ </button>
234
+ <button
235
+ onClick={(e) => {
236
+ if (!isLoading) {
237
+ e.stopPropagation();
238
+ setIsModelDropdownOpen(!isModelDropdownOpen);
239
+ }
240
+ }}
241
+ aria-label="Select model"
242
+ className="px-4 py-3 border-l border-white/15 bg-[#0f62fe] hover:bg-[#0043ce] transition-colors text-white disabled:cursor-not-allowed disabled:bg-white/5"
243
+ disabled={isLoading}
244
+ >
245
+ <ChevronDown size={24} />
246
+ </button>
247
+ </div>
248
+
249
+ {isModelDropdownOpen && (
250
+ <div className="absolute left-0 right-0 bottom-full mb-3 bg-[#02102c]/98 border border-white/12 rounded-xl shadow-[0_22px_55px_rgba(3,27,78,0.55)] z-10 w-full overflow-visible backdrop-blur-2xl">
251
+ {MODEL_OPTIONS.map((option) => (
252
+ <button
253
+ key={option.id}
254
+ onClick={() => handleModelSelect(option.id)}
255
+ className={`w-full px-5 py-3 text-left text-sm font-medium rounded-lg border transition-all ${
256
+ selectedModelId === option.id
257
+ ? "border-[#78a9ff]/60 bg-[#0f62fe]/25 text-white shadow-[0_10px_25px_rgba(15,98,254,0.25)]"
258
+ : "border-transparent text-[#d0e2ff] hover:border-[#78a9ff]/30 hover:bg-white/12 hover:text-white"
259
+ }`}
260
+ >
261
+ <div className="font-medium">{option.label}</div>
262
+ <div className="text-md text-[#95a8dd]">{option.size}</div>
263
+ </button>
264
+ ))}
265
+ </div>
266
+ )}
267
+ </div>
268
+
269
+ {error && (
270
+ <div className="bg-[#2d0709]/70 border border-[#ff8389]/40 rounded-2xl p-4 w-full max-w-md text-center shadow-[0_15px_35px_rgba(45,7,9,0.4)]">
271
+ <p className="text-sm text-[#ffb3b8]">Error: {error}</p>
272
+ <button
273
+ onClick={loadSelectedModel}
274
+ className="mt-3 text-sm px-4 py-2 rounded-full bg-white/15 hover:bg-white/25 border border-white/20 text-white font-semibold transition-all"
275
+ >
276
+ Retry
277
+ </button>
278
+ </div>
279
+ )}
280
+ </div>
281
+
282
+ {/* Click-away listener for dropdown */}
283
+ {isModelDropdownOpen && (
284
+ <div
285
+ className="fixed inset-0 z-5"
286
+ onClick={() => setIsModelDropdownOpen(false)}
287
+ />
288
+ )}
289
+ </div>
290
+ );
291
+ };
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,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ const toolCalls = extractToolCallContent(content);
10
+ const displayContent = toolCalls?.join("\n") ?? "...";
11
+
12
+ return (
13
+ <div
14
+ className={`transition-all duration-500 ease-in-out rounded-lg p-4 ${
15
+ isRunning
16
+ ? "bg-gradient-to-r from-yellow-900/30 to-orange-900/30 border border-yellow-600/50"
17
+ : hasError
18
+ ? "bg-gradient-to-r from-red-900/30 to-rose-900/30 border border-red-600/50"
19
+ : "bg-gradient-to-r from-green-900/30 to-emerald-900/30 border border-green-600/50"
20
+ }`}
21
+ >
22
+ <div className="flex items-start space-x-3">
23
+ <div className="flex-shrink-0">
24
+ <div className="relative size-8">
25
+ {/* Spinner for running */}
26
+ <div
27
+ className={`absolute inset-0 flex items-center justify-center transition-opacity duration-500 ${
28
+ isRunning ? "opacity-100" : "opacity-0 pointer-events-none"
29
+ }`}
30
+ >
31
+ <div className="size-8 bg-green-400/0 border-2 border-yellow-400 border-t-transparent rounded-full animate-spin"></div>
32
+ </div>
33
+
34
+ {/* Cross for error */}
35
+ <div
36
+ className={`absolute inset-0 flex items-center justify-center transition-opacity duration-500 ${
37
+ hasError ? "opacity-100" : "opacity-0 pointer-events-none"
38
+ }`}
39
+ >
40
+ <div className="size-8 bg-red-400/100 rounded-full flex items-center justify-center transition-colors duration-500 ease-in-out">
41
+ <span className="text-md text-gray-900 font-bold">✗</span>
42
+ </div>
43
+ </div>
44
+
45
+ {/* Tick for success */}
46
+ <div
47
+ className={`absolute inset-0 flex items-center justify-center transition-opacity duration-500 ${
48
+ !isRunning && !hasError
49
+ ? "opacity-100"
50
+ : "opacity-0 pointer-events-none"
51
+ }`}
52
+ >
53
+ <div className="size-8 bg-green-400/100 rounded-full flex items-center justify-center transition-colors duration-500 ease-in-out">
54
+ <span className="text-md text-gray-900 font-bold">✓</span>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ <div className="flex-grow min-w-0">
60
+ <div className="flex items-center space-x-2 mb-2">
61
+ <span
62
+ className={`font-semibold text-sm transition-colors duration-500 ease-in-out ${
63
+ isRunning
64
+ ? "text-yellow-400"
65
+ : hasError
66
+ ? "text-red-400"
67
+ : "text-green-400"
68
+ }`}
69
+ >
70
+ 🔧 Tool Call
71
+ </span>
72
+ {isRunning && (
73
+ <span className="text-yellow-300 text-xs animate-pulse">
74
+ Running...
75
+ </span>
76
+ )}
77
+ </div>
78
+ <div className="bg-gray-800/50 rounded p-2 mb-2">
79
+ <code className="text-sm text-gray-300 font-mono break-all">
80
+ {displayContent}
81
+ </code>
82
+ </div>
83
+ <p
84
+ className={`text-sm transition-colors duration-500 ease-in-out ${
85
+ isRunning
86
+ ? "text-yellow-200"
87
+ : hasError
88
+ ? "text-red-200"
89
+ : "text-green-200"
90
+ }`}
91
+ >
92
+ {isRunning
93
+ ? "Executing tool call..."
94
+ : hasError
95
+ ? "Tool call failed"
96
+ : "Tool call completed"}
97
+ </p>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ );
102
+ };
103
+
104
+ export default ToolCallIndicator;
src/components/ToolItem.tsx ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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={`rounded-2xl p-5 border border-white/10 bg-white/5 backdrop-blur-sm transition-all shadow-[0_18px_55px_rgba(3,27,78,0.45)] ${!tool.enabled ? "opacity-40" : ""}`}
41
+ >
42
+ <div
43
+ className="flex justify-between items-center cursor-pointer"
44
+ onClick={onToggleCollapsed}
45
+ >
46
+ <div>
47
+ <h3 className="text-lg font-semibold text-[#78a9ff] font-mono">
48
+ {schema.name}
49
+ </h3>
50
+ <div className="text-xs text-[#a6c8ff]/80 mt-1">
51
+ {schema.description}
52
+ </div>
53
+ </div>
54
+ <div className="flex items-center space-x-3">
55
+ <button
56
+ onClick={(e) => {
57
+ e.stopPropagation();
58
+ onToggleEnabled();
59
+ }}
60
+ className={`p-1.5 rounded-full border transition-all ${tool.enabled ? "border-[#0f62fe]/50 bg-[#0f62fe]/20 text-[#78a9ff]" : "border-white/15 bg-white/5 text-white/60"}`}
61
+ >
62
+ <Power size={18} />
63
+ </button>
64
+ <button
65
+ onClick={(e) => {
66
+ e.stopPropagation();
67
+ onDelete();
68
+ }}
69
+ className="p-2 text-[#ff8389] hover:text-white border border-transparent hover:border-[#ff8389]/40 hover:bg-[#ff8389]/10 rounded-full transition-all"
70
+ >
71
+ <Trash2 size={18} />
72
+ </button>
73
+ <button
74
+ onClick={(e) => {
75
+ e.stopPropagation();
76
+ onToggleCollapsed();
77
+ }}
78
+ className="p-2 text-white/70 hover:text-white border border-white/10 hover:border-white/30 hover:bg-white/10 rounded-full transition-all"
79
+ >
80
+ {tool.isCollapsed ? (
81
+ <ChevronDown size={20} />
82
+ ) : (
83
+ <ChevronUp size={20} />
84
+ )}
85
+ </button>
86
+ </div>
87
+ </div>
88
+ {!tool.isCollapsed && (
89
+ <div className="mt-4 grid grid-cols-1 md:grid-cols-3 gap-4">
90
+ <div className="md:col-span-2">
91
+ <label className="text-xs font-semibold uppercase tracking-[0.2em] text-[#a6c8ff]/80">
92
+ Implementation & Renderer
93
+ </label>
94
+ <div
95
+ className="mt-1 rounded-2xl overflow-visible border border-white/12 bg-[#031b4e]/60"
96
+ style={{ overflow: "visible" }}
97
+ >
98
+ <Editor
99
+ height="300px"
100
+ language="javascript"
101
+ theme="vs-dark"
102
+ value={tool.code}
103
+ onChange={(value) => onCodeChange(value || "")}
104
+ options={{
105
+ minimap: { enabled: false },
106
+ scrollbar: { verticalScrollbarSize: 10 },
107
+ fontSize: 14,
108
+ lineDecorationsWidth: 0,
109
+ lineNumbersMinChars: 3,
110
+ scrollBeyondLastLine: false,
111
+ }}
112
+ />
113
+ </div>
114
+ </div>
115
+ <div className="flex flex-col">
116
+ <label className="text-xs font-semibold uppercase tracking-[0.2em] text-[#a6c8ff]/80">
117
+ Generated Schema
118
+ </label>
119
+ <div className="mt-1 rounded-2xl flex-grow overflow-visible border border-white/12 bg-[#031b4e]/60">
120
+ <Editor
121
+ height="300px"
122
+ language="json"
123
+ theme="vs-dark"
124
+ value={JSON.stringify(schema, null, 2)}
125
+ options={{
126
+ readOnly: true,
127
+ minimap: { enabled: false },
128
+ scrollbar: { verticalScrollbarSize: 10 },
129
+ lineNumbers: "off",
130
+ glyphMargin: false,
131
+ folding: false,
132
+ lineDecorationsWidth: 0,
133
+ lineNumbersMinChars: 0,
134
+ scrollBeyondLastLine: false,
135
+ fontSize: 12,
136
+ }}
137
+ />
138
+ </div>
139
+ </div>
140
+ </div>
141
+ )}
142
+ </div>
143
+ );
144
+ };
145
+
146
+ 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/IBMLogo.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 1000 400"
8
+ fill="currentColor"
9
+ >
10
+ <path d="M0 0v27.367h194.648V0H0zm222.226 0v27.367h277.383S471.276 0 433.75 0H222.226zm331.797 0v27.367h167.812L711.875 0H554.023zm288.125 0l-9.961 27.367h166.289V0H842.148zM0 53.222v27.367h194.648V53.222H0zm222.226.039V80.59h309.57s-3.615-21.063-9.922-27.329H222.226zm331.797 0V80.59h186.211l-9.219-27.329H554.023zm268.203 0l-9.219 27.329h185.469V53.261h-176.25zM55.937 106.444v27.406h84.297v-27.406H55.937zm222.227 0v27.406h84.297v-27.406h-84.297zm166.289 0v27.406h84.297s5.352-14.473 5.352-27.406h-89.649zm165.508 0v27.406h149.453l-9.961-27.406H609.961zm193.906 0l-10 27.406h150.195v-27.406H803.867zm-747.93 53.262v27.367h84.297v-27.367H55.937zm222.227 0v27.367h215.312s18.012-14.042 23.75-27.367H278.164zm331.797 0v27.367h84.297v-15.234l5.352 15.234h154.414l5.742-15.234v15.234h84.297v-27.367H785.82l-8.398 23.18-8.438-23.18H609.961zM55.937 212.928v27.367h84.297v-27.367H55.937zm222.227 0v27.367h239.062c-5.739-13.281-23.75-27.367-23.75-27.367H278.164zm331.797 0v27.367h84.297v-27.367h-84.297zm99.609 0l10.195 27.367h115.781l9.688-27.367H709.57zm150.195 0v27.367h84.297v-27.367h-84.297zM55.937 266.15v27.366h84.297V266.15H55.937zm222.227 0v27.366h84.297V266.15h-84.297zm166.289 0v27.366h89.648c0-12.915-5.352-27.366-5.352-27.366h-84.296zm165.508 0v27.366h84.297V266.15h-84.297zm118.75 0l9.883 27.366h77.617l9.961-27.366h-97.461zm131.054 0v27.366h84.297V266.15h-84.297zM1.523 319.372v27.406h194.648v-27.406H1.523zm220.703 0v27.406h299.648c6.307-6.275 9.922-27.406 9.922-27.406h-309.57zm333.321 0v27.406h138.711v-27.406H555.547zm192.343 0l10.156 27.406h39.492l9.531-27.406H747.89zm111.875 0v27.406H1000v-27.406H859.765zM1.523 372.633V400h194.648v-27.367H1.523zm220.703 0v27.328H433.75c37.526 0 65.859-27.328 65.859-27.328H222.226zm333.321 0V400h138.711v-27.367H555.547zm211.601 0l9.766 27.29 1.68.038 9.922-27.328h-21.368zm92.617 0V400H1000v-27.367H859.765z" />
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: "Tell me a joke.",
16
+ messageText: "Tell me a joke.",
17
+ },
18
+ {
19
+ icon: "🔢",
20
+ displayText: "Solve a math problem",
21
+ messageText: "What is 123 times 456 divided by 789?",
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 this website: "https://www.youtube.com/embed/dQw4w9WgXcQ?autoplay=1" and do nothing else.',
38
+ },
39
+ ];
src/constants/models.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const MODEL_OPTIONS = [
2
+ {
3
+ id: "350M",
4
+ modelId: "onnx-community/granite-4.0-350m-ONNX-web",
5
+ dtype: "fp16",
6
+ label: "Granite-4.0 350M (fp16)",
7
+ size: "A lightweight 350M instruct model (~709 MB in size).",
8
+ },
9
+ {
10
+ id: "1B",
11
+ modelId: "onnx-community/granite-4.0-1b-ONNX-web",
12
+ dtype: "q4",
13
+ label: "Granite-4.0 1B (q4)",
14
+ size: "A medium-sized instruct model (~1.78 GB in size).",
15
+ },
16
+ {
17
+ id: "3B",
18
+ modelId: "onnx-community/granite-4.0-micro-ONNX-web",
19
+ dtype: "q4f16",
20
+ label: "Granite-4.0 3B (q4f16)",
21
+ size: "A large 3B parameter instruct model (~2.3 GB in size).",
22
+ },
23
+ ] as const;
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
+ import { MODEL_OPTIONS } from "../constants/models";
8
+
9
+ interface LLMState {
10
+ isLoading: boolean;
11
+ isReady: boolean;
12
+ error: string | null;
13
+ progress: number;
14
+ }
15
+
16
+ interface LLMInstance {
17
+ model: any;
18
+ tokenizer: any;
19
+ }
20
+
21
+ let moduleCache: {
22
+ [modelId: string]: {
23
+ instance: LLMInstance | null;
24
+ loadingPromise: Promise<LLMInstance> | null;
25
+ };
26
+ } = {};
27
+
28
+ export const useLLM = (modelName?: string) => {
29
+ const [state, setState] = useState<LLMState>({
30
+ isLoading: false,
31
+ isReady: false,
32
+ error: null,
33
+ progress: 0,
34
+ });
35
+
36
+ const instanceRef = useRef<LLMInstance | null>(null);
37
+ const loadingPromiseRef = useRef<Promise<LLMInstance> | null>(null);
38
+
39
+ const abortControllerRef = useRef<AbortController | null>(null);
40
+ const pastKeyValuesRef = useRef<any>(null);
41
+
42
+ const { modelId, dtype } = MODEL_OPTIONS.find((opt) => opt.id === modelName)!;
43
+ const loadModel = useCallback(async () => {
44
+ if (!modelId) {
45
+ throw new Error("Model ID is required");
46
+ }
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 progress_callback = (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(modelId, {
109
+ progress_callback,
110
+ });
111
+
112
+ const model = await AutoModelForCausalLM.from_pretrained(modelId, {
113
+ dtype,
114
+ device: "webgpu",
115
+ progress_callback,
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 <|end_of_text|>) 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(/<\|end_of_text\|>$/, "");
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,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 {boolean} - Whether the tool was executed successfully.
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
+ return true;
16
+ }
17
+
18
+ export default (input, output) =>
19
+ 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
+ "Speech Synthesis",
37
+ ),
38
+ ),
39
+ React.createElement(
40
+ "div",
41
+ { className: "text-sm space-y-1" },
42
+ React.createElement(
43
+ "p",
44
+ { className: "text-blue-700 font-medium" },
45
+ `Speaking: "${input.text || "Unknown text"}"`,
46
+ ),
47
+ input.voice &&
48
+ React.createElement(
49
+ "p",
50
+ { className: "text-blue-600 text-xs" },
51
+ `Voice: ${input.voice}`,
52
+ ),
53
+ React.createElement(
54
+ "p",
55
+ { className: "text-blue-600 text-xs" },
56
+ typeof output === "string" ? output : "Speech completed successfully",
57
+ ),
58
+ ),
59
+ );
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,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ interface Schema {
2
+ name: string;
3
+ description: string;
4
+ parameters: {
5
+ type: string;
6
+ properties: Record<
7
+ string,
8
+ {
9
+ type: string;
10
+ description: string;
11
+ default?: any;
12
+ }
13
+ >;
14
+ required: string[];
15
+ };
16
+ }
17
+
18
+ interface JSDocParam {
19
+ type: string;
20
+ description: string;
21
+ isOptional: boolean;
22
+ defaultValue?: string;
23
+ }
24
+
25
+ export const extractFunctionAndRenderer = (
26
+ code: string,
27
+ ): { functionCode: string; rendererCode?: string } => {
28
+ if (typeof code !== "string") {
29
+ return { functionCode: code };
30
+ }
31
+
32
+ const exportMatch = code.match(/export\s+default\s+/);
33
+ if (!exportMatch) {
34
+ return { functionCode: code };
35
+ }
36
+
37
+ const exportIndex = exportMatch.index!;
38
+ const functionCode = code.substring(0, exportIndex).trim();
39
+ const rendererCode = code.substring(exportIndex).trim();
40
+
41
+ return { functionCode, rendererCode };
42
+ };
43
+
44
+ /**
45
+ * Helper function to extract JSDoc parameters from JSDoc comments.
46
+ */
47
+ const extractJSDocParams = (
48
+ jsdoc: string,
49
+ ): Record<string, JSDocParam & { jsdocDefault?: string }> => {
50
+ const jsdocParams: Record<string, JSDocParam & { jsdocDefault?: string }> =
51
+ {};
52
+ const lines = jsdoc
53
+ .split("\n")
54
+ .map((line) => line.trim().replace(/^\*\s?/, ""));
55
+ const paramRegex =
56
+ /@param\s+\{([^}]+)\}\s+(\[?[a-zA-Z0-9_]+(?:=[^\]]+)?\]?|\S+)\s*-?\s*(.*)?/;
57
+
58
+ for (const line of lines) {
59
+ const paramMatch = line.match(paramRegex);
60
+ if (paramMatch) {
61
+ let [, type, namePart, description] = paramMatch;
62
+ description = description || "";
63
+ let isOptional = false;
64
+ let name = namePart;
65
+ let jsdocDefault: string | undefined = undefined;
66
+
67
+ if (name.startsWith("[") && name.endsWith("]")) {
68
+ isOptional = true;
69
+ name = name.slice(1, -1);
70
+ }
71
+ if (name.includes("=")) {
72
+ const [n, def] = name.split("=");
73
+ name = n.trim();
74
+ jsdocDefault = def.trim().replace(/['"]/g, "");
75
+ }
76
+
77
+ jsdocParams[name] = {
78
+ type: type.toLowerCase(),
79
+ description: description.trim(),
80
+ isOptional,
81
+ defaultValue: undefined,
82
+ jsdocDefault,
83
+ };
84
+ }
85
+ }
86
+ return jsdocParams;
87
+ };
88
+
89
+ /**
90
+ * Helper function to extract function signature information.
91
+ */
92
+ const extractFunctionSignature = (
93
+ functionCode: string,
94
+ ): {
95
+ name: string;
96
+ params: { name: string; defaultValue?: string }[];
97
+ } | null => {
98
+ const functionSignatureMatch = functionCode.match(
99
+ /function\s+([a-zA-Z0-9_]+)\s*\(([^)]*)\)/,
100
+ );
101
+ if (!functionSignatureMatch) {
102
+ return null;
103
+ }
104
+
105
+ const functionName = functionSignatureMatch[1];
106
+ const params = functionSignatureMatch[2]
107
+ .split(",")
108
+ .map((p) => p.trim())
109
+ .filter(Boolean)
110
+ .map((p) => {
111
+ const [name, defaultValue] = p.split("=").map((s) => s.trim());
112
+ return { name, defaultValue };
113
+ });
114
+
115
+ return { name: functionName, params };
116
+ };
117
+
118
+ export const generateSchemaFromCode = (code: string): Schema => {
119
+ const { functionCode } = extractFunctionAndRenderer(code);
120
+
121
+ if (typeof functionCode !== "string") {
122
+ return {
123
+ name: "invalid_code",
124
+ description: "Code is not a valid string.",
125
+ parameters: { type: "object", properties: {}, required: [] },
126
+ };
127
+ }
128
+
129
+ // 1. Extract function signature, name, and parameter names directly from the code
130
+ const signatureInfo = extractFunctionSignature(functionCode);
131
+ if (!signatureInfo) {
132
+ return {
133
+ name: "invalid_function",
134
+ description: "Could not parse function signature.",
135
+ parameters: { type: "object", properties: {}, required: [] },
136
+ };
137
+ }
138
+
139
+ const { name: functionName, params: paramsFromSignature } = signatureInfo;
140
+
141
+ const schema: Schema = {
142
+ name: functionName,
143
+ description: "",
144
+ parameters: {
145
+ type: "object",
146
+ properties: {},
147
+ required: [],
148
+ },
149
+ };
150
+
151
+ // 2. Parse JSDoc comments to get descriptions and types
152
+ const jsdocMatch = functionCode.match(/\/\*\*([\s\S]*?)\*\//);
153
+ let jsdocParams: Record<string, JSDocParam & { jsdocDefault?: string }> = {};
154
+ if (jsdocMatch) {
155
+ const jsdoc = jsdocMatch[1];
156
+ jsdocParams = extractJSDocParams(jsdoc);
157
+
158
+ const descriptionLines = jsdoc
159
+ .split("\n")
160
+ .map((line) => line.trim().replace(/^\*\s?/, ""))
161
+ .filter((line) => !line.startsWith("@") && line);
162
+
163
+ schema.description = descriptionLines.join(" ").trim();
164
+ }
165
+
166
+ // 3. Combine signature parameters with JSDoc info
167
+ for (const param of paramsFromSignature) {
168
+ const paramName = param.name;
169
+ const jsdocInfo = jsdocParams[paramName];
170
+ schema.parameters.properties[paramName] = {
171
+ type: jsdocInfo ? jsdocInfo.type : "any",
172
+ description: jsdocInfo ? jsdocInfo.description : "",
173
+ };
174
+
175
+ // Prefer default from signature, then from JSDoc
176
+ if (param.defaultValue !== undefined) {
177
+ // Try to parse as JSON, fallback to string
178
+ try {
179
+ schema.parameters.properties[paramName].default = JSON.parse(
180
+ param.defaultValue.replace(/'/g, '"'),
181
+ );
182
+ } catch {
183
+ schema.parameters.properties[paramName].default = param.defaultValue;
184
+ }
185
+ } else if (jsdocInfo && jsdocInfo.jsdocDefault !== undefined) {
186
+ schema.parameters.properties[paramName].default = jsdocInfo.jsdocDefault;
187
+ }
188
+
189
+ // A parameter is required if:
190
+ // - Not optional in JSDoc
191
+ // - No default in signature
192
+ // - No default in JSDoc
193
+ const hasDefault =
194
+ param.defaultValue !== undefined ||
195
+ (jsdocInfo && jsdocInfo.jsdocDefault !== undefined);
196
+ if (!jsdocInfo || (!jsdocInfo.isOptional && !hasDefault)) {
197
+ schema.parameters.required.push(paramName);
198
+ }
199
+ }
200
+
201
+ return schema;
202
+ };
203
+
204
+ /**
205
+ * Extracts tool call content from a string using the tool call markers.
206
+ */
207
+ export const extractToolCallContent = (content: string): string[] | null => {
208
+ if (typeof content !== "string") return null;
209
+ const matches = [...content.matchAll(/<tool_call>([\s\S]*?)<\/tool_call>/g)];
210
+ return matches.length ? matches.map(([, inner]) => inner.trim()) : null;
211
+ };
212
+
213
+ export const getErrorMessage = (error: unknown): string => {
214
+ if (error instanceof Error) {
215
+ return error.message;
216
+ }
217
+ if (typeof error === "string") {
218
+ return error;
219
+ }
220
+ if (error && typeof error === "object") {
221
+ return JSON.stringify(error);
222
+ }
223
+ return String(error);
224
+ };
225
+
226
+ /**
227
+ * Adapted from https://stackoverflow.com/questions/11381673/detecting-a-mobile-browser.
228
+ */
229
+ export function isMobileOrTablet() {
230
+ let check = false;
231
+ (function (a: string) {
232
+ if (
233
+ /(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(
234
+ a,
235
+ ) ||
236
+ /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(
237
+ a.slice(0, 4),
238
+ )
239
+ )
240
+ check = true;
241
+ })(
242
+ navigator.userAgent ||
243
+ navigator.vendor ||
244
+ ("opera" in window && typeof window.opera === "string"
245
+ ? window.opera
246
+ : ""),
247
+ );
248
+ return check;
249
+ }
src/vite-env.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ /// <reference types="vite/client" />
style.css DELETED
@@ -1,76 +0,0 @@
1
- * {
2
- box-sizing: border-box;
3
- padding: 0;
4
- margin: 0;
5
- font-family: sans-serif;
6
- }
7
-
8
- html,
9
- body {
10
- height: 100%;
11
- }
12
-
13
- body {
14
- padding: 32px;
15
- }
16
-
17
- body,
18
- #container {
19
- display: flex;
20
- flex-direction: column;
21
- justify-content: center;
22
- align-items: center;
23
- }
24
-
25
- #container {
26
- position: relative;
27
- gap: 0.4rem;
28
-
29
- width: 640px;
30
- height: 640px;
31
- max-width: 100%;
32
- max-height: 100%;
33
-
34
- border: 2px dashed #D1D5DB;
35
- border-radius: 0.75rem;
36
- overflow: hidden;
37
- cursor: pointer;
38
- margin: 1rem;
39
-
40
- background-size: 100% 100%;
41
- background-position: center;
42
- background-repeat: no-repeat;
43
- font-size: 18px;
44
- }
45
-
46
- #upload {
47
- display: none;
48
- }
49
-
50
- svg {
51
- pointer-events: none;
52
- }
53
-
54
- #example {
55
- font-size: 14px;
56
- text-decoration: underline;
57
- cursor: pointer;
58
- }
59
-
60
- #example:hover {
61
- color: #2563EB;
62
- }
63
-
64
- .bounding-box {
65
- position: absolute;
66
- box-sizing: border-box;
67
- border: solid 2px;
68
- }
69
-
70
- .bounding-box-label {
71
- color: white;
72
- position: absolute;
73
- font-size: 12px;
74
- margin: -16px 0 0 -2px;
75
- padding: 1px;
76
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ });