summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/ollama_web/static/app.js45
1 files changed, 34 insertions, 11 deletions
diff --git a/src/ollama_web/static/app.js b/src/ollama_web/static/app.js
index 4c22f3f..00269b3 100644
--- a/src/ollama_web/static/app.js
+++ b/src/ollama_web/static/app.js
@@ -547,8 +547,10 @@ async function send() {
let buffer = "";
let placeholderCleared = false;
+ let streamRafId = null;
- function appendDelta(content) {
+ function flushStreamFrame() {
+ streamRafId = null;
if (!placeholderCleared) {
assistantBody.innerHTML = "";
placeholderCleared = true;
@@ -558,13 +560,26 @@ async function send() {
if (waiting) {
waiting.remove();
}
- assistantText += content;
- assistantBody.innerHTML = renderMarkdown(assistantText);
- highlightIn(assistantBody);
- renderMathIn(assistantBody);
+ // During the stream we render raw text only. Full markdown, syntax
+ // highlighting and math rendering are deferred to the final renderSession()
+ // call so mobile WebKit is not overwhelmed by rebuilding the DOM for
+ // every single token.
+ assistantBody.textContent = assistantText;
+ assistantBody.style.whiteSpace = "pre-wrap";
chatEl.scrollTop = chatEl.scrollHeight;
}
+ function scheduleStreamUpdate() {
+ if (streamRafId === null) {
+ streamRafId = requestAnimationFrame(flushStreamFrame);
+ }
+ }
+
+ function appendDelta(content) {
+ assistantText += content;
+ scheduleStreamUpdate();
+ }
+
while (true) {
const { done, value } = await reader.read();
if (done) break;
@@ -625,7 +640,7 @@ async function send() {
} else if (ev.type === "thinking") {
if (!thinkingBody) thinkingBody = addThinkingBubble(assistantExtras);
thinkingBody.textContent += ev.content;
- chatEl.scrollTop = chatEl.scrollHeight;
+ scheduleStreamUpdate();
} else if (ev.type === "delta") {
appendDelta(ev.content);
} else if (ev.type === "error") {
@@ -644,6 +659,8 @@ async function send() {
}
}
+ // Flush any trailing SSE data that did not end with a double newline.
+ let hadTrailingDelta = false;
if (buffer.trim()) {
for (const payload of parseSseEvent(buffer)) {
if (!payload.trim()) continue;
@@ -655,13 +672,19 @@ async function send() {
}
if (ev.type === "delta") {
assistantText += ev.content;
+ hadTrailingDelta = true;
}
}
- if (assistantText) {
- assistantBody.innerHTML = renderMarkdown(assistantText);
- highlightIn(assistantBody);
- renderMathIn(assistantBody);
- }
+ }
+ // Cancel any pending animation frame and render the trailing text immediately
+ // so the user does not see raw text linger before renderSession() rebuilds
+ // the pane.
+ if (hadTrailingDelta && streamRafId !== null) {
+ cancelAnimationFrame(streamRafId);
+ streamRafId = null;
+ }
+ if (assistantText) {
+ flushStreamFrame();
}
if (!assistantText && !pendingTools.length && !thinkingBody && !gotError) {