| <!doctype html> |
| <html lang="id"> |
| <head> |
| <meta charset="utf-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> |
| <title>Chatbot Browser — Transformers.js</title> |
| <link rel="stylesheet" href="style.css" /> |
| </head> |
| <body> |
| <main class="app"> |
| <header class="topbar"> |
| <div class="title">🐠 Chatbot Browser</div> |
| <div class="controls"> |
| <select id="model"> |
| <option value="onnx-community/Qwen2.5-0.5B-Instruct">Qwen2.5 0.5B (cepat)</option> |
| <option value="HuggingFaceTB/SmolLM2-360M-Instruct">SmolLM2 360M (ringan)</option> |
| <option value="onnx-community/Llama-3.2-1B-Instruct-q4f16">Llama 3.2 1B (pintar)</option> |
| </select> |
| <button id="load">Muat</button> |
| <button id="clear" title="Bersihkan chat">🗑</button> |
| </div> |
| </header> |
|
|
| <div id="chat" class="chat"> |
| <div class="msg bot"> |
| <div class="bubble">Halo! Pilih model lalu klik <b>Muat</b>, terus mulai ngobrol. Semua jalan di browser kamu — tanpa server. 🚀</div> |
| </div> |
| </div> |
|
|
| <div class="status" id="status">Belum ada model dimuat.</div> |
|
|
| <form id="form" class="composer"> |
| <input id="input" type="text" placeholder="Tulis pesan…" autocomplete="off" disabled /> |
| <button id="send" type="submit" disabled>Kirim</button> |
| </form> |
| </main> |
|
|
| <script type="module"> |
| import { pipeline, TextStreamer } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.0.2"; |
| |
| const chat = document.getElementById("chat"); |
| const form = document.getElementById("form"); |
| const input = document.getElementById("input"); |
| const sendBtn = document.getElementById("send"); |
| const loadBtn = document.getElementById("load"); |
| const clearBtn = document.getElementById("clear"); |
| const modelSel = document.getElementById("model"); |
| const statusEl = document.getElementById("status"); |
| |
| const hasWebGPU = !!navigator.gpu; |
| let generator = null; |
| let busy = false; |
| let messages = [ |
| { role: "system", content: "You are a helpful, friendly assistant. Reply concisely." }, |
| ]; |
| |
| function addMsg(role, text) { |
| const wrap = document.createElement("div"); |
| wrap.className = "msg " + (role === "user" ? "user" : "bot"); |
| const bubble = document.createElement("div"); |
| bubble.className = "bubble"; |
| bubble.textContent = text; |
| wrap.appendChild(bubble); |
| chat.appendChild(wrap); |
| chat.scrollTop = chat.scrollHeight; |
| return bubble; |
| } |
| |
| async function loadModel() { |
| const id = modelSel.value; |
| busy = true; |
| loadBtn.disabled = true; |
| input.disabled = true; |
| sendBtn.disabled = true; |
| generator = null; |
| statusEl.textContent = `Mengunduh ${id}… (sekali saja, bisa beberapa menit)`; |
| try { |
| generator = await pipeline("text-generation", id, { |
| dtype: hasWebGPU ? "q4f16" : "q4", |
| device: hasWebGPU ? "webgpu" : "wasm", |
| progress_callback: (p) => { |
| if (p.status === "progress" && p.file) { |
| const pct = Math.round(p.progress || 0); |
| statusEl.textContent = `Mengunduh ${p.file}: ${pct}%`; |
| } |
| }, |
| }); |
| statusEl.textContent = `Siap ✔ (${id} · ${hasWebGPU ? "WebGPU" : "WASM/CPU"})`; |
| input.disabled = false; |
| sendBtn.disabled = false; |
| input.focus(); |
| } catch (e) { |
| statusEl.textContent = "Gagal memuat model: " + e.message; |
| } |
| busy = false; |
| loadBtn.disabled = false; |
| } |
| |
| async function send(text) { |
| if (!generator || busy) return; |
| busy = true; |
| input.disabled = true; |
| sendBtn.disabled = true; |
| |
| addMsg("user", text); |
| messages.push({ role: "user", content: text }); |
| |
| const bubble = addMsg("bot", ""); |
| bubble.classList.add("typing"); |
| let acc = ""; |
| |
| const streamer = new TextStreamer(generator.tokenizer, { |
| skip_prompt: true, |
| skip_special_tokens: true, |
| callback_function: (t) => { |
| acc += t; |
| bubble.textContent = acc; |
| chat.scrollTop = chat.scrollHeight; |
| }, |
| }); |
| |
| try { |
| await generator(messages, { |
| max_new_tokens: 512, |
| do_sample: true, |
| temperature: 0.7, |
| top_p: 0.9, |
| streamer, |
| }); |
| messages.push({ role: "assistant", content: acc.trim() }); |
| } catch (e) { |
| bubble.textContent = "⚠ Error: " + e.message; |
| } |
| |
| bubble.classList.remove("typing"); |
| busy = false; |
| input.disabled = false; |
| sendBtn.disabled = false; |
| input.focus(); |
| } |
| |
| form.addEventListener("submit", (e) => { |
| e.preventDefault(); |
| const text = input.value.trim(); |
| if (!text) return; |
| input.value = ""; |
| send(text); |
| }); |
| |
| loadBtn.addEventListener("click", loadModel); |
| |
| clearBtn.addEventListener("click", () => { |
| chat.innerHTML = ""; |
| messages = messages.slice(0, 1); |
| addMsg("bot", "Chat dibersihkan. Lanjut ngobrol! 🙂"); |
| }); |
| |
| statusEl.textContent = hasWebGPU |
| ? "WebGPU terdeteksi. Pilih model & klik Muat." |
| : "WebGPU tidak ada — pakai CPU (lebih lambat). Pilih model & klik Muat."; |
| </script> |
| </body> |
| </html> |
|
|