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:
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%;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user