Spaces:
Running
Running
| <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> |