mernasameh5 commited on
Commit
dfaa13f
·
verified ·
1 Parent(s): 424f813

Update src/App.jsx

Browse files
Files changed (1) hide show
  1. src/App.jsx +71 -234
src/App.jsx CHANGED
@@ -1,5 +1,4 @@
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";
@@ -7,27 +6,25 @@ import Progress from "./components/Progress";
7
 
8
  const IS_WEBGPU_AVAILABLE = !!navigator.gpu;
9
  const STICKY_SCROLL_THRESHOLD = 120;
 
 
10
  const EXAMPLES = [
11
- "Give me some tips to improve my time management skills.",
12
- "What is the difference between AI and ML?",
13
- "Write python code to compute the nth fibonacci number.",
14
  ];
15
 
16
  function App() {
17
- // Create a reference to the worker object.
18
  const worker = useRef(null);
19
-
20
  const textareaRef = useRef(null);
21
  const chatContainerRef = useRef(null);
22
 
23
- // Model loading and progress
24
  const [status, setStatus] = useState(null);
25
  const [error, setError] = useState(null);
26
  const [loadingMessage, setLoadingMessage] = useState("");
27
  const [progressItems, setProgressItems] = useState([]);
28
  const [isRunning, setIsRunning] = useState(false);
29
 
30
- // Inputs and outputs
31
  const [input, setInput] = useState("");
32
  const [messages, setMessages] = useState([]);
33
  const [tps, setTps] = useState(null);
@@ -41,8 +38,6 @@ function App() {
41
  }
42
 
43
  function onInterrupt() {
44
- // NOTE: We do not set isRunning to false here because the worker
45
- // will send a 'complete' message when it is done.
46
  worker.current.postMessage({ type: "interrupt" });
47
  }
48
 
@@ -52,244 +47,134 @@ function App() {
52
 
53
  function resizeInput() {
54
  if (!textareaRef.current) return;
55
-
56
  const target = textareaRef.current;
57
  target.style.height = "auto";
58
  const newHeight = Math.min(Math.max(target.scrollHeight, 24), 200);
59
  target.style.height = `${newHeight}px`;
60
  }
61
 
62
- // We use the `useEffect` hook to setup the worker as soon as the `App` component is mounted.
63
  useEffect(() => {
64
- // Create the worker if it does not yet exist.
65
  if (!worker.current) {
66
  worker.current = new Worker(new URL("./worker.js", import.meta.url), {
67
  type: "module",
68
  });
69
- worker.current.postMessage({ type: "check" }); // Do a feature check
70
  }
71
 
72
- // Create a callback function for messages from the worker thread.
73
  const onMessageReceived = (e) => {
74
  switch (e.data.status) {
75
  case "loading":
76
- // Model file start load: add a new progress item to the list.
77
  setStatus("loading");
78
  setLoadingMessage(e.data.data);
79
  break;
80
-
81
  case "initiate":
82
  setProgressItems((prev) => [...prev, e.data]);
83
  break;
84
-
85
  case "progress":
86
- // Model file progress: update one of the progress items.
87
  setProgressItems((prev) =>
88
- prev.map((item) => {
89
- if (item.file === e.data.file) {
90
- return { ...item, ...e.data };
91
- }
92
- return item;
93
- }),
94
  );
95
  break;
96
-
97
  case "done":
98
- // Model file loaded: remove the progress item from the list.
99
- setProgressItems((prev) =>
100
- prev.filter((item) => item.file !== e.data.file),
101
- );
102
  break;
103
-
104
  case "ready":
105
- // Pipeline ready: the worker is ready to accept messages.
106
  setStatus("ready");
107
  break;
108
-
109
  case "start":
110
- {
111
- // Start generation
112
- setMessages((prev) => [
113
- ...prev,
114
- { role: "assistant", content: "" },
115
- ]);
116
- }
117
  break;
118
-
119
  case "update":
120
- {
121
- // Generation update: update the output text.
122
- // Parse messages
123
- const { output, tps, numTokens } = e.data;
124
- setTps(tps);
125
- setNumTokens(numTokens);
126
- setMessages((prev) => {
127
- const cloned = [...prev];
128
- const last = cloned.at(-1);
129
- cloned[cloned.length - 1] = {
130
- ...last,
131
- content: last.content + output,
132
- };
133
- return cloned;
134
- });
135
- }
136
  break;
137
-
138
  case "complete":
139
- // Generation complete: re-enable the "Generate" button
140
  setIsRunning(false);
141
  break;
142
-
143
  case "error":
144
  setError(e.data.data);
145
  break;
146
  }
147
  };
148
 
149
- const onErrorReceived = (e) => {
150
- console.error("Worker error:", e);
151
- };
152
-
153
- // Attach the callback function as an event listener.
154
  worker.current.addEventListener("message", onMessageReceived);
155
- worker.current.addEventListener("error", onErrorReceived);
156
-
157
- // Define a cleanup function for when the component is unmounted.
158
- return () => {
159
- worker.current.removeEventListener("message", onMessageReceived);
160
- worker.current.removeEventListener("error", onErrorReceived);
161
- };
162
  }, []);
163
 
164
- // Send the messages to the worker thread whenever the `messages` state changes.
165
  useEffect(() => {
166
- if (messages.filter((x) => x.role === "user").length === 0) {
167
- // No user messages yet: do nothing.
168
- return;
169
- }
170
- if (messages.at(-1).role === "assistant") {
171
- // Do not update if the last message is from the assistant
172
- return;
173
- }
174
- setTps(null);
175
  worker.current.postMessage({ type: "generate", data: messages });
176
  }, [messages, isRunning]);
177
 
178
  useEffect(() => {
179
  if (!chatContainerRef.current || !isRunning) return;
180
  const element = chatContainerRef.current;
181
- if (
182
- element.scrollHeight - element.scrollTop - element.clientHeight <
183
- STICKY_SCROLL_THRESHOLD
184
- ) {
185
  element.scrollTop = element.scrollHeight;
186
  }
187
  }, [messages, isRunning]);
188
 
189
  return IS_WEBGPU_AVAILABLE ? (
190
- <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">
191
  {status === null && messages.length === 0 && (
192
- <div className="h-full overflow-auto scrollbar-thin flex justify-center items-center flex-col relative">
193
- <div className="flex flex-col items-center mb-1 max-w-[320px] text-center">
194
- <img
195
- src="logo.png"
196
- width="80%"
197
- height="auto"
198
- className="block"
199
- ></img>
200
- <h1 className="text-4xl font-bold mb-1">SmolLM2 WebGPU</h1>
201
- <h2 className="font-semibold">
202
- A blazingly fast and powerful AI chatbot that runs locally in your
203
- browser.
204
  </h2>
205
  </div>
206
 
207
  <div className="flex flex-col items-center px-4">
208
- <p className="max-w-[480px] mb-4">
209
- <br />
210
- You are about to load{" "}
211
- <a
212
- href="https://huggingface.co/HuggingFaceTB/SmolLM2-1.7B-Instruct"
213
- target="_blank"
214
- rel="noreferrer"
215
- className="font-medium underline"
216
- >
217
- SmolLM2-1.7B-Instruct
218
- </a>
219
- , a 1.7B parameter LLM optimized for in-browser inference.
220
- Everything runs entirely in your browser with{" "}
221
- <a
222
- href="https://huggingface.co/docs/transformers.js"
223
- target="_blank"
224
- rel="noreferrer"
225
- className="underline"
226
- >
227
- 🤗&nbsp;Transformers.js
228
- </a>{" "}
229
- and ONNX Runtime Web, meaning no data is sent to a server. Once
230
- loaded, it can even be used offline. The source code for the demo
231
- is available on{" "}
232
- <a
233
- href="https://github.com/huggingface/transformers.js-examples/tree/main/smollm-webgpu"
234
- target="_blank"
235
- rel="noreferrer"
236
- className="font-medium underline"
237
- >
238
- GitHub
239
- </a>
240
- .
241
  </p>
242
 
243
- {error && (
244
- <div className="text-red-500 text-center mb-2">
245
- <p className="mb-1">
246
- Unable to load model due to the following error:
247
- </p>
248
- <p className="text-sm">{error}</p>
249
- </div>
250
- )}
251
-
252
  <button
253
- className="border px-4 py-2 rounded-lg bg-blue-400 text-white hover:bg-blue-500 disabled:bg-blue-100 disabled:cursor-not-allowed select-none"
254
  onClick={() => {
255
  worker.current.postMessage({ type: "load" });
256
  setStatus("loading");
257
  }}
258
  disabled={status !== null || error !== null}
259
  >
260
- Load model
261
  </button>
262
  </div>
263
  </div>
264
  )}
 
265
  {status === "loading" && (
266
- <>
267
- <div className="w-full max-w-[500px] text-left mx-auto p-4 bottom-0 mt-auto">
268
- <p className="text-center mb-1">{loadingMessage}</p>
269
- {progressItems.map(({ file, progress, total }, i) => (
270
- <Progress
271
- key={i}
272
- text={file}
273
- percentage={progress}
274
- total={total}
275
- />
276
- ))}
277
- </div>
278
- </>
279
  )}
280
 
281
  {status === "ready" && (
282
- <div
283
- ref={chatContainerRef}
284
- className="overflow-y-auto scrollbar-thin w-full flex flex-col items-center h-full"
285
- >
286
  <Chat messages={messages} />
287
  {messages.length === 0 && (
288
- <div>
 
289
  {EXAMPLES.map((msg, i) => (
290
  <div
291
  key={i}
292
- className="m-1 border dark:border-gray-600 rounded-md p-2 bg-gray-100 dark:bg-gray-700 cursor-pointer"
293
  onClick={() => onEnter(msg)}
294
  >
295
  {msg}
@@ -297,98 +182,50 @@ function App() {
297
  ))}
298
  </div>
299
  )}
300
- <p className="text-center text-sm min-h-6 text-gray-500 dark:text-gray-300">
301
- {tps && messages.length > 0 && (
302
- <>
303
- {!isRunning && (
304
- <span>
305
- Generated {numTokens} tokens in{" "}
306
- {(numTokens / tps).toFixed(2)} seconds&nbsp;&#40;
307
- </span>
308
- )}
309
- {
310
- <>
311
- <span className="font-medium text-center mr-1 text-black dark:text-white">
312
- {tps.toFixed(2)}
313
- </span>
314
- <span className="text-gray-500 dark:text-gray-300">
315
- tokens/second
316
- </span>
317
- </>
318
- }
319
- {!isRunning && (
320
- <>
321
- <span className="mr-1">&#41;.</span>
322
- <span
323
- className="underline cursor-pointer"
324
- onClick={() => {
325
- worker.current.postMessage({ type: "reset" });
326
- setMessages([]);
327
- }}
328
- >
329
- Reset
330
- </span>
331
- </>
332
- )}
333
- </>
334
- )}
335
- </p>
336
  </div>
337
  )}
338
 
339
- <div className="mt-2 border dark:bg-gray-700 rounded-lg w-[600px] max-w-[80%] max-h-[200px] mx-auto relative mb-3 flex">
340
  <textarea
341
  ref={textareaRef}
342
- className="scrollbar-thin w-[550px] dark:bg-gray-700 px-3 py-4 rounded-lg bg-transparent border-none outline-none text-gray-800 disabled:text-gray-400 dark:text-gray-200 placeholder-gray-500 dark:placeholder-gray-400 disabled:placeholder-gray-200 resize-none disabled:cursor-not-allowed"
343
- placeholder="Type your message..."
344
- type="text"
345
  rows={1}
346
  value={input}
347
  disabled={status !== "ready"}
348
- title={status === "ready" ? "Model is ready" : "Model not loaded yet"}
349
  onKeyDown={(e) => {
350
- if (
351
- input.length > 0 &&
352
- !isRunning &&
353
- e.key === "Enter" &&
354
- !e.shiftKey
355
- ) {
356
- e.preventDefault(); // Prevent default behavior of Enter key
357
  onEnter(input);
358
  }
359
  }}
360
  onInput={(e) => setInput(e.target.value)}
361
  />
362
- {isRunning ? (
363
- <div className="cursor-pointer" onClick={onInterrupt}>
364
- <StopIcon className="h-8 w-8 p-1 rounded-md text-gray-800 dark:text-gray-100 absolute right-3 bottom-3" />
365
- </div>
366
- ) : input.length > 0 ? (
367
- <div className="cursor-pointer" onClick={() => onEnter(input)}>
368
- <ArrowRightIcon
369
- 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`}
370
- />
371
- </div>
372
- ) : (
373
- <div>
374
- <ArrowRightIcon
375
- 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`}
376
- />
377
- </div>
378
- )}
379
  </div>
380
 
381
- <p className="text-xs text-gray-400 text-center mb-3">
382
- Disclaimer: Generated content may be inaccurate or false.
383
  </p>
384
  </div>
385
  ) : (
386
- <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">
387
- WebGPU is not supported
388
- <br />
389
- by this browser :&#40;
390
  </div>
391
  );
392
  }
393
 
394
- export default App;
 
1
  import { useEffect, useState, useRef } from "react";
 
2
  import Chat from "./components/Chat";
3
  import ArrowRightIcon from "./components/icons/ArrowRightIcon";
4
  import StopIcon from "./components/icons/StopIcon";
 
6
 
7
  const IS_WEBGPU_AVAILABLE = !!navigator.gpu;
8
  const STICKY_SCROLL_THRESHOLD = 120;
9
+
10
+
11
  const EXAMPLES = [
12
+ "Triage: Patient with sudden chest pain and sweating.",
13
+ "ABCDE assessment for an unconscious patient after a fall.",
14
+ "Initial steps for a severe allergic reaction (Anaphylaxis).",
15
  ];
16
 
17
  function App() {
 
18
  const worker = useRef(null);
 
19
  const textareaRef = useRef(null);
20
  const chatContainerRef = useRef(null);
21
 
 
22
  const [status, setStatus] = useState(null);
23
  const [error, setError] = useState(null);
24
  const [loadingMessage, setLoadingMessage] = useState("");
25
  const [progressItems, setProgressItems] = useState([]);
26
  const [isRunning, setIsRunning] = useState(false);
27
 
 
28
  const [input, setInput] = useState("");
29
  const [messages, setMessages] = useState([]);
30
  const [tps, setTps] = useState(null);
 
38
  }
39
 
40
  function onInterrupt() {
 
 
41
  worker.current.postMessage({ type: "interrupt" });
42
  }
43
 
 
47
 
48
  function resizeInput() {
49
  if (!textareaRef.current) return;
 
50
  const target = textareaRef.current;
51
  target.style.height = "auto";
52
  const newHeight = Math.min(Math.max(target.scrollHeight, 24), 200);
53
  target.style.height = `${newHeight}px`;
54
  }
55
 
 
56
  useEffect(() => {
 
57
  if (!worker.current) {
58
  worker.current = new Worker(new URL("./worker.js", import.meta.url), {
59
  type: "module",
60
  });
61
+ worker.current.postMessage({ type: "check" });
62
  }
63
 
 
64
  const onMessageReceived = (e) => {
65
  switch (e.data.status) {
66
  case "loading":
 
67
  setStatus("loading");
68
  setLoadingMessage(e.data.data);
69
  break;
 
70
  case "initiate":
71
  setProgressItems((prev) => [...prev, e.data]);
72
  break;
 
73
  case "progress":
 
74
  setProgressItems((prev) =>
75
+ prev.map((item) => (item.file === e.data.file ? { ...item, ...e.data } : item))
 
 
 
 
 
76
  );
77
  break;
 
78
  case "done":
79
+ setProgressItems((prev) => prev.filter((item) => item.file !== e.data.file));
 
 
 
80
  break;
 
81
  case "ready":
 
82
  setStatus("ready");
83
  break;
 
84
  case "start":
85
+ setMessages((prev) => [...prev, { role: "assistant", content: "" }]);
 
 
 
 
 
 
86
  break;
 
87
  case "update":
88
+ const { output, tps, numTokens } = e.data;
89
+ setTps(tps);
90
+ setNumTokens(numTokens);
91
+ setMessages((prev) => {
92
+ const cloned = [...prev];
93
+ const last = cloned.at(-1);
94
+ cloned[cloned.length - 1] = { ...last, content: last.content + output };
95
+ return cloned;
96
+ });
 
 
 
 
 
 
 
97
  break;
 
98
  case "complete":
 
99
  setIsRunning(false);
100
  break;
 
101
  case "error":
102
  setError(e.data.data);
103
  break;
104
  }
105
  };
106
 
 
 
 
 
 
107
  worker.current.addEventListener("message", onMessageReceived);
108
+ return () => worker.current.removeEventListener("message", onMessageReceived);
 
 
 
 
 
 
109
  }, []);
110
 
 
111
  useEffect(() => {
112
+ if (messages.filter((x) => x.role === "user").length === 0) return;
113
+ if (messages.at(-1).role === "assistant") return;
 
 
 
 
 
 
 
114
  worker.current.postMessage({ type: "generate", data: messages });
115
  }, [messages, isRunning]);
116
 
117
  useEffect(() => {
118
  if (!chatContainerRef.current || !isRunning) return;
119
  const element = chatContainerRef.current;
120
+ if (element.scrollHeight - element.scrollTop - element.clientHeight < STICKY_SCROLL_THRESHOLD) {
 
 
 
121
  element.scrollTop = element.scrollHeight;
122
  }
123
  }, [messages, isRunning]);
124
 
125
  return IS_WEBGPU_AVAILABLE ? (
126
+ <div className="flex flex-col h-screen mx-auto justify-end text-gray-800 dark:text-gray-200 bg-white dark:bg-gray-900">
127
  {status === null && messages.length === 0 && (
128
+ <div className="h-full overflow-auto flex justify-center items-center flex-col relative">
129
+ <div className="flex flex-col items-center mb-1 max-w-[400px] text-center">
130
+ {/* أيقونة طبية بسيطة أو لوجو */}
131
+ <div className="text-6xl mb-4">🏥</div>
132
+ <h1 className="text-4xl font-bold mb-1 text-red-600 dark:text-red-500">ER Assistant</h1>
133
+ <h2 className="font-semibold px-4">
134
+ AI-Powered Support for Emergency Room Physicians.
135
+ <br/><span className="text-sm font-normal text-gray-500">Optimized for rapid triage and ABCDE protocols.</span>
 
 
 
 
136
  </h2>
137
  </div>
138
 
139
  <div className="flex flex-col items-center px-4">
140
+ <p className="max-w-[480px] mb-4 text-sm text-center">
141
+ This tool runs <b>locally</b> on your device via WebGPU.
142
+ Patient data never leaves this browser.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  </p>
144
 
 
 
 
 
 
 
 
 
 
145
  <button
146
+ className="border px-6 py-3 rounded-lg bg-red-600 text-white hover:bg-red-700 disabled:bg-gray-300 select-none font-bold"
147
  onClick={() => {
148
  worker.current.postMessage({ type: "load" });
149
  setStatus("loading");
150
  }}
151
  disabled={status !== null || error !== null}
152
  >
153
+ Initialize ER Assistant
154
  </button>
155
  </div>
156
  </div>
157
  )}
158
+
159
  {status === "loading" && (
160
+ <div className="w-full max-w-[500px] mx-auto p-4 mt-auto">
161
+ <p className="text-center mb-2 font-medium">Preparing Clinical Database...</p>
162
+ {progressItems.map(({ file, progress, total }, i) => (
163
+ <Progress key={i} text={file} percentage={progress} total={total} />
164
+ ))}
165
+ </div>
 
 
 
 
 
 
 
166
  )}
167
 
168
  {status === "ready" && (
169
+ <div ref={chatContainerRef} className="overflow-y-auto w-full flex flex-col items-center h-full pt-8">
 
 
 
170
  <Chat messages={messages} />
171
  {messages.length === 0 && (
172
+ <div className="grid grid-cols-1 gap-2 max-w-[500px] w-full px-4">
173
+ <p className="text-center text-gray-500 mb-2">Select a scenario to start:</p>
174
  {EXAMPLES.map((msg, i) => (
175
  <div
176
  key={i}
177
+ className="border dark:border-gray-600 rounded-lg p-3 bg-gray-50 dark:bg-gray-800 cursor-pointer hover:bg-red-50 dark:hover:bg-red-900/20 transition"
178
  onClick={() => onEnter(msg)}
179
  >
180
  {msg}
 
182
  ))}
183
  </div>
184
  )}
185
+ {/* إحصائيات الأداء */}
186
+ <div className="p-4 text-xs text-gray-400">
187
+ {tps && `Speed: ${tps.toFixed(2)} tokens/sec`}
188
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  </div>
190
  )}
191
 
192
+ <div className="mt-2 border dark:bg-gray-800 rounded-lg w-[600px] max-w-[90%] mx-auto relative mb-3 flex shadow-lg">
193
  <textarea
194
  ref={textareaRef}
195
+ className="w-full dark:bg-gray-800 px-4 py-4 rounded-lg bg-transparent border-none outline-none resize-none"
196
+ placeholder="Describe patient symptoms (e.g., Male, 45y, chest pain)..."
 
197
  rows={1}
198
  value={input}
199
  disabled={status !== "ready"}
 
200
  onKeyDown={(e) => {
201
+ if (input.length > 0 && !isRunning && e.key === "Enter" && !e.shiftKey) {
202
+ e.preventDefault();
 
 
 
 
 
203
  onEnter(input);
204
  }
205
  }}
206
  onInput={(e) => setInput(e.target.value)}
207
  />
208
+ <div className="flex items-center pr-3">
209
+ {isRunning ? (
210
+ <StopIcon className="h-8 w-8 cursor-pointer text-red-500" onClick={onInterrupt} />
211
+ ) : (
212
+ <ArrowRightIcon
213
+ className={`h-8 w-8 p-1 rounded-md ${input.length > 0 ? 'bg-red-600 text-white' : 'bg-gray-200 text-gray-400'}`}
214
+ onClick={() => input.length > 0 && onEnter(input)}
215
+ />
216
+ )}
217
+ </div>
 
 
 
 
 
 
 
218
  </div>
219
 
220
+ <p className="text-[10px] text-gray-400 text-center mb-3 px-4">
221
+ NOTICE: This AI is a supportive tool for healthcare professionals. Final clinical decisions must be made by a qualified physician.
222
  </p>
223
  </div>
224
  ) : (
225
+ <div className="fixed w-screen h-screen bg-black text-white flex justify-center items-center text-center p-10">
226
+ WebGPU is not supported by this browser. Please use Chrome or Edge.
 
 
227
  </div>
228
  );
229
  }
230
 
231
+ export default App;