File size: 5,023 Bytes
b8ce00d
29265d4
b8ce00d
 
29265d4
0eacb73
b8ce00d
 
 
0eacb73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29265d4
 
0eacb73
29265d4
0eacb73
 
 
 
29265d4
 
 
0eacb73
29265d4
0eacb73
 
29265d4
0eacb73
 
 
 
29265d4
 
0eacb73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29265d4
 
0eacb73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29265d4
 
0eacb73
 
 
 
 
29265d4
0eacb73
 
 
 
29265d4
0eacb73
 
 
 
29265d4
b8ce00d
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
<!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>