Spaces:
Running
Running
| <script lang="ts"> | |
| import { onMount, onDestroy } from "svelte"; | |
| import gsap from "gsap"; | |
| export let status: "thinking" | "streaming" | "completing" = "thinking"; | |
| export let content: string = ""; | |
| export let startTime: number = Date.now(); | |
| export let isExpanded: boolean = false; | |
| export let autoCollapse: boolean = true; | |
| let blockElement: HTMLDivElement; | |
| let contentElement: HTMLDivElement; | |
| let expandIcon: HTMLSpanElement; | |
| let statusElement: HTMLSpanElement; | |
| let progressBar: HTMLDivElement; | |
| let collapseTimeout: number | null = null; | |
| let prevStatus: string = status; | |
| let timeline: gsap.core.Timeline | null = null; | |
| $: duration = Date.now() - startTime; | |
| $: formattedDuration = formatDuration(duration); | |
| $: if (status !== prevStatus && statusElement) { | |
| animateStatusTransition(prevStatus, status); | |
| prevStatus = status; | |
| } | |
| function formatDuration(ms: number): string { | |
| if (ms < 1000) return `${ms}ms`; | |
| return `${(ms / 1000).toFixed(1)}s`; | |
| } | |
| function toggleExpanded() { | |
| isExpanded = !isExpanded; | |
| updateExpandState(); | |
| } | |
| function updateExpandState() { | |
| if (!expandIcon || !contentElement) return; | |
| if (timeline) timeline.kill(); | |
| timeline = gsap.timeline(); | |
| if (isExpanded) { | |
| timeline | |
| .to(expandIcon, { | |
| rotation: 90, | |
| duration: 0.2, | |
| ease: "power2.out", | |
| }) | |
| .set(contentElement, { display: "block" }) | |
| .fromTo( | |
| contentElement, | |
| { opacity: 0, maxHeight: 0, y: -10 }, | |
| { opacity: 1, maxHeight: 500, y: 0, duration: 0.3, ease: "power2.out" }, | |
| "-=0.1" | |
| ); | |
| } else { | |
| timeline | |
| .to(expandIcon, { | |
| rotation: 0, | |
| duration: 0.2, | |
| ease: "power2.in", | |
| }) | |
| .to(contentElement, { | |
| opacity: 0, | |
| maxHeight: 0, | |
| y: -5, | |
| duration: 0.2, | |
| ease: "power2.in", | |
| onComplete: () => { | |
| gsap.set(contentElement, { display: "none" }); | |
| }, | |
| }, "-=0.1"); | |
| } | |
| } | |
| function animateStatusTransition(_from: string, to: string) { | |
| if (!statusElement || !blockElement) return; | |
| const tl = gsap.timeline(); | |
| tl.to(blockElement, { | |
| duration: 0.4, | |
| ease: "power2.inOut", | |
| onUpdate: function() { | |
| updateBlockStyle(to); | |
| } | |
| }); | |
| tl.to(statusElement, { | |
| scale: 0.9, | |
| opacity: 0, | |
| duration: 0.15, | |
| ease: "power2.in", | |
| onComplete: () => {} | |
| }) | |
| .to(statusElement, { | |
| scale: 1, | |
| opacity: 1, | |
| duration: 0.15, | |
| ease: "power2.out" | |
| }); | |
| if (progressBar) { | |
| if (to === "streaming") { | |
| gsap.to(progressBar, { | |
| width: "60%", | |
| duration: 1, | |
| ease: "power2.out" | |
| }); | |
| } else if (to === "completing") { | |
| gsap.to(progressBar, { | |
| width: "90%", | |
| duration: 0.5, | |
| ease: "power2.out" | |
| }); | |
| } | |
| } | |
| } | |
| function updateBlockStyle(status: string) { | |
| if (!blockElement) return; | |
| blockElement.className = `in-progress-block ${status}`; | |
| } | |
| function getStatusIcon() { | |
| switch (status) { | |
| case "thinking": | |
| return "🤔"; | |
| case "streaming": | |
| return "✍️"; | |
| case "completing": | |
| return "✨"; | |
| default: | |
| return "⚡"; | |
| } | |
| } | |
| function getStatusText() { | |
| switch (status) { | |
| case "thinking": | |
| return "Thinking"; | |
| case "streaming": | |
| return "Writing response"; | |
| case "completing": | |
| return "Finalizing response"; | |
| default: | |
| return "Processing"; | |
| } | |
| } | |
| $: if (status === "completing" && autoCollapse && isExpanded) { | |
| if (collapseTimeout) clearTimeout(collapseTimeout); | |
| collapseTimeout = window.setTimeout(() => { | |
| isExpanded = false; | |
| updateExpandState(); | |
| }, 500); | |
| } | |
| onMount(() => { | |
| if (blockElement) { | |
| gsap.fromTo( | |
| blockElement, | |
| { opacity: 0, y: -10, scale: 0.95 }, | |
| { opacity: 1, y: 0, scale: 1, duration: 0.4, ease: "back.out(1.2)" }, | |
| ); | |
| } | |
| if (progressBar) { | |
| gsap.fromTo(progressBar, | |
| { width: "0%" }, | |
| { width: "30%", duration: 0.5, ease: "power2.out" } | |
| ); | |
| } | |
| const interval = setInterval(() => { | |
| duration = Date.now() - startTime; | |
| }, 100); | |
| return () => clearInterval(interval); | |
| }); | |
| onDestroy(() => { | |
| if (collapseTimeout) clearTimeout(collapseTimeout); | |
| if (timeline) timeline.kill(); | |
| }); | |
| </script> | |
| <div class="in-progress-block {status}" bind:this={blockElement}> | |
| <div class="progress-bar-track"> | |
| <div bind:this={progressBar} class="progress-bar"></div> | |
| </div> | |
| <button class="progress-header" on:click={toggleExpanded} aria-expanded={isExpanded}> | |
| <span class="status-icon"> | |
| {#if status === "streaming"} | |
| <span class="animated-icon">{getStatusIcon()}</span> | |
| {:else if status === "thinking"} | |
| <span class="spinning-icon">{getStatusIcon()}</span> | |
| {:else} | |
| {getStatusIcon()} | |
| {/if} | |
| </span> | |
| <span bind:this={statusElement} class="status-text"> | |
| {getStatusText()}<span class="dots"></span> | |
| </span> | |
| <span class="progress-meta"> | |
| <span class="duration">{formattedDuration}</span> | |
| {#if content} | |
| <span class="char-count">{content.length} chars</span> | |
| {/if} | |
| </span> | |
| <span bind:this={expandIcon} class="expand-icon">▶</span> | |
| </button> | |
| <div bind:this={contentElement} class="progress-content" style="display: none;"> | |
| {#if content} | |
| <div class="content-wrapper"> | |
| <div class="content-text">{content}</div> | |
| <span class="cursor">▊</span> | |
| </div> | |
| {:else} | |
| <div class="waiting-message">Waiting for response to begin...</div> | |
| {/if} | |
| </div> | |
| </div> | |
| <style> | |
| .in-progress-block { | |
| margin: 0.4rem 0; | |
| border-radius: 4px; | |
| overflow: hidden; | |
| border: 1px solid rgba(255, 210, 30, 0.2); | |
| background: rgba(255, 210, 30, 0.05); | |
| transition: all 0.2s ease; | |
| position: relative; | |
| } | |
| .in-progress-block.thinking { | |
| border-color: rgba(255, 210, 30, 0.3); | |
| background: rgba(255, 210, 30, 0.08); | |
| } | |
| .in-progress-block.streaming { | |
| border-color: rgba(65, 105, 225, 0.3); | |
| background: rgba(65, 105, 225, 0.08); | |
| } | |
| .in-progress-block.completing { | |
| border-color: rgba(0, 255, 0, 0.2); | |
| background: rgba(0, 255, 0, 0.05); | |
| } | |
| .progress-bar-track { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 2px; | |
| background: rgba(255, 255, 255, 0.05); | |
| overflow: hidden; | |
| } | |
| .progress-bar { | |
| height: 100%; | |
| background: linear-gradient(90deg, | |
| rgba(255, 210, 30, 0.6) 0%, | |
| rgba(65, 105, 225, 0.6) 50%, | |
| rgba(0, 255, 0, 0.6) 100%); | |
| box-shadow: 0 0 10px rgba(65, 105, 225, 0.4); | |
| transition: width 0.3s ease; | |
| } | |
| .in-progress-block.thinking .progress-bar { | |
| background: rgba(255, 210, 30, 0.6); | |
| box-shadow: 0 0 10px rgba(255, 210, 30, 0.4); | |
| } | |
| .in-progress-block.streaming .progress-bar { | |
| background: rgba(65, 105, 225, 0.6); | |
| box-shadow: 0 0 10px rgba(65, 105, 225, 0.4); | |
| } | |
| .in-progress-block.completing .progress-bar { | |
| background: rgba(0, 255, 0, 0.6); | |
| box-shadow: 0 0 10px rgba(0, 255, 0, 0.4); | |
| } | |
| .progress-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| width: 100%; | |
| padding: 0.5rem 0.75rem; | |
| background: transparent; | |
| border: none; | |
| color: inherit; | |
| font: inherit; | |
| text-align: left; | |
| cursor: pointer; | |
| transition: background 0.2s ease; | |
| } | |
| .progress-header:hover { | |
| background: rgba(255, 255, 255, 0.02); | |
| } | |
| .status-icon { | |
| font-size: 1rem; | |
| width: 1.5rem; | |
| text-align: center; | |
| } | |
| .spinning-icon { | |
| display: inline-block; | |
| animation: spin 1s linear infinite; | |
| } | |
| .animated-icon { | |
| display: inline-block; | |
| animation: bounce 1s ease-in-out infinite; | |
| } | |
| @keyframes spin { | |
| from { | |
| transform: rotate(0deg); | |
| } | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| @keyframes bounce { | |
| 0%, | |
| 100% { | |
| transform: translateY(0); | |
| } | |
| 50% { | |
| transform: translateY(-2px); | |
| } | |
| } | |
| .status-text { | |
| flex: 1; | |
| color: rgba(255, 255, 255, 0.9); | |
| font-size: 0.875rem; | |
| } | |
| .dots::after { | |
| content: ""; | |
| animation: dots 1.5s steps(4, end) infinite; | |
| } | |
| @keyframes dots { | |
| 0%, | |
| 20% { | |
| content: ""; | |
| } | |
| 40% { | |
| content: "."; | |
| } | |
| 60% { | |
| content: ".."; | |
| } | |
| 80%, | |
| 100% { | |
| content: "..."; | |
| } | |
| } | |
| .progress-meta { | |
| display: flex; | |
| gap: 1rem; | |
| align-items: center; | |
| color: rgba(255, 255, 255, 0.4); | |
| font-size: 0.75rem; | |
| font-family: "Monaco", "Menlo", monospace; | |
| } | |
| .duration { | |
| min-width: 3rem; | |
| } | |
| .char-count { | |
| opacity: 0.7; | |
| } | |
| .expand-icon { | |
| font-size: 0.7rem; | |
| color: rgba(255, 255, 255, 0.4); | |
| transition: transform 0.2s ease; | |
| transform-origin: center; | |
| } | |
| .progress-content { | |
| padding: 0.75rem; | |
| background: rgba(0, 0, 0, 0.2); | |
| border-top: 1px solid rgba(255, 255, 255, 0.05); | |
| max-height: 300px; | |
| overflow-y: auto; | |
| } | |
| .content-wrapper { | |
| font-family: "Monaco", "Menlo", monospace; | |
| font-size: 0.8rem; | |
| line-height: 1.5; | |
| color: rgba(255, 255, 255, 0.8); | |
| } | |
| .content-text { | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| display: inline; | |
| } | |
| .cursor { | |
| display: inline-block; | |
| animation: blink 1s infinite; | |
| color: rgba(65, 105, 225, 0.8); | |
| font-weight: bold; | |
| } | |
| @keyframes blink { | |
| 0%, | |
| 50% { | |
| opacity: 1; | |
| } | |
| 51%, | |
| 100% { | |
| opacity: 0; | |
| } | |
| } | |
| .waiting-message { | |
| text-align: center; | |
| color: rgba(255, 255, 255, 0.4); | |
| font-style: italic; | |
| font-size: 0.85rem; | |
| padding: 1rem; | |
| } | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 3px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| } | |
| </style> | |