Xenova HF Staff commited on
Commit
a0c1ef5
·
verified ·
0 Parent(s):

Super-squash branch 'main' using huggingface_hub

Browse files
.gitattributes ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ public/banner.png filter=lfs diff=lfs merge=lfs -text
37
+ public/logo.png filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: SmolLM3 WebGPU
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: pink
6
+ sdk: static
7
+ pinned: false
8
+ thumbnail: >-
9
+ https://huggingface.co/spaces/HuggingFaceTB/SmolLM3-3B-WebGPU/resolve/main/public/banner.png
10
+ short_description: A dual reasoning model that runs locally in your browser.
11
+ app_build_command: npm run build
12
+ app_file: dist/index.html
13
+ models:
14
+ - HuggingFaceTB/SmolLM3-3B-ONNX
15
+ ---
16
+
17
+ # SmolLM3 WebGPU
18
+
19
+ ## Getting Started
20
+
21
+ Follow the steps below to set up and run the application.
22
+
23
+ ### 1. Clone the Repository
24
+
25
+ Clone the examples repository from GitHub:
26
+
27
+ ```sh
28
+ git clone https://github.com/huggingface/transformers.js-examples.git
29
+ ```
30
+
31
+ ### 2. Navigate to the Project Directory
32
+
33
+ Change your working directory to the `smollm3-webgpu` folder:
34
+
35
+ ```sh
36
+ cd transformers.js-examples/smollm3-webgpu
37
+ ```
38
+
39
+ ### 3. Install Dependencies
40
+
41
+ Install the necessary dependencies using npm:
42
+
43
+ ```sh
44
+ npm i
45
+ ```
46
+
47
+ ### 4. Run the Development Server
48
+
49
+ Start the development server:
50
+
51
+ ```sh
52
+ npm run dev
53
+ ```
54
+
55
+ The application should now be running locally. Open your browser and go to `http://localhost:5173` to see it in action.
eslint.config.js ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import react from "eslint-plugin-react";
4
+ import reactHooks from "eslint-plugin-react-hooks";
5
+ import reactRefresh from "eslint-plugin-react-refresh";
6
+
7
+ export default [
8
+ { ignores: ["dist"] },
9
+ {
10
+ files: ["**/*.{js,jsx}"],
11
+ languageOptions: {
12
+ ecmaVersion: 2020,
13
+ globals: globals.browser,
14
+ parserOptions: {
15
+ ecmaVersion: "latest",
16
+ ecmaFeatures: { jsx: true },
17
+ sourceType: "module",
18
+ },
19
+ },
20
+ settings: { react: { version: "18.3" } },
21
+ plugins: {
22
+ react,
23
+ "react-hooks": reactHooks,
24
+ "react-refresh": reactRefresh,
25
+ },
26
+ rules: {
27
+ ...js.configs.recommended.rules,
28
+ ...react.configs.recommended.rules,
29
+ ...react.configs["jsx-runtime"].rules,
30
+ ...reactHooks.configs.recommended.rules,
31
+ "react/jsx-no-target-blank": "off",
32
+ "react-refresh/only-export-components": [
33
+ "warn",
34
+ { allowConstantExport: true },
35
+ ],
36
+ },
37
+ },
38
+ ];
index.html ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/png" href="/logo.png" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>SmolLM3 WebGPU</title>
8
+ </head>
9
+
10
+ <body>
11
+ <div id="root"></div>
12
+ <script type="module" src="/src/main.jsx"></script>
13
+ </body>
14
+ </html>
package.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "smollm3-webgpu",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@huggingface/transformers": "^3.6.3",
14
+ "@tailwindcss/vite": "^4.1.4",
15
+ "better-react-mathjax": "^2.0.3",
16
+ "dompurify": "^3.2.3",
17
+ "marked": "^15.0.5",
18
+ "react": "^18.3.1",
19
+ "react-dom": "^18.3.1",
20
+ "tailwindcss": "^4.1.4"
21
+ },
22
+ "devDependencies": {
23
+ "@eslint/js": "^9.17.0",
24
+ "@types/react": "^18.3.17",
25
+ "@types/react-dom": "^18.3.5",
26
+ "@vitejs/plugin-react": "^4.3.4",
27
+ "eslint": "^9.17.0",
28
+ "eslint-plugin-react": "^7.37.2",
29
+ "eslint-plugin-react-hooks": "^5.0.0",
30
+ "eslint-plugin-react-refresh": "^0.4.16",
31
+ "globals": "^15.13.0",
32
+ "vite": "^6.0.3"
33
+ }
34
+ }
public/banner.png ADDED

Git LFS Details

  • SHA256: b6e2c541a74ff2d1cfebd57f8ddda5e715c35fff0048c657e0d423f5086dc8e8
  • Pointer size: 131 Bytes
  • Size of remote file: 807 kB
public/logo.png ADDED

Git LFS Details

  • SHA256: 8e75f10fb95c73a2f41f59ed823baba6b8873708c553a5e4e2691c45ccd729a9
  • Pointer size: 131 Bytes
  • Size of remote file: 153 kB
