feat(ui): display thinking tags separately

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto 2025-05-30 21:55:50 +02:00
parent 09ea55385f
commit 6fb849ee07
2 changed files with 132 additions and 2 deletions

View file

@ -191,6 +191,9 @@ async function promptGPT(systemPrompt, input) {
let buffer = ""; let buffer = "";
let contentBuffer = []; let contentBuffer = [];
let thinkingContent = "";
let isThinking = false;
let lastThinkingMessageIndex = -1;
try { try {
while (true) { while (true) {
@ -214,8 +217,45 @@ async function promptGPT(systemPrompt, input) {
const token = jsonData.choices[0].delta.content; const token = jsonData.choices[0].delta.content;
if (token) { if (token) {
// Check for thinking tags
if (token.includes("<thinking>") || token.includes("<think>")) {
isThinking = true;
thinkingContent = "";
lastThinkingMessageIndex = -1;
return;
}
if (token.includes("</thinking>") || token.includes("</think>")) {
isThinking = false;
if (thinkingContent.trim()) {
// Only add the final thinking message if we don't already have one
if (lastThinkingMessageIndex === -1) {
Alpine.store("chat").add("thinking", thinkingContent);
}
}
return;
}
// Handle content based on thinking state
if (isThinking) {
thinkingContent += token;
// Update the last thinking message or create a new one
if (lastThinkingMessageIndex === -1) {
// Create new thinking message
Alpine.store("chat").add("thinking", thinkingContent);
lastThinkingMessageIndex = Alpine.store("chat").history.length - 1;
} else {
// Update existing thinking message
const chatStore = Alpine.store("chat");
const lastMessage = chatStore.history[lastThinkingMessageIndex];
if (lastMessage && lastMessage.role === "thinking") {
lastMessage.content = thinkingContent;
lastMessage.html = DOMPurify.sanitize(marked.parse(thinkingContent));
}
}
} else {
contentBuffer.push(token); contentBuffer.push(token);
} }
}
} catch (error) { } catch (error) {
console.error("Failed to parse line:", line, error); console.error("Failed to parse line:", line, error);
} }
@ -233,6 +273,9 @@ async function promptGPT(systemPrompt, input) {
if (contentBuffer.length > 0) { if (contentBuffer.length > 0) {
addToChat(contentBuffer.join("")); addToChat(contentBuffer.join(""));
} }
if (thinkingContent.trim() && lastThinkingMessageIndex === -1) {
Alpine.store("chat").add("thinking", thinkingContent);
}
// Highlight all code blocks once at the end // Highlight all code blocks once at the end
hljs.highlightAll(); hljs.highlightAll();
@ -274,3 +317,77 @@ marked.setOptions({
return hljs.highlightAuto(code).value; return hljs.highlightAuto(code).value;
}, },
}); });
document.addEventListener("alpine:init", () => {
Alpine.store("chat", {
history: [],
languages: [undefined],
systemPrompt: "",
clear() {
this.history.length = 0;
},
add(role, content, image, audio) {
const N = this.history.length - 1;
// For thinking messages, always create a new message
if (role === "thinking") {
let c = "";
const lines = content.split("\n");
lines.forEach((line) => {
c += DOMPurify.sanitize(marked.parse(line));
});
this.history.push({ role, content, html: c, image, audio });
}
// For other messages, merge if same role
else if (this.history.length && this.history[N].role === role) {
this.history[N].content += content;
this.history[N].html = DOMPurify.sanitize(
marked.parse(this.history[N].content)
);
// Merge new images and audio with existing ones
if (image && image.length > 0) {
this.history[N].image = [...(this.history[N].image || []), ...image];
}
if (audio && audio.length > 0) {
this.history[N].audio = [...(this.history[N].audio || []), ...audio];
}
} else {
let c = "";
const lines = content.split("\n");
lines.forEach((line) => {
c += DOMPurify.sanitize(marked.parse(line));
});
this.history.push({
role,
content,
html: c,
image: image || [],
audio: audio || []
});
}
document.getElementById('messages').scrollIntoView(false);
const parser = new DOMParser();
const html = parser.parseFromString(
this.history[this.history.length - 1].html,
"text/html"
);
const code = html.querySelectorAll("pre code");
if (!code.length) return;
code.forEach((el) => {
const language = el.className.split("language-")[1];
if (this.languages.includes(language)) return;
const script = document.createElement("script");
script.src = `https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.8.0/build/languages/${language}.min.js`;
document.head.appendChild(script);
this.languages.push(language);
});
},
messages() {
return this.history.map((message) => ({
role: message.role,
content: message.content,
image: message.image,
audio: message.audio,
}));
},
});
});

View file

@ -237,7 +237,20 @@ SOFTWARE.
</div> </div>
</div> </div>
</template> </template>
<template x-if="message.role != 'user'"> <template x-if="message.role === 'thinking'">
<div class="flex items-center space-x-2 w-full">
<div class="flex flex-col flex-1">
<div class="p-2 flex-1 rounded bg-blue-900/50 text-blue-100 border border-blue-700/50">
<div class="flex items-center space-x-2">
<i class="fa-solid fa-brain text-blue-400"></i>
<span class="text-xs font-semibold text-blue-300">Thinking</span>
</div>
<div class="mt-1" x-html="message.html"></div>
</div>
</div>
</div>
</template>
<template x-if="message.role != 'user' && message.role != 'thinking'">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
{{ if $galleryConfig }} {{ if $galleryConfig }}
{{ if $galleryConfig.Icon }}<img src="{{$galleryConfig.Icon}}" class="rounded-lg mt-2 max-w-8 max-h-8">{{end}} {{ if $galleryConfig.Icon }}<img src="{{$galleryConfig.Icon}}" class="rounded-lg mt-2 max-w-8 max-h-8">{{end}}