VibeGame / src /lib /components /chat /ToolCallBlock.svelte
dylanebert's picture
improved AI chat
ebb12a0
raw
history blame
11.1 kB
<script lang="ts">
import { onMount } from "svelte";
import gsap from "gsap";
import type { ToolExecution } from "../../stores/agent";
export let toolExecutions: ToolExecution[] = [];
let blockElement: HTMLDivElement;
let expandedTools: Set<string> = new Set();
const toolIcons: Record<string, string> = {
read_editor: "πŸ“„",
write_editor: "✏️",
observe_console: "πŸ“Ÿ",
default: "πŸ”§",
};
const statusIcons: Record<string, string> = {
pending: "⏳",
running: "⚑",
completed: "βœ…",
error: "❌",
};
const statusMessages: Record<string, (name: string) => string> = {
read_editor: (name) => ({
pending: "Preparing to read code...",
running: "Reading editor content...",
completed: "Code read successfully",
error: "Failed to read code"
}[name] || name),
write_editor: (name) => ({
pending: "Preparing to write code...",
running: "Writing code and reloading game...",
completed: "Code updated successfully",
error: "Failed to write code"
}[name] || name),
observe_console: (name) => ({
pending: "Preparing to read console...",
running: "Reading console output...",
completed: "Console read successfully",
error: "Failed to read console"
}[name] || name),
};
function toggleTool(toolId: string) {
if (expandedTools.has(toolId)) {
expandedTools.delete(toolId);
} else {
expandedTools.add(toolId);
}
expandedTools = expandedTools;
}
function getStatusMessage(tool: ToolExecution): string {
const messageFunc = statusMessages[tool.name] || statusMessages.default;
return messageFunc ? messageFunc(tool.status) : `${tool.name}: ${tool.status}`;
}
function formatDuration(startTime: number, endTime?: number): string {
const duration = (endTime || Date.now()) - startTime;
if (duration < 1000) {
return `${duration}ms`;
}
return `${(duration / 1000).toFixed(1)}s`;
}
onMount(() => {
gsap.fromTo(
blockElement,
{ opacity: 0, y: -10 },
{ opacity: 1, y: 0, duration: 0.3, ease: "power2.out" }
);
});
</script>
<div class="tool-block" bind:this={blockElement}>
{#each toolExecutions as tool (tool.id)}
<div class="tool-item {tool.status}" class:expanded={expandedTools.has(tool.id)}>
<button
class="tool-item-header"
on:click={() => toggleTool(tool.id)}
aria-expanded={expandedTools.has(tool.id)}
>
<span class="tool-status-icon">
{#if tool.status === "running"}
<span class="spinner-small">{statusIcons[tool.status]}</span>
{:else}
{statusIcons[tool.status]}
{/if}
</span>
<span class="tool-icon">{toolIcons[tool.name] || toolIcons.default}</span>
<span class="tool-name">{getStatusMessage(tool)}</span>
<span class="tool-duration">
{formatDuration(tool.startTime, tool.endTime)}
</span>
<span class="expand-icon" class:rotated={expandedTools.has(tool.id)}>
β–Ά
</span>
</button>
{#if expandedTools.has(tool.id)}
<div class="tool-details">
{#if tool.args && Object.keys(tool.args).length > 0}
<div class="tool-section">
<div class="section-title">Parameters:</div>
<div class="params">
{#each Object.entries(tool.args) as [key, value]}
<div class="param">
<span class="param-key">{key}:</span>
<span class="param-value">
{#if typeof value === 'string' && value.length > 100}
<pre>{value}</pre>
{:else}
{JSON.stringify(value)}
{/if}
</span>
</div>
{/each}
</div>
</div>
{/if}
{#if tool.output}
<div class="tool-section">
<div class="section-title">Output:</div>
<pre class="tool-output">{tool.output}</pre>
</div>
{/if}
{#if tool.consoleOutput && tool.consoleOutput.length > 0}
<div class="tool-section">
<div class="section-title">Console Output:</div>
<div class="console-output">
{#each tool.consoleOutput as line}
<div class="console-line">{line}</div>
{/each}
</div>
</div>
{/if}
{#if tool.error}
<div class="tool-section error">
<div class="section-title">Error:</div>
<div class="error-message">{tool.error}</div>
</div>
{/if}
{#if tool.status === "running" && !tool.output}
<div class="tool-section">
<div class="loading-indicator">
<span class="loading-dots">Processing</span>
</div>
</div>
{/if}
</div>
{/if}
</div>
{/each}
</div>
<style>
.tool-block {
margin: 0.25rem 0;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.tool-item {
border-radius: 4px;
overflow: hidden;
transition: all 0.2s ease;
border: 1px solid rgba(65, 105, 225, 0.2);
background: rgba(65, 105, 225, 0.05);
}
.tool-item.running {
background: rgba(255, 210, 30, 0.08);
border: 1px solid rgba(255, 210, 30, 0.3);
}
.tool-item.completed {
background: rgba(0, 255, 0, 0.05);
border: 1px solid rgba(0, 255, 0, 0.2);
}
.tool-item.error {
background: rgba(255, 0, 0, 0.08);
border: 1px solid rgba(255, 0, 0, 0.3);
}
.tool-item-header {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.4rem 0.6rem;
background: transparent;
border: none;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
transition: background 0.2s ease;
}
.tool-item-header:hover {
background: rgba(255, 255, 255, 0.02);
}
.tool-status-icon {
font-size: 0.9rem;
width: 1.2rem;
text-align: center;
}
.tool-icon {
font-size: 1rem;
}
.tool-name {
flex: 1;
color: rgba(255, 255, 255, 0.9);
font-size: 0.825rem;
}
.tool-duration {
color: rgba(255, 255, 255, 0.4);
font-size: 0.75rem;
font-family: "Monaco", "Menlo", monospace;
}
.expand-icon {
font-size: 0.7rem;
color: rgba(255, 255, 255, 0.4);
transition: transform 0.2s ease;
}
.expand-icon.rotated {
transform: rotate(90deg);
}
.tool-details {
padding: 0.75rem;
background: rgba(0, 0, 0, 0.2);
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.tool-section {
margin-bottom: 0.75rem;
}
.tool-section:last-child {
margin-bottom: 0;
}
.section-title {
color: rgba(255, 255, 255, 0.5);
font-size: 0.75rem;
font-weight: 600;
margin-bottom: 0.25rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.params {
font-family: "Monaco", "Menlo", monospace;
font-size: 0.8rem;
}
.param {
display: flex;
gap: 0.5rem;
margin: 0.25rem 0;
}
.param-key {
color: rgba(255, 255, 255, 0.5);
}
.param-value {
color: rgba(255, 210, 30, 0.8);
word-break: break-all;
}
.param-value pre {
margin: 0;
padding: 0.5rem;
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
font-size: 0.75rem;
overflow-x: auto;
max-height: 200px;
}
.tool-output, .console-output {
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
padding: 0.5rem;
font-family: "Monaco", "Menlo", monospace;
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.8);
overflow-x: auto;
max-height: 300px;
overflow-y: auto;
}
.console-line {
margin: 0.1rem 0;
}
.error-message {
color: #ff6b6b;
font-family: "Monaco", "Menlo", monospace;
font-size: 0.8rem;
padding: 0.5rem;
background: rgba(255, 0, 0, 0.1);
border-radius: 4px;
}
.loading-indicator {
text-align: center;
padding: 1rem;
color: rgba(255, 255, 255, 0.5);
font-size: 0.85rem;
}
.loading-dots::after {
content: "";
animation: dots 1.5s steps(4, end) infinite;
}
@keyframes dots {
0%, 20% { content: ""; }
40% { content: "."; }
60% { content: ".."; }
80%, 100% { content: "..."; }
}
.spinner {
display: inline-block;
animation: spin 1s linear infinite;
}
.spinner-small {
display: inline-block;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
::-webkit-scrollbar {
width: 6px;
height: 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>