Spaces:
Running
Running
| <script lang="ts"> | |
| import { onMount, afterUpdate } from "svelte"; | |
| import gsap from "gsap"; | |
| import { StreamingToolCallParser, filterToolCalls } from "../../utils/tool-call-parser"; | |
| export let content: string; | |
| export let streaming: boolean = false; | |
| let textEl: HTMLDivElement; | |
| let cursorEl: HTMLSpanElement; | |
| let lastContentLength = 0; | |
| let newContentEl: HTMLSpanElement; | |
| let parser: StreamingToolCallParser | null = null; | |
| // Initialize parser for streaming mode | |
| $: { | |
| if (streaming && !parser) { | |
| parser = new StreamingToolCallParser(); | |
| } else if (!streaming && parser) { | |
| parser = null; | |
| } | |
| } | |
| // Filter content based on streaming state | |
| $: filteredContent = (() => { | |
| if (streaming && parser) { | |
| // Use streaming parser to incrementally process content | |
| parser.reset(); | |
| const result = parser.process(content); | |
| return result.safeText; | |
| } else { | |
| // Use static filter for non-streaming content | |
| return filterToolCalls(content); | |
| } | |
| })(); | |
| onMount(() => { | |
| if (textEl && !streaming) { | |
| gsap.fromTo(textEl, | |
| { opacity: 0, y: 3, scale: 0.99 }, | |
| { opacity: 1, y: 0, scale: 1, duration: 0.3, ease: "power2.out" } | |
| ); | |
| } | |
| }); | |
| // Animate new characters appearing during streaming | |
| afterUpdate(() => { | |
| if (streaming && filteredContent.length > lastContentLength) { | |
| const newChars = filteredContent.slice(lastContentLength); | |
| if (newContentEl && newChars.length > 0) { | |
| gsap.fromTo(newContentEl, | |
| { opacity: 0, scale: 0.95 }, | |
| { opacity: 1, scale: 1, duration: 0.15, ease: "power2.out" } | |
| ); | |
| } | |
| lastContentLength = filteredContent.length; | |
| } | |
| if (!streaming) { | |
| lastContentLength = 0; | |
| } | |
| }); | |
| $: if (streaming && cursorEl) { | |
| gsap.to(cursorEl, { | |
| opacity: 0.3, | |
| duration: 0.6, | |
| yoyo: true, | |
| repeat: -1, | |
| ease: "power2.inOut" | |
| }); | |
| } | |
| $: if (!streaming && cursorEl) { | |
| gsap.killTweensOf(cursorEl); | |
| gsap.to(cursorEl, { | |
| opacity: 0, | |
| duration: 0.3, | |
| ease: "power2.out", | |
| onComplete: () => { | |
| // Small celebration pulse on complete | |
| if (textEl) { | |
| gsap.to(textEl, { | |
| borderLeftColor: "rgba(76, 175, 80, 0.3)", | |
| duration: 0.3, | |
| yoyo: true, | |
| repeat: 1, | |
| ease: "power2.inOut" | |
| }); | |
| } | |
| } | |
| }); | |
| } | |
| </script> | |
| <div class="text-renderer" bind:this={textEl}> | |
| <span class="content"> | |
| {#if streaming && lastContentLength > 0} | |
| {filteredContent.slice(0, lastContentLength)} | |
| <span bind:this={newContentEl}>{filteredContent.slice(lastContentLength)}</span> | |
| {:else} | |
| {filteredContent} | |
| {/if} | |
| {#if streaming}<span class="cursor" bind:this={cursorEl}>▊</span>{/if} | |
| </span> | |
| </div> | |
| <style> | |
| .text-renderer { | |
| padding: 0.125rem 0; | |
| line-height: 1.6; | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| } | |
| .content { | |
| color: rgba(251, 248, 244, 0.9); | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Monaco", "Menlo", monospace; | |
| font-size: 0.875rem; | |
| line-height: 1.6; | |
| letter-spacing: 0.01em; | |
| } | |
| .cursor { | |
| display: inline-block; | |
| color: rgba(124, 152, 133, 0.7); | |
| font-weight: 400; | |
| margin-left: 1px; | |
| } | |
| </style> |