Skip to content

Commit 9480047

Browse files
committed
fix: fix chat webui
Signed-off-by: Sertac Ozercan <[email protected]>
1 parent d38e909 commit 9480047

File tree

2 files changed

+83
-39
lines changed

2 files changed

+83
-39
lines changed

core/http/static/chat.js

Lines changed: 76 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ function submitSystemPrompt(event) {
3838
localStorage.setItem("system_prompt", document.getElementById("systemPrompt").value);
3939
document.getElementById("systemPrompt").blur();
4040
}
41-
41+
4242
var image = "";
4343

4444
function submitPrompt(event) {
@@ -54,15 +54,15 @@ function submitPrompt(event) {
5454
}
5555

5656
function readInputImage() {
57-
57+
5858
if (!this.files || !this.files[0]) return;
59-
59+
6060
const FR = new FileReader();
61-
61+
6262
FR.addEventListener("load", function(evt) {
6363
image = evt.target.result;
64-
});
65-
64+
});
65+
6666
FR.readAsDataURL(this.files[0]);
6767
}
6868

@@ -155,50 +155,94 @@ function readInputImage() {
155155
stream: true,
156156
}),
157157
});
158-
158+
159159
if (!response.ok) {
160160
Alpine.store("chat").add(
161161
"assistant",
162162
`<span class='error'>Error: POST /v1/chat/completions ${response.status}</span>`,
163163
);
164164
return;
165165
}
166-
166+
167167
const reader = response.body
168168
?.pipeThrough(new TextDecoderStream())
169169
.getReader();
170-
170+
171171
if (!reader) {
172172
Alpine.store("chat").add(
173173
"assistant",
174174
`<span class='error'>Error: Failed to decode API response</span>`,
175175
);
176176
return;
177177
}
178-
179-
while (true) {
180-
const { value, done } = await reader.read();
181-
if (done) break;
182-
let dataDone = false;
183-
const arr = value.split("\n");
184-
arr.forEach((data) => {
185-
if (data.length === 0) return;
186-
if (data.startsWith(":")) return;
187-
if (data === "data: [DONE]") {
188-
dataDone = true;
189-
return;
190-
}
191-
const token = JSON.parse(data.substring(6)).choices[0].delta.content;
192-
if (!token) {
193-
return;
178+
179+
// Function to add content to the chat and handle DOM updates efficiently
180+
const addToChat = (token) => {
181+
const chatStore = Alpine.store("chat");
182+
chatStore.add("assistant", token);
183+
// Efficiently scroll into view without triggering multiple reflows
184+
const messages = document.getElementById('messages');
185+
messages.scrollTop = messages.scrollHeight;
186+
};
187+
188+
let buffer = "";
189+
let contentBuffer = [];
190+
191+
try {
192+
while (true) {
193+
const { value, done } = await reader.read();
194+
if (done) break;
195+
196+
buffer += value;
197+
198+
let lines = buffer.split("\n");
199+
buffer = lines.pop(); // Retain any incomplete line in the buffer
200+
201+
lines.forEach((line) => {
202+
if (line.length === 0 || line.startsWith(":")) return;
203+
if (line === "data: [DONE]") {
204+
return;
205+
}
206+
207+
if (line.startsWith("data: ")) {
208+
try {
209+
const jsonData = JSON.parse(line.substring(6));
210+
const token = jsonData.choices[0].delta.content;
211+
212+
if (token) {
213+
contentBuffer.push(token);
214+
}
215+
} catch (error) {
216+
console.error("Failed to parse line:", line, error);
217+
}
218+
}
219+
});
220+
221+
// Efficiently update the chat in batch
222+
if (contentBuffer.length > 0) {
223+
addToChat(contentBuffer.join(""));
224+
contentBuffer = [];
194225
}
195-
hljs.highlightAll();
196-
Alpine.store("chat").add("assistant", token);
197-
document.getElementById('messages').scrollIntoView(false)
198-
});
226+
}
227+
228+
// Final content flush if any data remains
229+
if (contentBuffer.length > 0) {
230+
addToChat(contentBuffer.join(""));
231+
}
232+
233+
// Highlight all code blocks once at the end
199234
hljs.highlightAll();
200-
if (dataDone) break;
235+
} catch (error) {
236+
console.error("An error occurred while reading the stream:", error);
237+
Alpine.store("chat").add(
238+
"assistant",
239+
`<span class='error'>Error: Failed to process stream</span>`,
240+
);
241+
} finally {
242+
// Perform any cleanup if necessary
243+
reader.releaseLock();
201244
}
245+
202246
// Remove class "loader" from the element with "loader" id
203247
//document.getElementById("loader").classList.remove("loader");
204248
document.getElementById("loader").style.display = "none";
@@ -209,7 +253,7 @@ function readInputImage() {
209253
// set focus to the input
210254
document.getElementById("input").focus();
211255
}
212-
256+
213257
document.getElementById("key").addEventListener("submit", submitKey);
214258
document.getElementById("system_prompt").addEventListener("submit", submitSystemPrompt);
215259

@@ -230,7 +274,7 @@ function readInputImage() {
230274
} else {
231275
document.getElementById("systemPrompt").value = null;
232276
}
233-
277+
234278
marked.setOptions({
235279
highlight: function (code) {
236280
return hljs.highlightAuto(code).value;

core/http/views/index.html

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,22 @@
44

55
<body class="bg-gray-900 text-gray-200">
66
<div class="flex flex-col min-h-screen">
7-
7+
88
{{template "views/partials/navbar" .}}
9-
9+
1010
<div class="container mx-auto px-4 flex-grow">
1111
<div class="header text-center py-12">
1212
<h1 class="text-5xl font-bold text-gray-100">Welcome to <i>your</i> LocalAI instance!</h1>
1313
<p class="mt-4 text-lg">The FOSS alternative to OpenAI, Claude, ...</p>
1414
<a href="https://localai.io" target="_blank" class="mt-4 inline-block bg-blue-500 text-white py-2 px-4 rounded-lg shadow transition duration-300 ease-in-out hover:bg-blue-700 hover:shadow-lg">
1515
<i class="fas fa-book-reader pr-2"></i>Documentation
16-
</a>
16+
</a>
1717
</div>
1818

1919
<div class="models mt-4">
2020

2121
{{template "views/partials/inprogress" .}}
22-
22+
2323
{{ if eq (len .ModelsConfig) 0 }}
2424
<h2 class="text-center text-3xl font-semibold text-gray-100"> <i class="text-yellow-200 ml-2 fa-solid fa-triangle-exclamation animate-pulse"></i> Ouch! seems you don't have any models installed!</h2>
2525
<p class="text-center mt-4 text-xl">..install something from the <a class="text-gray-400 hover:text-white ml-1 px-3 py-2 rounded" href="/browse">🖼️ Gallery</a> or check the <a href="https://localai.io/basics/getting_started/" class="text-gray-400 hover:text-white ml-1 px-3 py-2 rounded"> <i class="fa-solid fa-book"></i> Getting started documentation </a></p>
@@ -70,9 +70,9 @@ <h2 class="text-center text-3xl font-semibold text-gray-100">Installed models</h
7070
{{ end }}
7171
</td>
7272

73-
<td class="px-4 py-3">
74-
<button
75-
class="float-right inline-block rounded bg-red-800 px-6 pb-2.5 mb-3 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-primary-3 transition duration-150 ease-in-out hover:bg-red-accent-300 hover:shadow-red-2 focus:bg-red-accent-300 focus:shadow-primary-2 focus:outline-none focus:ring-0 active:bg-red-600 active:shadow-primary-2 dark:shadow-black/30 dark:hover:shadow-dark-strong dark:focus:shadow-dark-strong dark:active:shadow-dark-strong"
73+
<td class="px-4 py-3">
74+
<button
75+
class="float-right inline-block rounded bg-red-800 px-6 pb-2.5 mb-3 pt-2.5 text-xs font-medium uppercase leading-normal text-white shadow-primary-3 transition duration-150 ease-in-out hover:bg-red-accent-300 hover:shadow-red-2 focus:bg-red-accent-300 focus:shadow-primary-2 focus:outline-none focus:ring-0 active:bg-red-600 active:shadow-primary-2 dark:shadow-black/30 dark:hover:shadow-dark-strong dark:focus:shadow-dark-strong dark:active:shadow-dark-strong"
7676
data-twe-ripple-color="light" data-twe-ripple-init="" hx-confirm="Are you sure you wish to delete the model?" hx-post="/browse/delete/model/{{.Name}}" hx-swap="outerHTML"><i class="fa-solid fa-cancel pr-2"></i>Delete</button>
7777
</td>
7878
{{ end }}

0 commit comments

Comments
 (0)