neetnestor commited on
Commit
36784c4
1 Parent(s): 407ff24

setup: WebLLM simple-chat-js

Browse files
Files changed (4) hide show
  1. README.md +2 -2
  2. index.html +20 -9
  3. index.js +150 -0
  4. style.css +97 -19
README.md CHANGED
@@ -1,6 +1,6 @@
1
  ---
2
  title: Webllm Chat
3
- emoji: 🔥
4
  colorFrom: blue
5
  colorTo: indigo
6
  sdk: static
@@ -8,4 +8,4 @@ pinned: false
8
  license: apache-2.0
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
  title: Webllm Chat
3
+ emoji: 💬
4
  colorFrom: blue
5
  colorTo: indigo
6
  sdk: static
 
8
  license: apache-2.0
9
  ---
10
 
11
+ A space to chat with LLMs running locally in your browser, powered by [WebLLM](https://github.com/mlc-ai/web-llm/).
index.html CHANGED
@@ -3,17 +3,28 @@
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
  <link rel="stylesheet" href="style.css" />
8
  </head>
 
9
  <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
  </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width" />
6
+ <title>WebLLM Chat Space</title>
7
  <link rel="stylesheet" href="style.css" />
8
  </head>
9
+
10
  <body>
11
+ <p>Step 1: Initialize WebLLM and Download Model</p>
12
+ <div class="download-container">
13
+ <select id="model-selection"></select>
14
+ <button id="download">Download</button>
 
 
 
15
  </div>
16
+ <p id="download-status" class="hidden"></p>
17
+
18
+ <p>Step 2: Chat</p>
19
+ <div class="chat-container">
20
+ <div id="chat-box" class="chat-box"></div>
21
+ <div id="chat-stats" class="chat-stats hidden"></div>
22
+ <div class="chat-input-container">
23
+ <input type="text" id="user-input" placeholder="Type a message..." />
24
+ <button id="send" disabled>Send</button>
25
+ </div>
26
+ </div>
27
+
28
+ <script src="./index.js" type="module"></script>
29
+ </body>
30
  </html>
index.js ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as webllm from "https://esm.run/@mlc-ai/web-llm";
2
+
3
+ /*************** WebLLM logic ***************/
4
+ const messages = [
5
+ {
6
+ content: "You are a helpful AI agent helping users.",
7
+ role: "system",
8
+ },
9
+ ];
10
+
11
+ const availableModels = webllm.prebuiltAppConfig.model_list.map(
12
+ (m) => m.model_id,
13
+ );
14
+ let selectedModel = "Llama-3.1-8B-Instruct-q4f32_1-1k";
15
+
16
+ // Callback function for initializing progress
17
+ function updateEngineInitProgressCallback(report) {
18
+ console.log("initialize", report.progress);
19
+ document.getElementById("download-status").textContent = report.text;
20
+ }
21
+
22
+ // Create engine instance
23
+ const engine = new webllm.MLCEngine();
24
+ engine.setInitProgressCallback(updateEngineInitProgressCallback);
25
+
26
+ async function initializeWebLLMEngine() {
27
+ document.getElementById("download-status").classList.remove("hidden");
28
+ selectedModel = document.getElementById("model-selection").value;
29
+ const config = {
30
+ temperature: 1.0,
31
+ top_p: 1,
32
+ };
33
+ await engine.reload(selectedModel, config);
34
+ }
35
+
36
+ async function streamingGenerating(messages, onUpdate, onFinish, onError) {
37
+ try {
38
+ let curMessage = "";
39
+ let usage;
40
+ const completion = await engine.chat.completions.create({
41
+ stream: true,
42
+ messages,
43
+ stream_options: { include_usage: true },
44
+ });
45
+ for await (const chunk of completion) {
46
+ const curDelta = chunk.choices[0]?.delta.content;
47
+ if (curDelta) {
48
+ curMessage += curDelta;
49
+ }
50
+ if (chunk.usage) {
51
+ usage = chunk.usage;
52
+ }
53
+ onUpdate(curMessage);
54
+ }
55
+ const finalMessage = await engine.getMessage();
56
+ onFinish(finalMessage, usage);
57
+ } catch (err) {
58
+ onError(err);
59
+ }
60
+ }
61
+
62
+ /*************** UI logic ***************/
63
+ function onMessageSend() {
64
+ const input = document.getElementById("user-input").value.trim();
65
+ const message = {
66
+ content: input,
67
+ role: "user",
68
+ };
69
+ if (input.length === 0) {
70
+ return;
71
+ }
72
+ document.getElementById("send").disabled = true;
73
+
74
+ messages.push(message);
75
+ appendMessage(message);
76
+
77
+ document.getElementById("user-input").value = "";
78
+ document
79
+ .getElementById("user-input")
80
+ .setAttribute("placeholder", "Generating...");
81
+
82
+ const aiMessage = {
83
+ content: "typing...",
84
+ role: "assistant",
85
+ };
86
+ appendMessage(aiMessage);
87
+
88
+ const onFinishGenerating = (finalMessage, usage) => {
89
+ updateLastMessage(finalMessage);
90
+ document.getElementById("send").disabled = false;
91
+ const usageText =
92
+ `prompt_tokens: ${usage.prompt_tokens}, ` +
93
+ `completion_tokens: ${usage.completion_tokens}, ` +
94
+ `prefill: ${usage.extra.prefill_tokens_per_s.toFixed(4)} tokens/sec, ` +
95
+ `decoding: ${usage.extra.decode_tokens_per_s.toFixed(4)} tokens/sec`;
96
+ document.getElementById("chat-stats").classList.remove("hidden");
97
+ document.getElementById("chat-stats").textContent = usageText;
98
+ };
99
+
100
+ streamingGenerating(
101
+ messages,
102
+ updateLastMessage,
103
+ onFinishGenerating,
104
+ console.error,
105
+ );
106
+ }
107
+
108
+ function appendMessage(message) {
109
+ const chatBox = document.getElementById("chat-box");
110
+ const container = document.createElement("div");
111
+ container.classList.add("message-container");
112
+ const newMessage = document.createElement("div");
113
+ newMessage.classList.add("message");
114
+ newMessage.textContent = message.content;
115
+
116
+ if (message.role === "user") {
117
+ container.classList.add("user");
118
+ } else {
119
+ container.classList.add("assistant");
120
+ }
121
+
122
+ container.appendChild(newMessage);
123
+ chatBox.appendChild(container);
124
+ chatBox.scrollTop = chatBox.scrollHeight; // Scroll to the latest message
125
+ }
126
+
127
+ function updateLastMessage(content) {
128
+ const messageDoms = document
129
+ .getElementById("chat-box")
130
+ .querySelectorAll(".message");
131
+ const lastMessageDom = messageDoms[messageDoms.length - 1];
132
+ lastMessageDom.textContent = content;
133
+ }
134
+
135
+ /*************** UI binding ***************/
136
+ availableModels.forEach((modelId) => {
137
+ const option = document.createElement("option");
138
+ option.value = modelId;
139
+ option.textContent = modelId;
140
+ document.getElementById("model-selection").appendChild(option);
141
+ });
142
+ document.getElementById("model-selection").value = selectedModel;
143
+ document.getElementById("download").addEventListener("click", function () {
144
+ initializeWebLLMEngine().then(() => {
145
+ document.getElementById("send").disabled = false;
146
+ });
147
+ });
148
+ document.getElementById("send").addEventListener("click", function () {
149
+ onMessageSend();
150
+ });
style.css CHANGED
@@ -1,28 +1,106 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
 
4
  }
