feat(open-webui): animated typing indicator while awaiting reply

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.
This commit is contained in:
Lok3sh-Kumar committed 2026-06-27 14:16:29 +05:30
1 parent 78bd66e803
commit d74728c28d
2 files changed
+75 -2

No files matched your search

@@ -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%;
}
+23 -2
View File
@@ -535,7 +535,7 @@ export function Chat({ options, integrationIds, isEditMode }: WidgetComponentPro
{messages.map((message, index) => (
<MessageBubble key={index} from={message.role} content={message.content} images={message.images} />
))}
{isStreaming ? <MessageBubble from="assistant" content={streamingText || "…"} /> : null}
{isStreaming ? <MessageBubble from="assistant" content={streamingText} loading={!streamingText} /> : null}
{error ? (
<Group gap="xs" justify="center" wrap="nowrap">
<Text size="xs" c="red.6">
@@ -1158,7 +1158,27 @@ function AttachmentBadge({
);
}
function MessageBubble({ from, content, images }: { from: ChatMessage["role"]; content: string; images?: string[] }) {
function TypingDots() {
return (
<span className={classes.typing} aria-label="…">
<span className={classes.typingDot} />
<span className={classes.typingDot} />
<span className={classes.typingDot} />
</span>
);
}
function MessageBubble({
from,
content,
images,
loading,
}: {
from: ChatMessage["role"];
content: string;
images?: string[];
loading?: boolean;
}) {
const isUser = from === "user";
const avatar = (
<Avatar size={22} radius="xl" variant="filled" color={isUser ? "gray" : "blue"} className={classes.avatar}>
@@ -1182,6 +1202,7 @@ function MessageBubble({ from, content, images }: { from: ChatMessage["role"]; c
))}
</Group>
) : null}
{loading && !content ? <TypingDots /> : null}
{content ? (
<Text size="sm" style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
{content}