ux: LBC Styling, feat: thinking, feat: more tools:

This commit is contained in:
2026-05-06 01:15:37 -04:00
parent 36c91ce500
commit 5850674448
9 changed files with 1160 additions and 165 deletions
+142 -3
View File
@@ -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, "");
}
}