import { useEffect, useState, useRef } from "react"; import Chat from "./components/Chat"; import ArrowRightIcon from "./components/icons/ArrowRightIcon"; import StopIcon from "./components/icons/StopIcon"; import Progress from "./components/Progress"; import LightBulbIcon from "./components/icons/LightBulbIcon"; const IS_WEBGPU_AVAILABLE = !!navigator.gpu; const STICKY_SCROLL_THRESHOLD = 120; const EXAMPLES = [ "Solve the equation x^2 - 3x + 2 = 0", "How do you say 'I love you' in French, Spanish, and German? Respond in a table.", "Explain the concept of gravity in simple terms.", ]; function App() { // Create a reference to the worker object. const worker = useRef(null); const textareaRef = useRef(null); const chatContainerRef = useRef(null); // Model loading and progress const [status, setStatus] = useState(null); const [error, setError] = useState(null); const [loadingMessage, setLoadingMessage] = useState(""); const [progressItems, setProgressItems] = useState([]); const [isRunning, setIsRunning] = useState(false); // Inputs and outputs const [input, setInput] = useState(""); const [messages, setMessages] = useState([]); const [tps, setTps] = useState(null); const [numTokens, setNumTokens] = useState(null); const [reasonEnabled, setReasonEnabled] = useState(false); function onEnter(message) { setMessages((prev) => [...prev, { role: "user", content: message }]); setTps(null); setIsRunning(true); setInput(""); } function onInterrupt() { // NOTE: We do not set isRunning to false here because the worker // will send a 'complete' message when it is done. worker.current.postMessage({ type: "interrupt" }); } useEffect(() => { resizeInput(); }, [input]); function resizeInput() { if (!textareaRef.current) return; const target = textareaRef.current; target.style.height = "auto"; const newHeight = Math.min(Math.max(target.scrollHeight, 24), 200); target.style.height = `${newHeight}px`; } // We use the `useEffect` hook to setup the worker as soon as the `App` component is mounted. useEffect(() => { // Create the worker if it does not yet exist. if (!worker.current) { worker.current = new Worker(new URL("./worker.js", import.meta.url), { type: "module", }); worker.current.postMessage({ type: "check" }); // Do a feature check } // Create a callback function for messages from the worker thread. const onMessageReceived = (e) => { switch (e.data.status) { case "loading": // Model file start load: add a new progress item to the list. setStatus("loading"); setLoadingMessage(e.data.data); break; case "initiate": setProgressItems((prev) => [...prev, e.data]); break; case "progress": // Model file progress: update one of the progress items. setProgressItems((prev) => prev.map((item) => { if (item.file === e.data.file) { return { ...item, ...e.data }; } return item; }), ); break; case "done": // Model file loaded: remove the progress item from the list. setProgressItems((prev) => prev.filter((item) => item.file !== e.data.file), ); break; case "ready": // Pipeline ready: the worker is ready to accept messages. setStatus("ready"); break; case "start": { // Start generation setMessages((prev) => [ ...prev, { role: "assistant", content: "" }, ]); } break; case "update": { // Generation update: update the output text. // Parse messages const { output, tps, numTokens, state } = e.data; setTps(tps); setNumTokens(numTokens); setMessages((prev) => { const cloned = [...prev]; const last = cloned.at(-1); const data = { ...last, content: last.content + output, }; if (data.answerIndex === undefined && state === "answering") { // When state changes to answering, we set the answerIndex data.answerIndex = last.content.length; } cloned[cloned.length - 1] = data; return cloned; }); } break; case "complete": // Generation complete: re-enable the "Generate" button setIsRunning(false); break; case "error": setError(e.data.data); break; } }; const onErrorReceived = (e) => { console.error("Worker error:", e); }; // Attach the callback function as an event listener. worker.current.addEventListener("message", onMessageReceived); worker.current.addEventListener("error", onErrorReceived); // Define a cleanup function for when the component is unmounted. return () => { worker.current.removeEventListener("message", onMessageReceived); worker.current.removeEventListener("error", onErrorReceived); }; }, []); // Send the messages to the worker thread whenever the `messages` state changes. useEffect(() => { if (messages.filter((x) => x.role === "user").length === 0) { // No user messages yet: do nothing. return; } if (messages.at(-1).role === "assistant") { // Do not update if the last message is from the assistant return; } setTps(null); worker.current.postMessage({ type: "generate", data: { messages, reasonEnabled }, }); }, [messages, isRunning]); useEffect(() => { if (!chatContainerRef.current) return; const element = chatContainerRef.current; if ( element.scrollHeight - element.scrollTop - element.clientHeight < STICKY_SCROLL_THRESHOLD ) { element.scrollTop = element.scrollHeight; } }, [messages, isRunning]); return IS_WEBGPU_AVAILABLE ? (
{status === null && messages.length === 0 && (

SmolLM3 WebGPU

A dual reasoning model that runs locally in
your browser with WebGPU acceleration.


You are about to load{" "} SmolLM3-3B , a 3B parameter reasoning LLM optimized for in-browser inference. Everything runs entirely in your browser with{" "} 🤗 Transformers.js {" "} and ONNX Runtime Web, meaning no data is sent to a server. Once loaded, it can even be used offline. The source code for the demo is available on{" "} GitHub .

{error && (

Unable to load model due to the following error:

{error}

)}
)} {status === "loading" && ( <>

{loadingMessage}

{progressItems.map(({ file, progress, total }, i) => ( ))}
)} {status === "ready" && ( <>
{messages.length === 0 && (
{EXAMPLES.map((msg, i) => (
onEnter(msg)} > {msg}
))}
)}

{tps && messages.length > 0 && ( <> {!isRunning && ( Generated {numTokens} tokens in{" "} {(numTokens / tps).toFixed(2)} seconds ( )} { <> {tps.toFixed(2)} tokens/second } {!isRunning && ( <> ). { worker.current.postMessage({ type: "reset" }); setMessages([]); }} > Reset )} )}

)}