From d74728c28d6e2591f443988bd966539dff278447 Mon Sep 17 00:00:00 2001 From: Lok3sh-Kumar Date: Sat, 27 Jun 2026 14:16:29 +0530 Subject: [PATCH] feat(open-webui): animated typing indicator while awaiting reply MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the static "…" placeholder with a three-dot wave animation shown in the assistant bubble until the first token streams in. Falls back to a fade for users who prefer reduced motion. --- .../widgets/src/open-webui/chat.module.css | 52 +++++++++++++++++++ packages/widgets/src/open-webui/chat.tsx | 25 ++++++++- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/packages/widgets/src/open-webui/chat.module.css b/packages/widgets/src/open-webui/chat.module.css index 3aa564197..844944f73 100644 --- a/packages/widgets/src/open-webui/chat.module.css +++ b/packages/widgets/src/open-webui/chat.module.css @@ -34,6 +34,58 @@ margin-top: 2px; } +.typing { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 2px; +} + +.typingDot { + width: 6px; + height: 6px; + border-radius: 50%; + background-color: var(--mantine-color-dimmed); + animation: owui-typing 1.2s infinite ease-in-out both; +} + +.typingDot:nth-child(2) { + animation-delay: 0.15s; +} + +.typingDot:nth-child(3) { + animation-delay: 0.3s; +} + +@keyframes owui-typing { + 0%, + 60%, + 100% { + transform: translateY(0); + opacity: 0.35; + } + 30% { + transform: translateY(-4px); + opacity: 1; + } +} + +@media (prefers-reduced-motion: reduce) { + .typingDot { + animation: owui-typing-fade 1.2s infinite ease-in-out both; + } + + @keyframes owui-typing-fade { + 0%, + 100% { + opacity: 0.35; + } + 50% { + opacity: 1; + } + } +} + .bubble { max-width: 85%; } diff --git a/packages/widgets/src/open-webui/chat.tsx b/packages/widgets/src/open-webui/chat.tsx index 2a701cf27..771e91359 100644 --- a/packages/widgets/src/open-webui/chat.tsx +++ b/packages/widgets/src/open-webui/chat.tsx @@ -535,7 +535,7 @@ export function Chat({ options, integrationIds, isEditMode }: WidgetComponentPro {messages.map((message, index) => ( ))} - {isStreaming ? : null} + {isStreaming ? : null} {error ? ( @@ -1158,7 +1158,27 @@ function AttachmentBadge({ ); } -function MessageBubble({ from, content, images }: { from: ChatMessage["role"]; content: string; images?: string[] }) { +function TypingDots() { + return ( + + + + + + ); +} + +function MessageBubble({ + from, + content, + images, + loading, +}: { + from: ChatMessage["role"]; + content: string; + images?: string[]; + loading?: boolean; +}) { const isUser = from === "user"; const avatar = ( @@ -1182,6 +1202,7 @@ function MessageBubble({ from, content, images }: { from: ChatMessage["role"]; c ))} ) : null} + {loading && !content ? : null} {content ? ( {content}