ux improved
This commit is contained in:
@@ -10,17 +10,18 @@ class RunTimeline extends HTMLElement {
|
||||
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host { display:block; padding:12px; }
|
||||
.box { background:#121823; border:1px solid #1e2a3a; border-radius:10px; padding:10px; margin-bottom:10px; }
|
||||
:host { display:block; padding:12px; height:100%; overflow:hidden; }
|
||||
.box { background:#121823; border:1px solid #1e2a3a; border-radius:10px; padding:10px; height:100%; display:flex; flex-direction:column; min-height:0; box-sizing:border-box; }
|
||||
.row { display:flex; gap:8px; align-items:center; }
|
||||
.muted { color:#8aa0b5; font-size:12px; }
|
||||
.title { font-weight:800; }
|
||||
.chatlog { display:flex; flex-direction:column; gap:8px; max-height:520px; overflow:auto; padding-right:6px; margin-top:8px; }
|
||||
.chatlog { display:flex; flex-direction:column; gap:0; overflow-y:auto; padding-right:6px; margin-top:8px; flex:1; min-height:0; }
|
||||
/* WhatsApp-ish dark theme bubbles */
|
||||
.bubble { max-width:90%; padding:8px 10px; border-radius:14px; border:1px solid #253245; font-size:13px; line-height:1.35; white-space:pre-wrap; word-break:break-word; box-shadow: 0 1px 0 rgba(0,0,0,.35); }
|
||||
.bubble { max-width:90%; margin-bottom:12px; padding:8px 10px; border-radius:14px; border:1px solid #253245; font-size:13px; line-height:1.35; white-space:pre-wrap; word-break:break-word; box-shadow: 0 1px 0 rgba(0,0,0,.35); box-sizing:border-box; }
|
||||
.bubble.user { align-self:flex-end; background:#0f2a1a; border-color:#1f6f43; color:#e7eef7; }
|
||||
.bubble.bot { align-self:flex-start; background:#111b2a; border-color:#2a3a55; color:#e7eef7; }
|
||||
.bubble.err { align-self:flex-start; background:#241214; border-color:#e74c3c; color:#ffe9ea; cursor:pointer; }
|
||||
.bubble.active { outline:2px solid #1f6feb; box-shadow: 0 0 0 2px rgba(31,111,235,.25); }
|
||||
.name { display:block; font-size:12px; font-weight:800; margin-bottom:4px; opacity:.95; }
|
||||
.bubble.user .name { color:#cdebd8; text-align:right; }
|
||||
.bubble.bot .name { color:#c7d8ee; }
|
||||
@@ -62,11 +63,30 @@ class RunTimeline extends HTMLElement {
|
||||
this.loadMessages();
|
||||
}
|
||||
});
|
||||
|
||||
this._unsubHighlight = on("ui:highlightMessage", ({ message_id }) => {
|
||||
this.highlightMessage(message_id);
|
||||
});
|
||||
|
||||
// Listen for inspector heights to sync bubble heights
|
||||
this._unsubInspector = on("ui:inspectorLayout", ({ chat_id, items }) => {
|
||||
if (!this.chatId || chat_id !== this.chatId) return;
|
||||
this.applyInspectorHeights(items);
|
||||
});
|
||||
|
||||
// Listen for optimistic messages (show bubble immediately before API response)
|
||||
this._unsubOptimistic = on("message:optimistic", (msg) => {
|
||||
if (!this.chatId || msg.chat_id !== this.chatId) return;
|
||||
this.addOptimisticBubble(msg);
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this._unsubSel?.();
|
||||
this._unsubRun?.();
|
||||
this._unsubHighlight?.();
|
||||
this._unsubInspector?.();
|
||||
this._unsubOptimistic?.();
|
||||
}
|
||||
|
||||
async loadMessages() {
|
||||
@@ -122,6 +142,7 @@ class RunTimeline extends HTMLElement {
|
||||
const isErr = this.isErrorMsg(m);
|
||||
const bubble = document.createElement("div");
|
||||
bubble.className = `bubble ${isErr ? "err" : who}`;
|
||||
bubble.dataset.messageId = m.message_id;
|
||||
|
||||
const nameEl = document.createElement("span");
|
||||
nameEl.className = "name";
|
||||
@@ -146,6 +167,118 @@ class RunTimeline extends HTMLElement {
|
||||
// auto-scroll
|
||||
log.scrollTop = log.scrollHeight;
|
||||
|
||||
requestAnimationFrame(() => this.emitLayout());
|
||||
this.bindScroll(log);
|
||||
|
||||
}
|
||||
|
||||
bindScroll(log) {
|
||||
if (this._scrollBound) return;
|
||||
this._scrollBound = true;
|
||||
log.addEventListener("scroll", () => {
|
||||
emit("ui:chatScroll", { chat_id: this.chatId, scrollTop: log.scrollTop });
|
||||
});
|
||||
}
|
||||
|
||||
emitLayout() {
|
||||
const log = this.shadowRoot.getElementById("log");
|
||||
const box = this.shadowRoot.querySelector(".box");
|
||||
const bubbles = [...log.querySelectorAll(".bubble")];
|
||||
const items = bubbles.map((el) => {
|
||||
const styles = window.getComputedStyle(el);
|
||||
const marginBottom = parseInt(styles.marginBottom || "0", 10) || 0;
|
||||
return {
|
||||
message_id: el.dataset.messageId || null,
|
||||
height: (el.offsetHeight || 0) + marginBottom,
|
||||
};
|
||||
});
|
||||
emit("ui:bubblesLayout", { chat_id: this.chatId, items });
|
||||
// #region agent log
|
||||
fetch("http://127.0.0.1:7242/ingest/86c7b1cd-c414-4eae-852c-08e57e562b3b", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
sessionId: "debug-session",
|
||||
runId: "pre-fix",
|
||||
hypothesisId: "H15",
|
||||
location: "run-timeline.js:180",
|
||||
message: "bubbles_layout",
|
||||
data: {
|
||||
count: items.length,
|
||||
chat_id: this.chatId || null,
|
||||
scroll_height: log.scrollHeight,
|
||||
client_height: log.clientHeight,
|
||||
host_height: this.getBoundingClientRect().height,
|
||||
box_height: box ? box.getBoundingClientRect().height : null,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
}).catch(() => {});
|
||||
// #endregion
|
||||
}
|
||||
|
||||
highlightMessage(message_id) {
|
||||
const log = this.shadowRoot.getElementById("log");
|
||||
if (!log) return;
|
||||
const bubbles = [...log.querySelectorAll(".bubble")];
|
||||
for (const el of bubbles) {
|
||||
el.classList.toggle("active", el.dataset.messageId === message_id);
|
||||
}
|
||||
// No auto-scroll - mantener posición actual del usuario
|
||||
}
|
||||
|
||||
applyInspectorHeights(items) {
|
||||
const log = this.shadowRoot.getElementById("log");
|
||||
if (!log) return;
|
||||
const BUBBLE_MARGIN = 12;
|
||||
const MIN_HEIGHT = 120;
|
||||
const heightMap = new Map((items || []).map((it) => [it.message_id, it.height || 0]));
|
||||
|
||||
const bubbles = [...log.querySelectorAll(".bubble")];
|
||||
for (const el of bubbles) {
|
||||
const messageId = el.dataset.messageId;
|
||||
const inspectorHeight = heightMap.get(messageId) || 0;
|
||||
// Inspector height includes margin, extract content height
|
||||
const inspectorContentHeight = Math.max(0, inspectorHeight - BUBBLE_MARGIN);
|
||||
// Use max between inspector height and our minimum
|
||||
const targetHeight = Math.max(inspectorContentHeight, MIN_HEIGHT);
|
||||
// Always apply to ensure sync
|
||||
el.style.minHeight = `${targetHeight}px`;
|
||||
el.style.marginBottom = `${BUBBLE_MARGIN}px`;
|
||||
}
|
||||
}
|
||||
|
||||
addOptimisticBubble(msg) {
|
||||
const log = this.shadowRoot.getElementById("log");
|
||||
if (!log) return;
|
||||
|
||||
// Check if already exists (by optimistic ID pattern)
|
||||
const existing = log.querySelector(`.bubble[data-message-id^="optimistic-"]`);
|
||||
if (existing) existing.remove();
|
||||
|
||||
const bubble = document.createElement("div");
|
||||
bubble.className = "bubble user";
|
||||
bubble.dataset.messageId = msg.message_id;
|
||||
|
||||
const nameEl = document.createElement("span");
|
||||
nameEl.className = "name";
|
||||
nameEl.textContent = msg.pushName || "test_lucas";
|
||||
bubble.appendChild(nameEl);
|
||||
|
||||
const textEl = document.createElement("div");
|
||||
textEl.textContent = msg.text || "—";
|
||||
bubble.appendChild(textEl);
|
||||
|
||||
const metaEl = document.createElement("span");
|
||||
metaEl.className = "meta";
|
||||
metaEl.textContent = `${new Date(msg.ts).toLocaleString()} • ${msg.provider} • enviando...`;
|
||||
bubble.appendChild(metaEl);
|
||||
|
||||
log.appendChild(bubble);
|
||||
log.scrollTop = log.scrollHeight;
|
||||
|
||||
// Emit layout update
|
||||
requestAnimationFrame(() => this.emitLayout());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user