5
 
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
 
9
  }
10
 
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
 
16
  }
17
 
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
  }
25
 
26
- .card p:last-child {
27
- margin-bottom: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
 
1
+ body,
2
+ html {
3
+ font-family: Arial, sans-serif;
4
+ padding: 10px 20px;
5
  }
6
 
7
+ .download-container {
8
+ display: flex;
9
+ justify-content: space-between;
10
+ margin-bottom: 20px;
11
  }
12
 
13
+ #download-status {
14
+ border: solid 1px black;
15
+ box-shadow:
16
+ 0 10px 15px -3px rgba(0, 0, 0, 0.1),
17
+ 0 4px 6px -2px rgba(0, 0, 0, 0.05);
18
+ padding: 10px;
19
  }
20
 
21
+ .chat-container {
22
+ height: 400px;
23
+ width: 100%;
24
+ border: 2px solid black;
25
+ display: flex;
26
+ flex-direction: column;
27
  }
28
 
29
+ .chat-box {
30
+ overflow-y: scroll;
31
+ background-color: #c3c3c3;
32
+ border: 1px solid #ccc;
33
+ padding: 5px;
34
+ flex: 1 1;
35
+ }
36
+
37
+ .chat-stats {
38
+ background-color: #d3eceb;
39
+ flex: 0 0;
40
+ padding: 10px;
41
+ font-size: 0.75rem;
42
+ }
43
+
44
+ .message-container {
45
+ width: 100%;
46
+ display: flex;
47
+ }
48
+
49
+ .message {
50
+ padding: 10px;
51
+ margin: 10px 0;
52
+ border-radius: 10px;
53
+ width: fit-content;
54
+ }
55
+
56
+ .message-container.user {
57
+ justify-content: end;
58
+ }
59
+
60
+ .message-container.assistant {
61
+ justify-content: start;
62
+ }
63
+
64
+ .message-container.user .message {
65
+ background: #007bff;
66
+ color: #fff;
67
+ }
68
+
69
+ .message-container.assistant .message {
70
+ background: #f1f0f0;
71
+ color: #333;
72
+ }
73
+
74
+ .chat-input-container {
75
+ min-height: 40px;
76
+ flex: 0 0;
77
+ display: flex;
78
+ }
79
+
80
+ #user-input {
81
+ width: 70%;
82
+ padding: 10px;
83
+ border: 1px solid #ccc;
84
+ }
85
+
86
+ button {
87
+ width: 25%;
88
+ padding: 10px;
89
+ border: none;
90
+ background-color: #007bff;
91
+ color: white;
92
+ cursor: pointer;
93
+ }
94
+
95
+ button:disabled {
96
+ background-color: lightgray;
97
+ cursor: not-allowed;
98
+ }
99
+
100
+ button:hover:not(:disabled) {
101
+ background-color: #0056b3;
102
+ }
103
+
104
+ .hidden {
105
+ display: none;
106
  }