src/App.jsx ADDED
@@ -0,0 +1,430 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState, useRef } from "react";
2
+
3
+ import Chat from "./components/Chat";
4
+ import ArrowRightIcon from "./components/icons/ArrowRightIcon";
5
+ import StopIcon from "./components/icons/StopIcon";
6
+ import Progress from "./components/Progress";
7
+ import LightBulbIcon from "./components/icons/LightBulbIcon";
8
+
9
+ const IS_WEBGPU_AVAILABLE = !!navigator.gpu;
10
+ const STICKY_SCROLL_THRESHOLD = 120;
11
+ const EXAMPLES = [
12
+ "Solve the equation x^2 - 3x + 2 = 0",
13
+ "How do you say 'I love you' in French, Spanish, and German? Respond in a table.",
14
+ "Explain the concept of gravity in simple terms.",
15
+ ];
16
+
17
+ function App() {
18
+ // Create a reference to the worker object.
19
+ const worker = useRef(null);
20
+
21
+ const textareaRef = useRef(null);
22
+ const chatContainerRef = useRef(null);
23
+
24
+ // Model loading and progress
25
+ const [status, setStatus] = useState(null);
26
+ const [error, setError] = useState(null);
27
+ const [loadingMessage, setLoadingMessage] = useState("");
28
+ const [progressItems, setProgressItems] = useState([]);
29
+ const [isRunning, setIsRunning] = useState(false);
30
+
31
+ // Inputs and outputs
32
+ const [input, setInput] = useState("");
33
+ const [messages, setMessages] = useState([]);
34
+ const [tps, setTps] = useState(null);
35
+ const [numTokens, setNumTokens] = useState(null);
36
+ const [reasonEnabled, setReasonEnabled] = useState(false);
37
+
38
+ function onEnter(message) {
39
+ setMessages((prev) => [...prev, { role: "user", content: message }]);
40
+ setTps(null);
41
+ setIsRunning(true);
42
+ setInput("");
43
+ }
44
+
45
+ function onInterrupt() {
46
+ // NOTE: We do not set isRunning to false here because the worker
47
+ // will send a 'complete' message when it is done.
48
+ worker.current.postMessage({ type: "interrupt" });
49
+ }
50
+
51
+ useEffect(() => {
52
+ resizeInput();
53
+ }, [input]);
54
+
55
+ function resizeInput() {
56
+ if (!textareaRef.current) return;
57
+
58
+ const target = textareaRef.current;
59
+ target.style.height = "auto";
60
+ const newHeight = Math.min(Math.max(target.scrollHeight, 24), 200);
61
+ target.style.height = `${newHeight}px`;
62
+ }
63
+
64
+ // We use the `useEffect` hook to setup the worker as soon as the `App` component is mounted.
65
+ useEffect(() => {
66
+ // Create the worker if it does not yet exist.
67
+ if (!worker.current) {
68
+ worker.current = new Worker(new URL("./worker.js", import.meta.url), {
69
+ type: "module",
70
+ });
71
+ worker.current.postMessage({ type: "check" }); // Do a feature check
72
+ }
73
+
74
+ // Create a callback function for messages from the worker thread.
75
+ const onMessageReceived = (e) => {
76
+ switch (e.data.status) {
77
+ case "loading":
78
+ // Model file start load: add a new progress item to the list.
79
+ setStatus("loading");
80
+ setLoadingMessage(e.data.data);
81
+ break;
82
+
83
+ case "initiate":
84
+ setProgressItems((prev) => [...prev, e.data]);
85
+ break;
86
+
87
+ case "progress":
88
+ // Model file progress: update one of the progress items.
89
+ setProgressItems((prev) =>
90
+ prev.map((item) => {
91
+ if (item.file === e.data.file) {
92
+ return { ...item, ...e.data };
93
+ }
94
+ return item;
95
+ }),
96
+ );
97
+ break;
98
+
99
+ case "done":
100
+ // Model file loaded: remove the progress item from the list.
101
+ setProgressItems((prev) =>
102
+ prev.filter((item) => item.file !== e.data.file),
103
+ );
104
+ break;
105
+
106
+ case "ready":
107
+ // Pipeline ready: the worker is ready to accept messages.
108
+ setStatus("ready");
109
+ break;
110
+
111
+ case "start":
112
+ {
113
+ // Start generation
114
+ setMessages((prev) => [
115
+ ...prev,
116
+ { role: "assistant", content: "" },
117
+ ]);
118
+ }
119
+ break;
120
+
121
+ case "update":
122
+ {
123
+ // Generation update: update the output text.
124
+ // Parse messages
125
+ const { output, tps, numTokens, state } = e.data;
126
+ setTps(tps);
127
+ setNumTokens(numTokens);
128
+ setMessages((prev) => {
129
+ const cloned = [...prev];
130
+ const last = cloned.at(-1);
131
+ const data = {
132
+ ...last,
133
+ content: last.content + output,
134
+ };
135
+ if (data.answerIndex === undefined && state === "answering") {
136
+ // When state changes to answering, we set the answerIndex
137
+ data.answerIndex = last.content.length;
138
+ }
139
+ cloned[cloned.length - 1] = data;
140
+ return cloned;
141
+ });
142
+ }
143
+ break;
144
+
145
+ case "complete":
146
+ // Generation complete: re-enable the "Generate" button
147
+ setIsRunning(false);
148
+ break;
149
+
150
+ case "error":
151
+ setError(e.data.data);
152
+ break;
153
+ }
154
+ };
155
+
156
+ const onErrorReceived = (e) => {
157
+ console.error("Worker error:", e);
158
+ };
159
+
160
+ // Attach the callback function as an event listener.
161
+ worker.current.addEventListener("message", onMessageReceived);
162
+ worker.current.addEventListener("error", onErrorReceived);
163
+
164
+ // Define a cleanup function for when the component is unmounted.
165
+ return () => {
166
+ worker.current.removeEventListener("message", onMessageReceived);
167
+ worker.current.removeEventListener("error", onErrorReceived);
168
+ };
169
+ }, []);
170
+
171
+ // Send the messages to the worker thread whenever the `messages` state changes.
172
+ useEffect(() => {
173
+ if (messages.filter((x) => x.role === "user").length === 0) {
174
+ // No user messages yet: do nothing.
175
+ return;
176
+ }
177
+ if (messages.at(-1).role === "assistant") {
178
+ // Do not update if the last message is from the assistant
179
+ return;
180
+ }
181
+ setTps(null);
182
+ worker.current.postMessage({
183
+ type: "generate",
184
+ data: { messages, reasonEnabled },
185
+ });
186
+ }, [messages, isRunning]);
187
+
188
+ useEffect(() => {
189
+ if (!chatContainerRef.current) return;
190
+ const element = chatContainerRef.current;
191
+ if (
192
+ element.scrollHeight - element.scrollTop - element.clientHeight <
193
+ STICKY_SCROLL_THRESHOLD
194
+ ) {
195
+ element.scrollTop = element.scrollHeight;
196
+ }
197
+ }, [messages, isRunning]);
198
+
199
+ return IS_WEBGPU_AVAILABLE ? (
200
+ <div className="flex flex-col h-screen mx-auto items justify-end text-gray-800 dark:text-gray-200 bg-white dark:bg-gray-900">
201
+ {status === null && messages.length === 0 && (
202
+ <div className="h-full overflow-auto scrollbar-thin flex justify-center items-center flex-col relative">
203
+ <div className="flex flex-col items-center mb-1 max-w-[360px] text-center">
204
+ <img
205
+ src="logo.png"
206
+ width="80%"
207
+ height="auto"
208
+ className="block drop-shadow-lg bg-transparent"
209
+ ></img>
210
+ <h1 className="text-4xl font-bold my-1">SmolLM3 WebGPU</h1>
211
+ <h2 className="font-semibold">
212
+ A dual reasoning model that runs locally in <br />
213
+ your browser with WebGPU acceleration.
214
+ </h2>
215
+ </div>
216
+
217
+ <div className="flex flex-col items-center px-4">
218
+ <p className="max-w-[480px] mb-4">
219
+ <br />
220
+ You are about to load{" "}
221
+ <a
222
+ href="https://huggingface.co/HuggingFaceTB/SmolLM3-3B-ONNX"
223
+ target="_blank"
224
+ rel="noreferrer"
225
+ className="font-medium underline"
226
+ >
227
+ SmolLM3-3B
228
+ </a>
229
+ , a 3B parameter reasoning LLM optimized for in-browser inference.
230
+ Everything runs entirely in your browser with{" "}
231
+ <a
232
+ href="https://huggingface.co/docs/transformers.js"
233
+ target="_blank"
234
+ rel="noreferrer"
235
+ className="underline"
236
+ >
237
+ 🤗&nbsp;Transformers.js
238
+ </a>{" "}
239
+ and ONNX Runtime Web, meaning no data is sent to a server. Once
240
+ loaded, it can even be used offline. The source code for the demo
241
+ is available on{" "}
242
+ <a
243
+ href="https://github.com/huggingface/transformers.js-examples/tree/main/smollm3-webgpu"
244
+ target="_blank"
245
+ rel="noreferrer"
246
+ className="font-medium underline"
247
+ >
248
+ GitHub
249
+ </a>
250
+ .
251
+ </p>
252
+
253
+ {error && (
254
+ <div className="text-red-500 text-center mb-2">
255
+ <p className="mb-1">
256
+ Unable to load model due to the following error:
257
+ </p>
258
+ <p className="text-sm">{error}</p>
259
+ </div>
260
+ )}
261
+
262
+ <button
263
+ className="border px-4 py-2 rounded-lg bg-blue-400 text-white hover:bg-blue-500 disabled:bg-blue-100 cursor-pointer disabled:cursor-not-allowed select-none"
264
+ onClick={() => {
265
+ worker.current.postMessage({ type: "load" });
266
+ setStatus("loading");
267
+ }}
268
+ disabled={status !== null || error !== null}
269
+ >
270
+ Load model
271
+ </button>
272
+ </div>
273
+ </div>
274
+ )}
275
+ {status === "loading" && (
276
+ <>
277
+ <div className="w-full max-w-[500px] text-left mx-auto p-4 bottom-0 mt-auto">
278
+ <p className="text-center mb-1">{loadingMessage}</p>
279
+ {progressItems.map(({ file, progress, total }, i) => (
280
+ <Progress
281
+ key={i}
282
+ text={file}
283
+ percentage={progress}
284
+ total={total}
285
+ />
286
+ ))}
287
+ </div>
288
+ </>
289
+ )}
290
+
291
+ {status === "ready" && (
292
+ <>
293
+ <div
294
+ ref={chatContainerRef}
295
+ className="overflow-y-auto scrollbar-thin w-full flex flex-col items-center h-full"
296
+ >
297
+ <Chat messages={messages} />
298
+ {messages.length === 0 && (
299
+ <div>
300
+ {EXAMPLES.map((msg, i) => (
301
+ <div
302
+ key={i}
303
+ className="m-1 border border-gray-300 dark:border-gray-600 rounded-md p-2 bg-gray-100 dark:bg-gray-700 cursor-pointer max-w-[500px]"
304
+ onClick={() => onEnter(msg)}
305
+ >
306
+ {msg}
307
+ </div>
308
+ ))}
309
+ </div>
310
+ )}
311
+ </div>
312
+ <p className="text-center text-sm min-h-6 text-gray-500 dark:text-gray-300 mt-2 mb-1">
313
+ {tps && messages.length > 0 && (
314
+ <>
315
+ {!isRunning && (
316
+ <span>
317
+ Generated {numTokens} tokens in{" "}
318
+ {(numTokens / tps).toFixed(2)} seconds&nbsp;&#40;
319
+ </span>
320
+ )}
321
+ {
322
+ <>
323
+ <span className="font-medium text-center mr-1 text-black dark:text-white">
324
+ {tps.toFixed(2)}
325
+ </span>
326
+ <span className="text-gray-500 dark:text-gray-300">
327
+ tokens/second
328
+ </span>
329
+ </>
330
+ }
331
+ {!isRunning && (
332
+ <>
333
+ <span className="mr-1">&#41;.</span>
334
+ <span
335
+ className="underline cursor-pointer"
336
+ onClick={() => {
337
+ worker.current.postMessage({ type: "reset" });
338
+ setMessages([]);
339
+ }}
340
+ >
341
+ Reset
342
+ </span>
343
+ </>
344
+ )}
345
+ </>
346
+ )}
347
+ </p>
348
+ </>
349
+ )}
350
+
351
+ <div className="w-[600px] max-w-[80%] mx-auto mt-2 mb-3">
352
+ <div className="border border-gray-300 dark:border-gray-500 dark:bg-gray-700 rounded-lg max-h-[200px] relative flex">
353
+ <textarea
354
+ ref={textareaRef}
355
+ className="scrollbar-thin w-[550px] px-3 py-4 rounded-lg bg-transparent border-none outline-hidden text-gray-800 disabled:text-gray-400 dark:text-gray-200 placeholder-gray-500 dark:placeholder-gray-300 disabled:placeholder-gray-200 dark:disabled:placeholder-gray-500 resize-none disabled:cursor-not-allowed"
356
+ placeholder="Type your message..."
357
+ type="text"
358
+ rows={1}
359
+ value={input}
360
+ disabled={status !== "ready"}
361
+ title={
362
+ status === "ready" ? "Model is ready" : "Model not loaded yet"
363
+ }
364
+ onKeyDown={(e) => {
365
+ if (
366
+ input.length > 0 &&
367
+ !isRunning &&
368
+ e.key === "Enter" &&
369
+ !e.shiftKey
370
+ ) {
371
+ e.preventDefault(); // Prevent default behavior of Enter key
372
+ onEnter(input);
373
+ }
374
+ }}
375
+ onInput={(e) => setInput(e.target.value)}
376
+ />
377
+ {isRunning ? (
378
+ <div className="cursor-pointer" onClick={onInterrupt}>
379
+ <StopIcon className="h-8 w-8 p-1 rounded-md text-gray-800 dark:text-gray-100 absolute right-3 bottom-3" />
380
+ </div>
381
+ ) : input.length > 0 ? (
382
+ <div className="cursor-pointer" onClick={() => onEnter(input)}>
383
+ <ArrowRightIcon
384
+ className={`h-8 w-8 p-1 bg-gray-800 dark:bg-gray-100 text-white dark:text-black rounded-md absolute right-3 bottom-3`}
385
+ />
386
+ </div>
387
+ ) : (
388
+ <div>
389
+ <ArrowRightIcon
390
+ className={`h-8 w-8 p-1 bg-gray-200 dark:bg-gray-600 text-gray-50 dark:text-gray-800 rounded-md absolute right-3 bottom-3`}
391
+ />
392
+ </div>
393
+ )}
394
+ </div>
395
+ <div className="flex justify-end">
396
+ <div
397
+ className={`border mt-1 inline-flex items-center p-2 gap-1 rounded-xl text-sm cursor-pointer ${
398
+ reasonEnabled
399
+ ? "border-blue-500 bg-blue-100 text-blue-500 dark:bg-blue-600 dark:text-gray-200"
400
+ : "dark:border-gray-700 bg-gray-800 text-gray-200 dark:text-gray-400"
401
+ } ${
402
+ messages.length === 0
403
+ ? "pointer-events-auto"
404
+ : "pointer-events-none opacity-50"
405
+ }`}
406
+ onClick={() => setReasonEnabled((prev) => !prev)}
407
+ >
408
+ <LightBulbIcon
409
+ className={`h-4 w-4 ${
410
+ reasonEnabled ? "" : "stroke-gray-600 dark:stroke-gray-400"
411
+ }`}
412
+ />
413
+ Reason
414
+ </div>
415
+ </div>
416
+ </div>
417
+ <p className="text-xs text-gray-400 text-center mb-3">
418
+ Disclaimer: Generated content may be inaccurate or false.
419
+ </p>
420
+ </div>
421
+ ) : (
422
+ <div className="fixed w-screen h-screen bg-black z-10 bg-opacity-[92%] text-white text-2xl font-semibold flex justify-center items-center text-center">
423
+ WebGPU is not supported
424
+ <br />
425
+ by this browser :&#40;
426
+ </div>
427
+ );
428
+ }
429
+
430
+ export default App;
src/components/Chat.css ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @scope (.markdown) {
2
+ /* Code blocks */
3
+ pre {
4
+ margin: 0.5rem 0;
5
+ white-space: break-spaces;
6
+ }
7
+
8
+ code {
9
+ padding: 0.2em 0.4em;
10
+ border-radius: 4px;
11
+ font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
12
+ font-size: 0.9em;
13
+ }
14
+
15
+ pre,
16
+ code {
17
+ background-color: #f2f2f2;
18
+ }
19
+
20
+ @media (prefers-color-scheme: dark) {
21
+ pre,
22
+ code {
23
+ background-color: #333;
24
+ }
25
+ }
26
+
27
+ pre:has(code) {
28
+ padding: 1rem 0.5rem;
29
+ }
30
+
31
+ pre > code {
32
+ padding: 0;
33
+ }
34
+
35
+ /* Headings */
36
+ h1,
37
+ h2,
38
+ h3,
39
+ h4,
40
+ h5,
41
+ h6 {
42
+ font-weight: 600;
43
+ line-height: 1.2;
44
+ }
45
+
46
+ h1 {
47
+ font-size: 2em;
48
+ margin: 1rem 0;
49
+ }
50
+
51
+ h2 {
52
+ font-size: 1.5em;
53
+ margin: 0.83rem 0;
54
+ }
55
+
56
+ h3 {
57
+ font-size: 1.25em;
58
+ margin: 0.67rem 0;
59
+ }
60
+
61
+ h4 {
62
+ font-size: 1em;
63
+ margin: 0.5rem 0;
64
+ }
65
+
66
+ h5 {
67
+ font-size: 0.875em;
68
+ margin: 0.33rem 0;
69
+ }
70
+
71
+ h6 {
72
+ font-size: 0.75em;
73
+ margin: 0.25rem 0;
74
+ }
75
+
76
+ h1,
77
+ h2,
78
+ h3,
79
+ h4,
80
+ h5,
81
+ h6:first-child {
82
+ margin-top: 0;
83
+ }
84
+
85
+ /* Unordered List */
86
+ ul {
87
+ list-style-type: disc;
88
+ margin-left: 1.5rem;
89
+ }
90
+
91
+ /* Ordered List */
92
+ ol {
93
+ list-style-type: decimal;
94
+ margin-left: 1.5rem;
95
+ }
96
+
97
+ /* List Items */
98
+ li {
99
+ margin: 0.25rem 0;
100
+ }
101
+
102
+ p:not(:first-child) {
103
+ margin-top: 0.75rem;
104
+ }
105
+
106
+ p:not(:last-child) {
107
+ margin-bottom: 0.75rem;
108
+ }
109
+
110
+ ul > li {
111
+ margin-left: 1rem;
112
+ }
113
+
114
+ /* Table */
115
+ table,
116
+ th,
117
+ td {
118
+ border: 1px solid lightgray;
119
+ padding: 0.25rem;
120
+ }
121
+
122
+ @media (prefers-color-scheme: dark) {
123
+ table,
124
+ th,
125
+ td {
126
+ border: 1px solid #f2f2f2;
127
+ }
128
+ }
129
+
130
+ hr {
131
+ margin: 1.5rem 0;
132
+ }
133
+ }
src/components/Chat.jsx ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { marked } from "marked";
3
+ import DOMPurify from "dompurify";
4
+
5
+ import BotIcon from "./icons/BotIcon";
6
+ import BrainIcon from "./icons/BrainIcon";
7
+ import UserIcon from "./icons/UserIcon";
8
+
9
+ import { MathJaxContext, MathJax } from "better-react-mathjax";
10
+ import "./Chat.css";
11
+
12
+ function render(text) {
13
+ // Replace all instances of single backslashes before brackets with double backslashes
14
+ // See https://github.com/markedjs/marked/issues/546 for more information.
15
+ text = text.replace(/\\([\[\]\(\)])/g, "\\\\$1");
16
+
17
+ const result = DOMPurify.sanitize(
18
+ marked.parse(text, {
19
+ async: false,
20
+ breaks: true,
21
+ }),
22
+ );
23
+ return result;
24
+ }
25
+ function Message({ role, content, answerIndex }) {
26
+ const thinking =
27
+ answerIndex !== undefined ? content.slice(0, answerIndex) : content;
28
+ const answer = answerIndex !== undefined ? content.slice(answerIndex) : "";
29
+
30
+ const [showThinking, setShowThinking] = useState(false);
31
+ const doneThinking = answerIndex === 0 || answer.length > 0;
32
+ return (
33
+ <div className="flex items-start space-x-4">
34
+ {role === "assistant" ? (
35
+ <>
36
+ <BotIcon className="h-6 w-6 min-h-6 min-w-6 my-3 text-gray-500 dark:text-gray-300" />
37
+ <div className="bg-gray-200 dark:bg-gray-700 rounded-lg p-4">
38
+ <div className="min-h-6 text-gray-800 dark:text-gray-200 overflow-wrap-anywhere">
39
+ {answerIndex === 0 || thinking.length > 0 ? (
40
+ <>
41
+ {thinking.length > 0 && (
42
+ <div className="bg-white dark:bg-gray-800 rounded-lg flex flex-col mb-2">
43
+ <button
44
+ className="flex items-center gap-2 cursor-pointer p-4 hover:bg-gray-50 dark:hover:bg-gray-900 rounded-lg "
45
+ onClick={() => setShowThinking((prev) => !prev)}
46
+ style={{ width: showThinking ? "100%" : "auto" }}
47
+ >
48
+ <BrainIcon
49
+ className={doneThinking ? "" : "animate-pulse"}
50
+ />
51
+ <span>
52
+ {doneThinking ? "View reasoning." : "Thinking..."}
53
+ </span>
54
+ <span className="ml-auto text-gray-700">
55
+ {showThinking ? "▲" : "▼"}
56
+ </span>
57
+ </button>
58
+ {showThinking && (
59
+ <MathJax
60
+ className="border-t border-gray-200 dark:border-gray-700 px-4 py-2"
61
+ dynamic
62
+ >
63
+ <span
64
+ className="markdown"
65
+ dangerouslySetInnerHTML={{
66
+ __html: render(thinking),
67
+ }}
68
+ />
69
+ </MathJax>
70
+ )}
71
+ </div>
72
+ )}
73
+ {doneThinking && (
74
+ <MathJax dynamic>
75
+ <span
76
+ className="markdown"
77
+ dangerouslySetInnerHTML={{
78
+ __html: render(answer),
79
+ }}
80
+ />
81
+ </MathJax>
82
+ )}
83
+ </>
84
+ ) : (
85
+ <span className="h-6 flex items-center gap-1">
86
+ <span className="w-2.5 h-2.5 bg-gray-600 dark:bg-gray-300 rounded-full animate-pulse"></span>
87
+ <span className="w-2.5 h-2.5 bg-gray-600 dark:bg-gray-300 rounded-full animate-pulse animation-delay-200"></span>
88
+ <span className="w-2.5 h-2.5 bg-gray-600 dark:bg-gray-300 rounded-full animate-pulse animation-delay-400"></span>
89
+ </span>
90
+ )}
91
+ </div>
92
+ </div>
93
+ </>
94
+ ) : (
95
+ <>
96
+ <UserIcon className="h-6 w-6 min-h-6 min-w-6 my-3 text-gray-500 dark:text-gray-300" />
97
+ <div className="bg-blue-500 text-white rounded-lg p-4">
98
+ <p className="min-h-6 overflow-wrap-anywhere">{content}</p>
99
+ </div>
100
+ </>
101
+ )}
102
+ </div>
103
+ );
104
+ }
105
+
106
+ export default function Chat({ messages }) {
107
+ const empty = messages.length === 0;
108
+
109
+ return (
110
+ <div
111
+ className={`flex-1 p-6 pb-2 max-w-[960px] w-full ${empty ? "flex flex-col items-center justify-end" : "space-y-4"}`}
112
+ >
113
+ <MathJaxContext>
114
+ {empty ? (
115
+ <div className="text-xl">Ready!</div>
116
+ ) : (
117
+ messages.map((msg, i) => <Message key={`message-${i}`} {...msg} />)
118
+ )}
119
+ </MathJaxContext>
120
+ </div>
121
+ );
122
+ }
src/components/Progress.jsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function formatBytes(size) {
2
+ const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
3
+ return (
4
+ +(size / Math.pow(1024, i)).toFixed(2) * 1 +
5
+ ["B", "kB", "MB", "GB", "TB"][i]
6
+ );
7
+ }
8
+
9
+ export default function Progress({ text, percentage, total }) {
10
+ percentage ??= 0;
11
+ return (
12
+ <div className="w-full bg-gray-100 dark:bg-gray-700 text-left rounded-lg overflow-hidden mb-0.5">
13
+ <div
14
+ className="bg-blue-400 whitespace-nowrap px-1 text-sm"
15
+ style={{ width: `${percentage}%` }}
16
+ >
17
+ {text} ({percentage.toFixed(2)}%
18
+ {isNaN(total) ? "" : ` of ${formatBytes(total)}`})
19
+ </div>
20
+ </div>
21
+ );
22
+ }
src/components/icons/ArrowRightIcon.jsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function ArrowRightIcon(props) {
2
+ return (
3
+ <svg
4
+ {...props}
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ width="24"
7
+ height="24"
8
+ viewBox="0 0 24 24"
9
+ fill="none"
10
+ stroke="currentColor"
11
+ strokeWidth="2"
12
+ strokeLinecap="round"
13
+ strokeLinejoin="round"
14
+ >
15
+ <path d="M5 12h14" />
16
+ <path d="m12 5 7 7-7 7" />
17
+ </svg>
18
+ );
19
+ }
src/components/icons/BotIcon.jsx ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function BotIcon(props) {
2
+ return (
3
+ <svg
4
+ {...props}
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ width="24"
7
+ height="24"
8
+ viewBox="0 0 24 24"
9
+ fill="none"
10
+ stroke="currentColor"
11
+ strokeWidth="2"
12
+ strokeLinecap="round"
13
+ strokeLinejoin="round"
14
+ >
15
+ <path d="M12 8V4H8" />
16
+ <rect width="16" height="12" x="4" y="8" rx="2" />
17
+ <path d="M2 14h2" />
18
+ <path d="M20 14h2" />
19
+ <path d="M15 13v2" />
20
+ <path d="M9 13v2" />
21
+ </svg>
22
+ );
23
+ }
src/components/icons/BrainIcon.jsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function BotIcon(props) {
2
+ return (
3
+ <svg
4
+ {...props}
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ width="24"
7
+ height="24"
8
+ viewBox="0 0 32 32"
9
+ fill="none"
10
+ stroke="currentColor"
11
+ strokeWidth="2"
12
+ strokeLinecap="round"
13
+ strokeLinejoin="round"
14
+ >
15
+ <path
16
+ className="stroke-gray-600 dark:stroke-gray-400"
17
+ d="M16 6v3.33M16 6c0-2.65 3.25-4.3 5.4-2.62 1.2.95 1.6 2.65.95 4.04a3.63 3.63 0 0 1 4.61.16 3.45 3.45 0 0 1 .46 4.37 5.32 5.32 0 0 1 1.87 4.75c-.22 1.66-1.39 3.6-3.07 4.14M16 6c0-2.65-3.25-4.3-5.4-2.62a3.37 3.37 0 0 0-.95 4.04 3.65 3.65 0 0 0-4.6.16 3.37 3.37 0 0 0-.49 4.27 5.57 5.57 0 0 0-1.85 4.85 5.3 5.3 0 0 0 3.07 4.15M16 9.33v17.34m0-17.34c0 2.18 1.82 4 4 4m6.22 7.5c.67 1.3.56 2.91-.27 4.11a4.05 4.05 0 0 1-4.62 1.5c0 1.53-1.05 2.9-2.66 2.9A2.7 2.7 0 0 1 16 26.66m10.22-5.83a4.05 4.05 0 0 0-3.55-2.17m-16.9 2.18a4.05 4.05 0 0 0 .28 4.1c1 1.44 2.92 2.09 4.59 1.5 0 1.52 1.12 2.88 2.7 2.88A2.7 2.7 0 0 0 16 26.67M5.78 20.85a4.04 4.04 0 0 1 3.55-2.18"
18
+ ></path>
19
+ </svg>
20
+ );
21
+ }
src/components/icons/LightBulbIcon.jsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function LightBulbIcon(props) {
2
+ return (
3
+ <svg
4
+ {...props}
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ width="24"
7
+ height="24"
8
+ viewBox="0 0 24 24"
9
+ fill="none"
10
+ stroke="currentColor"
11
+ strokeWidth="1.5"
12
+ strokeLinecap="round"
13
+ strokeLinejoin="round"
14
+ >
15
+ <path d="M12 18v-5.25m0 0a6.01 6.01 0 0 0 1.5-.189m-1.5.189a6.01 6.01 0 0 1-1.5-.189m3.75 7.478a12.06 12.06 0 0 1-4.5 0m3.75 2.383a14.406 14.406 0 0 1-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 1 0-7.517 0c.85.493 1.509 1.333 1.509 2.316V18"></path>
16
+ </svg>
17
+ );
18
+ }
src/components/icons/StopIcon.jsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function StopIcon(props) {
2
+ return (
3
+ <svg
4
+ {...props}
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ width="24"
7
+ height="24"
8
+ viewBox="0 0 24 24"
9
+ fill="none"
10
+ stroke="currentColor"
11
+ strokeWidth="2"
12
+ strokeLinecap="round"
13
+ strokeLinejoin="round"
14
+ >
15
+ <path d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
16
+ <path
17
+ fill="currentColor"
18
+ d="M9 9.563C9 9.252 9.252 9 9.563 9h4.874c.311 0 .563.252.563.563v4.874c0 .311-.252.563-.563.563H9.564A.562.562 0 0 1 9 14.437V9.564Z"
19
+ />
20
+ </svg>
21
+ );
22
+ }
src/components/icons/UserIcon.jsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export default function UserIcon(props) {
2
+ return (
3
+ <svg
4
+ {...props}
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ width="24"
7
+ height="24"
8
+ viewBox="0 0 24 24"
9
+ fill="none"
10
+ stroke="currentColor"
11
+ strokeWidth="2"
12
+ strokeLinecap="round"
13
+ strokeLinejoin="round"
14
+ >
15
+ <path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
16
+ <circle cx="12" cy="7" r="4" />
17
+ </svg>
18
+ );
19
+ }
src/index.css ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ /* Custom scrollbar styles */
4
+ .scrollbar-thin::-webkit-scrollbar {
5
+ width: 0.5rem; /* Equivalent to w-2 in Tailwind */
6
+ }
7
+
8
+ .scrollbar-thin::-webkit-scrollbar-track {
9
+ border-radius: 9999px; /* Equivalent to rounded-full in Tailwind */
10
+ background-color: #f3f4f6; /* Equivalent to bg-gray-100 in Tailwind */
11
+ }
12
+
13
+ .scrollbar-thin::-webkit-scrollbar-track.dark {
14
+ background-color: #374151; /* Equivalent to dark:bg-gray-700 in Tailwind */
15
+ }
16
+
17
+ .scrollbar-thin::-webkit-scrollbar-thumb {
18
+ border-radius: 9999px; /* Equivalent to rounded-full in Tailwind */
19
+ background-color: #d1d5db; /* Equivalent to bg-gray-300 in Tailwind */
20
+ }
21
+
22
+ .scrollbar-thin::-webkit-scrollbar-thumb:hover {
23
+ background-color: #6b7280; /* Equivalent to bg-gray-500 in Tailwind */
24
+ }
25
+
26
+ /* Animation delay classes */
27
+ .animation-delay-200 {
28
+ animation-delay: 200ms;
29
+ }
30
+
31
+ .animation-delay-400 {
32
+ animation-delay: 400ms;
33
+ }
34
+
35
+ /* Overflow wrap class */
36
+ .overflow-wrap-anywhere {
37
+ overflow-wrap: anywhere;
38
+ }
src/main.jsx 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.jsx";
5
+
6
+ createRoot(document.getElementById("root")).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ );
src/worker.js ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ AutoTokenizer,
3
+ AutoModelForCausalLM,
4
+ TextStreamer,
5
+ InterruptableStoppingCriteria,
6
+ } from "@huggingface/transformers";
7
+
8
+ /**
9
+ * Helper function to perform feature detection for WebGPU
10
+ */
11
+ async function check() {
12
+ try {
13
+ const adapter = await navigator.gpu.requestAdapter();
14
+ if (!adapter) {
15
+ throw new Error("WebGPU is not supported (no adapter found)");
16
+ }
17
+ if (!adapter.features.has("shader-f16")) {
18
+ throw new Error("shader-f16 is not supported in this browser");
19
+ }
20
+ } catch (e) {
21
+ self.postMessage({
22
+ status: "error",
23
+ data: e.toString(),
24
+ });
25
+ }
26
+ }
27
+
28
+ /**
29
+ * This class uses the Singleton pattern to enable lazy-loading of the pipeline
30
+ */
31
+ class TextGenerationPipeline {
32
+ static model_id = "HuggingFaceTB/SmolLM3-3B-ONNX";
33
+
34
+ static async getInstance(progress_callback = null) {
35
+ this.tokenizer ??= AutoTokenizer.from_pretrained(this.model_id, {
36
+ progress_callback,
37
+ });
38
+
39
+ this.model ??= AutoModelForCausalLM.from_pretrained(this.model_id, {
40
+ dtype: "q4f16",
41
+ device: "webgpu",
42
+ progress_callback,
43
+ });
44
+
45
+ return Promise.all([this.tokenizer, this.model]);
46
+ }
47
+ }
48
+
49
+ const stopping_criteria = new InterruptableStoppingCriteria();
50
+
51
+ let past_key_values_cache = null;
52
+ async function generate({ messages, reasonEnabled }) {
53
+ const [tokenizer, model] = await TextGenerationPipeline.getInstance();
54
+
55
+ const inputs = tokenizer.apply_chat_template(messages, {
56
+ enable_thinking: reasonEnabled,
57
+ add_generation_prompt: true,
58
+ return_dict: true,
59
+ });
60
+
61
+ const [START_THINKING_TOKEN_ID, END_THINKING_TOKEN_ID] = tokenizer.encode(
62
+ "<think></think>",
63
+ { add_special_tokens: false },
64
+ );
65
+
66
+ let state = "answering"; // 'thinking' or 'answering'
67
+ let startTime;
68
+ let numTokens = 0;
69
+ let tps;
70
+ const token_callback_function = (tokens) => {
71
+ startTime ??= performance.now();
72
+
73
+ if (numTokens++ > 0) {
74
+ tps = (numTokens / (performance.now() - startTime)) * 1000;
75
+ }
76
+ switch (Number(tokens[0])) {
77
+ case START_THINKING_TOKEN_ID:
78
+ state = "thinking";
79
+ break;
80
+ case END_THINKING_TOKEN_ID:
81
+ state = "answering";
82
+ break;
83
+ }
84
+ };
85
+ const callback_function = (output) => {
86
+ self.postMessage({
87
+ status: "update",
88
+ output,
89
+ tps,
90
+ numTokens,
91
+ state,
92
+ });
93
+ };
94
+
95
+ const streamer = new TextStreamer(tokenizer, {
96
+ skip_prompt: true,
97
+ skip_special_tokens: true,
98
+ callback_function,
99
+ token_callback_function,
100
+ });
101
+
102
+ // Tell the main thread we are starting
103
+ self.postMessage({ status: "start" });
104
+
105
+ const { past_key_values, sequences } = await model.generate({
106
+ ...inputs,
107
+ past_key_values: past_key_values_cache,
108
+
109
+ // Sampling
110
+ do_sample: !reasonEnabled,
111
+ repetition_penalty: reasonEnabled ? 1.1 : undefined,
112
+ top_k: 3,
113
+
114
+ max_new_tokens: reasonEnabled ? 4096 : 1024,
115
+ streamer,
116
+ stopping_criteria,
117
+ return_dict_in_generate: true,
118
+ });
119
+ past_key_values_cache = past_key_values;
120
+
121
+ const decoded = tokenizer.batch_decode(sequences, {
122
+ skip_special_tokens: true,
123
+ });
124
+
125
+ // Send the output back to the main thread
126
+ self.postMessage({
127
+ status: "complete",
128
+ output: decoded,
129
+ });
130
+ }
131
+
132
+ async function load() {
133
+ self.postMessage({
134
+ status: "loading",
135
+ data: "Loading model...",
136
+ });
137
+
138
+ // Load the pipeline and save it for future use.
139
+ const [tokenizer, model] = await TextGenerationPipeline.getInstance((x) => {
140
+ // We also add a progress callback to the pipeline so that we can
141
+ // track model loading.
142
+ self.postMessage(x);
143
+ });
144
+
145
+ self.postMessage({
146
+ status: "loading",
147
+ data: "Compiling shaders and warming up model...",
148
+ });
149
+
150
+ // Run model with dummy input to compile shaders
151
+ const inputs = tokenizer("a");
152
+ await model.generate({ ...inputs, max_new_tokens: 1 });
153
+ self.postMessage({ status: "ready" });
154
+ }
155
+ // Listen for messages from the main thread
156
+ self.addEventListener("message", async (e) => {
157
+ const { type, data } = e.data;
158
+
159
+ switch (type) {
160
+ case "check":
161
+ check();
162
+ break;
163
+
164
+ case "load":
165
+ load();
166
+ break;
167
+
168
+ case "generate":
169
+ stopping_criteria.reset();
170
+ generate(data);
171
+ break;
172
+
173
+ case "interrupt":
174
+ stopping_criteria.interrupt();
175
+ break;
176
+
177
+ case "reset":
178
+ past_key_values_cache = null;
179
+ stopping_criteria.reset();
180
+ break;
181
+ }
182
+ });
vite.config.js ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "vite";
2
+ import tailwindcss from "@tailwindcss/vite";
3
+ import react from "@vitejs/plugin-react";
4
+
5
+ // https://vite.dev/config/
6
+ export default defineConfig({
7
+ plugins: [tailwindcss(), react()],
8
+ });