dylanebert's picture
improved prompting/UX
db9635c
<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>