Spaces:
Running
Running
| <script lang="ts"> | |
| import { slide } from "svelte/transition"; | |
| import { onMount } from "svelte"; | |
| import gsap from "gsap"; | |
| import type { MessageSegment } from "../../../models/chat-data"; | |
| export let invocation: MessageSegment | null = null; | |
| export let result: MessageSegment | null = null; | |
| let isExpanded = false; | |
| let blockRef: HTMLDivElement; | |
| let progressRef: HTMLDivElement; | |
| let iconRef: HTMLSpanElement; | |
| $: segment = result || invocation; | |
| $: isRunning = segment?.streaming || segment?.toolStatus === "running"; | |
| $: isError = segment?.toolError || segment?.toolStatus === "error"; | |
| $: hasOutput = result?.toolOutput || result?.content; | |
| $: isSuccess = !isRunning && !isError && hasOutput; | |
| $: if (isError && !isExpanded) { | |
| isExpanded = true; | |
| } | |
| $: if (isSuccess && iconRef) { | |
| gsap.fromTo(iconRef, | |
| { scale: 1, rotation: 0 }, | |
| { | |
| scale: 1.2, | |
| rotation: 360, | |
| duration: 0.6, | |
| ease: "elastic.out(1, 0.5)", | |
| onComplete: () => { | |
| gsap.to(iconRef, { scale: 1, duration: 0.2 }); | |
| } | |
| } | |
| ); | |
| } | |
| onMount(() => { | |
| if (blockRef) { | |
| gsap.fromTo(blockRef, | |
| { opacity: 0, x: -10 }, | |
| { opacity: 1, x: 0, duration: 0.3, ease: "power2.out" } | |
| ); | |
| } | |
| if (isRunning && progressRef) { | |
| gsap.fromTo(progressRef, | |
| { scaleX: 0 }, | |
| { scaleX: 1, duration: 3, ease: "power1.inOut" } | |
| ); | |
| } | |
| }); | |
| function toggleExpanded() { | |
| isExpanded = !isExpanded; | |
| if (blockRef) { | |
| gsap.to(blockRef, { | |
| backgroundColor: isExpanded ? "rgba(255, 255, 255, 0.02)" : "rgba(255, 255, 255, 0.01)", | |
| duration: 0.2 | |
| }); | |
| } | |
| } | |
| function getToolIcon(): string { | |
| const name = invocation?.toolName || result?.toolName || ""; | |
| const lowerName = name.toLowerCase(); | |
| // Editor tools | |
| if (lowerName === "read_editor" || lowerName === "read_editor_lines") return "π"; | |
| if (lowerName === "write_editor") return "πΎ"; | |
| if (lowerName === "edit_editor") return "βοΈ"; | |
| if (lowerName === "search_editor") return "π"; | |
| // Task management | |
| if (lowerName === "plan_tasks") return "π"; | |
| if (lowerName === "update_task") return "β "; | |
| if (lowerName === "view_tasks") return "ποΈ"; | |
| // Documentation/library tools | |
| if (lowerName === "resolve_library_id") return "π"; | |
| if (lowerName === "get_library_docs") return "π"; | |
| // Console observation | |
| if (lowerName === "observe_console") return "π₯οΈ"; | |
| // Generic fallbacks | |
| if (lowerName.includes("read")) return "π"; | |
| if (lowerName.includes("write") || lowerName.includes("edit")) return "βοΈ"; | |
| if (lowerName.includes("search")) return "π"; | |
| if (lowerName.includes("task")) return "β "; | |
| return "π§"; | |
| } | |
| function getToolColor(): string { | |
| const name = invocation?.toolName || result?.toolName || ""; | |
| const lowerName = name.toLowerCase(); | |
| // Editor tools | |
| if (lowerName === "read_editor" || lowerName === "read_editor_lines") return "rgba(100, 149, 237, 0.08)"; // Cornflower blue | |
| if (lowerName === "write_editor") return "rgba(255, 165, 0, 0.08)"; // Orange | |
| if (lowerName === "edit_editor") return "rgba(255, 215, 0, 0.08)"; // Gold | |
| if (lowerName === "search_editor") return "rgba(147, 112, 219, 0.08)"; // Medium purple | |
| // Task management | |
| if (lowerName.includes("task")) return "rgba(76, 175, 80, 0.08)"; // Green | |
| // Documentation | |
| if (lowerName.includes("library") || lowerName.includes("docs")) return "rgba(70, 130, 180, 0.08)"; // Steel blue | |
| // Console | |
| if (lowerName === "observe_console") return "rgba(96, 125, 139, 0.08)"; // Blue grey | |
| return "rgba(255, 255, 255, 0.03)"; | |
| } | |
| function getToolName(): string { | |
| const name = invocation?.toolName || result?.toolName || "Tool"; | |
| return name.replace(/_/g, " "); | |
| } | |
| function getStatusText(): string { | |
| if (isRunning) return "Running..."; | |
| if (isError) return "Error"; | |
| if (result?.endTime && result?.startTime) { | |
| const duration = result.endTime - result.startTime; | |
| if (duration < 1000) return `${duration}ms`; | |
| return `${(duration / 1000).toFixed(1)}s`; | |
| } | |
| return ""; | |
| } | |
| function formatArgs(args: any): string { | |
| if (!args) return ""; | |
| return JSON.stringify(args, null, 2); | |
| } | |
| </script> | |
| <div class="tool-block" class:error={isError} class:success={isSuccess} bind:this={blockRef} style="background: {getToolColor()}"> | |
| {#if isRunning} | |
| <div class="progress-bar" bind:this={progressRef}></div> | |
| {/if} | |
| <button | |
| class="tool-header" | |
| on:click={toggleExpanded} | |
| class:expanded={isExpanded} | |
| > | |
| <span class="tool-icon" bind:this={iconRef}>{getToolIcon()}</span> | |
| <span class="tool-name">{getToolName()}</span> | |
| {#if isRunning} | |
| <span class="status running"> | |
| <span class="pulse-dot">β</span> | |
| {getStatusText()} | |
| </span> | |
| {:else if isError} | |
| <span class="status error">β {getStatusText()}</span> | |
| {:else if hasOutput} | |
| <span class="status completed">β {getStatusText()}</span> | |
| {/if} | |
| <span class="expand-icon" class:expanded={isExpanded}>βΆ</span> | |
| </button> | |
| {#if isExpanded} | |
| <div class="tool-content" transition:slide={{ duration: 200 }}> | |
| {#if invocation?.toolArgs} | |
| <div class="section args"> | |
| <div class="label">Arguments</div> | |
| <pre>{formatArgs(invocation.toolArgs)}</pre> | |
| </div> | |
| {/if} | |
| {#if isRunning && result?.content} | |
| <div class="section output streaming"> | |
| <div class="label">Output</div> | |
| <pre>{result.content}<span class="cursor">β</span></pre> | |
| </div> | |
| {:else if result?.toolError} | |
| <div class="section error"> | |
| <div class="label">Error</div> | |
| <pre>{result.toolError}</pre> | |
| </div> | |
| {:else if result?.toolOutput || result?.content} | |
| <div class="section output"> | |
| <div class="label">Output</div> | |
| <pre>{result.toolOutput || result.content}</pre> | |
| </div> | |
| {/if} | |
| </div> | |
| {/if} | |
| </div> | |
| <style> | |
| .tool-block { | |
| margin: 0.125rem 0; | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| border-radius: 4px; | |
| background: rgba(255, 255, 255, 0.01); | |
| overflow: hidden; | |
| position: relative; | |
| transition: all 0.3s ease; | |
| } | |
| .tool-block.error { | |
| border-color: rgba(244, 67, 54, 0.2); | |
| animation: errorShake 0.3s ease-in-out; | |
| } | |
| .tool-block.success { | |
| border-color: rgba(76, 175, 80, 0.15); | |
| } | |
| @keyframes errorShake { | |
| 0%, 100% { transform: translateX(0); } | |
| 25% { transform: translateX(-2px); } | |
| 75% { transform: translateX(2px); } | |
| } | |
| .progress-bar { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| height: 2px; | |
| width: 100%; | |
| background: linear-gradient(90deg, | |
| rgba(33, 150, 243, 0.8), | |
| rgba(100, 181, 246, 1), | |
| rgba(33, 150, 243, 0.8)); | |
| transform-origin: left; | |
| animation: shimmer 2s ease-in-out infinite; | |
| } | |
| @keyframes shimmer { | |
| 0%, 100% { opacity: 0.6; } | |
| 50% { opacity: 1; } | |
| } | |
| .tool-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| padding: 0.375rem 0.5rem; | |
| width: 100%; | |
| background: transparent; | |
| border: none; | |
| color: inherit; | |
| font: inherit; | |
| text-align: left; | |
| cursor: pointer; | |
| transition: background 0.15s ease; | |
| } | |
| .tool-header:hover { | |
| background: rgba(255, 255, 255, 0.02); | |
| } | |
| .tool-icon { | |
| font-size: 1rem; | |
| margin-right: 0.25rem; | |
| display: inline-block; | |
| } | |
| .tool-name { | |
| flex: 1; | |
| font-size: 0.8rem; | |
| color: rgba(255, 255, 255, 0.7); | |
| text-transform: capitalize; | |
| } | |
| .status { | |
| font-size: 0.75rem; | |
| padding: 0.125rem 0.375rem; | |
| border-radius: 3px; | |
| background: rgba(255, 255, 255, 0.05); | |
| color: rgba(255, 255, 255, 0.6); | |
| } | |
| .status.running { | |
| background: rgba(33, 150, 243, 0.1); | |
| color: rgba(33, 150, 243, 0.9); | |
| } | |
| .status.error { | |
| background: rgba(244, 67, 54, 0.1); | |
| color: rgba(244, 67, 54, 0.9); | |
| } | |
| .status.completed { | |
| background: rgba(76, 175, 80, 0.1); | |
| color: rgba(76, 175, 80, 0.9); | |
| } | |
| .pulse-dot { | |
| display: inline-block; | |
| animation: pulse 1.5s ease-in-out infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { | |
| opacity: 0.3; | |
| } | |
| 50% { | |
| opacity: 1; | |
| } | |
| } | |
| .expand-icon { | |
| font-size: 0.6rem; | |
| color: rgba(255, 255, 255, 0.3); | |
| transition: transform 0.15s ease; | |
| } | |
| .expand-icon.expanded { | |
| transform: rotate(90deg); | |
| } | |
| .tool-content { | |
| border-top: 1px solid rgba(255, 255, 255, 0.05); | |
| background: rgba(0, 0, 0, 0.1); | |
| overflow: hidden; | |
| } | |
| .section { | |
| padding: 0.5rem; | |
| } | |
| .section + .section { | |
| border-top: 1px solid rgba(255, 255, 255, 0.03); | |
| } | |
| .label { | |
| font-size: 0.7rem; | |
| color: rgba(255, 255, 255, 0.4); | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| margin-bottom: 0.25rem; | |
| } | |
| pre { | |
| margin: 0; | |
| font-family: "Monaco", "Menlo", "Consolas", monospace; | |
| font-size: 0.75rem; | |
| line-height: 1.4; | |
| color: rgba(255, 255, 255, 0.8); | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| } | |
| .section.args pre { | |
| color: rgba(255, 255, 255, 0.6); | |
| } | |
| .section.error pre { | |
| color: rgba(244, 67, 54, 0.9); | |
| } | |
| .cursor { | |
| display: inline-block; | |
| color: rgba(65, 105, 225, 0.6); | |
| animation: blink 1s infinite; | |
| } | |
| @keyframes blink { | |
| 0%, 50% { | |
| opacity: 1; | |
| } | |
| 51%, 100% { | |
| opacity: 0; | |
| } | |
| } | |
| </style> |