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"; const IS_WEBGPU_AVAILABLE = !!navigator.gpu; const STICKY_SCROLL_THRESHOLD = 120; const EXAMPLES = [ "Give me some tips to improve my time management skills.", "What is the difference between AI and ML?", "Write python code to compute the nth fibonacci number.", ]; 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); const [modelFiles, setModelFiles] = useState([]); // Inputs and outputs const [input, setInput] = useState(""); const [messages, setMessages] = useState([]); const [tps, setTps] = useState(null); const [numTokens, setNumTokens] = useState(null); const [attachedFile, setAttachedFile] = useState(null); async function onEnter(message) { let fileText = ""; if (attachedFile) { if (attachedFile.name.endsWith(".txt")) { fileText = await attachedFile.text(); } else if (attachedFile.name.endsWith(".pdf")) { // Dynamically import pdfjs-dist const pdfjsLib = await import("pdfjs-dist/build/pdf"); const workerSrc = (await import("pdfjs-dist/build/pdf.worker?url")).default; pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc; const arrayBuffer = await attachedFile.arrayBuffer(); const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; let pdfText = ""; for (let i = 1; i <= pdf.numPages; i++) { const page = await pdf.getPage(i); const content = await page.getTextContent(); pdfText += content.items.map(item => item.str).join(" ") + "\n"; } fileText = pdfText; } } let fullPrompt = message; if (fileText) { fullPrompt += "\n\n--- File Content ---\n" + fileText; } let userMsg = { role: "user", content: fullPrompt }; setMessages((prev) => [...prev, userMsg]); setTps(null); setIsRunning(true); setInput(""); setAttachedFile(null); } 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": setStatus("loading"); setLoadingMessage(e.data.data); break; case "initiate": setProgressItems((prev) => [...prev, e.data]); break; case "progress": setProgressItems((prev) => prev.map((item) => { if (item.file === e.data.file) { return { ...item, ...e.data }; } return item; }), ); break; case "done": setProgressItems((prev) => prev.filter((item) => item.file !== e.data.file), ); break; case "ready": setStatus("ready"); break; case "start": setMessages((prev) => [ ...prev, { role: "assistant", content: "" }, ]); break; case "update": const { output, tps, numTokens } = e.data; setTps(tps); setNumTokens(numTokens); setMessages((prev) => { const cloned = [...prev]; const last = cloned.at(-1); cloned[cloned.length - 1] = { ...last, content: last.content + output, }; return cloned; }); break; case "complete": setIsRunning(false); break; case "error": setError(e.data.data || "Unknown error during model loading."); setStatus(null); setLoadingMessage(""); 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 }); }, [messages, isRunning]); useEffect(() => { if (!chatContainerRef.current || !isRunning) 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 ? (
You are about to load{" "}
Granite-4.0 Micro
, a 3.4B parameter long-context instruct model 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 (≈ 2.3 GB), it can even be used offline.
Unable to load model due to the following error:
{error}
{loadingMessage}
{progressItems.map(({ file, progress, total }, i) => ( ))}{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 > )} > )}
Disclaimer: Generated content may be inaccurate or false.