ux: LBC Styling, feat: thinking, feat: more tools:
This commit is contained in:
+142
-3
@@ -27,6 +27,7 @@ function setMessageMarkdown(node, text) {
|
||||
function setMessageActivity(node, text, active = false) {
|
||||
const activity = node.querySelector(".message-activity");
|
||||
if (!activity) return;
|
||||
if (text) appendThinkingStep(node, reasoningSummaryForStatus(text), { fallback: true });
|
||||
const phase = activity.querySelector(".message-phase");
|
||||
phase.innerHTML = "";
|
||||
if (text) {
|
||||
@@ -48,6 +49,122 @@ function setMessageMetrics(node, metrics) {
|
||||
metricsEl.textContent = metrics || "";
|
||||
}
|
||||
|
||||
function appendThinkingStep(node, text, options = {}) {
|
||||
const steps = node.querySelector(".thinking-steps");
|
||||
if (!steps || !text) return;
|
||||
const previous = steps.lastElementChild?.textContent;
|
||||
if (previous === text) return;
|
||||
const item = document.createElement("li");
|
||||
if (options.fallback) item.dataset.fallback = "true";
|
||||
item.textContent = text;
|
||||
steps.appendChild(item);
|
||||
}
|
||||
|
||||
function appendThinkingText(node, text) {
|
||||
const steps = node.querySelector(".thinking-steps");
|
||||
if (!steps || !text) return;
|
||||
node.querySelectorAll(".thinking-steps [data-fallback='true']").forEach((item) => item.remove());
|
||||
node.dataset.hasModelThinking = "true";
|
||||
let item = steps.querySelector(".thinking-raw-step");
|
||||
if (!item) {
|
||||
item = document.createElement("li");
|
||||
item.className = "thinking-raw-step";
|
||||
steps.appendChild(item);
|
||||
}
|
||||
item.textContent += text;
|
||||
}
|
||||
|
||||
function createThinkTagParser(node) {
|
||||
let buffer = "";
|
||||
let inThinking = false;
|
||||
|
||||
const partialTagLength = (text) => {
|
||||
const lower = text.toLowerCase();
|
||||
const tags = ["<think>", "</think>"];
|
||||
for (const tag of tags) {
|
||||
for (let length = tag.length - 1; length > 0; length -= 1) {
|
||||
if (lower.endsWith(tag.slice(0, length))) return length;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const consume = (content, flush = false) => {
|
||||
buffer += content;
|
||||
let visible = "";
|
||||
|
||||
while (buffer) {
|
||||
const lower = buffer.toLowerCase();
|
||||
if (inThinking) {
|
||||
const closeIndex = lower.indexOf("</think>");
|
||||
if (closeIndex === -1) {
|
||||
if (flush) {
|
||||
appendThinkingText(node, buffer);
|
||||
buffer = "";
|
||||
} else {
|
||||
const keep = partialTagLength(buffer);
|
||||
appendThinkingText(node, buffer.slice(0, buffer.length - keep));
|
||||
buffer = buffer.slice(buffer.length - keep);
|
||||
}
|
||||
break;
|
||||
}
|
||||
appendThinkingText(node, buffer.slice(0, closeIndex));
|
||||
buffer = buffer.slice(closeIndex + "</think>".length);
|
||||
inThinking = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
const openIndex = lower.indexOf("<think>");
|
||||
if (openIndex === -1) {
|
||||
const keep = flush ? 0 : partialTagLength(buffer);
|
||||
visible += buffer.slice(0, buffer.length - keep);
|
||||
buffer = buffer.slice(buffer.length - keep);
|
||||
break;
|
||||
}
|
||||
|
||||
visible += buffer.slice(0, openIndex);
|
||||
buffer = buffer.slice(openIndex + "<think>".length);
|
||||
inThinking = true;
|
||||
}
|
||||
|
||||
return visible;
|
||||
};
|
||||
|
||||
return {
|
||||
consume,
|
||||
flush: () => consume("", true),
|
||||
};
|
||||
}
|
||||
|
||||
function reasoningSummaryForStatus(text) {
|
||||
const summaries = {
|
||||
Thinking: "Reading your request and deciding whether I need current UEX data, memory, or a draft action before answering.",
|
||||
"Searching UEX listings": "Checking current UEX marketplace listings so the answer is grounded in live item data instead of stale memory.",
|
||||
"Fetching listing details": "Opening the specific listing details to avoid guessing about price, seller, quantity, or status.",
|
||||
"Checking negotiations": "Looking through active negotiations because replies and offers can change what the next move should be.",
|
||||
"Reading negotiation messages": "Reading the negotiation thread so any drafted reply matches the actual conversation.",
|
||||
"Drafting message for approval": "Preparing the exact message as a pending action because marketplace writes need your approval first.",
|
||||
"Drafting listing for approval": "Preparing the listing payload as a pending action so you can review it before anything is posted.",
|
||||
"Checking UEX notifications": "Checking notifications for fresh replies or alerts that could change the recommendation.",
|
||||
"Writing response": "Turning the gathered context into a concise response with the relevant details and next action.",
|
||||
};
|
||||
if (summaries[text]) return summaries[text];
|
||||
if (text.startsWith("Running ")) {
|
||||
return `Using ${text.replace(/^Running\s+/, "")} to gather the missing context before answering.`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function finishThinking(node) {
|
||||
const thinking = node.querySelector(".thinking-log");
|
||||
const label = node.querySelector(".thinking-summary-label");
|
||||
if (!thinking || !label) return;
|
||||
const startedAt = Number(thinking.dataset.startedAt || Date.now());
|
||||
const elapsedSeconds = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
|
||||
label.textContent = `Thought for ${elapsedSeconds}s`;
|
||||
thinking.classList.remove("thinking-active");
|
||||
}
|
||||
|
||||
function ensureStreamingChrome(node) {
|
||||
if (node.querySelector(".message-activity")) return;
|
||||
node.innerHTML = "";
|
||||
@@ -57,10 +174,22 @@ function ensureStreamingChrome(node) {
|
||||
phase.className = "message-phase";
|
||||
const metrics = document.createElement("span");
|
||||
metrics.className = "message-metrics";
|
||||
const thinking = document.createElement("details");
|
||||
thinking.className = "thinking-log";
|
||||
thinking.classList.add("thinking-active");
|
||||
thinking.dataset.startedAt = String(Date.now());
|
||||
const thinkingSummary = document.createElement("summary");
|
||||
const thinkingLabel = document.createElement("span");
|
||||
thinkingLabel.className = "thinking-summary-label";
|
||||
thinkingLabel.textContent = "Thinking...";
|
||||
const thinkingSteps = document.createElement("ol");
|
||||
thinkingSteps.className = "thinking-steps";
|
||||
const body = document.createElement("div");
|
||||
body.className = "message-body";
|
||||
activity.append(phase, metrics);
|
||||
node.append(activity, body);
|
||||
thinkingSummary.appendChild(thinkingLabel);
|
||||
thinking.append(thinkingSummary, thinkingSteps);
|
||||
node.append(activity, thinking, body);
|
||||
}
|
||||
|
||||
function renderMarkdown(text) {
|
||||
@@ -464,6 +593,7 @@ async function sendMessage() {
|
||||
const assistantNode = addMessage("assistant streaming", "");
|
||||
ensureStreamingChrome(assistantNode);
|
||||
let assistantText = "";
|
||||
const thinkParser = createThinkTagParser(assistantNode);
|
||||
statusEl.textContent = "Working";
|
||||
setMessageActivity(assistantNode, "Thinking", true);
|
||||
setMessageMetrics(assistantNode, "");
|
||||
@@ -498,10 +628,18 @@ async function sendMessage() {
|
||||
assistantText += event.message;
|
||||
setMessageMarkdown(assistantNode, assistantText);
|
||||
} else if (event.type === "token") {
|
||||
assistantText += event.content;
|
||||
setMessageMarkdown(assistantNode, assistantText);
|
||||
const visibleContent = thinkParser.consume(event.content);
|
||||
if (visibleContent) {
|
||||
assistantText += visibleContent;
|
||||
setMessageMarkdown(assistantNode, assistantText);
|
||||
}
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
} else if (event.type === "done") {
|
||||
const visibleContent = thinkParser.flush();
|
||||
if (visibleContent) {
|
||||
assistantText += visibleContent;
|
||||
setMessageMarkdown(assistantNode, assistantText);
|
||||
}
|
||||
renderPending(event.pending_actions || []);
|
||||
}
|
||||
}
|
||||
@@ -517,6 +655,7 @@ async function sendMessage() {
|
||||
input.disabled = false;
|
||||
input.focus();
|
||||
statusEl.textContent = "Ready";
|
||||
finishThinking(assistantNode);
|
||||
setMessageActivity(assistantNode, "");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user