From 389c80d7a8c25940c9ae7fb20c883577008e4c7c Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 30 Oct 2025 20:42:04 +0100 Subject: [PATCH] Improve web-ui message and tool UX - Add collapsible thinking blocks with shimmer animation during streaming - Update user messages to use orange gradient pill styling (matching sitegeist) - Fix cost display to only show for completed messages, not while streaming - Update tool renderers to use ChevronsUpDown/ChevronUp icons instead of rotating ChevronRight - Export ThinkingBlock component from public API --- packages/web-ui/src/app.css | 23 ++++++++++ packages/web-ui/src/components/MessageList.ts | 2 +- packages/web-ui/src/components/Messages.ts | 35 ++++++++------- .../web-ui/src/components/ThinkingBlock.ts | 43 +++++++++++++++++++ packages/web-ui/src/index.ts | 1 + .../web-ui/src/tools/renderer-registry.ts | 23 +++++++--- 6 files changed, 106 insertions(+), 21 deletions(-) create mode 100644 packages/web-ui/src/components/ThinkingBlock.ts diff --git a/packages/web-ui/src/app.css b/packages/web-ui/src/app.css index 540964d5..05adb5cf 100644 --- a/packages/web-ui/src/app.css +++ b/packages/web-ui/src/app.css @@ -42,3 +42,26 @@ body { .fixed.inset-0 button[type="button"] { cursor: pointer; } + +/* Shimmer animation for thinking text */ +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} + +.animate-shimmer { + animation: shimmer 2s ease-in-out infinite; +} + +/* User message with fancy pill styling */ +.user-message-container { + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + background: linear-gradient(135deg, rgba(217, 79, 0, 0.12), rgba(255, 107, 0, 0.12), rgba(212, 165, 0, 0.12)); + border: 1px solid rgba(255, 107, 0, 0.25); + backdrop-filter: blur(10px); +} diff --git a/packages/web-ui/src/components/MessageList.ts b/packages/web-ui/src/components/MessageList.ts index 806ddec1..8f507ad8 100644 --- a/packages/web-ui/src/components/MessageList.ts +++ b/packages/web-ui/src/components/MessageList.ts @@ -65,7 +65,7 @@ export class MessageList extends LitElement { template: html` c.type === "text")?.text || ""; return html` -
- - ${ - this.message.attachments && this.message.attachments.length > 0 - ? html` -
- ${this.message.attachments.map( - (attachment) => html` `, - )} -
- ` - : "" - } +
+
+ + ${ + this.message.attachments && this.message.attachments.length > 0 + ? html` +
+ ${this.message.attachments.map( + (attachment) => html` `, + )} +
+ ` + : "" + } +
`; } @@ -107,7 +110,9 @@ export class AssistantMessage extends LitElement { if (chunk.type === "text" && chunk.text.trim() !== "") { orderedParts.push(html``); } else if (chunk.type === "thinking" && chunk.thinking.trim() !== "") { - orderedParts.push(html` `); + orderedParts.push( + html``, + ); } else if (chunk.type === "toolCall") { if (!this.hideToolCalls) { const tool = this.tools?.find((t) => t.name === chunk.name); @@ -133,7 +138,7 @@ export class AssistantMessage extends LitElement {
${orderedParts.length ? html`
${orderedParts}
` : ""} ${ - this.message.usage + this.message.usage && !this.isStreaming ? this.onCostClick ? html`
${formatUsage(this.message.usage)}
` : html`
${formatUsage(this.message.usage)}
` diff --git a/packages/web-ui/src/components/ThinkingBlock.ts b/packages/web-ui/src/components/ThinkingBlock.ts new file mode 100644 index 00000000..8dd7ce45 --- /dev/null +++ b/packages/web-ui/src/components/ThinkingBlock.ts @@ -0,0 +1,43 @@ +import { icon } from "@mariozechner/mini-lit"; +import { html, LitElement } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import { ChevronRight } from "lucide"; + +@customElement("thinking-block") +export class ThinkingBlock extends LitElement { + @property() content!: string; + @property({ type: Boolean }) isStreaming = false; + @state() private isExpanded = false; + + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this; + } + + override connectedCallback(): void { + super.connectedCallback(); + this.style.display = "block"; + } + + private toggleExpanded() { + this.isExpanded = !this.isExpanded; + } + + override render() { + const shimmerClasses = this.isStreaming + ? "animate-shimmer bg-gradient-to-r from-muted-foreground via-foreground to-muted-foreground bg-[length:200%_100%] bg-clip-text text-transparent" + : ""; + + return html` +
+
+ ${icon(ChevronRight, "sm")} + Thinking... +
+ ${this.isExpanded ? html`` : ""} +
+ `; + } +} diff --git a/packages/web-ui/src/index.ts b/packages/web-ui/src/index.ts index a55394a0..d8b2e19a 100644 --- a/packages/web-ui/src/index.ts +++ b/packages/web-ui/src/index.ts @@ -46,6 +46,7 @@ export { export { RuntimeMessageBridge } from "./components/sandbox/RuntimeMessageBridge.js"; export { RUNTIME_MESSAGE_ROUTER } from "./components/sandbox/RuntimeMessageRouter.js"; export type { SandboxRuntimeProvider } from "./components/sandbox/SandboxRuntimeProvider.js"; +export { ThinkingBlock } from "./components/ThinkingBlock.js"; export { ApiKeyPromptDialog } from "./dialogs/ApiKeyPromptDialog.js"; export { AttachmentOverlay } from "./dialogs/AttachmentOverlay.js"; // Dialogs diff --git a/packages/web-ui/src/tools/renderer-registry.ts b/packages/web-ui/src/tools/renderer-registry.ts index 76d25e6e..8e22c0bf 100644 --- a/packages/web-ui/src/tools/renderer-registry.ts +++ b/packages/web-ui/src/tools/renderer-registry.ts @@ -1,7 +1,7 @@ import { html, icon, type TemplateResult } from "@mariozechner/mini-lit"; import type { Ref } from "lit/directives/ref.js"; import { ref } from "lit/directives/ref.js"; -import { ChevronRight, Loader } from "lucide"; +import { ChevronsUpDown, ChevronUp, Loader } from "lucide"; import type { ToolRenderer } from "./types.js"; // Registry of tool renderers @@ -85,11 +85,23 @@ export function renderCollapsibleHeader( if (isCollapsed) { content.classList.remove("max-h-0"); content.classList.add("max-h-[2000px]", "mt-3"); - chevron.classList.add("rotate-90"); + // Show ChevronUp, hide ChevronsUpDown + const upIcon = chevron.querySelector(".chevron-up"); + const downIcon = chevron.querySelector(".chevrons-up-down"); + if (upIcon && downIcon) { + upIcon.classList.remove("hidden"); + downIcon.classList.add("hidden"); + } } else { content.classList.remove("max-h-[2000px]", "mt-3"); content.classList.add("max-h-0"); - chevron.classList.remove("rotate-90"); + // Show ChevronsUpDown, hide ChevronUp + const upIcon = chevron.querySelector(".chevron-up"); + const downIcon = chevron.querySelector(".chevrons-up-down"); + if (upIcon && downIcon) { + upIcon.classList.add("hidden"); + downIcon.classList.remove("hidden"); + } } } }; @@ -108,8 +120,9 @@ export function renderCollapsibleHeader( ${statusIcon(toolIcon, toolIconColor)} ${text}
- - ${icon(ChevronRight, "sm")} + + ${icon(ChevronUp, "sm")} + ${icon(ChevronsUpDown, "sm")} `;