Add StreamingIndicator component and replace static streaming indicator in ChatArea

This commit is contained in:
Andrew Beal 2025-09-28 23:31:35 +01:00
parent 854aa2a98d
commit 2de8c683ac
2 changed files with 255 additions and 12 deletions

View file

@ -3,6 +3,7 @@
import { Services } from "Services/Services";
import type { StreamingMarkdownService } from "Services/StreamingMarkdownService";
import ChatAreaThought from "./ChatAreaThought.svelte";
import StreamingIndicator from "./StreamingIndicator.svelte";
export let messages: Array<{id: string, content: string, isUser: boolean, isStreaming: boolean}> = [];
export let chatContainer: HTMLDivElement;
@ -98,7 +99,7 @@
<!-- Streaming message: use action for initialization -->
{#if message.isStreaming}
<div use:streamingAction={message.id} class="streaming-content"></div>
<span class="streaming-indicator">● ● ●</span>
<StreamingIndicator/>
<ChatAreaThought/>
{:else}
<!-- Static message: use traditional rendering -->
@ -168,15 +169,4 @@
min-height: 1em; /* Ensure the element exists for binding */
}
.streaming-indicator {
display: inline-block;
color: var(--text-accent);
animation: pulse 1.5s infinite;
margin-left: 4px;
}
@keyframes pulse {
0%, 100% { opacity: 0.3; }
50% { opacity: 1; }
}
</style>

View file

@ -0,0 +1,253 @@
<div class="container">
<span class="📦"></span>
<span class="📦"></span>
<span class="📦"></span>
<span class="📦"></span>
<span class="📦"></span>
</div>
<style>
:root {
--duration: 1.5s;
--container-size: 100px;
--box-size: 12px;
--box-border-radius: 15%;
}
.container {
width: var(--container-size);
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
margin-left: 4px;
}
.📦 {
width: var(--box-size);
height: var(--box-size);
position: relative;
display: block;
transform-origin: -50% center;
border-radius: var(--box-border-radius);
}
.📦:after {
content: "";
width: 100%;
height: 100%;
position: absolute;
top: 0;
right: 0;
background-color: color-mix(in srgb, var(--interactive-accent) 85%, white 15%);
border-radius: var(--box-border-radius);
box-shadow: 0px 0px 8px 2px color-mix(in srgb, var(--interactive-accent) 40%, transparent 60%);
}
.📦:nth-child(1) {
animation: slide var(--duration) ease-in-out infinite alternate;
}
.📦:nth-child(1):after {
animation: color-change var(--duration) ease-in-out infinite alternate;
}
.📦:nth-child(2) {
animation: flip-1 var(--duration) ease-in-out infinite alternate;
}
.📦:nth-child(2):after {
animation: squidge-1 var(--duration) ease-in-out infinite alternate;
}
.📦:nth-child(3) {
animation: flip-2 var(--duration) ease-in-out infinite alternate;
}
.📦:nth-child(3):after {
animation: squidge-2 var(--duration) ease-in-out infinite alternate;
}
.📦:nth-child(4) {
animation: flip-3 var(--duration) ease-in-out infinite alternate;
}
.📦:nth-child(4):after {
animation: squidge-3 var(--duration) ease-in-out infinite alternate;
}
.📦:nth-child(5) {
animation: flip-4 var(--duration) ease-in-out infinite alternate;
}
.📦:nth-child(5):after {
animation: squidge-4 var(--duration) ease-in-out infinite alternate;
}
.📦:nth-child(2):after {
background-color: color-mix(in srgb, var(--interactive-accent) 75%, white 25%);
}
.📦:nth-child(3):after {
background-color: color-mix(in srgb, var(--interactive-accent) 80%, white 20%);
}
.📦:nth-child(4):after {
background-color: color-mix(in srgb, var(--interactive-accent) 90%, white 10%);
}
.📦:nth-child(5):after {
background-color: color-mix(in srgb, var(--interactive-accent) 95%, white 5%);
}
@keyframes slide {
0% {
background-color: color-mix(in srgb, var(--interactive-accent) 70%, black 30%);
transform: translatex(0vw);
}
100% {
background-color: color-mix(in srgb, var(--interactive-accent) 95%, white 5%);
transform: translatex(
calc(var(--container-size) - (var(--box-size) * 1.25))
);
}
}
@keyframes color-change {
0% {
background-color: color-mix(in srgb, var(--interactive-accent) 70%, black 30%);
}
100% {
background-color: color-mix(in srgb, var(--interactive-accent) 95%, white 5%);
}
}
@keyframes flip-1 {
0%,
15% {
transform: rotate(0);
}
35%,
100% {
transform: rotate(-180deg);
}
}
@keyframes squidge-1 {
5% {
transform-origin: center bottom;
transform: scalex(1) scaley(1);
}
15% {
transform-origin: center bottom;
transform: scalex(1.3) scaley(0.7);
}
25%,
20% {
transform-origin: center bottom;
transform: scalex(0.8) scaley(1.4);
}
55%,
100% {
transform-origin: center top;
transform: scalex(1) scaley(1);
}
40% {
transform-origin: center top;
transform: scalex(1.3) scaley(0.7);
}
}
@keyframes flip-2 {
0%,
30% {
transform: rotate(0);
}
50%,
100% {
transform: rotate(-180deg);
}
}
@keyframes squidge-2 {
20% {
transform-origin: center bottom;
transform: scalex(1) scaley(1);
}
30% {
transform-origin: center bottom;
transform: scalex(1.3) scaley(0.7);
}
40%,
35% {
transform-origin: center bottom;
transform: scalex(0.8) scaley(1.4);
}
70%,
100% {
transform-origin: center top;
transform: scalex(1) scaley(1);
}
55% {
transform-origin: center top;
transform: scalex(1.3) scaley(0.7);
}
}
@keyframes flip-3 {
0%,
45% {
transform: rotate(0);
}
65%,
100% {
transform: rotate(-180deg);
}
}
@keyframes squidge-3 {
35% {
transform-origin: center bottom;
transform: scalex(1) scaley(1);
}
45% {
transform-origin: center bottom;
transform: scalex(1.3) scaley(0.7);
}
55%,
50% {
transform-origin: center bottom;
transform: scalex(0.8) scaley(1.4);
}
85%,
100% {
transform-origin: center top;
transform: scalex(1) scaley(1);
}
70% {
transform-origin: center top;
transform: scalex(1.3) scaley(0.7);
}
}
@keyframes flip-4 {
0%,
60% {
transform: rotate(0);
}
80%,
100% {
transform: rotate(-180deg);
}
}
@keyframes squidge-4 {
50% {
transform-origin: center bottom;
transform: scalex(1) scaley(1);
}
60% {
transform-origin: center bottom;
transform: scalex(1.3) scaley(0.7);
}
70%,
65% {
transform-origin: center bottom;
transform: scalex(0.8) scaley(1.4);
}
100%,
100% {
transform-origin: center top;
transform: scalex(1) scaley(1);
}
85% {
transform-origin: center top;
transform: scalex(1.3) scaley(0.7);
}
}
</style>