Spaces:
Running
Running
Commit
·
3342a1d
1
Parent(s):
ebb12a0
improve chat
Browse files- src/lib/components/chat/ChatPanel.svelte +131 -43
- src/lib/components/chat/InProgressBlock.svelte +141 -30
- src/lib/components/chat/MarkdownRenderer.svelte +26 -12
- src/lib/components/chat/MessageSegment.svelte +70 -0
- src/lib/components/chat/ReasoningBlock.svelte +73 -26
- src/lib/components/chat/StreamingText.svelte +144 -0
- src/lib/components/chat/ToolInvocation.svelte +458 -0
- src/lib/components/chat/context.md +15 -13
- src/lib/server/api.ts +21 -5
- src/lib/server/context.md +15 -21
- src/lib/server/langgraph-agent.ts +187 -81
- src/lib/stores/agent.ts +225 -38
- src/lib/stores/editor.ts +2 -0
src/lib/components/chat/ChatPanel.svelte
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { onMount, onDestroy, afterUpdate } from "svelte";
|
|
|
|
| 3 |
import { agentStore, isConnected, isProcessing } from "../../stores/agent";
|
| 4 |
import { authStore } from "../../services/auth";
|
| 5 |
import gsap from "gsap";
|
|
@@ -8,6 +9,7 @@
|
|
| 8 |
import ToolCallDisplay from "./ToolCallDisplay.svelte";
|
| 9 |
import ToolCallBlock from "./ToolCallBlock.svelte";
|
| 10 |
import InProgressBlock from "./InProgressBlock.svelte";
|
|
|
|
| 11 |
|
| 12 |
let inputValue = "";
|
| 13 |
let messagesContainer: HTMLDivElement;
|
|
@@ -92,12 +94,59 @@
|
|
| 92 |
});
|
| 93 |
}
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
afterUpdate(() => {
|
| 96 |
-
if (messagesContainer) {
|
| 97 |
-
|
| 98 |
}
|
| 99 |
});
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
function handleSubmit() {
|
| 102 |
if (inputValue.trim() && $authStore.isAuthenticated && $isConnected && !$isProcessing) {
|
| 103 |
if (sendButton) {
|
|
@@ -187,7 +236,20 @@
|
|
| 187 |
</script>
|
| 188 |
|
| 189 |
<div class="chat-panel">
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
{#if !$authStore.isAuthenticated && !$authStore.loading}
|
| 192 |
<div class="auth-prompt">
|
| 193 |
<p>Sign in to chat.</p>
|
|
@@ -200,16 +262,7 @@
|
|
| 200 |
<div class="ready-message">Ready to Chat!</div>
|
| 201 |
{/if}
|
| 202 |
|
| 203 |
-
{#
|
| 204 |
-
<InProgressBlock
|
| 205 |
-
status={$agentStore.streamingStatus === "completing" ? "completing" : $agentStore.streamingStatus}
|
| 206 |
-
content={$agentStore.streamingContent}
|
| 207 |
-
startTime={$agentStore.thinkingStartTime || Date.now()}
|
| 208 |
-
isExpanded={false}
|
| 209 |
-
/>
|
| 210 |
-
{/if}
|
| 211 |
-
|
| 212 |
-
{#each $agentStore.messages as message}
|
| 213 |
{#if message.role === "tool" && message.toolExecutions}
|
| 214 |
<ToolCallBlock toolExecutions={message.toolExecutions} />
|
| 215 |
{:else}
|
|
@@ -217,26 +270,41 @@
|
|
| 217 |
{#if message.reasoning && message.role === "assistant"}
|
| 218 |
<ReasoningBlock reasoning={message.reasoning} />
|
| 219 |
{/if}
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
{
|
| 223 |
-
|
| 224 |
-
{#if part.type === 'text' && part.content.trim()}
|
| 225 |
-
<MarkdownRenderer content={part.content} streaming={false} />
|
| 226 |
-
{:else if part.type === 'tool' && part.toolName}
|
| 227 |
-
<ToolCallDisplay toolName={part.toolName} parameters={part.params} />
|
| 228 |
-
{/if}
|
| 229 |
{/each}
|
| 230 |
-
|
| 231 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
{/if}
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
{/if}
|
| 236 |
-
</div>
|
| 237 |
</div>
|
| 238 |
{/if}
|
| 239 |
{/each}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
{#if $agentStore.error}
|
| 241 |
<div class="error-message">
|
| 242 |
Error: {$agentStore.error}
|
|
@@ -284,6 +352,36 @@
|
|
| 284 |
width: 100%;
|
| 285 |
background: rgba(20, 20, 20, 0.5);
|
| 286 |
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
}
|
| 288 |
|
| 289 |
.messages {
|
|
@@ -332,6 +430,11 @@
|
|
| 332 |
display: block;
|
| 333 |
}
|
| 334 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
.message.user .message-content {
|
| 336 |
color: rgba(255, 255, 255, 0.9);
|
| 337 |
line-height: 1.5;
|
|
@@ -339,21 +442,6 @@
|
|
| 339 |
word-wrap: break-word;
|
| 340 |
}
|
| 341 |
|
| 342 |
-
.cursor {
|
| 343 |
-
animation: blink 1s infinite;
|
| 344 |
-
}
|
| 345 |
-
|
| 346 |
-
@keyframes blink {
|
| 347 |
-
0%,
|
| 348 |
-
50% {
|
| 349 |
-
opacity: 1;
|
| 350 |
-
}
|
| 351 |
-
51%,
|
| 352 |
-
100% {
|
| 353 |
-
opacity: 0;
|
| 354 |
-
}
|
| 355 |
-
}
|
| 356 |
-
|
| 357 |
.error-message {
|
| 358 |
background: rgba(244, 67, 54, 0.1);
|
| 359 |
border-left: 2px solid #f44336;
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { onMount, onDestroy, afterUpdate } from "svelte";
|
| 3 |
+
import { fade } from "svelte/transition";
|
| 4 |
import { agentStore, isConnected, isProcessing } from "../../stores/agent";
|
| 5 |
import { authStore } from "../../services/auth";
|
| 6 |
import gsap from "gsap";
|
|
|
|
| 9 |
import ToolCallDisplay from "./ToolCallDisplay.svelte";
|
| 10 |
import ToolCallBlock from "./ToolCallBlock.svelte";
|
| 11 |
import InProgressBlock from "./InProgressBlock.svelte";
|
| 12 |
+
import MessageSegment from "./MessageSegment.svelte";
|
| 13 |
|
| 14 |
let inputValue = "";
|
| 15 |
let messagesContainer: HTMLDivElement;
|
|
|
|
| 94 |
});
|
| 95 |
}
|
| 96 |
|
| 97 |
+
let autoScroll = true;
|
| 98 |
+
let isUserScrolling = false;
|
| 99 |
+
let scrollAnimation: gsap.core.Tween | null = null;
|
| 100 |
+
|
| 101 |
afterUpdate(() => {
|
| 102 |
+
if (messagesContainer && autoScroll) {
|
| 103 |
+
setTimeout(() => smoothScrollToBottom(), 100);
|
| 104 |
}
|
| 105 |
});
|
| 106 |
|
| 107 |
+
function smoothScrollToBottom() {
|
| 108 |
+
if (!messagesContainer || isUserScrolling) return;
|
| 109 |
+
|
| 110 |
+
if (scrollAnimation) {
|
| 111 |
+
scrollAnimation.kill();
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
const targetScroll = messagesContainer.scrollHeight - messagesContainer.clientHeight;
|
| 115 |
+
const currentScroll = messagesContainer.scrollTop;
|
| 116 |
+
const distance = Math.abs(targetScroll - currentScroll);
|
| 117 |
+
const duration = Math.min(0.5, distance / 1000);
|
| 118 |
+
|
| 119 |
+
scrollAnimation = gsap.to(messagesContainer, {
|
| 120 |
+
scrollTop: targetScroll,
|
| 121 |
+
duration: duration,
|
| 122 |
+
ease: "power2.out",
|
| 123 |
+
onComplete: () => {
|
| 124 |
+
scrollAnimation = null;
|
| 125 |
+
}
|
| 126 |
+
});
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
function handleScroll() {
|
| 130 |
+
if (!messagesContainer) return;
|
| 131 |
+
|
| 132 |
+
const isNearBottom =
|
| 133 |
+
messagesContainer.scrollHeight - messagesContainer.scrollTop - messagesContainer.clientHeight < 100;
|
| 134 |
+
|
| 135 |
+
autoScroll = isNearBottom;
|
| 136 |
+
|
| 137 |
+
if (!autoScroll && !isUserScrolling) {
|
| 138 |
+
isUserScrolling = true;
|
| 139 |
+
} else if (autoScroll) {
|
| 140 |
+
isUserScrolling = false;
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
function scrollToBottom() {
|
| 145 |
+
autoScroll = true;
|
| 146 |
+
isUserScrolling = false;
|
| 147 |
+
smoothScrollToBottom();
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
function handleSubmit() {
|
| 151 |
if (inputValue.trim() && $authStore.isAuthenticated && $isConnected && !$isProcessing) {
|
| 152 |
if (sendButton) {
|
|
|
|
| 236 |
</script>
|
| 237 |
|
| 238 |
<div class="chat-panel">
|
| 239 |
+
{#if isUserScrolling}
|
| 240 |
+
<button
|
| 241 |
+
class="scroll-to-bottom"
|
| 242 |
+
on:click={scrollToBottom}
|
| 243 |
+
title="Scroll to bottom"
|
| 244 |
+
transition:fade={{ duration: 200 }}
|
| 245 |
+
>
|
| 246 |
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
| 247 |
+
<path d="M10 14L5 9L6.41 7.59L10 11.17L13.59 7.59L15 9L10 14Z" />
|
| 248 |
+
</svg>
|
| 249 |
+
</button>
|
| 250 |
+
{/if}
|
| 251 |
+
|
| 252 |
+
<div class="messages" bind:this={messagesContainer} on:scroll={handleScroll}>
|
| 253 |
{#if !$authStore.isAuthenticated && !$authStore.loading}
|
| 254 |
<div class="auth-prompt">
|
| 255 |
<p>Sign in to chat.</p>
|
|
|
|
| 262 |
<div class="ready-message">Ready to Chat!</div>
|
| 263 |
{/if}
|
| 264 |
|
| 265 |
+
{#each $agentStore.messages as message (message.id)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
{#if message.role === "tool" && message.toolExecutions}
|
| 267 |
<ToolCallBlock toolExecutions={message.toolExecutions} />
|
| 268 |
{:else}
|
|
|
|
| 270 |
{#if message.reasoning && message.role === "assistant"}
|
| 271 |
<ReasoningBlock reasoning={message.reasoning} />
|
| 272 |
{/if}
|
| 273 |
+
{#if message.segments && message.segments.length > 0}
|
| 274 |
+
<div class="message-segments">
|
| 275 |
+
{#each message.segments as segment (segment.id)}
|
| 276 |
+
<MessageSegment {segment} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
{/each}
|
| 278 |
+
</div>
|
| 279 |
+
{:else}
|
| 280 |
+
<div class="message-content">
|
| 281 |
+
{#if message.role === "assistant"}
|
| 282 |
+
{@const parts = parseMessageContent(message.content.trim())}
|
| 283 |
+
{#each parts as part}
|
| 284 |
+
{#if part.type === 'text' && part.content.trim()}
|
| 285 |
+
<MarkdownRenderer content={part.content} streaming={false} />
|
| 286 |
+
{:else if part.type === 'tool' && part.toolName}
|
| 287 |
+
<ToolCallDisplay toolName={part.toolName} parameters={part.params} />
|
| 288 |
+
{/if}
|
| 289 |
+
{/each}
|
| 290 |
+
{:else}
|
| 291 |
+
{message.content.trim()}
|
| 292 |
{/if}
|
| 293 |
+
</div>
|
| 294 |
+
{/if}
|
|
|
|
|
|
|
| 295 |
</div>
|
| 296 |
{/if}
|
| 297 |
{/each}
|
| 298 |
+
|
| 299 |
+
{#if $agentStore.streamingStatus !== "idle" && (!$agentStore.streamingContent || $agentStore.streamingStatus === "thinking")}
|
| 300 |
+
<InProgressBlock
|
| 301 |
+
status={$agentStore.streamingStatus === "completing" ? "completing" : $agentStore.streamingStatus}
|
| 302 |
+
content={$agentStore.streamingContent}
|
| 303 |
+
startTime={$agentStore.thinkingStartTime || Date.now()}
|
| 304 |
+
isExpanded={false}
|
| 305 |
+
/>
|
| 306 |
+
{/if}
|
| 307 |
+
|
| 308 |
{#if $agentStore.error}
|
| 309 |
<div class="error-message">
|
| 310 |
Error: {$agentStore.error}
|
|
|
|
| 352 |
width: 100%;
|
| 353 |
background: rgba(20, 20, 20, 0.5);
|
| 354 |
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
| 355 |
+
position: relative;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
.scroll-to-bottom {
|
| 359 |
+
position: absolute;
|
| 360 |
+
bottom: 4.5rem;
|
| 361 |
+
right: 1rem;
|
| 362 |
+
width: 36px;
|
| 363 |
+
height: 36px;
|
| 364 |
+
border-radius: 50%;
|
| 365 |
+
background: rgba(65, 105, 225, 0.2);
|
| 366 |
+
border: 1px solid rgba(65, 105, 225, 0.3);
|
| 367 |
+
color: rgba(65, 105, 225, 0.9);
|
| 368 |
+
display: flex;
|
| 369 |
+
align-items: center;
|
| 370 |
+
justify-content: center;
|
| 371 |
+
cursor: pointer;
|
| 372 |
+
z-index: 10;
|
| 373 |
+
transition: all 0.2s ease;
|
| 374 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
.scroll-to-bottom:hover {
|
| 378 |
+
background: rgba(65, 105, 225, 0.3);
|
| 379 |
+
border-color: rgba(65, 105, 225, 0.5);
|
| 380 |
+
transform: scale(1.1);
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
.scroll-to-bottom:active {
|
| 384 |
+
transform: scale(0.95);
|
| 385 |
}
|
| 386 |
|
| 387 |
.messages {
|
|
|
|
| 430 |
display: block;
|
| 431 |
}
|
| 432 |
|
| 433 |
+
.message-segments {
|
| 434 |
+
display: flex;
|
| 435 |
+
flex-direction: column;
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
.message.user .message-content {
|
| 439 |
color: rgba(255, 255, 255, 0.9);
|
| 440 |
line-height: 1.5;
|
|
|
|
| 442 |
word-wrap: break-word;
|
| 443 |
}
|
| 444 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
.error-message {
|
| 446 |
background: rgba(244, 67, 54, 0.1);
|
| 447 |
border-left: 2px solid #f44336;
|
src/lib/components/chat/InProgressBlock.svelte
CHANGED
|
@@ -11,11 +11,20 @@
|
|
| 11 |
let blockElement: HTMLDivElement;
|
| 12 |
let contentElement: HTMLDivElement;
|
| 13 |
let expandIcon: HTMLSpanElement;
|
|
|
|
|
|
|
| 14 |
let collapseTimeout: number | null = null;
|
|
|
|
|
|
|
| 15 |
|
| 16 |
$: duration = Date.now() - startTime;
|
| 17 |
$: formattedDuration = formatDuration(duration);
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
function formatDuration(ms: number): string {
|
| 20 |
if (ms < 1000) return `${ms}ms`;
|
| 21 |
return `${(ms / 1000).toFixed(1)}s`;
|
|
@@ -29,38 +38,92 @@
|
|
| 29 |
function updateExpandState() {
|
| 30 |
if (!expandIcon || !contentElement) return;
|
| 31 |
|
| 32 |
-
if (
|
| 33 |
-
|
| 34 |
-
rotation: 90,
|
| 35 |
-
duration: 0.2,
|
| 36 |
-
ease: "power2.out",
|
| 37 |
-
});
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
} else {
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
}
|
| 62 |
}
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
function getStatusIcon() {
|
| 65 |
switch (status) {
|
| 66 |
case "thinking":
|
|
@@ -99,8 +162,15 @@
|
|
| 99 |
if (blockElement) {
|
| 100 |
gsap.fromTo(
|
| 101 |
blockElement,
|
| 102 |
-
{ opacity: 0, y: -10 },
|
| 103 |
-
{ opacity: 1, y: 0, duration: 0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
);
|
| 105 |
}
|
| 106 |
|
|
@@ -113,10 +183,15 @@
|
|
| 113 |
|
| 114 |
onDestroy(() => {
|
| 115 |
if (collapseTimeout) clearTimeout(collapseTimeout);
|
|
|
|
| 116 |
});
|
| 117 |
</script>
|
| 118 |
|
| 119 |
<div class="in-progress-block {status}" bind:this={blockElement}>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
<button class="progress-header" on:click={toggleExpanded} aria-expanded={isExpanded}>
|
| 121 |
<span class="status-icon">
|
| 122 |
{#if status === "streaming"}
|
|
@@ -128,7 +203,7 @@
|
|
| 128 |
{/if}
|
| 129 |
</span>
|
| 130 |
|
| 131 |
-
<span class="status-text">
|
| 132 |
{getStatusText()}<span class="dots"></span>
|
| 133 |
</span>
|
| 134 |
|
|
@@ -162,6 +237,7 @@
|
|
| 162 |
border: 1px solid rgba(255, 210, 30, 0.2);
|
| 163 |
background: rgba(255, 210, 30, 0.05);
|
| 164 |
transition: all 0.2s ease;
|
|
|
|
| 165 |
}
|
| 166 |
|
| 167 |
.in-progress-block.thinking {
|
|
@@ -179,6 +255,41 @@
|
|
| 179 |
background: rgba(0, 255, 0, 0.05);
|
| 180 |
}
|
| 181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
.progress-header {
|
| 183 |
display: flex;
|
| 184 |
align-items: center;
|
|
|
|
| 11 |
let blockElement: HTMLDivElement;
|
| 12 |
let contentElement: HTMLDivElement;
|
| 13 |
let expandIcon: HTMLSpanElement;
|
| 14 |
+
let statusElement: HTMLSpanElement;
|
| 15 |
+
let progressBar: HTMLDivElement;
|
| 16 |
let collapseTimeout: number | null = null;
|
| 17 |
+
let prevStatus: string = status;
|
| 18 |
+
let timeline: gsap.core.Timeline | null = null;
|
| 19 |
|
| 20 |
$: duration = Date.now() - startTime;
|
| 21 |
$: formattedDuration = formatDuration(duration);
|
| 22 |
|
| 23 |
+
$: if (status !== prevStatus && statusElement) {
|
| 24 |
+
animateStatusTransition(prevStatus, status);
|
| 25 |
+
prevStatus = status;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
function formatDuration(ms: number): string {
|
| 29 |
if (ms < 1000) return `${ms}ms`;
|
| 30 |
return `${(ms / 1000).toFixed(1)}s`;
|
|
|
|
| 38 |
function updateExpandState() {
|
| 39 |
if (!expandIcon || !contentElement) return;
|
| 40 |
|
| 41 |
+
if (timeline) timeline.kill();
|
| 42 |
+
timeline = gsap.timeline();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
+
if (isExpanded) {
|
| 45 |
+
timeline
|
| 46 |
+
.to(expandIcon, {
|
| 47 |
+
rotation: 90,
|
| 48 |
+
duration: 0.2,
|
| 49 |
+
ease: "power2.out",
|
| 50 |
+
})
|
| 51 |
+
.set(contentElement, { display: "block" })
|
| 52 |
+
.fromTo(
|
| 53 |
+
contentElement,
|
| 54 |
+
{ opacity: 0, maxHeight: 0, y: -10 },
|
| 55 |
+
{ opacity: 1, maxHeight: 500, y: 0, duration: 0.3, ease: "power2.out" },
|
| 56 |
+
"-=0.1"
|
| 57 |
+
);
|
| 58 |
} else {
|
| 59 |
+
timeline
|
| 60 |
+
.to(expandIcon, {
|
| 61 |
+
rotation: 0,
|
| 62 |
+
duration: 0.2,
|
| 63 |
+
ease: "power2.in",
|
| 64 |
+
})
|
| 65 |
+
.to(contentElement, {
|
| 66 |
+
opacity: 0,
|
| 67 |
+
maxHeight: 0,
|
| 68 |
+
y: -5,
|
| 69 |
+
duration: 0.2,
|
| 70 |
+
ease: "power2.in",
|
| 71 |
+
onComplete: () => {
|
| 72 |
+
gsap.set(contentElement, { display: "none" });
|
| 73 |
+
},
|
| 74 |
+
}, "-=0.1");
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
function animateStatusTransition(_from: string, to: string) {
|
| 79 |
+
if (!statusElement || !blockElement) return;
|
| 80 |
+
|
| 81 |
+
const tl = gsap.timeline();
|
| 82 |
+
|
| 83 |
+
tl.to(blockElement, {
|
| 84 |
+
duration: 0.4,
|
| 85 |
+
ease: "power2.inOut",
|
| 86 |
+
onUpdate: function() {
|
| 87 |
+
updateBlockStyle(to);
|
| 88 |
+
}
|
| 89 |
+
});
|
| 90 |
+
|
| 91 |
+
tl.to(statusElement, {
|
| 92 |
+
scale: 0.9,
|
| 93 |
+
opacity: 0,
|
| 94 |
+
duration: 0.15,
|
| 95 |
+
ease: "power2.in",
|
| 96 |
+
onComplete: () => {}
|
| 97 |
+
})
|
| 98 |
+
.to(statusElement, {
|
| 99 |
+
scale: 1,
|
| 100 |
+
opacity: 1,
|
| 101 |
+
duration: 0.15,
|
| 102 |
+
ease: "power2.out"
|
| 103 |
+
});
|
| 104 |
+
|
| 105 |
+
if (progressBar) {
|
| 106 |
+
if (to === "streaming") {
|
| 107 |
+
gsap.to(progressBar, {
|
| 108 |
+
width: "60%",
|
| 109 |
+
duration: 1,
|
| 110 |
+
ease: "power2.out"
|
| 111 |
+
});
|
| 112 |
+
} else if (to === "completing") {
|
| 113 |
+
gsap.to(progressBar, {
|
| 114 |
+
width: "90%",
|
| 115 |
+
duration: 0.5,
|
| 116 |
+
ease: "power2.out"
|
| 117 |
+
});
|
| 118 |
+
}
|
| 119 |
}
|
| 120 |
}
|
| 121 |
|
| 122 |
+
function updateBlockStyle(status: string) {
|
| 123 |
+
if (!blockElement) return;
|
| 124 |
+
blockElement.className = `in-progress-block ${status}`;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
function getStatusIcon() {
|
| 128 |
switch (status) {
|
| 129 |
case "thinking":
|
|
|
|
| 162 |
if (blockElement) {
|
| 163 |
gsap.fromTo(
|
| 164 |
blockElement,
|
| 165 |
+
{ opacity: 0, y: -10, scale: 0.95 },
|
| 166 |
+
{ opacity: 1, y: 0, scale: 1, duration: 0.4, ease: "back.out(1.2)" },
|
| 167 |
+
);
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
if (progressBar) {
|
| 171 |
+
gsap.fromTo(progressBar,
|
| 172 |
+
{ width: "0%" },
|
| 173 |
+
{ width: "30%", duration: 0.5, ease: "power2.out" }
|
| 174 |
);
|
| 175 |
}
|
| 176 |
|
|
|
|
| 183 |
|
| 184 |
onDestroy(() => {
|
| 185 |
if (collapseTimeout) clearTimeout(collapseTimeout);
|
| 186 |
+
if (timeline) timeline.kill();
|
| 187 |
});
|
| 188 |
</script>
|
| 189 |
|
| 190 |
<div class="in-progress-block {status}" bind:this={blockElement}>
|
| 191 |
+
<div class="progress-bar-track">
|
| 192 |
+
<div bind:this={progressBar} class="progress-bar"></div>
|
| 193 |
+
</div>
|
| 194 |
+
|
| 195 |
<button class="progress-header" on:click={toggleExpanded} aria-expanded={isExpanded}>
|
| 196 |
<span class="status-icon">
|
| 197 |
{#if status === "streaming"}
|
|
|
|
| 203 |
{/if}
|
| 204 |
</span>
|
| 205 |
|
| 206 |
+
<span bind:this={statusElement} class="status-text">
|
| 207 |
{getStatusText()}<span class="dots"></span>
|
| 208 |
</span>
|
| 209 |
|
|
|
|
| 237 |
border: 1px solid rgba(255, 210, 30, 0.2);
|
| 238 |
background: rgba(255, 210, 30, 0.05);
|
| 239 |
transition: all 0.2s ease;
|
| 240 |
+
position: relative;
|
| 241 |
}
|
| 242 |
|
| 243 |
.in-progress-block.thinking {
|
|
|
|
| 255 |
background: rgba(0, 255, 0, 0.05);
|
| 256 |
}
|
| 257 |
|
| 258 |
+
.progress-bar-track {
|
| 259 |
+
position: absolute;
|
| 260 |
+
top: 0;
|
| 261 |
+
left: 0;
|
| 262 |
+
right: 0;
|
| 263 |
+
height: 2px;
|
| 264 |
+
background: rgba(255, 255, 255, 0.05);
|
| 265 |
+
overflow: hidden;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.progress-bar {
|
| 269 |
+
height: 100%;
|
| 270 |
+
background: linear-gradient(90deg,
|
| 271 |
+
rgba(255, 210, 30, 0.6) 0%,
|
| 272 |
+
rgba(65, 105, 225, 0.6) 50%,
|
| 273 |
+
rgba(0, 255, 0, 0.6) 100%);
|
| 274 |
+
box-shadow: 0 0 10px rgba(65, 105, 225, 0.4);
|
| 275 |
+
transition: width 0.3s ease;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.in-progress-block.thinking .progress-bar {
|
| 279 |
+
background: rgba(255, 210, 30, 0.6);
|
| 280 |
+
box-shadow: 0 0 10px rgba(255, 210, 30, 0.4);
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
.in-progress-block.streaming .progress-bar {
|
| 284 |
+
background: rgba(65, 105, 225, 0.6);
|
| 285 |
+
box-shadow: 0 0 10px rgba(65, 105, 225, 0.4);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.in-progress-block.completing .progress-bar {
|
| 289 |
+
background: rgba(0, 255, 0, 0.6);
|
| 290 |
+
box-shadow: 0 0 10px rgba(0, 255, 0, 0.4);
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
.progress-header {
|
| 294 |
display: flex;
|
| 295 |
align-items: center;
|
src/lib/components/chat/MarkdownRenderer.svelte
CHANGED
|
@@ -1,13 +1,15 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { marked } from "marked";
|
|
|
|
| 3 |
|
| 4 |
export let content: string;
|
| 5 |
export let streaming: boolean = false;
|
| 6 |
|
| 7 |
let htmlContent = "";
|
|
|
|
| 8 |
|
| 9 |
const renderer = new marked.Renderer();
|
| 10 |
-
|
| 11 |
renderer.code = ({ text, lang }) => {
|
| 12 |
const language = lang || "text";
|
| 13 |
return `<pre><code class="language-${language}">${escapeHtml(text)}</code></pre>`;
|
|
@@ -31,21 +33,33 @@
|
|
| 31 |
gfm: true,
|
| 32 |
});
|
| 33 |
|
| 34 |
-
$:
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
}
|
| 40 |
}
|
| 41 |
</script>
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
<style>
|
| 51 |
.markdown-content {
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
import { marked } from "marked";
|
| 3 |
+
import StreamingText from "./StreamingText.svelte";
|
| 4 |
|
| 5 |
export let content: string;
|
| 6 |
export let streaming: boolean = false;
|
| 7 |
|
| 8 |
let htmlContent = "";
|
| 9 |
+
let useStreamingText = false;
|
| 10 |
|
| 11 |
const renderer = new marked.Renderer();
|
| 12 |
+
|
| 13 |
renderer.code = ({ text, lang }) => {
|
| 14 |
const language = lang || "text";
|
| 15 |
return `<pre><code class="language-${language}">${escapeHtml(text)}</code></pre>`;
|
|
|
|
| 33 |
gfm: true,
|
| 34 |
});
|
| 35 |
|
| 36 |
+
$: {
|
| 37 |
+
if (streaming && content) {
|
| 38 |
+
useStreamingText = true;
|
| 39 |
+
htmlContent = "";
|
| 40 |
+
} else if (content) {
|
| 41 |
+
useStreamingText = false;
|
| 42 |
+
try {
|
| 43 |
+
htmlContent = marked.parse(content) as string;
|
| 44 |
+
} catch {
|
| 45 |
+
htmlContent = escapeHtml(content);
|
| 46 |
+
}
|
| 47 |
}
|
| 48 |
}
|
| 49 |
</script>
|
| 50 |
|
| 51 |
+
{#if useStreamingText}
|
| 52 |
+
<div class="markdown-content">
|
| 53 |
+
<StreamingText {content} {streaming} speed={120} />
|
| 54 |
+
</div>
|
| 55 |
+
{:else}
|
| 56 |
+
<div class="markdown-content">
|
| 57 |
+
{@html htmlContent}
|
| 58 |
+
{#if streaming}
|
| 59 |
+
<span class="cursor">▊</span>
|
| 60 |
+
{/if}
|
| 61 |
+
</div>
|
| 62 |
+
{/if}
|
| 63 |
|
| 64 |
<style>
|
| 65 |
.markdown-content {
|
src/lib/components/chat/MessageSegment.svelte
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import type { MessageSegment } from "../../stores/agent";
|
| 3 |
+
import MarkdownRenderer from "./MarkdownRenderer.svelte";
|
| 4 |
+
import ToolInvocation from "./ToolInvocation.svelte";
|
| 5 |
+
|
| 6 |
+
export let segment: MessageSegment;
|
| 7 |
+
export let hideToolResult: boolean = false;
|
| 8 |
+
</script>
|
| 9 |
+
|
| 10 |
+
{#if segment.type === "text"}
|
| 11 |
+
{#if segment.content.trim()}
|
| 12 |
+
<div class="text-segment">
|
| 13 |
+
<MarkdownRenderer content={segment.content} streaming={segment.streaming} />
|
| 14 |
+
</div>
|
| 15 |
+
{/if}
|
| 16 |
+
{:else if segment.type === "tool-invocation"}
|
| 17 |
+
<ToolInvocation {segment} />
|
| 18 |
+
{:else if segment.type === "tool-result"}
|
| 19 |
+
{#if !hideToolResult}
|
| 20 |
+
<ToolInvocation {segment} />
|
| 21 |
+
{/if}
|
| 22 |
+
{:else if segment.type === "reasoning"}
|
| 23 |
+
<div class="reasoning-segment">
|
| 24 |
+
<details class="reasoning-details">
|
| 25 |
+
<summary>Thinking...</summary>
|
| 26 |
+
<div class="reasoning-content">
|
| 27 |
+
{segment.content}
|
| 28 |
+
</div>
|
| 29 |
+
</details>
|
| 30 |
+
</div>
|
| 31 |
+
{/if}
|
| 32 |
+
|
| 33 |
+
<style>
|
| 34 |
+
.text-segment {
|
| 35 |
+
margin: 0.25rem 0;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.reasoning-segment {
|
| 39 |
+
margin: 0.25rem 0;
|
| 40 |
+
padding: 0.4rem 0.6rem;
|
| 41 |
+
background: rgba(255, 255, 255, 0.02);
|
| 42 |
+
border-radius: 3px;
|
| 43 |
+
border-left: 2px solid rgba(255, 255, 255, 0.1);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.reasoning-details {
|
| 47 |
+
cursor: pointer;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.reasoning-details summary {
|
| 51 |
+
color: rgba(255, 255, 255, 0.5);
|
| 52 |
+
font-size: 0.8rem;
|
| 53 |
+
user-select: none;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.reasoning-details summary:hover {
|
| 57 |
+
color: rgba(255, 255, 255, 0.7);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.reasoning-content {
|
| 61 |
+
margin-top: 0.5rem;
|
| 62 |
+
padding: 0.5rem;
|
| 63 |
+
background: rgba(0, 0, 0, 0.2);
|
| 64 |
+
border-radius: 3px;
|
| 65 |
+
color: rgba(255, 255, 255, 0.7);
|
| 66 |
+
font-size: 0.825rem;
|
| 67 |
+
font-family: "Monaco", "Menlo", monospace;
|
| 68 |
+
white-space: pre-wrap;
|
| 69 |
+
}
|
| 70 |
+
</style>
|
src/lib/components/chat/ReasoningBlock.svelte
CHANGED
|
@@ -1,30 +1,48 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
-
import { onMount } from "svelte";
|
| 3 |
import gsap from "gsap";
|
| 4 |
-
|
| 5 |
export let reasoning: string;
|
| 6 |
export let isExpanded = false;
|
| 7 |
-
|
|
|
|
|
|
|
| 8 |
let iconElement: HTMLSpanElement;
|
| 9 |
let contentElement: HTMLDivElement;
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
function toggleExpanded() {
|
| 12 |
isExpanded = !isExpanded;
|
| 13 |
-
|
| 14 |
if (!iconElement || !contentElement) return;
|
| 15 |
-
|
| 16 |
if (isExpanded) {
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
rotation: 180,
|
| 19 |
duration: 0.15,
|
| 20 |
ease: "power2.out"
|
| 21 |
-
})
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
display: 'block'
|
| 25 |
-
});
|
| 26 |
-
|
| 27 |
-
gsap.fromTo(contentElement, {
|
| 28 |
opacity: 0,
|
| 29 |
maxHeight: 0,
|
| 30 |
y: -10
|
|
@@ -34,16 +52,19 @@
|
|
| 34 |
y: 0,
|
| 35 |
duration: 0.2,
|
| 36 |
ease: "power2.out"
|
| 37 |
-
});
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
rotation: 0,
|
| 42 |
duration: 0.1,
|
| 43 |
ease: "power2.in"
|
| 44 |
-
})
|
| 45 |
-
|
| 46 |
-
gsap.to(contentElement, {
|
| 47 |
opacity: 0,
|
| 48 |
maxHeight: 0,
|
| 49 |
y: -5,
|
|
@@ -52,10 +73,19 @@
|
|
| 52 |
onComplete: () => {
|
| 53 |
gsap.set(contentElement, { display: 'none' });
|
| 54 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
});
|
| 56 |
-
}
|
| 57 |
}
|
| 58 |
-
|
| 59 |
onMount(() => {
|
| 60 |
if (iconElement) {
|
| 61 |
gsap.set(iconElement, {
|
|
@@ -63,7 +93,7 @@
|
|
| 63 |
rotation: isExpanded ? 180 : 0
|
| 64 |
});
|
| 65 |
}
|
| 66 |
-
|
| 67 |
if (contentElement) {
|
| 68 |
gsap.set(contentElement, {
|
| 69 |
display: isExpanded ? 'block' : 'none',
|
|
@@ -72,11 +102,28 @@
|
|
| 72 |
y: isExpanded ? 0 : -10
|
| 73 |
});
|
| 74 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
});
|
| 76 |
</script>
|
| 77 |
|
| 78 |
-
<div class="reasoning-block">
|
| 79 |
-
<button
|
| 80 |
class="reasoning-toggle"
|
| 81 |
on:click={toggleExpanded}
|
| 82 |
title={isExpanded ? "Hide AI thinking" : "Show AI thinking"}
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
+
import { onMount, onDestroy } from "svelte";
|
| 3 |
import gsap from "gsap";
|
| 4 |
+
|
| 5 |
export let reasoning: string;
|
| 6 |
export let isExpanded = false;
|
| 7 |
+
export let autoCollapse = true;
|
| 8 |
+
export let responseComplete = false;
|
| 9 |
+
|
| 10 |
let iconElement: HTMLSpanElement;
|
| 11 |
let contentElement: HTMLDivElement;
|
| 12 |
+
let blockElement: HTMLDivElement;
|
| 13 |
+
let collapseTimeout: number | null = null;
|
| 14 |
+
|
| 15 |
+
$: if (responseComplete && autoCollapse && isExpanded) {
|
| 16 |
+
if (collapseTimeout) clearTimeout(collapseTimeout);
|
| 17 |
+
collapseTimeout = window.setTimeout(() => {
|
| 18 |
+
isExpanded = false;
|
| 19 |
+
animateCollapse();
|
| 20 |
+
}, 800);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
function toggleExpanded() {
|
| 24 |
isExpanded = !isExpanded;
|
| 25 |
+
|
| 26 |
if (!iconElement || !contentElement) return;
|
| 27 |
+
|
| 28 |
if (isExpanded) {
|
| 29 |
+
animateExpand();
|
| 30 |
+
} else {
|
| 31 |
+
animateCollapse();
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
function animateExpand() {
|
| 36 |
+
if (!iconElement || !contentElement) return;
|
| 37 |
+
|
| 38 |
+
gsap.timeline()
|
| 39 |
+
.to(iconElement, {
|
| 40 |
rotation: 180,
|
| 41 |
duration: 0.15,
|
| 42 |
ease: "power2.out"
|
| 43 |
+
})
|
| 44 |
+
.set(contentElement, { display: 'block' }, 0)
|
| 45 |
+
.fromTo(contentElement, {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
opacity: 0,
|
| 47 |
maxHeight: 0,
|
| 48 |
y: -10
|
|
|
|
| 52 |
y: 0,
|
| 53 |
duration: 0.2,
|
| 54 |
ease: "power2.out"
|
| 55 |
+
}, 0);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
function animateCollapse() {
|
| 59 |
+
if (!iconElement || !contentElement) return;
|
| 60 |
+
|
| 61 |
+
gsap.timeline()
|
| 62 |
+
.to(iconElement, {
|
| 63 |
rotation: 0,
|
| 64 |
duration: 0.1,
|
| 65 |
ease: "power2.in"
|
| 66 |
+
})
|
| 67 |
+
.to(contentElement, {
|
|
|
|
| 68 |
opacity: 0,
|
| 69 |
maxHeight: 0,
|
| 70 |
y: -5,
|
|
|
|
| 73 |
onComplete: () => {
|
| 74 |
gsap.set(contentElement, { display: 'none' });
|
| 75 |
}
|
| 76 |
+
}, 0)
|
| 77 |
+
.to(blockElement, {
|
| 78 |
+
scale: 0.98,
|
| 79 |
+
duration: 0.1,
|
| 80 |
+
ease: "power2.in"
|
| 81 |
+
}, 0)
|
| 82 |
+
.to(blockElement, {
|
| 83 |
+
scale: 1,
|
| 84 |
+
duration: 0.2,
|
| 85 |
+
ease: "back.out(1.5)"
|
| 86 |
});
|
|
|
|
| 87 |
}
|
| 88 |
+
|
| 89 |
onMount(() => {
|
| 90 |
if (iconElement) {
|
| 91 |
gsap.set(iconElement, {
|
|
|
|
| 93 |
rotation: isExpanded ? 180 : 0
|
| 94 |
});
|
| 95 |
}
|
| 96 |
+
|
| 97 |
if (contentElement) {
|
| 98 |
gsap.set(contentElement, {
|
| 99 |
display: isExpanded ? 'block' : 'none',
|
|
|
|
| 102 |
y: isExpanded ? 0 : -10
|
| 103 |
});
|
| 104 |
}
|
| 105 |
+
|
| 106 |
+
if (blockElement) {
|
| 107 |
+
gsap.fromTo(blockElement,
|
| 108 |
+
{ opacity: 0, scale: 0.95, y: -5 },
|
| 109 |
+
{
|
| 110 |
+
opacity: 1,
|
| 111 |
+
scale: 1,
|
| 112 |
+
y: 0,
|
| 113 |
+
duration: 0.3,
|
| 114 |
+
ease: "power2.out"
|
| 115 |
+
}
|
| 116 |
+
);
|
| 117 |
+
}
|
| 118 |
+
});
|
| 119 |
+
|
| 120 |
+
onDestroy(() => {
|
| 121 |
+
if (collapseTimeout) clearTimeout(collapseTimeout);
|
| 122 |
});
|
| 123 |
</script>
|
| 124 |
|
| 125 |
+
<div bind:this={blockElement} class="reasoning-block">
|
| 126 |
+
<button
|
| 127 |
class="reasoning-toggle"
|
| 128 |
on:click={toggleExpanded}
|
| 129 |
title={isExpanded ? "Hide AI thinking" : "Show AI thinking"}
|
src/lib/components/chat/StreamingText.svelte
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import { onMount, afterUpdate } from "svelte";
|
| 3 |
+
import gsap from "gsap";
|
| 4 |
+
|
| 5 |
+
export let content: string = "";
|
| 6 |
+
export let streaming: boolean = false;
|
| 7 |
+
export let speed: number = 60;
|
| 8 |
+
export let onComplete: (() => void) | undefined = undefined;
|
| 9 |
+
|
| 10 |
+
let displayedContent = "";
|
| 11 |
+
let cursorElement: HTMLSpanElement;
|
| 12 |
+
let containerElement: HTMLDivElement;
|
| 13 |
+
let animationTimeline: any = null;
|
| 14 |
+
let buffer: string[] = [];
|
| 15 |
+
let isProcessing = false;
|
| 16 |
+
let lastProcessedLength = 0;
|
| 17 |
+
|
| 18 |
+
$: if (streaming && content) {
|
| 19 |
+
// Only process truly new content
|
| 20 |
+
if (content.length > lastProcessedLength) {
|
| 21 |
+
const newChars = content.slice(lastProcessedLength);
|
| 22 |
+
if (newChars) {
|
| 23 |
+
buffer.push(...newChars.split(''));
|
| 24 |
+
lastProcessedLength = content.length;
|
| 25 |
+
processBuffer();
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
} else if (!streaming && content !== displayedContent) {
|
| 29 |
+
displayedContent = content;
|
| 30 |
+
lastProcessedLength = content.length;
|
| 31 |
+
if (animationTimeline) {
|
| 32 |
+
animationTimeline.kill();
|
| 33 |
+
}
|
| 34 |
+
hideCursor();
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
async function processBuffer() {
|
| 38 |
+
if (isProcessing || buffer.length === 0) return;
|
| 39 |
+
isProcessing = true;
|
| 40 |
+
|
| 41 |
+
while (buffer.length > 0) {
|
| 42 |
+
// Process multiple characters at once for better performance
|
| 43 |
+
const chunkSize = Math.min(3, buffer.length);
|
| 44 |
+
const chunk = buffer.splice(0, chunkSize).join('');
|
| 45 |
+
displayedContent += chunk;
|
| 46 |
+
|
| 47 |
+
// Only delay if there are more characters to process
|
| 48 |
+
if (buffer.length > 0) {
|
| 49 |
+
await new Promise(resolve => setTimeout(resolve, 1000 / speed));
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
isProcessing = false;
|
| 54 |
+
|
| 55 |
+
if (!streaming && displayedContent === content) {
|
| 56 |
+
hideCursor();
|
| 57 |
+
onComplete?.();
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
function showCursor() {
|
| 62 |
+
if (cursorElement) {
|
| 63 |
+
gsap.set(cursorElement, { display: "inline-block", opacity: 1 });
|
| 64 |
+
gsap.to(cursorElement, {
|
| 65 |
+
opacity: 0,
|
| 66 |
+
duration: 0.5,
|
| 67 |
+
repeat: -1,
|
| 68 |
+
yoyo: true,
|
| 69 |
+
ease: "steps(1)"
|
| 70 |
+
});
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
function hideCursor() {
|
| 75 |
+
if (cursorElement) {
|
| 76 |
+
gsap.killTweensOf(cursorElement);
|
| 77 |
+
gsap.to(cursorElement, {
|
| 78 |
+
opacity: 0,
|
| 79 |
+
duration: 0.2,
|
| 80 |
+
onComplete: () => {
|
| 81 |
+
if (cursorElement) {
|
| 82 |
+
gsap.set(cursorElement, { display: "none" });
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
});
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
onMount(() => {
|
| 90 |
+
if (streaming) {
|
| 91 |
+
showCursor();
|
| 92 |
+
// Reset tracking when component mounts with streaming
|
| 93 |
+
lastProcessedLength = 0;
|
| 94 |
+
displayedContent = "";
|
| 95 |
+
|
| 96 |
+
gsap.fromTo(containerElement,
|
| 97 |
+
{ opacity: 0, y: 5 },
|
| 98 |
+
{ opacity: 1, y: 0, duration: 0.3, ease: "power2.out" }
|
| 99 |
+
);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
return () => {
|
| 103 |
+
if (animationTimeline) {
|
| 104 |
+
animationTimeline.kill();
|
| 105 |
+
}
|
| 106 |
+
};
|
| 107 |
+
});
|
| 108 |
+
|
| 109 |
+
afterUpdate(() => {
|
| 110 |
+
if (streaming && cursorElement) {
|
| 111 |
+
showCursor();
|
| 112 |
+
}
|
| 113 |
+
});
|
| 114 |
+
</script>
|
| 115 |
+
|
| 116 |
+
<div class="streaming-text" bind:this={containerElement}>
|
| 117 |
+
<span class="text-content">{displayedContent}</span>
|
| 118 |
+
{#if streaming}
|
| 119 |
+
<span bind:this={cursorElement} class="cursor">▊</span>
|
| 120 |
+
{/if}
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
<style>
|
| 124 |
+
.streaming-text {
|
| 125 |
+
display: inline;
|
| 126 |
+
word-wrap: break-word;
|
| 127 |
+
white-space: pre-wrap;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.text-content {
|
| 131 |
+
color: inherit;
|
| 132 |
+
font-family: inherit;
|
| 133 |
+
line-height: inherit;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.cursor {
|
| 137 |
+
display: inline-block;
|
| 138 |
+
color: rgba(65, 105, 225, 0.8);
|
| 139 |
+
font-weight: bold;
|
| 140 |
+
margin-left: 1px;
|
| 141 |
+
animation: none;
|
| 142 |
+
vertical-align: baseline;
|
| 143 |
+
}
|
| 144 |
+
</style>
|
src/lib/components/chat/ToolInvocation.svelte
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import { onMount, onDestroy } from "svelte";
|
| 3 |
+
import gsap from "gsap";
|
| 4 |
+
import type { MessageSegment } from "../../stores/agent";
|
| 5 |
+
|
| 6 |
+
export let segment: MessageSegment;
|
| 7 |
+
|
| 8 |
+
let element: HTMLDivElement;
|
| 9 |
+
let expanded = false;
|
| 10 |
+
let statusIcon: HTMLSpanElement;
|
| 11 |
+
let progressRing: SVGCircleElement;
|
| 12 |
+
let detailsElement: HTMLDivElement;
|
| 13 |
+
let timeline: gsap.core.Timeline | null = null;
|
| 14 |
+
let prevStatus = segment.toolStatus;
|
| 15 |
+
|
| 16 |
+
const toolIcons: Record<string, string> = {
|
| 17 |
+
read_editor: "📄",
|
| 18 |
+
write_editor: "✏️",
|
| 19 |
+
observe_console: "📟",
|
| 20 |
+
default: "🔧",
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
const statusIcons: Record<string, string> = {
|
| 24 |
+
pending: "⏳",
|
| 25 |
+
running: "⚡",
|
| 26 |
+
completed: "✅",
|
| 27 |
+
error: "❌",
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
$: if (segment.toolStatus !== prevStatus && statusIcon) {
|
| 31 |
+
animateStatusChange(prevStatus, segment.toolStatus);
|
| 32 |
+
prevStatus = segment.toolStatus;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
function toggle() {
|
| 36 |
+
expanded = !expanded;
|
| 37 |
+
animateToggle();
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
function animateToggle() {
|
| 41 |
+
if (!detailsElement) return;
|
| 42 |
+
|
| 43 |
+
if (timeline) timeline.kill();
|
| 44 |
+
timeline = gsap.timeline();
|
| 45 |
+
|
| 46 |
+
if (expanded) {
|
| 47 |
+
timeline
|
| 48 |
+
.set(detailsElement, { display: "block" })
|
| 49 |
+
.fromTo(detailsElement,
|
| 50 |
+
{ opacity: 0, height: 0, y: -10 },
|
| 51 |
+
{ opacity: 1, height: "auto", y: 0, duration: 0.3, ease: "power2.out" }
|
| 52 |
+
);
|
| 53 |
+
} else {
|
| 54 |
+
timeline
|
| 55 |
+
.to(detailsElement,
|
| 56 |
+
{ opacity: 0, height: 0, y: -5, duration: 0.2, ease: "power2.in" }
|
| 57 |
+
)
|
| 58 |
+
.set(detailsElement, { display: "none" });
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
function animateStatusChange(_from: string | undefined, to: string | undefined) {
|
| 63 |
+
if (!statusIcon || !element) return;
|
| 64 |
+
|
| 65 |
+
gsap.timeline()
|
| 66 |
+
.to(statusIcon, {
|
| 67 |
+
scale: 1.3,
|
| 68 |
+
duration: 0.15,
|
| 69 |
+
ease: "power2.in"
|
| 70 |
+
})
|
| 71 |
+
.to(statusIcon, {
|
| 72 |
+
scale: 1,
|
| 73 |
+
duration: 0.15,
|
| 74 |
+
ease: "back.out(2)"
|
| 75 |
+
});
|
| 76 |
+
|
| 77 |
+
if (to === "completed") {
|
| 78 |
+
gsap.to(element, {
|
| 79 |
+
borderColor: "rgba(0, 255, 0, 0.3)",
|
| 80 |
+
backgroundColor: "rgba(0, 255, 0, 0.05)",
|
| 81 |
+
duration: 0.3,
|
| 82 |
+
ease: "power2.out"
|
| 83 |
+
});
|
| 84 |
+
} else if (to === "error") {
|
| 85 |
+
gsap.to(element, {
|
| 86 |
+
borderColor: "rgba(255, 0, 0, 0.4)",
|
| 87 |
+
backgroundColor: "rgba(255, 0, 0, 0.1)",
|
| 88 |
+
duration: 0.3,
|
| 89 |
+
ease: "power2.out"
|
| 90 |
+
});
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
if (progressRing) {
|
| 94 |
+
if (to === "running") {
|
| 95 |
+
gsap.to(progressRing, {
|
| 96 |
+
strokeDashoffset: 50,
|
| 97 |
+
duration: 1,
|
| 98 |
+
ease: "power2.inOut",
|
| 99 |
+
repeat: -1,
|
| 100 |
+
yoyo: true
|
| 101 |
+
});
|
| 102 |
+
} else if (to === "completed") {
|
| 103 |
+
gsap.to(progressRing, {
|
| 104 |
+
strokeDashoffset: 0,
|
| 105 |
+
duration: 0.3,
|
| 106 |
+
ease: "power2.out"
|
| 107 |
+
});
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
function formatDuration(): string {
|
| 113 |
+
if (!segment.startTime) return "";
|
| 114 |
+
const duration = (segment.endTime || Date.now()) - segment.startTime;
|
| 115 |
+
if (duration < 1000) {
|
| 116 |
+
return `${duration}ms`;
|
| 117 |
+
}
|
| 118 |
+
return `${(duration / 1000).toFixed(1)}s`;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
onMount(() => {
|
| 122 |
+
gsap.fromTo(
|
| 123 |
+
element,
|
| 124 |
+
{ opacity: 0, x: -20, scale: 0.95 },
|
| 125 |
+
{
|
| 126 |
+
opacity: 1,
|
| 127 |
+
x: 0,
|
| 128 |
+
scale: 1,
|
| 129 |
+
duration: 0.4,
|
| 130 |
+
ease: "back.out(1.5)"
|
| 131 |
+
}
|
| 132 |
+
);
|
| 133 |
+
|
| 134 |
+
if (segment.toolStatus === "running" && progressRing) {
|
| 135 |
+
gsap.to(progressRing, {
|
| 136 |
+
strokeDashoffset: 50,
|
| 137 |
+
duration: 1,
|
| 138 |
+
ease: "power2.inOut",
|
| 139 |
+
repeat: -1,
|
| 140 |
+
yoyo: true
|
| 141 |
+
});
|
| 142 |
+
}
|
| 143 |
+
});
|
| 144 |
+
|
| 145 |
+
onDestroy(() => {
|
| 146 |
+
if (timeline) timeline.kill();
|
| 147 |
+
});
|
| 148 |
+
</script>
|
| 149 |
+
|
| 150 |
+
<div class="tool-invocation {segment.toolStatus}" bind:this={element}>
|
| 151 |
+
<button class="tool-header" on:click={toggle} aria-expanded={expanded}>
|
| 152 |
+
<div class="status-indicator">
|
| 153 |
+
<svg class="progress-ring" width="24" height="24">
|
| 154 |
+
<circle
|
| 155 |
+
cx="12"
|
| 156 |
+
cy="12"
|
| 157 |
+
r="10"
|
| 158 |
+
stroke="rgba(255, 255, 255, 0.1)"
|
| 159 |
+
stroke-width="2"
|
| 160 |
+
fill="none"
|
| 161 |
+
/>
|
| 162 |
+
<circle
|
| 163 |
+
bind:this={progressRing}
|
| 164 |
+
cx="12"
|
| 165 |
+
cy="12"
|
| 166 |
+
r="10"
|
| 167 |
+
stroke="currentColor"
|
| 168 |
+
stroke-width="2"
|
| 169 |
+
fill="none"
|
| 170 |
+
stroke-dasharray="62.83"
|
| 171 |
+
stroke-dashoffset="62.83"
|
| 172 |
+
transform="rotate(-90 12 12)"
|
| 173 |
+
/>
|
| 174 |
+
</svg>
|
| 175 |
+
<span bind:this={statusIcon} class="status-icon">
|
| 176 |
+
{#if segment.toolStatus === "running"}
|
| 177 |
+
<span class="spinner">{statusIcons[segment.toolStatus || "pending"]}</span>
|
| 178 |
+
{:else}
|
| 179 |
+
{statusIcons[segment.toolStatus || "pending"]}
|
| 180 |
+
{/if}
|
| 181 |
+
</span>
|
| 182 |
+
</div>
|
| 183 |
+
<span class="tool-icon">{toolIcons[segment.toolName || "default"] || toolIcons.default}</span>
|
| 184 |
+
<span class="tool-name">
|
| 185 |
+
{#if segment.toolStatus === "running"}
|
| 186 |
+
Calling {segment.toolName}...
|
| 187 |
+
{:else if segment.toolStatus === "completed"}
|
| 188 |
+
Called {segment.toolName}
|
| 189 |
+
{:else if segment.toolStatus === "error"}
|
| 190 |
+
Failed to call {segment.toolName}
|
| 191 |
+
{:else}
|
| 192 |
+
Preparing {segment.toolName}...
|
| 193 |
+
{/if}
|
| 194 |
+
</span>
|
| 195 |
+
{#if segment.startTime}
|
| 196 |
+
<span class="duration">{formatDuration()}</span>
|
| 197 |
+
{/if}
|
| 198 |
+
<span class="expand-icon" class:rotated={expanded}>▶</span>
|
| 199 |
+
</button>
|
| 200 |
+
|
| 201 |
+
<div bind:this={detailsElement} class="tool-details" style="display: none;">
|
| 202 |
+
{#if segment.toolArgs && Object.keys(segment.toolArgs).length > 0}
|
| 203 |
+
<div class="section-title">Parameters:</div>
|
| 204 |
+
<div class="params">
|
| 205 |
+
{#each Object.entries(segment.toolArgs) as [key, value]}
|
| 206 |
+
<div class="param">
|
| 207 |
+
<span class="param-key">{key}:</span>
|
| 208 |
+
<span class="param-value">
|
| 209 |
+
{#if typeof value === 'string' && value.length > 100}
|
| 210 |
+
<pre>{value}</pre>
|
| 211 |
+
{:else}
|
| 212 |
+
{JSON.stringify(value)}
|
| 213 |
+
{/if}
|
| 214 |
+
</span>
|
| 215 |
+
</div>
|
| 216 |
+
{/each}
|
| 217 |
+
</div>
|
| 218 |
+
{/if}
|
| 219 |
+
{#if segment.toolOutput || segment.toolResult}
|
| 220 |
+
<div class="section-title">Result:</div>
|
| 221 |
+
<div class="tool-output">
|
| 222 |
+
<pre>{segment.toolOutput || segment.toolResult}</pre>
|
| 223 |
+
</div>
|
| 224 |
+
{/if}
|
| 225 |
+
{#if segment.consoleOutput && segment.consoleOutput.length > 0}
|
| 226 |
+
<div class="section-title">Console Output:</div>
|
| 227 |
+
<div class="console-output">
|
| 228 |
+
{#each segment.consoleOutput as line}
|
| 229 |
+
<div class="console-line">{line}</div>
|
| 230 |
+
{/each}
|
| 231 |
+
</div>
|
| 232 |
+
{/if}
|
| 233 |
+
{#if segment.toolError}
|
| 234 |
+
<div class="section-title">Error:</div>
|
| 235 |
+
<div class="tool-error">
|
| 236 |
+
{segment.toolError}
|
| 237 |
+
</div>
|
| 238 |
+
{/if}
|
| 239 |
+
</div>
|
| 240 |
+
</div>
|
| 241 |
+
|
| 242 |
+
<style>
|
| 243 |
+
.tool-invocation {
|
| 244 |
+
margin: 0.5rem 0;
|
| 245 |
+
margin-left: 1rem;
|
| 246 |
+
border-radius: 4px;
|
| 247 |
+
overflow: hidden;
|
| 248 |
+
border: 1px solid rgba(65, 105, 225, 0.2);
|
| 249 |
+
background: rgba(65, 105, 225, 0.05);
|
| 250 |
+
transition: all 0.2s ease;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.tool-invocation.running {
|
| 254 |
+
background: rgba(255, 210, 30, 0.08);
|
| 255 |
+
border-color: rgba(255, 210, 30, 0.3);
|
| 256 |
+
animation: pulse 2s ease-in-out infinite;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
@keyframes pulse {
|
| 260 |
+
0%, 100% {
|
| 261 |
+
opacity: 1;
|
| 262 |
+
}
|
| 263 |
+
50% {
|
| 264 |
+
opacity: 0.8;
|
| 265 |
+
}
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.tool-invocation.completed {
|
| 269 |
+
background: rgba(0, 255, 0, 0.04);
|
| 270 |
+
border-color: rgba(0, 255, 0, 0.15);
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
.tool-invocation.error {
|
| 274 |
+
background: rgba(255, 0, 0, 0.08);
|
| 275 |
+
border-color: rgba(255, 0, 0, 0.3);
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.tool-header {
|
| 279 |
+
display: flex;
|
| 280 |
+
align-items: center;
|
| 281 |
+
gap: 0.5rem;
|
| 282 |
+
width: 100%;
|
| 283 |
+
padding: 0.35rem 0.5rem;
|
| 284 |
+
background: transparent;
|
| 285 |
+
border: none;
|
| 286 |
+
color: inherit;
|
| 287 |
+
font: inherit;
|
| 288 |
+
text-align: left;
|
| 289 |
+
cursor: pointer;
|
| 290 |
+
transition: background 0.2s ease;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
.tool-header:hover {
|
| 294 |
+
background: rgba(255, 255, 255, 0.02);
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.status-indicator {
|
| 298 |
+
position: relative;
|
| 299 |
+
width: 24px;
|
| 300 |
+
height: 24px;
|
| 301 |
+
display: flex;
|
| 302 |
+
align-items: center;
|
| 303 |
+
justify-content: center;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
.progress-ring {
|
| 307 |
+
position: absolute;
|
| 308 |
+
top: 0;
|
| 309 |
+
left: 0;
|
| 310 |
+
color: rgba(65, 105, 225, 0.6);
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
.tool-invocation.running .progress-ring {
|
| 314 |
+
color: rgba(255, 210, 30, 0.8);
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
.tool-invocation.completed .progress-ring {
|
| 318 |
+
color: rgba(0, 255, 0, 0.6);
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.tool-invocation.error .progress-ring {
|
| 322 |
+
color: rgba(255, 0, 0, 0.6);
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.status-icon {
|
| 326 |
+
font-size: 0.85rem;
|
| 327 |
+
position: relative;
|
| 328 |
+
z-index: 1;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.tool-icon {
|
| 332 |
+
font-size: 0.95rem;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
.tool-name {
|
| 336 |
+
flex: 1;
|
| 337 |
+
color: rgba(255, 255, 255, 0.85);
|
| 338 |
+
font-size: 0.8rem;
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.duration {
|
| 342 |
+
color: rgba(255, 255, 255, 0.4);
|
| 343 |
+
font-size: 0.7rem;
|
| 344 |
+
font-family: "Monaco", "Menlo", monospace;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.expand-icon {
|
| 348 |
+
font-size: 0.65rem;
|
| 349 |
+
color: rgba(255, 255, 255, 0.4);
|
| 350 |
+
transition: transform 0.2s ease;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
.expand-icon.rotated {
|
| 354 |
+
transform: rotate(90deg);
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
.tool-details {
|
| 358 |
+
padding: 0.5rem;
|
| 359 |
+
background: rgba(0, 0, 0, 0.2);
|
| 360 |
+
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.section-title {
|
| 364 |
+
color: rgba(255, 255, 255, 0.5);
|
| 365 |
+
font-size: 0.7rem;
|
| 366 |
+
font-weight: 600;
|
| 367 |
+
margin-bottom: 0.25rem;
|
| 368 |
+
text-transform: uppercase;
|
| 369 |
+
letter-spacing: 0.5px;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
.params {
|
| 373 |
+
font-family: "Monaco", "Menlo", monospace;
|
| 374 |
+
font-size: 0.75rem;
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
.param {
|
| 378 |
+
display: flex;
|
| 379 |
+
gap: 0.5rem;
|
| 380 |
+
margin: 0.2rem 0;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
.param-key {
|
| 384 |
+
color: rgba(255, 255, 255, 0.5);
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
.param-value {
|
| 388 |
+
color: rgba(255, 210, 30, 0.8);
|
| 389 |
+
word-break: break-all;
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
.param-value pre {
|
| 393 |
+
margin: 0;
|
| 394 |
+
padding: 0.4rem;
|
| 395 |
+
background: rgba(0, 0, 0, 0.3);
|
| 396 |
+
border-radius: 3px;
|
| 397 |
+
font-size: 0.7rem;
|
| 398 |
+
overflow-x: auto;
|
| 399 |
+
max-height: 150px;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.spinner {
|
| 403 |
+
display: inline-block;
|
| 404 |
+
animation: spin 0.8s linear infinite;
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
@keyframes spin {
|
| 408 |
+
from { transform: rotate(0deg); }
|
| 409 |
+
to { transform: rotate(360deg); }
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.tool-output {
|
| 413 |
+
margin-top: 0.5rem;
|
| 414 |
+
padding: 0.5rem;
|
| 415 |
+
background: rgba(0, 255, 0, 0.05);
|
| 416 |
+
border: 1px solid rgba(0, 255, 0, 0.1);
|
| 417 |
+
border-radius: 3px;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
.tool-output pre {
|
| 421 |
+
margin: 0;
|
| 422 |
+
font-family: "Monaco", "Menlo", monospace;
|
| 423 |
+
font-size: 0.7rem;
|
| 424 |
+
color: rgba(0, 255, 0, 0.8);
|
| 425 |
+
white-space: pre-wrap;
|
| 426 |
+
word-wrap: break-word;
|
| 427 |
+
max-height: 200px;
|
| 428 |
+
overflow-y: auto;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
.console-output {
|
| 432 |
+
margin-top: 0.5rem;
|
| 433 |
+
background: rgba(0, 0, 0, 0.3);
|
| 434 |
+
border-radius: 3px;
|
| 435 |
+
padding: 0.4rem;
|
| 436 |
+
font-family: "Monaco", "Menlo", monospace;
|
| 437 |
+
font-size: 0.7rem;
|
| 438 |
+
color: rgba(255, 255, 255, 0.8);
|
| 439 |
+
overflow-x: auto;
|
| 440 |
+
max-height: 200px;
|
| 441 |
+
overflow-y: auto;
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
.console-line {
|
| 445 |
+
margin: 0.1rem 0;
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
.tool-error {
|
| 449 |
+
margin-top: 0.5rem;
|
| 450 |
+
padding: 0.5rem;
|
| 451 |
+
background: rgba(255, 0, 0, 0.08);
|
| 452 |
+
border: 1px solid rgba(255, 0, 0, 0.2);
|
| 453 |
+
border-radius: 3px;
|
| 454 |
+
color: rgba(255, 100, 100, 0.9);
|
| 455 |
+
font-size: 0.75rem;
|
| 456 |
+
font-family: "Monaco", "Menlo", monospace;
|
| 457 |
+
}
|
| 458 |
+
</style>
|
src/lib/components/chat/context.md
CHANGED
|
@@ -1,20 +1,22 @@
|
|
| 1 |
# Chat Context
|
| 2 |
|
| 3 |
-
AI chat interface with
|
| 4 |
|
| 5 |
## Components
|
| 6 |
|
| 7 |
-
- `ChatPanel.svelte` - Main chat UI with message
|
| 8 |
-
- `
|
| 9 |
-
- `
|
| 10 |
-
- `
|
| 11 |
-
- `
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
-
##
|
| 14 |
|
| 15 |
-
-
|
| 16 |
-
-
|
| 17 |
-
-
|
| 18 |
-
-
|
| 19 |
-
-
|
| 20 |
-
- GSAP animations
|
|
|
|
| 1 |
# Chat Context
|
| 2 |
|
| 3 |
+
AI chat interface with real-time streaming.
|
| 4 |
|
| 5 |
## Components
|
| 6 |
|
| 7 |
+
- `ChatPanel.svelte` - Main chat UI with smooth scroll and keyed message iteration
|
| 8 |
+
- `MessageSegment.svelte` - Renders text, tool invocations, and results
|
| 9 |
+
- `StreamingText.svelte` - Optimized character streaming with state persistence
|
| 10 |
+
- `ToolInvocation.svelte` - Consolidated tool execution and results display
|
| 11 |
+
- `InProgressBlock.svelte` - Status indicator with progress bar
|
| 12 |
+
- `ReasoningBlock.svelte` - Auto-collapsing thinking viewer
|
| 13 |
+
- `MarkdownRenderer.svelte` - Markdown parser with configurable streaming speed
|
| 14 |
+
- `ToolCallDisplay.svelte`, `ToolCallBlock.svelte` - Legacy tool rendering (deprecated)
|
| 15 |
|
| 16 |
+
## Architecture
|
| 17 |
|
| 18 |
+
- Explicit message IDs with proper component keying
|
| 19 |
+
- StreamingText tracks processed content length to prevent duplication
|
| 20 |
+
- Batch character processing (3 chars/cycle) at 120 chars/sec
|
| 21 |
+
- Tool invocations and results merged in single visual blocks
|
| 22 |
+
- Segments keyed by ID for stable component identity
|
|
|
src/lib/server/api.ts
CHANGED
|
@@ -11,6 +11,9 @@ export interface WebSocketMessage {
|
|
| 11 |
| "error"
|
| 12 |
| "status"
|
| 13 |
| "stream"
|
|
|
|
|
|
|
|
|
|
| 14 |
| "auth"
|
| 15 |
| "editor_update"
|
| 16 |
| "editor_sync"
|
|
@@ -20,16 +23,17 @@ export interface WebSocketMessage {
|
|
| 20 |
content?: string;
|
| 21 |
role?: string;
|
| 22 |
chunk?: string;
|
|
|
|
| 23 |
error?: string;
|
| 24 |
processing?: boolean;
|
| 25 |
connected?: boolean;
|
| 26 |
message?: string;
|
| 27 |
-
token?: string;
|
| 28 |
toolName?: string;
|
| 29 |
toolArgs?: Record<string, unknown>;
|
| 30 |
toolResult?: string;
|
| 31 |
id?: string;
|
| 32 |
type?: string;
|
|
|
|
| 33 |
};
|
| 34 |
timestamp: number;
|
| 35 |
}
|
|
@@ -144,24 +148,36 @@ class WebSocketManager {
|
|
| 144 |
}
|
| 145 |
connectionData.messages.push(new HumanMessage(userMessage));
|
| 146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
const response = await connectionData.agent.processMessage(
|
| 148 |
userMessage,
|
| 149 |
connectionData.messages.slice(0, -1),
|
| 150 |
(chunk: string) => {
|
| 151 |
this.sendMessage(ws, {
|
| 152 |
-
type: "
|
| 153 |
-
payload: {
|
|
|
|
|
|
|
|
|
|
| 154 |
timestamp: Date.now(),
|
| 155 |
});
|
| 156 |
},
|
|
|
|
| 157 |
);
|
| 158 |
|
| 159 |
connectionData.messages.push(new AIMessage(response));
|
| 160 |
|
| 161 |
this.sendMessage(ws, {
|
| 162 |
-
type: "
|
| 163 |
payload: {
|
| 164 |
-
|
| 165 |
content: response,
|
| 166 |
},
|
| 167 |
timestamp: Date.now(),
|
|
|
|
| 11 |
| "error"
|
| 12 |
| "status"
|
| 13 |
| "stream"
|
| 14 |
+
| "stream_start"
|
| 15 |
+
| "stream_token"
|
| 16 |
+
| "stream_end"
|
| 17 |
| "auth"
|
| 18 |
| "editor_update"
|
| 19 |
| "editor_sync"
|
|
|
|
| 23 |
content?: string;
|
| 24 |
role?: string;
|
| 25 |
chunk?: string;
|
| 26 |
+
token?: string;
|
| 27 |
error?: string;
|
| 28 |
processing?: boolean;
|
| 29 |
connected?: boolean;
|
| 30 |
message?: string;
|
|
|
|
| 31 |
toolName?: string;
|
| 32 |
toolArgs?: Record<string, unknown>;
|
| 33 |
toolResult?: string;
|
| 34 |
id?: string;
|
| 35 |
type?: string;
|
| 36 |
+
messageId?: string;
|
| 37 |
};
|
| 38 |
timestamp: number;
|
| 39 |
}
|
|
|
|
| 148 |
}
|
| 149 |
connectionData.messages.push(new HumanMessage(userMessage));
|
| 150 |
|
| 151 |
+
const messageId = `msg_${Date.now()}`;
|
| 152 |
+
|
| 153 |
+
this.sendMessage(ws, {
|
| 154 |
+
type: "stream_start",
|
| 155 |
+
payload: { messageId },
|
| 156 |
+
timestamp: Date.now(),
|
| 157 |
+
});
|
| 158 |
+
|
| 159 |
const response = await connectionData.agent.processMessage(
|
| 160 |
userMessage,
|
| 161 |
connectionData.messages.slice(0, -1),
|
| 162 |
(chunk: string) => {
|
| 163 |
this.sendMessage(ws, {
|
| 164 |
+
type: "stream_token",
|
| 165 |
+
payload: {
|
| 166 |
+
token: chunk,
|
| 167 |
+
messageId,
|
| 168 |
+
},
|
| 169 |
timestamp: Date.now(),
|
| 170 |
});
|
| 171 |
},
|
| 172 |
+
messageId,
|
| 173 |
);
|
| 174 |
|
| 175 |
connectionData.messages.push(new AIMessage(response));
|
| 176 |
|
| 177 |
this.sendMessage(ws, {
|
| 178 |
+
type: "stream_end",
|
| 179 |
payload: {
|
| 180 |
+
messageId,
|
| 181 |
content: response,
|
| 182 |
},
|
| 183 |
timestamp: Date.now(),
|
src/lib/server/context.md
CHANGED
|
@@ -1,35 +1,29 @@
|
|
| 1 |
# Server Context
|
| 2 |
|
| 3 |
-
WebSocket server with
|
| 4 |
|
| 5 |
## Key Components
|
| 6 |
|
| 7 |
-
- **api.ts** - WebSocket routing
|
| 8 |
-
- **langgraph-agent.ts** -
|
| 9 |
-
- **tools.ts** - Editor read/write with game reload
|
| 10 |
-
- **console-buffer.ts** -
|
| 11 |
- **documentation.ts** - VibeGame documentation loader
|
| 12 |
-
- **prompts.ts** - Documentation formatting utilities
|
| 13 |
|
| 14 |
## Architecture
|
| 15 |
|
| 16 |
-
|
| 17 |
|
| 18 |
-
-
|
| 19 |
-
- Tool
|
| 20 |
-
-
|
| 21 |
-
- Write operations wait for game state before returning
|
| 22 |
|
| 23 |
-
## Message
|
| 24 |
|
| 25 |
- `auth` - HF token authentication
|
| 26 |
- `chat` - User messages
|
| 27 |
-
- `
|
| 28 |
-
- `
|
| 29 |
-
- `
|
| 30 |
-
- `
|
| 31 |
-
- `
|
| 32 |
-
- `tool_complete` - Tool execution completed
|
| 33 |
-
- `tool_error` - Tool execution failed
|
| 34 |
-
- `stream` - Response streaming
|
| 35 |
-
- `status` - Connection and processing status
|
|
|
|
| 1 |
# Server Context
|
| 2 |
|
| 3 |
+
WebSocket server with LangGraph agent for AI-assisted game development.
|
| 4 |
|
| 5 |
## Key Components
|
| 6 |
|
| 7 |
+
- **api.ts** - WebSocket message routing
|
| 8 |
+
- **langgraph-agent.ts** - LangGraph agent with character streaming
|
| 9 |
+
- **tools.ts** - Editor read/write with game reload
|
| 10 |
+
- **console-buffer.ts** - Console message storage
|
| 11 |
- **documentation.ts** - VibeGame documentation loader
|
|
|
|
| 12 |
|
| 13 |
## Architecture
|
| 14 |
|
| 15 |
+
LangGraph state machine with real-time streaming:
|
| 16 |
|
| 17 |
+
- Streams text segments character-by-character as they arrive
|
| 18 |
+
- Tool invocations interrupt text streaming
|
| 19 |
+
- Explicit message IDs required for all segment operations
|
|
|
|
| 20 |
|
| 21 |
+
## Message Protocol
|
| 22 |
|
| 23 |
- `auth` - HF token authentication
|
| 24 |
- `chat` - User messages
|
| 25 |
+
- `stream_start/token/end` - Legacy streaming
|
| 26 |
+
- `segment_start/token/end` - Segment streaming
|
| 27 |
+
- `editor_sync` - Sync editor content
|
| 28 |
+
- `console_sync` - Forward console messages
|
| 29 |
+
- `status` - Connection and processing state
|
|
|
|
|
|
|
|
|
|
|
|
src/lib/server/langgraph-agent.ts
CHANGED
|
@@ -60,28 +60,113 @@ export class LangGraphAgent {
|
|
| 60 |
private setupGraph() {
|
| 61 |
const graph = new StateGraph(AgentState);
|
| 62 |
|
| 63 |
-
graph.addNode("agent", async (state) => {
|
| 64 |
const systemPrompt = this.buildSystemPrompt();
|
| 65 |
const messages = this.formatMessages(state.messages, systemPrompt);
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
return {
|
| 78 |
-
messages: [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
hasToolCalls: true,
|
| 80 |
};
|
| 81 |
}
|
| 82 |
|
| 83 |
return {
|
| 84 |
-
messages: [new AIMessage(
|
| 85 |
hasToolCalls: false,
|
| 86 |
};
|
| 87 |
});
|
|
@@ -173,21 +258,31 @@ Be concise, accurate, and focus on practical solutions.`;
|
|
| 173 |
return formatted;
|
| 174 |
}
|
| 175 |
|
| 176 |
-
private async
|
| 177 |
messages: Array<{ role: string; content: string }>,
|
| 178 |
-
):
|
| 179 |
if (!this.client) {
|
| 180 |
throw new Error("Agent not initialized");
|
| 181 |
}
|
| 182 |
|
| 183 |
-
|
|
|
|
|
|
|
| 184 |
model: this.model,
|
| 185 |
messages,
|
| 186 |
temperature: 0.3,
|
| 187 |
max_tokens: 2048,
|
| 188 |
});
|
| 189 |
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
}
|
| 192 |
|
| 193 |
private parseToolCalls(
|
|
@@ -221,24 +316,38 @@ Be concise, accurate, and focus on practical solutions.`;
|
|
| 221 |
return toolCalls;
|
| 222 |
}
|
| 223 |
|
| 224 |
-
private async
|
| 225 |
toolCalls: Array<{ name: string; args: Record<string, unknown> }>,
|
|
|
|
| 226 |
): Promise<BaseMessage[]> {
|
| 227 |
const results = [];
|
| 228 |
|
| 229 |
for (const call of toolCalls) {
|
| 230 |
-
const
|
| 231 |
|
| 232 |
try {
|
| 233 |
if (this.ws && this.ws.readyState === this.ws.OPEN) {
|
| 234 |
this.ws.send(
|
| 235 |
JSON.stringify({
|
| 236 |
-
type: "
|
| 237 |
payload: {
|
| 238 |
-
|
|
|
|
|
|
|
| 239 |
toolName: call.name,
|
| 240 |
toolArgs: call.args,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
toolStatus: "running",
|
|
|
|
| 242 |
},
|
| 243 |
timestamp: Date.now(),
|
| 244 |
}),
|
|
@@ -246,27 +355,18 @@ Be concise, accurate, and focus on practical solutions.`;
|
|
| 246 |
}
|
| 247 |
|
| 248 |
let result;
|
|
|
|
|
|
|
| 249 |
if (call.name === "read_editor") {
|
| 250 |
result = await readEditorTool.func("");
|
| 251 |
} else if (call.name === "write_editor") {
|
| 252 |
result = await writeEditorTool.func(call.args as { content: string });
|
| 253 |
|
| 254 |
const consoleMatch = result.match(/Console output:\n([\s\S]*?)$/);
|
| 255 |
-
if (consoleMatch
|
| 256 |
-
|
| 257 |
.split("\n")
|
| 258 |
.filter((line) => line.trim());
|
| 259 |
-
this.ws.send(
|
| 260 |
-
JSON.stringify({
|
| 261 |
-
type: "tool_output",
|
| 262 |
-
payload: {
|
| 263 |
-
toolId,
|
| 264 |
-
toolOutput: "",
|
| 265 |
-
consoleOutput: consoleLines,
|
| 266 |
-
},
|
| 267 |
-
timestamp: Date.now(),
|
| 268 |
-
}),
|
| 269 |
-
);
|
| 270 |
}
|
| 271 |
} else if (call.name === "observe_console") {
|
| 272 |
result = await observeConsoleTool.func("");
|
|
@@ -277,12 +377,40 @@ Be concise, accurate, and focus on practical solutions.`;
|
|
| 277 |
if (this.ws && this.ws.readyState === this.ws.OPEN) {
|
| 278 |
this.ws.send(
|
| 279 |
JSON.stringify({
|
| 280 |
-
type: "
|
| 281 |
payload: {
|
| 282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
toolName: call.name,
|
| 284 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
toolStatus: "completed",
|
|
|
|
| 286 |
},
|
| 287 |
timestamp: Date.now(),
|
| 288 |
}),
|
|
@@ -292,7 +420,7 @@ Be concise, accurate, and focus on practical solutions.`;
|
|
| 292 |
results.push(
|
| 293 |
new ToolMessage({
|
| 294 |
content: result,
|
| 295 |
-
tool_call_id:
|
| 296 |
name: call.name,
|
| 297 |
}),
|
| 298 |
);
|
|
@@ -300,12 +428,13 @@ Be concise, accurate, and focus on practical solutions.`;
|
|
| 300 |
if (this.ws && this.ws.readyState === this.ws.OPEN) {
|
| 301 |
this.ws.send(
|
| 302 |
JSON.stringify({
|
| 303 |
-
type: "
|
| 304 |
payload: {
|
| 305 |
-
|
| 306 |
-
toolName: call.name,
|
| 307 |
-
error: error instanceof Error ? error.message : String(error),
|
| 308 |
toolStatus: "error",
|
|
|
|
|
|
|
|
|
|
| 309 |
},
|
| 310 |
timestamp: Date.now(),
|
| 311 |
}),
|
|
@@ -315,7 +444,7 @@ Be concise, accurate, and focus on practical solutions.`;
|
|
| 315 |
results.push(
|
| 316 |
new ToolMessage({
|
| 317 |
content: `Error executing ${call.name}: ${error}`,
|
| 318 |
-
tool_call_id:
|
| 319 |
name: call.name,
|
| 320 |
}),
|
| 321 |
);
|
|
@@ -329,58 +458,35 @@ Be concise, accurate, and focus on practical solutions.`;
|
|
| 329 |
message: string,
|
| 330 |
messageHistory: BaseMessage[] = [],
|
| 331 |
onStream?: (chunk: string) => void,
|
|
|
|
| 332 |
): Promise<string> {
|
| 333 |
if (!this.client) {
|
| 334 |
throw new Error("Agent not initialized");
|
| 335 |
}
|
| 336 |
|
| 337 |
-
const
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
|
| 344 |
let fullResponse = "";
|
| 345 |
-
let isStreaming = false;
|
| 346 |
-
let lastAIResponse = "";
|
| 347 |
-
let hasExecutedTools = false;
|
| 348 |
|
| 349 |
for await (const chunk of stream) {
|
| 350 |
-
if (chunk
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
if (msg.additional_kwargs?.has_tool_calls) {
|
| 356 |
-
hasExecutedTools = true;
|
| 357 |
-
lastAIResponse = content;
|
| 358 |
-
} else {
|
| 359 |
-
if (!isStreaming) {
|
| 360 |
-
isStreaming = true;
|
| 361 |
-
for (const char of content) {
|
| 362 |
-
fullResponse += char;
|
| 363 |
-
onStream?.(char);
|
| 364 |
-
await new Promise((resolve) => setTimeout(resolve, 5));
|
| 365 |
-
}
|
| 366 |
-
} else {
|
| 367 |
-
fullResponse = content;
|
| 368 |
-
}
|
| 369 |
-
lastAIResponse = content;
|
| 370 |
-
}
|
| 371 |
-
} else if (msg instanceof ToolMessage) {
|
| 372 |
-
if (onStream && !hasExecutedTools) {
|
| 373 |
-
const toolNotification = `\n🔧 ${msg.name}\n`;
|
| 374 |
-
for (const char of toolNotification) {
|
| 375 |
-
onStream(char);
|
| 376 |
-
await new Promise((resolve) => setTimeout(resolve, 5));
|
| 377 |
-
}
|
| 378 |
-
}
|
| 379 |
-
}
|
| 380 |
}
|
| 381 |
}
|
| 382 |
}
|
| 383 |
|
| 384 |
-
return fullResponse
|
| 385 |
}
|
| 386 |
}
|
|
|
|
| 60 |
private setupGraph() {
|
| 61 |
const graph = new StateGraph(AgentState);
|
| 62 |
|
| 63 |
+
graph.addNode("agent", async (state, config) => {
|
| 64 |
const systemPrompt = this.buildSystemPrompt();
|
| 65 |
const messages = this.formatMessages(state.messages, systemPrompt);
|
| 66 |
|
| 67 |
+
let fullResponse = "";
|
| 68 |
+
let currentSegmentId: string | null = null;
|
| 69 |
+
let currentSegmentContent = "";
|
| 70 |
+
let inToolCall = false;
|
| 71 |
+
const messageId = config?.metadata?.messageId;
|
| 72 |
|
| 73 |
+
for await (const token of this.streamModelResponse(messages)) {
|
| 74 |
+
fullResponse += token;
|
| 75 |
+
config?.writer?.({ type: "token", content: token });
|
| 76 |
+
|
| 77 |
+
const isToolStart = token.includes("[TOOL:");
|
| 78 |
+
const isToolEnd = inToolCall && token.includes("]");
|
| 79 |
+
|
| 80 |
+
if (isToolStart) {
|
| 81 |
+
if (currentSegmentId && currentSegmentContent.trim() && this.ws) {
|
| 82 |
+
this.ws.send(
|
| 83 |
+
JSON.stringify({
|
| 84 |
+
type: "segment_end",
|
| 85 |
+
payload: {
|
| 86 |
+
segmentId: currentSegmentId,
|
| 87 |
+
content: currentSegmentContent,
|
| 88 |
+
messageId,
|
| 89 |
+
},
|
| 90 |
+
timestamp: Date.now(),
|
| 91 |
+
}),
|
| 92 |
+
);
|
| 93 |
+
}
|
| 94 |
+
currentSegmentId = null;
|
| 95 |
+
currentSegmentContent = "";
|
| 96 |
+
inToolCall = true;
|
| 97 |
+
} else if (isToolEnd) {
|
| 98 |
+
inToolCall = false;
|
| 99 |
+
} else if (!inToolCall) {
|
| 100 |
+
if (!currentSegmentId && token.trim() && this.ws) {
|
| 101 |
+
currentSegmentId = `seg_${Date.now()}_${Math.random()}`;
|
| 102 |
+
currentSegmentContent = "";
|
| 103 |
+
this.ws.send(
|
| 104 |
+
JSON.stringify({
|
| 105 |
+
type: "segment_start",
|
| 106 |
+
payload: {
|
| 107 |
+
segmentId: currentSegmentId,
|
| 108 |
+
segmentType: "text",
|
| 109 |
+
messageId,
|
| 110 |
+
},
|
| 111 |
+
timestamp: Date.now(),
|
| 112 |
+
}),
|
| 113 |
+
);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
if (currentSegmentId) {
|
| 117 |
+
currentSegmentContent += token;
|
| 118 |
+
if (this.ws) {
|
| 119 |
+
this.ws.send(
|
| 120 |
+
JSON.stringify({
|
| 121 |
+
type: "segment_token",
|
| 122 |
+
payload: {
|
| 123 |
+
segmentId: currentSegmentId,
|
| 124 |
+
token,
|
| 125 |
+
messageId,
|
| 126 |
+
},
|
| 127 |
+
timestamp: Date.now(),
|
| 128 |
+
}),
|
| 129 |
+
);
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
if (currentSegmentId && currentSegmentContent.trim() && this.ws) {
|
| 136 |
+
this.ws.send(
|
| 137 |
+
JSON.stringify({
|
| 138 |
+
type: "segment_end",
|
| 139 |
+
payload: {
|
| 140 |
+
segmentId: currentSegmentId,
|
| 141 |
+
content: currentSegmentContent,
|
| 142 |
+
messageId,
|
| 143 |
+
},
|
| 144 |
+
timestamp: Date.now(),
|
| 145 |
+
}),
|
| 146 |
+
);
|
| 147 |
+
}
|
| 148 |
|
| 149 |
+
const toolCalls = this.parseToolCalls(fullResponse);
|
| 150 |
+
|
| 151 |
+
if (toolCalls.length > 0) {
|
| 152 |
+
const toolResults = await this.executeToolsWithSegments(
|
| 153 |
+
toolCalls,
|
| 154 |
+
config?.metadata?.messageId as string | undefined,
|
| 155 |
+
);
|
| 156 |
return {
|
| 157 |
+
messages: [
|
| 158 |
+
new AIMessage({
|
| 159 |
+
content: fullResponse,
|
| 160 |
+
additional_kwargs: { has_tool_calls: true },
|
| 161 |
+
}),
|
| 162 |
+
...toolResults,
|
| 163 |
+
],
|
| 164 |
hasToolCalls: true,
|
| 165 |
};
|
| 166 |
}
|
| 167 |
|
| 168 |
return {
|
| 169 |
+
messages: [new AIMessage(fullResponse)],
|
| 170 |
hasToolCalls: false,
|
| 171 |
};
|
| 172 |
});
|
|
|
|
| 258 |
return formatted;
|
| 259 |
}
|
| 260 |
|
| 261 |
+
private async *streamModelResponse(
|
| 262 |
messages: Array<{ role: string; content: string }>,
|
| 263 |
+
): AsyncGenerator<string, string, unknown> {
|
| 264 |
if (!this.client) {
|
| 265 |
throw new Error("Agent not initialized");
|
| 266 |
}
|
| 267 |
|
| 268 |
+
let fullContent = "";
|
| 269 |
+
|
| 270 |
+
const stream = this.client.chatCompletionStream({
|
| 271 |
model: this.model,
|
| 272 |
messages,
|
| 273 |
temperature: 0.3,
|
| 274 |
max_tokens: 2048,
|
| 275 |
});
|
| 276 |
|
| 277 |
+
for await (const chunk of stream) {
|
| 278 |
+
const token = chunk.choices[0]?.delta?.content || "";
|
| 279 |
+
if (token) {
|
| 280 |
+
fullContent += token;
|
| 281 |
+
yield token;
|
| 282 |
+
}
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
return fullContent;
|
| 286 |
}
|
| 287 |
|
| 288 |
private parseToolCalls(
|
|
|
|
| 316 |
return toolCalls;
|
| 317 |
}
|
| 318 |
|
| 319 |
+
private async executeToolsWithSegments(
|
| 320 |
toolCalls: Array<{ name: string; args: Record<string, unknown> }>,
|
| 321 |
+
messageId?: string,
|
| 322 |
): Promise<BaseMessage[]> {
|
| 323 |
const results = [];
|
| 324 |
|
| 325 |
for (const call of toolCalls) {
|
| 326 |
+
const segmentId = `seg_tool_${Date.now()}_${Math.random()}`;
|
| 327 |
|
| 328 |
try {
|
| 329 |
if (this.ws && this.ws.readyState === this.ws.OPEN) {
|
| 330 |
this.ws.send(
|
| 331 |
JSON.stringify({
|
| 332 |
+
type: "segment_start",
|
| 333 |
payload: {
|
| 334 |
+
segmentId,
|
| 335 |
+
segmentType: "tool-invocation",
|
| 336 |
+
messageId,
|
| 337 |
toolName: call.name,
|
| 338 |
toolArgs: call.args,
|
| 339 |
+
},
|
| 340 |
+
timestamp: Date.now(),
|
| 341 |
+
}),
|
| 342 |
+
);
|
| 343 |
+
|
| 344 |
+
this.ws.send(
|
| 345 |
+
JSON.stringify({
|
| 346 |
+
type: "segment_end",
|
| 347 |
+
payload: {
|
| 348 |
+
segmentId,
|
| 349 |
toolStatus: "running",
|
| 350 |
+
messageId,
|
| 351 |
},
|
| 352 |
timestamp: Date.now(),
|
| 353 |
}),
|
|
|
|
| 355 |
}
|
| 356 |
|
| 357 |
let result;
|
| 358 |
+
let consoleOutput: string[] = [];
|
| 359 |
+
|
| 360 |
if (call.name === "read_editor") {
|
| 361 |
result = await readEditorTool.func("");
|
| 362 |
} else if (call.name === "write_editor") {
|
| 363 |
result = await writeEditorTool.func(call.args as { content: string });
|
| 364 |
|
| 365 |
const consoleMatch = result.match(/Console output:\n([\s\S]*?)$/);
|
| 366 |
+
if (consoleMatch) {
|
| 367 |
+
consoleOutput = consoleMatch[1]
|
| 368 |
.split("\n")
|
| 369 |
.filter((line) => line.trim());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
}
|
| 371 |
} else if (call.name === "observe_console") {
|
| 372 |
result = await observeConsoleTool.func("");
|
|
|
|
| 377 |
if (this.ws && this.ws.readyState === this.ws.OPEN) {
|
| 378 |
this.ws.send(
|
| 379 |
JSON.stringify({
|
| 380 |
+
type: "segment_end",
|
| 381 |
payload: {
|
| 382 |
+
segmentId,
|
| 383 |
+
toolStatus: "completed",
|
| 384 |
+
messageId,
|
| 385 |
+
},
|
| 386 |
+
timestamp: Date.now(),
|
| 387 |
+
}),
|
| 388 |
+
);
|
| 389 |
+
|
| 390 |
+
const resultSegmentId = `seg_result_${Date.now()}_${Math.random()}`;
|
| 391 |
+
this.ws.send(
|
| 392 |
+
JSON.stringify({
|
| 393 |
+
type: "segment_start",
|
| 394 |
+
payload: {
|
| 395 |
+
segmentId: resultSegmentId,
|
| 396 |
+
segmentType: "tool-result",
|
| 397 |
+
messageId,
|
| 398 |
toolName: call.name,
|
| 399 |
+
},
|
| 400 |
+
timestamp: Date.now(),
|
| 401 |
+
}),
|
| 402 |
+
);
|
| 403 |
+
|
| 404 |
+
this.ws.send(
|
| 405 |
+
JSON.stringify({
|
| 406 |
+
type: "segment_end",
|
| 407 |
+
payload: {
|
| 408 |
+
segmentId: resultSegmentId,
|
| 409 |
+
toolOutput: result,
|
| 410 |
+
consoleOutput:
|
| 411 |
+
consoleOutput.length > 0 ? consoleOutput : undefined,
|
| 412 |
toolStatus: "completed",
|
| 413 |
+
messageId,
|
| 414 |
},
|
| 415 |
timestamp: Date.now(),
|
| 416 |
}),
|
|
|
|
| 420 |
results.push(
|
| 421 |
new ToolMessage({
|
| 422 |
content: result,
|
| 423 |
+
tool_call_id: segmentId,
|
| 424 |
name: call.name,
|
| 425 |
}),
|
| 426 |
);
|
|
|
|
| 428 |
if (this.ws && this.ws.readyState === this.ws.OPEN) {
|
| 429 |
this.ws.send(
|
| 430 |
JSON.stringify({
|
| 431 |
+
type: "segment_end",
|
| 432 |
payload: {
|
| 433 |
+
segmentId,
|
|
|
|
|
|
|
| 434 |
toolStatus: "error",
|
| 435 |
+
toolError:
|
| 436 |
+
error instanceof Error ? error.message : String(error),
|
| 437 |
+
messageId,
|
| 438 |
},
|
| 439 |
timestamp: Date.now(),
|
| 440 |
}),
|
|
|
|
| 444 |
results.push(
|
| 445 |
new ToolMessage({
|
| 446 |
content: `Error executing ${call.name}: ${error}`,
|
| 447 |
+
tool_call_id: segmentId,
|
| 448 |
name: call.name,
|
| 449 |
}),
|
| 450 |
);
|
|
|
|
| 458 |
message: string,
|
| 459 |
messageHistory: BaseMessage[] = [],
|
| 460 |
onStream?: (chunk: string) => void,
|
| 461 |
+
messageId?: string,
|
| 462 |
): Promise<string> {
|
| 463 |
if (!this.client) {
|
| 464 |
throw new Error("Agent not initialized");
|
| 465 |
}
|
| 466 |
|
| 467 |
+
const stream = await this.graph.stream(
|
| 468 |
+
{
|
| 469 |
+
messages: [...messageHistory, new HumanMessage(message)],
|
| 470 |
+
hasToolCalls: false,
|
| 471 |
+
},
|
| 472 |
+
{
|
| 473 |
+
streamMode: ["custom", "updates"] as const,
|
| 474 |
+
metadata: { messageId },
|
| 475 |
+
},
|
| 476 |
+
);
|
| 477 |
|
| 478 |
let fullResponse = "";
|
|
|
|
|
|
|
|
|
|
| 479 |
|
| 480 |
for await (const chunk of stream) {
|
| 481 |
+
if (Array.isArray(chunk)) {
|
| 482 |
+
const [mode, data] = chunk;
|
| 483 |
+
if (mode === "custom" && data?.type === "token") {
|
| 484 |
+
fullResponse += data.content;
|
| 485 |
+
onStream?.(data.content);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 486 |
}
|
| 487 |
}
|
| 488 |
}
|
| 489 |
|
| 490 |
+
return fullResponse;
|
| 491 |
}
|
| 492 |
}
|
src/lib/stores/agent.ts
CHANGED
|
@@ -15,6 +15,30 @@ export interface ToolExecution {
|
|
| 15 |
expanded?: boolean;
|
| 16 |
}
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
export interface ChatMessage {
|
| 19 |
id: string;
|
| 20 |
role: "user" | "assistant" | "system" | "tool";
|
|
@@ -24,6 +48,7 @@ export interface ChatMessage {
|
|
| 24 |
reasoning?: string;
|
| 25 |
showReasoning?: boolean;
|
| 26 |
toolExecutions?: ToolExecution[];
|
|
|
|
| 27 |
}
|
| 28 |
|
| 29 |
export interface AgentState {
|
|
@@ -131,6 +156,8 @@ function createAgentStore() {
|
|
| 131 |
processing?: boolean;
|
| 132 |
connected?: boolean;
|
| 133 |
chunk?: string;
|
|
|
|
|
|
|
| 134 |
reasoning?: string;
|
| 135 |
role?: string;
|
| 136 |
content?: string;
|
|
@@ -142,7 +169,10 @@ function createAgentStore() {
|
|
| 142 |
toolId?: string;
|
| 143 |
toolStatus?: "pending" | "running" | "completed" | "error";
|
| 144 |
toolOutput?: string;
|
|
|
|
| 145 |
consoleOutput?: string[];
|
|
|
|
|
|
|
| 146 |
};
|
| 147 |
}) {
|
| 148 |
switch (message.type) {
|
|
@@ -164,45 +194,73 @@ function createAgentStore() {
|
|
| 164 |
}
|
| 165 |
break;
|
| 166 |
|
| 167 |
-
case "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
update((state) => {
|
| 169 |
const newContent =
|
| 170 |
-
state.streamingContent + (message.payload.
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
id
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
};
|
| 205 |
-
}
|
| 206 |
});
|
| 207 |
break;
|
| 208 |
|
|
@@ -374,6 +432,134 @@ function createAgentStore() {
|
|
| 374 |
return { ...state, messages };
|
| 375 |
});
|
| 376 |
break;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
}
|
| 378 |
}
|
| 379 |
|
|
@@ -384,10 +570,11 @@ function createAgentStore() {
|
|
| 384 |
}
|
| 385 |
|
| 386 |
const userMessage: ChatMessage = {
|
| 387 |
-
id: `
|
| 388 |
role: "user",
|
| 389 |
content,
|
| 390 |
timestamp: Date.now(),
|
|
|
|
| 391 |
};
|
| 392 |
|
| 393 |
update((state) => ({
|
|
|
|
| 15 |
expanded?: boolean;
|
| 16 |
}
|
| 17 |
|
| 18 |
+
export type MessageSegmentType =
|
| 19 |
+
| "text"
|
| 20 |
+
| "tool-invocation"
|
| 21 |
+
| "tool-result"
|
| 22 |
+
| "reasoning";
|
| 23 |
+
|
| 24 |
+
export interface MessageSegment {
|
| 25 |
+
id: string;
|
| 26 |
+
type: MessageSegmentType;
|
| 27 |
+
content: string;
|
| 28 |
+
toolName?: string;
|
| 29 |
+
toolArgs?: Record<string, unknown>;
|
| 30 |
+
toolStatus?: "pending" | "running" | "completed" | "error";
|
| 31 |
+
toolOutput?: string;
|
| 32 |
+
toolResult?: string;
|
| 33 |
+
toolError?: string;
|
| 34 |
+
startTime?: number;
|
| 35 |
+
endTime?: number;
|
| 36 |
+
consoleOutput?: string[];
|
| 37 |
+
expanded?: boolean;
|
| 38 |
+
streaming?: boolean;
|
| 39 |
+
mergeWithPrevious?: boolean;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
export interface ChatMessage {
|
| 43 |
id: string;
|
| 44 |
role: "user" | "assistant" | "system" | "tool";
|
|
|
|
| 48 |
reasoning?: string;
|
| 49 |
showReasoning?: boolean;
|
| 50 |
toolExecutions?: ToolExecution[];
|
| 51 |
+
segments?: MessageSegment[];
|
| 52 |
}
|
| 53 |
|
| 54 |
export interface AgentState {
|
|
|
|
| 156 |
processing?: boolean;
|
| 157 |
connected?: boolean;
|
| 158 |
chunk?: string;
|
| 159 |
+
token?: string;
|
| 160 |
+
messageId?: string;
|
| 161 |
reasoning?: string;
|
| 162 |
role?: string;
|
| 163 |
content?: string;
|
|
|
|
| 169 |
toolId?: string;
|
| 170 |
toolStatus?: "pending" | "running" | "completed" | "error";
|
| 171 |
toolOutput?: string;
|
| 172 |
+
toolError?: string;
|
| 173 |
consoleOutput?: string[];
|
| 174 |
+
segmentId?: string;
|
| 175 |
+
segmentType?: MessageSegmentType;
|
| 176 |
};
|
| 177 |
}) {
|
| 178 |
switch (message.type) {
|
|
|
|
| 194 |
}
|
| 195 |
break;
|
| 196 |
|
| 197 |
+
case "stream_start": {
|
| 198 |
+
const assistantId =
|
| 199 |
+
message.payload.messageId || `assistant_${Date.now()}`;
|
| 200 |
+
currentStreamId = assistantId;
|
| 201 |
+
update((state) => {
|
| 202 |
+
return {
|
| 203 |
+
...state,
|
| 204 |
+
streamingContent: "",
|
| 205 |
+
messages: [
|
| 206 |
+
...state.messages,
|
| 207 |
+
{
|
| 208 |
+
id: assistantId,
|
| 209 |
+
role: "assistant",
|
| 210 |
+
content: "",
|
| 211 |
+
timestamp: Date.now(),
|
| 212 |
+
streaming: true,
|
| 213 |
+
segments: [],
|
| 214 |
+
},
|
| 215 |
+
],
|
| 216 |
+
streamingStatus: "streaming",
|
| 217 |
+
thinkingStartTime: null,
|
| 218 |
+
};
|
| 219 |
+
});
|
| 220 |
+
break;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
case "stream_token":
|
| 224 |
+
if (!message.payload.messageId) {
|
| 225 |
+
console.error("stream_token without messageId");
|
| 226 |
+
break;
|
| 227 |
+
}
|
| 228 |
update((state) => {
|
| 229 |
const newContent =
|
| 230 |
+
state.streamingContent + (message.payload.token || "");
|
| 231 |
+
return {
|
| 232 |
+
...state,
|
| 233 |
+
streamingContent: newContent,
|
| 234 |
+
messages: state.messages.map((msg) =>
|
| 235 |
+
msg.id === message.payload.messageId
|
| 236 |
+
? { ...msg, content: newContent }
|
| 237 |
+
: msg,
|
| 238 |
+
),
|
| 239 |
+
streamingStatus: "streaming",
|
| 240 |
+
};
|
| 241 |
+
});
|
| 242 |
+
break;
|
| 243 |
+
|
| 244 |
+
case "stream_end":
|
| 245 |
+
if (!message.payload.messageId) {
|
| 246 |
+
console.error("stream_end without messageId");
|
| 247 |
+
break;
|
| 248 |
+
}
|
| 249 |
+
update((state) => {
|
| 250 |
+
const finalContent =
|
| 251 |
+
message.payload.content || state.streamingContent;
|
| 252 |
+
currentStreamId = null;
|
| 253 |
+
return {
|
| 254 |
+
...state,
|
| 255 |
+
streamingContent: "",
|
| 256 |
+
messages: state.messages.map((msg) =>
|
| 257 |
+
msg.id === message.payload.messageId
|
| 258 |
+
? { ...msg, content: finalContent, streaming: false }
|
| 259 |
+
: msg,
|
| 260 |
+
),
|
| 261 |
+
streamingStatus: "idle",
|
| 262 |
+
thinkingStartTime: null,
|
| 263 |
+
};
|
|
|
|
|
|
|
| 264 |
});
|
| 265 |
break;
|
| 266 |
|
|
|
|
| 432 |
return { ...state, messages };
|
| 433 |
});
|
| 434 |
break;
|
| 435 |
+
|
| 436 |
+
case "segment_start":
|
| 437 |
+
update((state) => {
|
| 438 |
+
const segmentId = message.payload.segmentId || `seg_${Date.now()}`;
|
| 439 |
+
const segmentType = message.payload.segmentType as MessageSegmentType;
|
| 440 |
+
const newSegment: MessageSegment = {
|
| 441 |
+
id: segmentId,
|
| 442 |
+
type: segmentType,
|
| 443 |
+
content: "",
|
| 444 |
+
toolName: message.payload.toolName,
|
| 445 |
+
toolArgs: message.payload.toolArgs,
|
| 446 |
+
startTime: Date.now(),
|
| 447 |
+
streaming: segmentType === "text",
|
| 448 |
+
toolStatus:
|
| 449 |
+
segmentType === "tool-invocation" ? "pending" : undefined,
|
| 450 |
+
};
|
| 451 |
+
|
| 452 |
+
if (!message.payload.messageId) {
|
| 453 |
+
console.error("segment_start without messageId");
|
| 454 |
+
return state;
|
| 455 |
+
}
|
| 456 |
+
return {
|
| 457 |
+
...state,
|
| 458 |
+
messages: state.messages.map((msg) => {
|
| 459 |
+
if (msg.id === message.payload.messageId) {
|
| 460 |
+
return {
|
| 461 |
+
...msg,
|
| 462 |
+
segments: [...(msg.segments || []), newSegment],
|
| 463 |
+
};
|
| 464 |
+
}
|
| 465 |
+
return msg;
|
| 466 |
+
}),
|
| 467 |
+
};
|
| 468 |
+
});
|
| 469 |
+
break;
|
| 470 |
+
|
| 471 |
+
case "segment_token":
|
| 472 |
+
if (!message.payload.messageId) {
|
| 473 |
+
console.error("segment_token without messageId");
|
| 474 |
+
break;
|
| 475 |
+
}
|
| 476 |
+
update((state) => {
|
| 477 |
+
return {
|
| 478 |
+
...state,
|
| 479 |
+
messages: state.messages.map((msg) => {
|
| 480 |
+
if (msg.id === message.payload.messageId) {
|
| 481 |
+
const segments = msg.segments?.map((seg) => {
|
| 482 |
+
if (seg.id === message.payload.segmentId) {
|
| 483 |
+
return {
|
| 484 |
+
...seg,
|
| 485 |
+
content: seg.content + (message.payload.token || ""),
|
| 486 |
+
};
|
| 487 |
+
}
|
| 488 |
+
return seg;
|
| 489 |
+
});
|
| 490 |
+
return { ...msg, segments };
|
| 491 |
+
}
|
| 492 |
+
return msg;
|
| 493 |
+
}),
|
| 494 |
+
};
|
| 495 |
+
});
|
| 496 |
+
break;
|
| 497 |
+
|
| 498 |
+
case "segment_end":
|
| 499 |
+
if (!message.payload.messageId) {
|
| 500 |
+
console.error("segment_end without messageId");
|
| 501 |
+
break;
|
| 502 |
+
}
|
| 503 |
+
update((state) => {
|
| 504 |
+
return {
|
| 505 |
+
...state,
|
| 506 |
+
messages: state.messages.map((msg) => {
|
| 507 |
+
if (msg.id === message.payload.messageId) {
|
| 508 |
+
const segments = msg.segments?.map((seg, index, allSegs) => {
|
| 509 |
+
if (seg.id === message.payload.segmentId) {
|
| 510 |
+
const updatedSegment = {
|
| 511 |
+
...seg,
|
| 512 |
+
streaming: false,
|
| 513 |
+
content: message.payload.content || seg.content,
|
| 514 |
+
toolOutput: message.payload.toolOutput,
|
| 515 |
+
toolResult: message.payload.toolResult,
|
| 516 |
+
toolStatus: message.payload.toolStatus || seg.toolStatus,
|
| 517 |
+
toolError: message.payload.toolError,
|
| 518 |
+
endTime: Date.now(),
|
| 519 |
+
consoleOutput: message.payload.consoleOutput,
|
| 520 |
+
};
|
| 521 |
+
|
| 522 |
+
if (seg.type === "tool-result" && index > 0) {
|
| 523 |
+
const prevSegment = allSegs[index - 1];
|
| 524 |
+
if (
|
| 525 |
+
prevSegment.type === "tool-invocation" &&
|
| 526 |
+
prevSegment.toolName === seg.toolName
|
| 527 |
+
) {
|
| 528 |
+
return { ...updatedSegment, mergeWithPrevious: true };
|
| 529 |
+
}
|
| 530 |
+
}
|
| 531 |
+
return updatedSegment;
|
| 532 |
+
}
|
| 533 |
+
return seg;
|
| 534 |
+
});
|
| 535 |
+
|
| 536 |
+
const mergedSegments = segments?.reduce((acc, seg, index) => {
|
| 537 |
+
if (seg.mergeWithPrevious && index > 0 && acc.length > 0) {
|
| 538 |
+
const prevIndex = acc.length - 1;
|
| 539 |
+
acc[prevIndex] = {
|
| 540 |
+
...acc[prevIndex],
|
| 541 |
+
toolOutput: seg.toolOutput || acc[prevIndex].toolOutput,
|
| 542 |
+
toolResult: seg.toolResult || seg.toolOutput,
|
| 543 |
+
toolError: seg.toolError || acc[prevIndex].toolError,
|
| 544 |
+
toolStatus: seg.toolStatus || acc[prevIndex].toolStatus,
|
| 545 |
+
consoleOutput:
|
| 546 |
+
seg.consoleOutput || acc[prevIndex].consoleOutput,
|
| 547 |
+
endTime: seg.endTime,
|
| 548 |
+
};
|
| 549 |
+
return acc;
|
| 550 |
+
}
|
| 551 |
+
const cleanSegment = { ...seg };
|
| 552 |
+
delete cleanSegment.mergeWithPrevious;
|
| 553 |
+
return [...acc, cleanSegment];
|
| 554 |
+
}, [] as MessageSegment[]);
|
| 555 |
+
|
| 556 |
+
return { ...msg, segments: mergedSegments };
|
| 557 |
+
}
|
| 558 |
+
return msg;
|
| 559 |
+
}),
|
| 560 |
+
};
|
| 561 |
+
});
|
| 562 |
+
break;
|
| 563 |
}
|
| 564 |
}
|
| 565 |
|
|
|
|
| 570 |
}
|
| 571 |
|
| 572 |
const userMessage: ChatMessage = {
|
| 573 |
+
id: `user_${Date.now()}`,
|
| 574 |
role: "user",
|
| 575 |
content,
|
| 576 |
timestamp: Date.now(),
|
| 577 |
+
streaming: false,
|
| 578 |
};
|
| 579 |
|
| 580 |
update((state) => ({
|
src/lib/stores/editor.ts
CHANGED
|
@@ -10,6 +10,8 @@ const DEFAULT_CONTENT =
|
|
| 10 |
`<canvas id="game-canvas"></canvas>
|
| 11 |
|
| 12 |
<world canvas="#game-canvas" sky="#87ceeb">
|
|
|
|
|
|
|
| 13 |
<!-- Ground -->
|
| 14 |
<static-part pos="0 -0.5 0" shape="box" size="20 1 20" color="#90ee90"></static-part>
|
| 15 |
|
|
|
|
| 10 |
`<canvas id="game-canvas"></canvas>
|
| 11 |
|
| 12 |
<world canvas="#game-canvas" sky="#87ceeb">
|
| 13 |
+
<player pos="0 0 0"></player>
|
| 14 |
+
|
| 15 |
<!-- Ground -->
|
| 16 |
<static-part pos="0 -0.5 0" shape="box" size="20 1 20" color="#90ee90"></static-part>
|
| 17 |
|