VibeGame / src /lib /components /chat /StreamingText.svelte
dylanebert's picture
improve chat
3342a1d
raw
history blame
4.01 kB
<script lang="ts">
import { onMount, afterUpdate } from "svelte";
import gsap from "gsap";
export let content: string = "";
export let streaming: boolean = false;
export let speed: number = 60;
export let onComplete: (() => void) | undefined = undefined;
let displayedContent = "";
let cursorElement: HTMLSpanElement;
let containerElement: HTMLDivElement;
let animationTimeline: any = null;
let buffer: string[] = [];
let isProcessing = false;
let lastProcessedLength = 0;
$: if (streaming && content) {
// Only process truly new content
if (content.length > lastProcessedLength) {
const newChars = content.slice(lastProcessedLength);
if (newChars) {
buffer.push(...newChars.split(''));
lastProcessedLength = content.length;
processBuffer();
}
}
} else if (!streaming && content !== displayedContent) {
displayedContent = content;
lastProcessedLength = content.length;
if (animationTimeline) {
animationTimeline.kill();
}
hideCursor();
}
async function processBuffer() {
if (isProcessing || buffer.length === 0) return;
isProcessing = true;
while (buffer.length > 0) {
// Process multiple characters at once for better performance
const chunkSize = Math.min(3, buffer.length);
const chunk = buffer.splice(0, chunkSize).join('');
displayedContent += chunk;
// Only delay if there are more characters to process
if (buffer.length > 0) {
await new Promise(resolve => setTimeout(resolve, 1000 / speed));
}
}
isProcessing = false;
if (!streaming && displayedContent === content) {
hideCursor();
onComplete?.();
}
}
function showCursor() {
if (cursorElement) {
gsap.set(cursorElement, { display: "inline-block", opacity: 1 });
gsap.to(cursorElement, {
opacity: 0,
duration: 0.5,
repeat: -1,
yoyo: true,
ease: "steps(1)"
});
}
}
function hideCursor() {
if (cursorElement) {
gsap.killTweensOf(cursorElement);
gsap.to(cursorElement, {
opacity: 0,
duration: 0.2,
onComplete: () => {
if (cursorElement) {
gsap.set(cursorElement, { display: "none" });
}
}
});
}
}
onMount(() => {
if (streaming) {
showCursor();
// Reset tracking when component mounts with streaming
lastProcessedLength = 0;
displayedContent = "";
gsap.fromTo(containerElement,
{ opacity: 0, y: 5 },
{ opacity: 1, y: 0, duration: 0.3, ease: "power2.out" }
);
}
return () => {
if (animationTimeline) {
animationTimeline.kill();
}
};
});
afterUpdate(() => {
if (streaming && cursorElement) {
showCursor();
}
});
</script>
<div class="streaming-text" bind:this={containerElement}>
<span class="text-content">{displayedContent}</span>
{#if streaming}
<span bind:this={cursorElement} class="cursor"></span>
{/if}
</div>
<style>
.streaming-text {
display: inline;
word-wrap: break-word;
white-space: pre-wrap;
}
.text-content {
color: inherit;
font-family: inherit;
line-height: inherit;
}
.cursor {
display: inline-block;
color: rgba(65, 105, 225, 0.8);
font-weight: bold;
margin-left: 1px;
animation: none;
vertical-align: baseline;
}
</style>