Spaces:
Running
Running
Commit
·
eb1a39a
1
Parent(s):
794cf6c
add tools
Browse files- PLAN.md +0 -870
- bun.lock +15 -0
- favicon.ico +0 -0
- index.html +2 -1
- package.json +3 -0
- src/lib/components/chat/ChatPanel.svelte +64 -10
- src/lib/components/chat/MarkdownRenderer.svelte +188 -0
- src/lib/components/chat/ReasoningBlock.svelte +237 -0
- src/lib/components/chat/ToolCallDisplay.svelte +89 -0
- src/lib/components/chat/context.md +11 -6
- src/lib/components/layout/SplitView.svelte +416 -344
- src/lib/server/agent-config.ts +19 -0
- src/lib/server/agent-runner.test.ts +100 -0
- src/lib/server/agent-runner.ts +127 -79
- src/lib/server/api.ts +7 -3
- src/lib/server/context.md +13 -8
- src/lib/server/documentation.ts +30 -0
- src/lib/server/hint.test.ts +31 -0
- src/lib/server/prompts.ts +50 -0
- src/lib/server/reasoning-extractor.test.ts +114 -0
- src/lib/server/reasoning-extractor.ts +131 -0
- src/lib/stores/agent.ts +9 -1
- src/lib/tools/context.md +37 -0
- src/lib/tools/executor.ts +36 -0
- src/lib/tools/index.ts +17 -0
- src/lib/tools/parser.ts +41 -0
- src/lib/tools/read-console-output.ts +47 -0
- src/lib/tools/read-game-code.ts +18 -0
- src/lib/tools/registry.ts +51 -0
- src/lib/tools/tools-integration.test.ts +110 -0
- src/lib/tools/write-game-code.ts +38 -0
- tsconfig.json +1 -1
PLAN.md
DELETED
|
@@ -1,870 +0,0 @@
|
|
| 1 |
-
# AI Agent Development Plan - Refined Strategy
|
| 2 |
-
|
| 3 |
-
## Overview
|
| 4 |
-
|
| 5 |
-
Iterative AI-driven game development system using **browser-native TypeScript architecture** with streaming AI agents for real-time VibeGame game modification and self-correcting feedback loops.
|
| 6 |
-
|
| 7 |
-
## Current Status
|
| 8 |
-
|
| 9 |
-
**Phase 1 - Foundation COMPLETED** ✅
|
| 10 |
-
|
| 11 |
-
- ✅ WebSocket server integrated with Vite
|
| 12 |
-
- ✅ HuggingFace Inference API connected
|
| 13 |
-
- ✅ Real-time streaming chat interface
|
| 14 |
-
- ✅ Agent state management with Svelte stores
|
| 15 |
-
- ✅ Bidirectional communication working
|
| 16 |
-
|
| 17 |
-
**Next: Tool Implementation** 🚀
|
| 18 |
-
|
| 19 |
-
## Core Strategy: Small, Testable Iterations
|
| 20 |
-
|
| 21 |
-
### Guiding Principles
|
| 22 |
-
|
| 23 |
-
1. **Start Ultra-Simple** - First tool in ~10 lines of code
|
| 24 |
-
2. **Test Continuously** - Every iteration delivers testable value
|
| 25 |
-
3. **Fail Visibly** - Errors shown immediately in UI
|
| 26 |
-
4. **Incremental Complexity** - Add features one at a time
|
| 27 |
-
5. **User-Centric Progress** - Each day delivers visible improvement
|
| 28 |
-
|
| 29 |
-
### Development Phases
|
| 30 |
-
|
| 31 |
-
- **Week 1**: Make it Work (basic tools)
|
| 32 |
-
- **Week 2**: Make it Good (error handling)
|
| 33 |
-
- **Week 3**: Make it Fast (optimization)
|
| 34 |
-
|
| 35 |
-
## Architecture Evolution
|
| 36 |
-
|
| 37 |
-
### Current Architecture (Simple)
|
| 38 |
-
|
| 39 |
-
```
|
| 40 |
-
Browser → WebSocket → Agent → Tools → Stores → UI
|
| 41 |
-
```
|
| 42 |
-
|
| 43 |
-
### Target Architecture (Advanced)
|
| 44 |
-
|
| 45 |
-
```
|
| 46 |
-
Browser → SSE/WebSocket → Composite Agent System → MCP Tools → Stores → UI
|
| 47 |
-
├── Base Model (Complex reasoning)
|
| 48 |
-
├── Quick Edit Model (Fast edits)
|
| 49 |
-
└── AutoFix Model (Error correction)
|
| 50 |
-
```
|
| 51 |
-
|
| 52 |
-
## Implementation Phases
|
| 53 |
-
|
| 54 |
-
### Phase 1: Minimal Viable Tools (Days 1-7)
|
| 55 |
-
|
| 56 |
-
**Goal:** Agent can read and edit game code
|
| 57 |
-
|
| 58 |
-
#### Day 1: First Tool - Read Game Code
|
| 59 |
-
|
| 60 |
-
- [ ] Create `src/lib/tools/registry.ts` with simple tool interface
|
| 61 |
-
- [ ] Implement `read-game-code.ts` (returns editor content)
|
| 62 |
-
- [ ] Update agent-runner to detect tool calls
|
| 63 |
-
- **Test:** "What's in the game?" → Agent describes scene
|
| 64 |
-
|
| 65 |
-
#### Day 2: Tool Execution
|
| 66 |
-
|
| 67 |
-
- [ ] Parse tool requests from agent responses
|
| 68 |
-
- [ ] Execute tools and return results
|
| 69 |
-
- [ ] Add tool context to agent
|
| 70 |
-
- **Test:** Agent accurately describes game elements
|
| 71 |
-
|
| 72 |
-
#### Day 3: Edit Game Code
|
| 73 |
-
|
| 74 |
-
- [ ] Implement `edit-game-code.ts` with string replacement
|
| 75 |
-
- [ ] Connect to editor store
|
| 76 |
-
- [ ] Basic XML validation
|
| 77 |
-
- **Test:** "Add a red box" → Box appears
|
| 78 |
-
|
| 79 |
-
#### Day 4: Console Reading
|
| 80 |
-
|
| 81 |
-
- [ ] Implement `read-console.ts`
|
| 82 |
-
- [ ] Filter and format messages
|
| 83 |
-
- **Test:** Agent reports console errors
|
| 84 |
-
|
| 85 |
-
#### Day 5-6: Tool Chaining
|
| 86 |
-
|
| 87 |
-
- [ ] Multiple tools per response
|
| 88 |
-
- [ ] Simple sequencing
|
| 89 |
-
- [ ] Context preservation
|
| 90 |
-
- **Test:** Multi-step operations work
|
| 91 |
-
|
| 92 |
-
#### Day 7: Testing & Stabilization
|
| 93 |
-
|
| 94 |
-
- [ ] Fix discovered bugs
|
| 95 |
-
- [ ] Improve error messages
|
| 96 |
-
- [ ] Add logging
|
| 97 |
-
- **Success Metrics:**
|
| 98 |
-
- ✅ Agent can describe game content
|
| 99 |
-
- ✅ Agent can add/modify elements
|
| 100 |
-
- ✅ Agent can read console
|
| 101 |
-
- ✅ 5 basic operations work reliably
|
| 102 |
-
|
| 103 |
-
### Phase 2: Error Detection & Correction (Days 8-14)
|
| 104 |
-
|
| 105 |
-
**Goal:** Agent detects and fixes simple errors
|
| 106 |
-
|
| 107 |
-
#### Day 8: Error Pattern Recognition
|
| 108 |
-
|
| 109 |
-
- [ ] Parse common VibeGame errors
|
| 110 |
-
- [ ] Categorize: syntax, physics, missing components
|
| 111 |
-
- **Test:** Agent identifies "no ground" error
|
| 112 |
-
|
| 113 |
-
#### Day 9: Simple Self-Correction
|
| 114 |
-
|
| 115 |
-
- [ ] Single retry on error
|
| 116 |
-
- [ ] Generate fix attempts
|
| 117 |
-
- **Test:** Agent adds missing ground
|
| 118 |
-
|
| 119 |
-
#### Day 10: Undo/Redo
|
| 120 |
-
|
| 121 |
-
- [ ] Edit history in editor store
|
| 122 |
-
- [ ] Rollback on failure
|
| 123 |
-
- **Test:** Failed edits can be undone
|
| 124 |
-
|
| 125 |
-
#### Day 11: Validation Tool
|
| 126 |
-
|
| 127 |
-
- [ ] Check required components
|
| 128 |
-
- [ ] Pre-validate changes
|
| 129 |
-
- **Test:** Prevents invalid states
|
| 130 |
-
|
| 131 |
-
#### Day 12-13: Better Prompting
|
| 132 |
-
|
| 133 |
-
- [ ] VibeGame-specific examples
|
| 134 |
-
- [ ] Few-shot learning
|
| 135 |
-
- **Test:** Higher first-attempt success
|
| 136 |
-
|
| 137 |
-
#### Day 14: Polish
|
| 138 |
-
|
| 139 |
-
- [ ] Clear error messages
|
| 140 |
-
- [ ] Better explanations
|
| 141 |
-
- **Success Metrics:**
|
| 142 |
-
- ✅ Agent detects common errors
|
| 143 |
-
- ✅ Agent fixes simple mistakes
|
| 144 |
-
- ✅ Edit history prevents data loss
|
| 145 |
-
- ✅ 80% success rate on common tasks
|
| 146 |
-
|
| 147 |
-
### Phase 3: Advanced Features (Days 15-21)
|
| 148 |
-
|
| 149 |
-
**Goal:** Faster, smarter, more capable
|
| 150 |
-
|
| 151 |
-
#### Day 15: Code-Based Tools
|
| 152 |
-
|
| 153 |
-
- [ ] Agent generates TypeScript for tools
|
| 154 |
-
- [ ] Controlled execution environment
|
| 155 |
-
- **Test:** More flexible tool usage
|
| 156 |
-
|
| 157 |
-
#### Day 16: Streaming Improvements
|
| 158 |
-
|
| 159 |
-
- [ ] Migrate to Server-Sent Events (SSE)
|
| 160 |
-
- [ ] Real-time tool visualization
|
| 161 |
-
- **Test:** Better feedback
|
| 162 |
-
|
| 163 |
-
#### Day 17: Quick Edit Model
|
| 164 |
-
|
| 165 |
-
- [ ] Route simple edits to Qwen3-Coder-7B
|
| 166 |
-
- [ ] Instant text/color changes
|
| 167 |
-
- **Test:** Sub-second simple edits
|
| 168 |
-
|
| 169 |
-
#### Day 18: Context Management
|
| 170 |
-
|
| 171 |
-
- [ ] Sliding window for long chats
|
| 172 |
-
- [ ] Iteration summarization
|
| 173 |
-
- **Test:** 50+ message sessions
|
| 174 |
-
|
| 175 |
-
#### Day 19: Template System
|
| 176 |
-
|
| 177 |
-
- [ ] Pre-built game templates
|
| 178 |
-
- [ ] Quick start patterns
|
| 179 |
-
- **Test:** "Make a platformer" works
|
| 180 |
-
|
| 181 |
-
#### Day 20: Thinking Visualization
|
| 182 |
-
|
| 183 |
-
- [ ] Show reasoning process
|
| 184 |
-
- [ ] Chain-of-thought UI
|
| 185 |
-
- **Test:** Users understand logic
|
| 186 |
-
|
| 187 |
-
#### Day 21: Final Polish
|
| 188 |
-
|
| 189 |
-
- [ ] Performance optimization
|
| 190 |
-
- [ ] Documentation
|
| 191 |
-
- **Success Metrics:**
|
| 192 |
-
- ✅ Sub-second simple edits
|
| 193 |
-
- ✅ Complex multi-step tasks work
|
| 194 |
-
- ✅ Users understand process
|
| 195 |
-
- ✅ Production-ready quality
|
| 196 |
-
|
| 197 |
-
## Tool Specifications
|
| 198 |
-
|
| 199 |
-
### Core Tool Interface (MCP-Compatible)
|
| 200 |
-
|
| 201 |
-
```typescript
|
| 202 |
-
interface Tool {
|
| 203 |
-
name: string;
|
| 204 |
-
description: string;
|
| 205 |
-
execute: (params: any) => Promise<ToolResult>;
|
| 206 |
-
}
|
| 207 |
-
|
| 208 |
-
interface ToolResult {
|
| 209 |
-
success: boolean;
|
| 210 |
-
data?: any;
|
| 211 |
-
error?: string;
|
| 212 |
-
}
|
| 213 |
-
```
|
| 214 |
-
|
| 215 |
-
### Initial Tool Set
|
| 216 |
-
|
| 217 |
-
1. **read_game_code** - Get current editor content
|
| 218 |
-
2. **edit_game_code** - Modify via search/replace
|
| 219 |
-
3. **read_console** - Get console messages
|
| 220 |
-
4. **validate_game** - Check VibeGame rules
|
| 221 |
-
|
| 222 |
-
### Future Tools
|
| 223 |
-
|
| 224 |
-
5. **list_entities** - Get all scene entities
|
| 225 |
-
6. **add_entity** - Add new entity
|
| 226 |
-
7. **remove_entity** - Delete entity
|
| 227 |
-
8. **test_physics** - Simulate and check
|
| 228 |
-
9. **reset_game** - Restart scene
|
| 229 |
-
|
| 230 |
-
## Model Strategy
|
| 231 |
-
|
| 232 |
-
### Phase 1: Single Model
|
| 233 |
-
|
| 234 |
-
- **Primary:** DeepSeek-R1-Distill-Qwen-1.5B (current)
|
| 235 |
-
- Simple prompting, basic tool use
|
| 236 |
-
|
| 237 |
-
### Phase 2: Fallback Chain
|
| 238 |
-
|
| 239 |
-
- **Primary:** DeepSeek-R1-Distill
|
| 240 |
-
- **Fallback:** Qwen3-Coder-7B
|
| 241 |
-
- Error recovery and retry logic
|
| 242 |
-
|
| 243 |
-
### Phase 3: Composite System
|
| 244 |
-
|
| 245 |
-
- **Complex Tasks:** ERNIE-4.5-21B-A3B-Thinking (128K context)
|
| 246 |
-
- **Quick Edits:** Qwen3-Coder-7B-Instruct
|
| 247 |
-
- **Error Fix:** Dedicated correction model
|
| 248 |
-
- **Streaming Post-Processor:** Real-time validation
|
| 249 |
-
|
| 250 |
-
## Key Improvements from Research
|
| 251 |
-
|
| 252 |
-
### From v0/Vercel
|
| 253 |
-
|
| 254 |
-
- Composite model architecture
|
| 255 |
-
- Streaming post-processing
|
| 256 |
-
- Quick edit optimization
|
| 257 |
-
- Real-time error correction
|
| 258 |
-
|
| 259 |
-
### From Smolagents
|
| 260 |
-
|
| 261 |
-
- Code-based tools (TypeScript instead of JSON)
|
| 262 |
-
- Direct execution without translation
|
| 263 |
-
- Simpler debugging
|
| 264 |
-
|
| 265 |
-
### From Modern Thinking Models
|
| 266 |
-
|
| 267 |
-
- 128K context windows
|
| 268 |
-
- Chain-of-thought reasoning
|
| 269 |
-
- Sparse MoE activation (3B active params)
|
| 270 |
-
|
| 271 |
-
### From MCP/Agentic Web
|
| 272 |
-
|
| 273 |
-
- Standardized tool protocols
|
| 274 |
-
- Browser-native execution
|
| 275 |
-
- SSE for streaming
|
| 276 |
-
- Direct store integration
|
| 277 |
-
|
| 278 |
-
## Testing Strategy
|
| 279 |
-
|
| 280 |
-
### Unit Tests
|
| 281 |
-
|
| 282 |
-
- Individual tool validation
|
| 283 |
-
- Mock stores for isolation
|
| 284 |
-
|
| 285 |
-
### Integration Tests
|
| 286 |
-
|
| 287 |
-
- Full flow: request → tools → response
|
| 288 |
-
- Multi-tool sequences
|
| 289 |
-
- Error recovery
|
| 290 |
-
|
| 291 |
-
### User Tests
|
| 292 |
-
|
| 293 |
-
- "Add jumping platforms"
|
| 294 |
-
- "Fix the bouncing balls"
|
| 295 |
-
- "Create a coin collector"
|
| 296 |
-
|
| 297 |
-
## Deployment
|
| 298 |
-
|
| 299 |
-
### Environment Variables
|
| 300 |
-
|
| 301 |
-
```env
|
| 302 |
-
HF_TOKEN=<user_token>
|
| 303 |
-
MODEL_PRIMARY=deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B
|
| 304 |
-
MODEL_QUICK=Qwen/Qwen3-Coder-7B-Instruct
|
| 305 |
-
MAX_ITERATIONS=10
|
| 306 |
-
ITERATION_TIMEOUT=30000
|
| 307 |
-
PORT=7860
|
| 308 |
-
```
|
| 309 |
-
|
| 310 |
-
### Single Container (HuggingFace Spaces)
|
| 311 |
-
|
| 312 |
-
```dockerfile
|
| 313 |
-
FROM oven/bun:1-alpine
|
| 314 |
-
WORKDIR /app
|
| 315 |
-
COPY package.json bun.lockb ./
|
| 316 |
-
RUN bun install --frozen-lockfile
|
| 317 |
-
COPY . .
|
| 318 |
-
RUN bun run build
|
| 319 |
-
EXPOSE 7860
|
| 320 |
-
CMD ["bun", "run", "preview", "--host", "0.0.0.0", "--port", "7860"]
|
| 321 |
-
```
|
| 322 |
-
|
| 323 |
-
## Implementation Details
|
| 324 |
-
|
| 325 |
-
### Tool System Architecture
|
| 326 |
-
|
| 327 |
-
#### Pattern Recognition in Agent Responses
|
| 328 |
-
|
| 329 |
-
The agent will naturally include tool calls in its responses using a simple pattern:
|
| 330 |
-
|
| 331 |
-
```
|
| 332 |
-
[TOOL: tool_name {"param": "value"}]
|
| 333 |
-
```
|
| 334 |
-
|
| 335 |
-
Example agent response:
|
| 336 |
-
|
| 337 |
-
```
|
| 338 |
-
I'll check what's currently in your game.
|
| 339 |
-
|
| 340 |
-
[TOOL: read_game_code]
|
| 341 |
-
|
| 342 |
-
Based on the code, I can see you have a ground platform and a red ball...
|
| 343 |
-
```
|
| 344 |
-
|
| 345 |
-
#### Complete Tool Registry Implementation
|
| 346 |
-
|
| 347 |
-
```typescript
|
| 348 |
-
// src/lib/tools/registry.ts - FULL CODE TO CREATE
|
| 349 |
-
|
| 350 |
-
export interface Tool {
|
| 351 |
-
name: string;
|
| 352 |
-
description: string;
|
| 353 |
-
execute: (params: any) => Promise<ToolResult>;
|
| 354 |
-
}
|
| 355 |
-
|
| 356 |
-
export interface ToolResult {
|
| 357 |
-
success: boolean;
|
| 358 |
-
data?: any;
|
| 359 |
-
error?: string;
|
| 360 |
-
}
|
| 361 |
-
|
| 362 |
-
export interface ToolCall {
|
| 363 |
-
tool: string;
|
| 364 |
-
parameters?: any;
|
| 365 |
-
}
|
| 366 |
-
|
| 367 |
-
class ToolRegistry {
|
| 368 |
-
private tools: Map<string, Tool> = new Map();
|
| 369 |
-
|
| 370 |
-
register(tool: Tool) {
|
| 371 |
-
if (this.tools.has(tool.name)) {
|
| 372 |
-
console.warn(`Tool ${tool.name} already registered, overwriting`);
|
| 373 |
-
}
|
| 374 |
-
this.tools.set(tool.name, tool);
|
| 375 |
-
console.log(`Registered tool: ${tool.name}`);
|
| 376 |
-
}
|
| 377 |
-
|
| 378 |
-
async execute(toolName: string, params?: any): Promise<ToolResult> {
|
| 379 |
-
const tool = this.tools.get(toolName);
|
| 380 |
-
|
| 381 |
-
if (!tool) {
|
| 382 |
-
return {
|
| 383 |
-
success: false,
|
| 384 |
-
error: `Tool '${toolName}' not found. Available: ${Array.from(this.tools.keys()).join(", ")}`,
|
| 385 |
-
};
|
| 386 |
-
}
|
| 387 |
-
|
| 388 |
-
try {
|
| 389 |
-
console.log(`Executing tool: ${toolName}`, params);
|
| 390 |
-
const result = await tool.execute(params);
|
| 391 |
-
console.log(`Tool ${toolName} result:`, result);
|
| 392 |
-
return result;
|
| 393 |
-
} catch (error) {
|
| 394 |
-
console.error(`Tool ${toolName} error:`, error);
|
| 395 |
-
return {
|
| 396 |
-
success: false,
|
| 397 |
-
error: error instanceof Error ? error.message : "Unknown error",
|
| 398 |
-
};
|
| 399 |
-
}
|
| 400 |
-
}
|
| 401 |
-
|
| 402 |
-
getTools(): Tool[] {
|
| 403 |
-
return Array.from(this.tools.values());
|
| 404 |
-
}
|
| 405 |
-
|
| 406 |
-
getToolNames(): string[] {
|
| 407 |
-
return Array.from(this.tools.keys());
|
| 408 |
-
}
|
| 409 |
-
|
| 410 |
-
getToolDescriptions(): string {
|
| 411 |
-
return Array.from(this.tools.values())
|
| 412 |
-
.map((tool) => `- ${tool.name}: ${tool.description}`)
|
| 413 |
-
.join("\n");
|
| 414 |
-
}
|
| 415 |
-
}
|
| 416 |
-
|
| 417 |
-
// Global registry instance
|
| 418 |
-
export const toolRegistry = new ToolRegistry();
|
| 419 |
-
|
| 420 |
-
// Helper function to parse tool calls from agent response
|
| 421 |
-
export function parseToolCalls(response: string): ToolCall[] {
|
| 422 |
-
const toolCalls: ToolCall[] = [];
|
| 423 |
-
|
| 424 |
-
// Pattern: [TOOL: tool_name] or [TOOL: tool_name {"param": "value"}]
|
| 425 |
-
const toolPattern = /\[TOOL:\s*(\w+)(?:\s+({[^}]+}))?\]/g;
|
| 426 |
-
|
| 427 |
-
let match;
|
| 428 |
-
while ((match = toolPattern.exec(response)) !== null) {
|
| 429 |
-
const toolName = match[1];
|
| 430 |
-
const paramsStr = match[2];
|
| 431 |
-
|
| 432 |
-
let parameters = undefined;
|
| 433 |
-
if (paramsStr) {
|
| 434 |
-
try {
|
| 435 |
-
parameters = JSON.parse(paramsStr);
|
| 436 |
-
} catch (e) {
|
| 437 |
-
console.warn(`Failed to parse tool parameters: ${paramsStr}`);
|
| 438 |
-
}
|
| 439 |
-
}
|
| 440 |
-
|
| 441 |
-
toolCalls.push({ tool: toolName, parameters });
|
| 442 |
-
}
|
| 443 |
-
|
| 444 |
-
return toolCalls;
|
| 445 |
-
}
|
| 446 |
-
|
| 447 |
-
// Helper to format tool results for agent context
|
| 448 |
-
export function formatToolResult(toolName: string, result: ToolResult): string {
|
| 449 |
-
if (result.success) {
|
| 450 |
-
return `[TOOL_RESULT: ${toolName}]\n${JSON.stringify(result.data, null, 2)}\n[/TOOL_RESULT]`;
|
| 451 |
-
} else {
|
| 452 |
-
return `[TOOL_ERROR: ${toolName}]\n${result.error}\n[/TOOL_ERROR]`;
|
| 453 |
-
}
|
| 454 |
-
}
|
| 455 |
-
```
|
| 456 |
-
|
| 457 |
-
#### Tool Implementation Examples
|
| 458 |
-
|
| 459 |
-
**read-game-code.ts** (Ultra-simple first tool):
|
| 460 |
-
|
| 461 |
-
```typescript
|
| 462 |
-
import { editorStore } from "$lib/stores/editor";
|
| 463 |
-
import { get } from "svelte/store";
|
| 464 |
-
|
| 465 |
-
export const readGameCodeTool: Tool = {
|
| 466 |
-
name: "read_game_code",
|
| 467 |
-
description: "Get the current game code from the editor",
|
| 468 |
-
execute: async () => {
|
| 469 |
-
const state = get(editorStore);
|
| 470 |
-
return {
|
| 471 |
-
success: true,
|
| 472 |
-
data: {
|
| 473 |
-
content: state.content,
|
| 474 |
-
language: state.language,
|
| 475 |
-
},
|
| 476 |
-
};
|
| 477 |
-
},
|
| 478 |
-
};
|
| 479 |
-
```
|
| 480 |
-
|
| 481 |
-
**edit-game-code.ts** (Simple string replacement):
|
| 482 |
-
|
| 483 |
-
```typescript
|
| 484 |
-
export const editGameCodeTool: Tool = {
|
| 485 |
-
name: "edit_game_code",
|
| 486 |
-
description: "Edit game code using search and replace",
|
| 487 |
-
execute: async (params: { search: string; replace: string }) => {
|
| 488 |
-
const state = get(editorStore);
|
| 489 |
-
const newContent = state.content.replace(params.search, params.replace);
|
| 490 |
-
editorStore.setContent(newContent);
|
| 491 |
-
return {
|
| 492 |
-
success: true,
|
| 493 |
-
data: { modified: true, newContent },
|
| 494 |
-
};
|
| 495 |
-
},
|
| 496 |
-
};
|
| 497 |
-
```
|
| 498 |
-
|
| 499 |
-
#### Agent Runner Updates
|
| 500 |
-
|
| 501 |
-
Add to `src/lib/server/agent-runner.ts`:
|
| 502 |
-
|
| 503 |
-
```typescript
|
| 504 |
-
import { parseToolCalls, toolRegistry, formatToolResult } from '../tools/registry';
|
| 505 |
-
|
| 506 |
-
async processMessage(message: string, onStream?: (chunk: string) => void) {
|
| 507 |
-
// ... existing code ...
|
| 508 |
-
|
| 509 |
-
// After getting response, check for tool calls
|
| 510 |
-
const toolCalls = parseToolCalls(fullResponse);
|
| 511 |
-
|
| 512 |
-
if (toolCalls.length > 0) {
|
| 513 |
-
// Execute tools and add results to context
|
| 514 |
-
for (const call of toolCalls) {
|
| 515 |
-
const result = await toolRegistry.execute(call.tool, call.parameters);
|
| 516 |
-
const formatted = formatToolResult(call.tool, result);
|
| 517 |
-
|
| 518 |
-
// Add to message history for context
|
| 519 |
-
this.messageHistory.push({
|
| 520 |
-
id: this.generateId(),
|
| 521 |
-
role: 'system',
|
| 522 |
-
content: formatted,
|
| 523 |
-
timestamp: Date.now()
|
| 524 |
-
});
|
| 525 |
-
}
|
| 526 |
-
|
| 527 |
-
// Continue conversation with tool results
|
| 528 |
-
// (In phase 2, we'll make this recursive for self-correction)
|
| 529 |
-
}
|
| 530 |
-
}
|
| 531 |
-
```
|
| 532 |
-
|
| 533 |
-
#### System Prompt Enhancement
|
| 534 |
-
|
| 535 |
-
```typescript
|
| 536 |
-
const systemPrompt = `You are an AI assistant helping to build games with VibeGame.
|
| 537 |
-
|
| 538 |
-
Available tools:
|
| 539 |
-
${toolRegistry.getToolDescriptions()}
|
| 540 |
-
|
| 541 |
-
To use a tool, include in your response: [TOOL: tool_name {"param": "value"}]
|
| 542 |
-
|
| 543 |
-
Example:
|
| 544 |
-
- To read game code: [TOOL: read_game_code]
|
| 545 |
-
- To edit: [TOOL: edit_game_code {"search": "old text", "replace": "new text"}]
|
| 546 |
-
|
| 547 |
-
Always validate changes and ensure the game remains playable.
|
| 548 |
-
Use small, incremental changes rather than large rewrites.`;
|
| 549 |
-
```
|
| 550 |
-
|
| 551 |
-
### Testing Scenarios
|
| 552 |
-
|
| 553 |
-
#### Phase 1 Test Cases
|
| 554 |
-
|
| 555 |
-
1. **"What's in the game?"**
|
| 556 |
-
- Expected: Agent uses `read_game_code`, describes scene elements
|
| 557 |
-
|
| 558 |
-
2. **"Add a blue box at position 5,2,0"**
|
| 559 |
-
- Expected: Agent uses `edit_game_code` to insert `<dynamic-part>`
|
| 560 |
-
|
| 561 |
-
3. **"Change the ball color to green"**
|
| 562 |
-
- Expected: Agent finds and replaces color attribute
|
| 563 |
-
|
| 564 |
-
4. **"What errors are in the console?"**
|
| 565 |
-
- Expected: Agent uses `read_console` tool
|
| 566 |
-
|
| 567 |
-
5. **"Check the code and fix any issues"**
|
| 568 |
-
- Expected: Agent chains multiple tools
|
| 569 |
-
|
| 570 |
-
### Current File Structure
|
| 571 |
-
|
| 572 |
-
```
|
| 573 |
-
src/lib/
|
| 574 |
-
├── server/
|
| 575 |
-
│ ├── agent-runner.ts (✅ exists - needs tool integration)
|
| 576 |
-
│ └── api.ts (✅ exists - WebSocket handler)
|
| 577 |
-
├── tools/
|
| 578 |
-
│ └── (empty directory - needs implementation)
|
| 579 |
-
└── stores/
|
| 580 |
-
├── agent.ts (✅ exists)
|
| 581 |
-
├── editor.ts (✅ exists)
|
| 582 |
-
├── console.ts (✅ exists)
|
| 583 |
-
├── game.ts (✅ exists)
|
| 584 |
-
└── ui.ts (✅ exists)
|
| 585 |
-
```
|
| 586 |
-
|
| 587 |
-
### Target File Structure After Implementation
|
| 588 |
-
|
| 589 |
-
```
|
| 590 |
-
src/lib/
|
| 591 |
-
├── server/
|
| 592 |
-
│ ├── agent-runner.ts (updated with tool execution)
|
| 593 |
-
│ └── api.ts (existing WebSocket handler)
|
| 594 |
-
├── tools/
|
| 595 |
-
│ ├── registry.ts (to create)
|
| 596 |
-
│ ├── read-game-code.ts (to create)
|
| 597 |
-
│ ├── edit-game-code.ts (to create)
|
| 598 |
-
│ ├── read-console.ts (to create)
|
| 599 |
-
│ └── index.ts (to create - exports all tools)
|
| 600 |
-
└── stores/
|
| 601 |
-
└── (all existing)
|
| 602 |
-
```
|
| 603 |
-
|
| 604 |
-
## Next Immediate Steps
|
| 605 |
-
|
| 606 |
-
1. ✅ Update PLAN.md with implementation details
|
| 607 |
-
2. ⏳ Create tool registry (`src/lib/tools/registry.ts`)
|
| 608 |
-
3. ⏳ Implement read-game-code tool
|
| 609 |
-
4. ⏳ Update agent-runner for tool execution
|
| 610 |
-
5. ⏳ Test in UI with "What's in the game?"
|
| 611 |
-
|
| 612 |
-
## Success Criteria
|
| 613 |
-
|
| 614 |
-
### Week 1
|
| 615 |
-
|
| 616 |
-
- [ ] Basic tools working
|
| 617 |
-
- [ ] Agent can modify games
|
| 618 |
-
- [ ] Console feedback visible
|
| 619 |
-
|
| 620 |
-
### Week 2
|
| 621 |
-
|
| 622 |
-
- [ ] Error detection working
|
| 623 |
-
- [ ] Self-correction attempts
|
| 624 |
-
- [ ] Higher success rate
|
| 625 |
-
|
| 626 |
-
### Week 3
|
| 627 |
-
|
| 628 |
-
- [ ] Fast response times
|
| 629 |
-
- [ ] Advanced features
|
| 630 |
-
- [ ] Production ready
|
| 631 |
-
|
| 632 |
-
## Model Configuration Details
|
| 633 |
-
|
| 634 |
-
### HuggingFace Inference API Setup
|
| 635 |
-
|
| 636 |
-
#### Available Models (via Serverless API)
|
| 637 |
-
|
| 638 |
-
```typescript
|
| 639 |
-
// Phase 1: Simple model
|
| 640 |
-
const MODELS = {
|
| 641 |
-
primary: "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B",
|
| 642 |
-
|
| 643 |
-
// Phase 2: Fallback chain
|
| 644 |
-
fallback: "Qwen/Qwen2.5-Coder-7B-Instruct",
|
| 645 |
-
|
| 646 |
-
// Phase 3: Thinking models (require special handling)
|
| 647 |
-
thinking: {
|
| 648 |
-
ernie: "baidu/ERNIE-4.5-21B-A3B-Thinking", // Apache 2.0, 128K context
|
| 649 |
-
qwen: "Qwen/Qwen3-Next-80B-A3B-Thinking", // Longer thinking chains
|
| 650 |
-
},
|
| 651 |
-
};
|
| 652 |
-
```
|
| 653 |
-
|
| 654 |
-
#### Thinking Model Integration
|
| 655 |
-
|
| 656 |
-
For thinking models, parse and display reasoning:
|
| 657 |
-
|
| 658 |
-
```typescript
|
| 659 |
-
// Response may contain thinking process
|
| 660 |
-
const thinkingPattern = /<thinking>(.*?)<\/thinking>/s;
|
| 661 |
-
const match = response.match(thinkingPattern);
|
| 662 |
-
if (match) {
|
| 663 |
-
// Store thinking for UI display
|
| 664 |
-
const thinking = match[1];
|
| 665 |
-
// Extract actual response
|
| 666 |
-
const answer = response.replace(thinkingPattern, "");
|
| 667 |
-
}
|
| 668 |
-
```
|
| 669 |
-
|
| 670 |
-
### Advanced Features Implementation
|
| 671 |
-
|
| 672 |
-
#### Server-Sent Events (SSE) Migration
|
| 673 |
-
|
| 674 |
-
Replace WebSocket with SSE for simpler streaming:
|
| 675 |
-
|
| 676 |
-
```typescript
|
| 677 |
-
// src/lib/server/sse.ts
|
| 678 |
-
export function createSSEStream(req: Request) {
|
| 679 |
-
const stream = new ReadableStream({
|
| 680 |
-
start(controller) {
|
| 681 |
-
controller.enqueue('data: {"type":"connected"}\n\n');
|
| 682 |
-
},
|
| 683 |
-
async pull(controller) {
|
| 684 |
-
// Stream agent responses
|
| 685 |
-
const chunk = await getNextChunk();
|
| 686 |
-
controller.enqueue(`data: ${JSON.stringify(chunk)}\n\n`);
|
| 687 |
-
},
|
| 688 |
-
});
|
| 689 |
-
|
| 690 |
-
return new Response(stream, {
|
| 691 |
-
headers: {
|
| 692 |
-
"Content-Type": "text/event-stream",
|
| 693 |
-
"Cache-Control": "no-cache",
|
| 694 |
-
Connection: "keep-alive",
|
| 695 |
-
},
|
| 696 |
-
});
|
| 697 |
-
}
|
| 698 |
-
```
|
| 699 |
-
|
| 700 |
-
#### Code-Based Tool Execution (Smolagents-inspired)
|
| 701 |
-
|
| 702 |
-
Allow agent to write TypeScript code for tools:
|
| 703 |
-
|
| 704 |
-
```typescript
|
| 705 |
-
// Agent generates code like:
|
| 706 |
-
const code = `
|
| 707 |
-
const editor = await tools.readGameCode();
|
| 708 |
-
const content = editor.data.content;
|
| 709 |
-
const modified = content.replace('color="#ff0000"', 'color="#00ff00"');
|
| 710 |
-
await tools.editGameCode({ search: content, replace: modified });
|
| 711 |
-
return "Changed red to green";
|
| 712 |
-
`;
|
| 713 |
-
|
| 714 |
-
// Execute in sandboxed context
|
| 715 |
-
const sandbox = {
|
| 716 |
-
tools: toolRegistry,
|
| 717 |
-
console: { log: (...args) => consoleStore.addMessage("log", args.join(" ")) },
|
| 718 |
-
};
|
| 719 |
-
const result = await executeInSandbox(code, sandbox);
|
| 720 |
-
```
|
| 721 |
-
|
| 722 |
-
#### Composite Model Router
|
| 723 |
-
|
| 724 |
-
Route requests based on complexity:
|
| 725 |
-
|
| 726 |
-
```typescript
|
| 727 |
-
class ModelRouter {
|
| 728 |
-
async route(message: string, context: AgentMessage[]): Promise<string> {
|
| 729 |
-
const complexity = this.assessComplexity(message);
|
| 730 |
-
|
| 731 |
-
if (complexity === "simple") {
|
| 732 |
-
// Text changes, color modifications
|
| 733 |
-
return this.quickEditModel(message);
|
| 734 |
-
} else if (complexity === "complex") {
|
| 735 |
-
// Multi-step reasoning, debugging
|
| 736 |
-
return this.thinkingModel(message);
|
| 737 |
-
} else {
|
| 738 |
-
// Default
|
| 739 |
-
return this.primaryModel(message);
|
| 740 |
-
}
|
| 741 |
-
}
|
| 742 |
-
|
| 743 |
-
private assessComplexity(message: string): "simple" | "medium" | "complex" {
|
| 744 |
-
// Simple heuristics
|
| 745 |
-
if (message.match(/change|color|text|rename/i)) return "simple";
|
| 746 |
-
if (message.match(/debug|fix|error|why/i)) return "complex";
|
| 747 |
-
return "medium";
|
| 748 |
-
}
|
| 749 |
-
}
|
| 750 |
-
```
|
| 751 |
-
|
| 752 |
-
## Common Patterns & Solutions
|
| 753 |
-
|
| 754 |
-
### Pattern: Adding Game Elements
|
| 755 |
-
|
| 756 |
-
```typescript
|
| 757 |
-
// User: "Add a platform at 0,3,0"
|
| 758 |
-
// Agent response with tool:
|
| 759 |
-
`I'll add a platform at position 0,3,0 for you.
|
| 760 |
-
|
| 761 |
-
[TOOL: edit_game_code {"search": "</world>", "replace": " <static-part pos=\"0 3 0\" shape=\"box\" size=\"3 0.5 3\" color=\"#808080\"></static-part>\n</world>"}]
|
| 762 |
-
|
| 763 |
-
I've added a gray platform at position (0, 3, 0).`;
|
| 764 |
-
```
|
| 765 |
-
|
| 766 |
-
### Pattern: Debugging Errors
|
| 767 |
-
|
| 768 |
-
```typescript
|
| 769 |
-
// User: "The player keeps falling"
|
| 770 |
-
// Agent workflow:
|
| 771 |
-
1. [TOOL: read_game_code] - Check for ground
|
| 772 |
-
2. [TOOL: read_console] - Check for physics errors
|
| 773 |
-
3. Identify: No ground platform
|
| 774 |
-
4. [TOOL: edit_game_code] - Add ground
|
| 775 |
-
5. [TOOL: validate_game] - Verify fix
|
| 776 |
-
```
|
| 777 |
-
|
| 778 |
-
### Pattern: Complex Modifications
|
| 779 |
-
|
| 780 |
-
```typescript
|
| 781 |
-
// User: "Make this into a platformer"
|
| 782 |
-
// Agent uses template:
|
| 783 |
-
const platformerTemplate = `
|
| 784 |
-
<!-- Platforms -->
|
| 785 |
-
<static-part pos="-5 2 0" shape="box" size="3 0.5 3" color="#808080"></static-part>
|
| 786 |
-
<static-part pos="0 4 0" shape="box" size="3 0.5 3" color="#808080"></static-part>
|
| 787 |
-
<static-part pos="5 6 0" shape="box" size="3 0.5 3" color="#808080"></static-part>
|
| 788 |
-
|
| 789 |
-
<!-- Moving platform -->
|
| 790 |
-
<kinematic-part pos="0 3 5" shape="box" size="4 0.5 4" color="#4169e1">
|
| 791 |
-
<tween target="body.pos-x" from="-10" to="10" duration="5" loop="ping-pong"></tween>
|
| 792 |
-
</kinematic-part>
|
| 793 |
-
`;
|
| 794 |
-
```
|
| 795 |
-
|
| 796 |
-
## Troubleshooting Guide
|
| 797 |
-
|
| 798 |
-
### Issue: Agent doesn't use tools
|
| 799 |
-
|
| 800 |
-
**Solution**: Enhance system prompt with examples
|
| 801 |
-
|
| 802 |
-
```typescript
|
| 803 |
-
const systemPrompt = `
|
| 804 |
-
IMPORTANT: You have tools available. Use them!
|
| 805 |
-
|
| 806 |
-
Examples of when to use tools:
|
| 807 |
-
- User asks "what's in the game?" → Use [TOOL: read_game_code]
|
| 808 |
-
- User says "add a box" → Use [TOOL: edit_game_code]
|
| 809 |
-
- User mentions "error" → Use [TOOL: read_console]
|
| 810 |
-
`;
|
| 811 |
-
```
|
| 812 |
-
|
| 813 |
-
### Issue: Tool execution fails
|
| 814 |
-
|
| 815 |
-
**Solution**: Add error recovery
|
| 816 |
-
|
| 817 |
-
```typescript
|
| 818 |
-
try {
|
| 819 |
-
const result = await tool.execute(params);
|
| 820 |
-
if (!result.success) {
|
| 821 |
-
// Retry with adjusted parameters
|
| 822 |
-
const adjusted = this.adjustParameters(params, result.error);
|
| 823 |
-
return await tool.execute(adjusted);
|
| 824 |
-
}
|
| 825 |
-
} catch (error) {
|
| 826 |
-
// Fallback to manual instruction
|
| 827 |
-
return {
|
| 828 |
-
success: false,
|
| 829 |
-
error: `Tool failed. Manual fix: ${this.getManualInstructions(tool.name)}`,
|
| 830 |
-
};
|
| 831 |
-
}
|
| 832 |
-
```
|
| 833 |
-
|
| 834 |
-
### Issue: Context overflow
|
| 835 |
-
|
| 836 |
-
**Solution**: Implement sliding window
|
| 837 |
-
|
| 838 |
-
```typescript
|
| 839 |
-
class ContextManager {
|
| 840 |
-
private maxTokens = 4000;
|
| 841 |
-
|
| 842 |
-
summarize(messages: AgentMessage[]): AgentMessage[] {
|
| 843 |
-
if (this.countTokens(messages) < this.maxTokens) {
|
| 844 |
-
return messages;
|
| 845 |
-
}
|
| 846 |
-
|
| 847 |
-
// Keep system, first user, and last N messages
|
| 848 |
-
const summary = this.createSummary(messages.slice(1, -5));
|
| 849 |
-
return [
|
| 850 |
-
messages[0], // system
|
| 851 |
-
{ role: "system", content: `Previous conversation summary: ${summary}` },
|
| 852 |
-
...messages.slice(-5), // recent context
|
| 853 |
-
];
|
| 854 |
-
}
|
| 855 |
-
}
|
| 856 |
-
```
|
| 857 |
-
|
| 858 |
-
## Resources
|
| 859 |
-
|
| 860 |
-
- [HuggingFace Inference API](https://huggingface.co/docs/api-inference/index)
|
| 861 |
-
- [MCP Specification](https://modelcontextprotocol.io/specification)
|
| 862 |
-
- [VibeGame Docs](llms.txt)
|
| 863 |
-
- [Smolagents](https://github.com/huggingface/smolagents)
|
| 864 |
-
- [AI SDK](https://ai-sdk.dev)
|
| 865 |
-
- [ERNIE-4.5 Model](https://huggingface.co/baidu/ERNIE-4.5-21B-A3B-Thinking)
|
| 866 |
-
- [Qwen3-Next Model](https://huggingface.co/Qwen/Qwen3-Next-80B-A3B-Thinking)
|
| 867 |
-
|
| 868 |
-
---
|
| 869 |
-
|
| 870 |
-
_This plan provides a complete roadmap for implementing an AI agent system for VibeGame game development. Start with Phase 1 for immediate value, then progressively add sophistication. Each iteration is designed to be completed in hours, not days, with immediate testing in the UI._
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bun.lock
CHANGED
|
@@ -6,8 +6,10 @@
|
|
| 6 |
"dependencies": {
|
| 7 |
"@huggingface/hub": "^2.6.3",
|
| 8 |
"@huggingface/inference": "^4.8.0",
|
|
|
|
| 9 |
"@types/node": "^24.3.3",
|
| 10 |
"gsap": "^3.13.0",
|
|
|
|
| 11 |
"monaco-editor": "^0.50.0",
|
| 12 |
"svelte-splitpanes": "^8.0.5",
|
| 13 |
"vibegame": "^0.1.1",
|
|
@@ -15,6 +17,7 @@
|
|
| 15 |
"devDependencies": {
|
| 16 |
"@eslint/js": "^9.33.0",
|
| 17 |
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
|
|
|
| 18 |
"@types/ws": "^8.18.1",
|
| 19 |
"@typescript-eslint/eslint-plugin": "^8.40.0",
|
| 20 |
"@typescript-eslint/parser": "^8.40.0",
|
|
@@ -180,16 +183,22 @@
|
|
| 180 |
|
| 181 |
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/[email protected]", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.0" } }, "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg=="],
|
| 182 |
|
|
|
|
|
|
|
| 183 |
"@types/estree": ["@types/[email protected]", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
| 184 |
|
| 185 |
"@types/json-schema": ["@types/[email protected]", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
| 186 |
|
| 187 |
"@types/json5": ["@types/[email protected]", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
|
| 188 |
|
|
|
|
|
|
|
| 189 |
"@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw=="],
|
| 190 |
|
| 191 |
"@types/pug": ["@types/[email protected]", "", {}, "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA=="],
|
| 192 |
|
|
|
|
|
|
|
| 193 |
"@types/ws": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
| 194 |
|
| 195 |
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/[email protected]", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/type-utils": "8.43.0", "@typescript-eslint/utils": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.43.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ=="],
|
|
@@ -258,6 +267,8 @@
|
|
| 258 |
|
| 259 |
"buffer-crc32": ["[email protected]", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="],
|
| 260 |
|
|
|
|
|
|
|
| 261 |
"call-bind": ["[email protected]", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
| 262 |
|
| 263 |
"call-bind-apply-helpers": ["[email protected]", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
|
@@ -284,6 +295,8 @@
|
|
| 284 |
|
| 285 |
"css-tree": ["[email protected]", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="],
|
| 286 |
|
|
|
|
|
|
|
| 287 |
"data-view-buffer": ["[email protected]", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
| 288 |
|
| 289 |
"data-view-byte-length": ["[email protected]", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
|
|
@@ -526,6 +539,8 @@
|
|
| 526 |
|
| 527 |
"magic-string": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
|
| 528 |
|
|
|
|
|
|
|
| 529 |
"math-intrinsics": ["[email protected]", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
| 530 |
|
| 531 |
"mdn-data": ["[email protected]", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="],
|
|
|
|
| 6 |
"dependencies": {
|
| 7 |
"@huggingface/hub": "^2.6.3",
|
| 8 |
"@huggingface/inference": "^4.8.0",
|
| 9 |
+
"@types/marked": "^6.0.0",
|
| 10 |
"@types/node": "^24.3.3",
|
| 11 |
"gsap": "^3.13.0",
|
| 12 |
+
"marked": "^16.2.1",
|
| 13 |
"monaco-editor": "^0.50.0",
|
| 14 |
"svelte-splitpanes": "^8.0.5",
|
| 15 |
"vibegame": "^0.1.1",
|
|
|
|
| 17 |
"devDependencies": {
|
| 18 |
"@eslint/js": "^9.33.0",
|
| 19 |
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
| 20 |
+
"@types/bun": "^1.2.21",
|
| 21 |
"@types/ws": "^8.18.1",
|
| 22 |
"@typescript-eslint/eslint-plugin": "^8.40.0",
|
| 23 |
"@typescript-eslint/parser": "^8.40.0",
|
|
|
|
| 183 |
|
| 184 |
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/[email protected]", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.0" } }, "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg=="],
|
| 185 |
|
| 186 |
+
"@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
|
| 187 |
+
|
| 188 |
"@types/estree": ["@types/[email protected]", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
| 189 |
|
| 190 |
"@types/json-schema": ["@types/[email protected]", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
| 191 |
|
| 192 |
"@types/json5": ["@types/[email protected]", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
|
| 193 |
|
| 194 |
+
"@types/marked": ["@types/[email protected]", "", { "dependencies": { "marked": "*" } }, "sha512-jmjpa4BwUsmhxcfsgUit/7A9KbrC48Q0q8KvnY107ogcjGgTFDlIL3RpihNpx2Mu1hM4mdFQjoVc4O6JoGKHsA=="],
|
| 195 |
+
|
| 196 |
"@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw=="],
|
| 197 |
|
| 198 |
"@types/pug": ["@types/[email protected]", "", {}, "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA=="],
|
| 199 |
|
| 200 |
+
"@types/react": ["@types/[email protected]", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="],
|
| 201 |
+
|
| 202 |
"@types/ws": ["@types/[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
| 203 |
|
| 204 |
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/[email protected]", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/type-utils": "8.43.0", "@typescript-eslint/utils": "8.43.0", "@typescript-eslint/visitor-keys": "8.43.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.43.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ=="],
|
|
|
|
| 267 |
|
| 268 |
"buffer-crc32": ["[email protected]", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="],
|
| 269 |
|
| 270 |
+
"bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
|
| 271 |
+
|
| 272 |
"call-bind": ["[email protected]", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
| 273 |
|
| 274 |
"call-bind-apply-helpers": ["[email protected]", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
|
|
|
| 295 |
|
| 296 |
"css-tree": ["[email protected]", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="],
|
| 297 |
|
| 298 |
+
"csstype": ["[email protected]", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
| 299 |
+
|
| 300 |
"data-view-buffer": ["[email protected]", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
| 301 |
|
| 302 |
"data-view-byte-length": ["[email protected]", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
|
|
|
|
| 539 |
|
| 540 |
"magic-string": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
|
| 541 |
|
| 542 |
+
"marked": ["[email protected]", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-r3UrXED9lMlHF97jJByry90cwrZBBvZmjG1L68oYfuPMW+uDTnuMbyJDymCWwbTE+f+3LhpNDKfpR3a3saFyjA=="],
|
| 543 |
+
|
| 544 |
"math-intrinsics": ["[email protected]", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
| 545 |
|
| 546 |
"mdn-data": ["[email protected]", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="],
|
favicon.ico
ADDED
|
|
index.html
CHANGED
|
@@ -3,7 +3,8 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
-
<
|
|
|
|
| 7 |
<style>
|
| 8 |
body {
|
| 9 |
margin: 0;
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<link rel="icon" href="/favicon.ico" />
|
| 7 |
+
<title>VibeGame</title>
|
| 8 |
<style>
|
| 9 |
body {
|
| 10 |
margin: 0;
|
package.json
CHANGED
|
@@ -20,6 +20,7 @@
|
|
| 20 |
"devDependencies": {
|
| 21 |
"@eslint/js": "^9.33.0",
|
| 22 |
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
|
|
|
| 23 |
"@types/ws": "^8.18.1",
|
| 24 |
"@typescript-eslint/eslint-plugin": "^8.40.0",
|
| 25 |
"@typescript-eslint/parser": "^8.40.0",
|
|
@@ -37,8 +38,10 @@
|
|
| 37 |
"dependencies": {
|
| 38 |
"@huggingface/hub": "^2.6.3",
|
| 39 |
"@huggingface/inference": "^4.8.0",
|
|
|
|
| 40 |
"@types/node": "^24.3.3",
|
| 41 |
"gsap": "^3.13.0",
|
|
|
|
| 42 |
"monaco-editor": "^0.50.0",
|
| 43 |
"svelte-splitpanes": "^8.0.5",
|
| 44 |
"vibegame": "^0.1.1"
|
|
|
|
| 20 |
"devDependencies": {
|
| 21 |
"@eslint/js": "^9.33.0",
|
| 22 |
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
| 23 |
+
"@types/bun": "^1.2.21",
|
| 24 |
"@types/ws": "^8.18.1",
|
| 25 |
"@typescript-eslint/eslint-plugin": "^8.40.0",
|
| 26 |
"@typescript-eslint/parser": "^8.40.0",
|
|
|
|
| 38 |
"dependencies": {
|
| 39 |
"@huggingface/hub": "^2.6.3",
|
| 40 |
"@huggingface/inference": "^4.8.0",
|
| 41 |
+
"@types/marked": "^6.0.0",
|
| 42 |
"@types/node": "^24.3.3",
|
| 43 |
"gsap": "^3.13.0",
|
| 44 |
+
"marked": "^16.2.1",
|
| 45 |
"monaco-editor": "^0.50.0",
|
| 46 |
"svelte-splitpanes": "^8.0.5",
|
| 47 |
"vibegame": "^0.1.1"
|
src/lib/components/chat/ChatPanel.svelte
CHANGED
|
@@ -3,6 +3,9 @@
|
|
| 3 |
import { agentStore, isConnected, isProcessing } from "../../stores/agent";
|
| 4 |
import { authStore } from "../../services/auth";
|
| 5 |
import gsap from "gsap";
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
let inputValue = "";
|
| 8 |
let messagesContainer: HTMLDivElement;
|
|
@@ -12,6 +15,40 @@
|
|
| 12 |
|
| 13 |
let hasConnected = false;
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
onMount(() => {
|
| 16 |
if ($authStore.isAuthenticated && !$authStore.loading) {
|
| 17 |
agentStore.connect();
|
|
@@ -162,10 +199,24 @@
|
|
| 162 |
{/if}
|
| 163 |
{#each $agentStore.messages as message}
|
| 164 |
<div class="message {message.role}">
|
|
|
|
|
|
|
|
|
|
| 165 |
<div class="message-content">
|
| 166 |
-
{message.
|
| 167 |
-
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
{/if}
|
| 170 |
</div>
|
| 171 |
</div>
|
|
@@ -232,7 +283,7 @@
|
|
| 232 |
.message {
|
| 233 |
padding: 0.4rem 0.6rem;
|
| 234 |
border-radius: 3px;
|
| 235 |
-
font-size: 0.
|
| 236 |
animation: fadeIn 0.2s ease-out;
|
| 237 |
}
|
| 238 |
|
|
@@ -260,13 +311,16 @@
|
|
| 260 |
}
|
| 261 |
|
| 262 |
.message-content {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
color: rgba(255, 255, 255, 0.9);
|
| 264 |
line-height: 1.5;
|
| 265 |
white-space: pre-line;
|
| 266 |
word-wrap: break-word;
|
| 267 |
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Monaco", "Menlo", monospace;
|
| 268 |
-
margin: 0;
|
| 269 |
-
display: block;
|
| 270 |
}
|
| 271 |
|
| 272 |
.cursor {
|
|
@@ -290,7 +344,7 @@
|
|
| 290 |
border-radius: 3px;
|
| 291 |
padding: 0.4rem 0.6rem;
|
| 292 |
color: #ff9999;
|
| 293 |
-
font-size: 0.
|
| 294 |
animation: fadeIn 0.2s ease-out;
|
| 295 |
}
|
| 296 |
|
|
@@ -311,7 +365,7 @@
|
|
| 311 |
padding: 0.4rem 0.6rem;
|
| 312 |
resize: none;
|
| 313 |
font-family: "Monaco", "Menlo", monospace;
|
| 314 |
-
font-size: 0.
|
| 315 |
line-height: 1.2;
|
| 316 |
}
|
| 317 |
|
|
@@ -408,7 +462,7 @@
|
|
| 408 |
|
| 409 |
.ready-message {
|
| 410 |
color: rgba(255, 255, 255, 0.2);
|
| 411 |
-
font-size: 0.
|
| 412 |
text-align: center;
|
| 413 |
font-style: italic;
|
| 414 |
display: flex;
|
|
|
|
| 3 |
import { agentStore, isConnected, isProcessing } from "../../stores/agent";
|
| 4 |
import { authStore } from "../../services/auth";
|
| 5 |
import gsap from "gsap";
|
| 6 |
+
import ReasoningBlock from "./ReasoningBlock.svelte";
|
| 7 |
+
import MarkdownRenderer from "./MarkdownRenderer.svelte";
|
| 8 |
+
import ToolCallDisplay from "./ToolCallDisplay.svelte";
|
| 9 |
|
| 10 |
let inputValue = "";
|
| 11 |
let messagesContainer: HTMLDivElement;
|
|
|
|
| 15 |
|
| 16 |
let hasConnected = false;
|
| 17 |
|
| 18 |
+
const TOOL_PATTERN = /\[TOOL:\s*(\w+)(?:\s+({[^}]+}))?\]/g;
|
| 19 |
+
|
| 20 |
+
function parseMessageContent(content: string) {
|
| 21 |
+
const parts: Array<{ type: 'text' | 'tool', content: string, toolName?: string, params?: any }> = [];
|
| 22 |
+
let lastIndex = 0;
|
| 23 |
+
|
| 24 |
+
const matches = Array.from(content.matchAll(TOOL_PATTERN));
|
| 25 |
+
|
| 26 |
+
for (const match of matches) {
|
| 27 |
+
const [fullMatch, toolName, paramsStr] = match;
|
| 28 |
+
const index = match.index!;
|
| 29 |
+
|
| 30 |
+
if (index > lastIndex) {
|
| 31 |
+
parts.push({ type: 'text', content: content.slice(lastIndex, index) });
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
let params = null;
|
| 35 |
+
if (paramsStr) {
|
| 36 |
+
try {
|
| 37 |
+
params = JSON.parse(paramsStr);
|
| 38 |
+
} catch {}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
parts.push({ type: 'tool', content: fullMatch, toolName, params });
|
| 42 |
+
lastIndex = index + fullMatch.length;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
if (lastIndex < content.length) {
|
| 46 |
+
parts.push({ type: 'text', content: content.slice(lastIndex) });
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
return parts.length > 0 ? parts : [{ type: 'text' as const, content }];
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
onMount(() => {
|
| 53 |
if ($authStore.isAuthenticated && !$authStore.loading) {
|
| 54 |
agentStore.connect();
|
|
|
|
| 199 |
{/if}
|
| 200 |
{#each $agentStore.messages as message}
|
| 201 |
<div class="message {message.role}">
|
| 202 |
+
{#if message.reasoning && message.role === "assistant"}
|
| 203 |
+
<ReasoningBlock reasoning={message.reasoning} />
|
| 204 |
+
{/if}
|
| 205 |
<div class="message-content">
|
| 206 |
+
{#if message.role === "assistant"}
|
| 207 |
+
{@const parts = parseMessageContent(message.content.trim())}
|
| 208 |
+
{#each parts as part}
|
| 209 |
+
{#if part.type === 'text' && part.content.trim()}
|
| 210 |
+
<MarkdownRenderer content={part.content} streaming={false} />
|
| 211 |
+
{:else if part.type === 'tool' && part.toolName}
|
| 212 |
+
<ToolCallDisplay toolName={part.toolName} parameters={part.params} />
|
| 213 |
+
{/if}
|
| 214 |
+
{/each}
|
| 215 |
+
{#if message.streaming}
|
| 216 |
+
<span class="cursor">▊</span>
|
| 217 |
+
{/if}
|
| 218 |
+
{:else}
|
| 219 |
+
{message.content.trim()}
|
| 220 |
{/if}
|
| 221 |
</div>
|
| 222 |
</div>
|
|
|
|
| 283 |
.message {
|
| 284 |
padding: 0.4rem 0.6rem;
|
| 285 |
border-radius: 3px;
|
| 286 |
+
font-size: 0.875rem;
|
| 287 |
animation: fadeIn 0.2s ease-out;
|
| 288 |
}
|
| 289 |
|
|
|
|
| 311 |
}
|
| 312 |
|
| 313 |
.message-content {
|
| 314 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Monaco", "Menlo", monospace;
|
| 315 |
+
margin: 0;
|
| 316 |
+
display: block;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.message.user .message-content {
|
| 320 |
color: rgba(255, 255, 255, 0.9);
|
| 321 |
line-height: 1.5;
|
| 322 |
white-space: pre-line;
|
| 323 |
word-wrap: break-word;
|
|
|
|
|
|
|
|
|
|
| 324 |
}
|
| 325 |
|
| 326 |
.cursor {
|
|
|
|
| 344 |
border-radius: 3px;
|
| 345 |
padding: 0.4rem 0.6rem;
|
| 346 |
color: #ff9999;
|
| 347 |
+
font-size: 0.875rem;
|
| 348 |
animation: fadeIn 0.2s ease-out;
|
| 349 |
}
|
| 350 |
|
|
|
|
| 365 |
padding: 0.4rem 0.6rem;
|
| 366 |
resize: none;
|
| 367 |
font-family: "Monaco", "Menlo", monospace;
|
| 368 |
+
font-size: 0.875rem;
|
| 369 |
line-height: 1.2;
|
| 370 |
}
|
| 371 |
|
|
|
|
| 462 |
|
| 463 |
.ready-message {
|
| 464 |
color: rgba(255, 255, 255, 0.2);
|
| 465 |
+
font-size: 0.875rem;
|
| 466 |
text-align: center;
|
| 467 |
font-style: italic;
|
| 468 |
display: flex;
|
src/lib/components/chat/MarkdownRenderer.svelte
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>`;
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
renderer.link = ({ href, title, tokens }) => {
|
| 17 |
+
const firstToken = tokens?.[0];
|
| 18 |
+
const text = (firstToken && 'text' in firstToken) ? firstToken.text : '';
|
| 19 |
+
return `<a href="${href}" target="_blank" rel="noopener noreferrer" title="${title || text}">${text}</a>`;
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
function escapeHtml(text: string): string {
|
| 23 |
+
const div = document.createElement("div");
|
| 24 |
+
div.textContent = text;
|
| 25 |
+
return div.innerHTML;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
marked.setOptions({
|
| 29 |
+
renderer,
|
| 30 |
+
breaks: true,
|
| 31 |
+
gfm: true,
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
$: if (content) {
|
| 35 |
+
try {
|
| 36 |
+
htmlContent = marked.parse(content) as string;
|
| 37 |
+
} catch {
|
| 38 |
+
htmlContent = escapeHtml(content);
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
</script>
|
| 42 |
+
|
| 43 |
+
<div class="markdown-content">
|
| 44 |
+
{@html htmlContent}
|
| 45 |
+
{#if streaming}
|
| 46 |
+
<span class="cursor">▊</span>
|
| 47 |
+
{/if}
|
| 48 |
+
</div>
|
| 49 |
+
|
| 50 |
+
<style>
|
| 51 |
+
.markdown-content {
|
| 52 |
+
color: rgba(255, 255, 255, 0.9);
|
| 53 |
+
line-height: 1.6;
|
| 54 |
+
word-wrap: break-word;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.markdown-content :global(p) {
|
| 58 |
+
margin: 0 0 0.5rem 0;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.markdown-content :global(p:last-child) {
|
| 62 |
+
margin-bottom: 0;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.markdown-content :global(h1),
|
| 66 |
+
.markdown-content :global(h2),
|
| 67 |
+
.markdown-content :global(h3),
|
| 68 |
+
.markdown-content :global(h4),
|
| 69 |
+
.markdown-content :global(h5),
|
| 70 |
+
.markdown-content :global(h6) {
|
| 71 |
+
margin: 0.75rem 0 0.5rem 0;
|
| 72 |
+
font-weight: 600;
|
| 73 |
+
color: rgba(255, 255, 255, 1);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.markdown-content :global(h1) {
|
| 77 |
+
font-size: 1.25rem;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.markdown-content :global(h2) {
|
| 81 |
+
font-size: 1.125rem;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.markdown-content :global(h3) {
|
| 85 |
+
font-size: 1rem;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.markdown-content :global(pre) {
|
| 89 |
+
background: rgba(0, 0, 0, 0.4);
|
| 90 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 91 |
+
border-radius: 4px;
|
| 92 |
+
padding: 0.75rem;
|
| 93 |
+
margin: 0.5rem 0;
|
| 94 |
+
overflow-x: auto;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.markdown-content :global(code) {
|
| 98 |
+
font-family: "Monaco", "Menlo", "Consolas", monospace;
|
| 99 |
+
font-size: 0.85rem;
|
| 100 |
+
background: rgba(0, 0, 0, 0.3);
|
| 101 |
+
padding: 0.125rem 0.25rem;
|
| 102 |
+
border-radius: 3px;
|
| 103 |
+
color: rgba(255, 210, 30, 0.9);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.markdown-content :global(pre code) {
|
| 107 |
+
background: none;
|
| 108 |
+
padding: 0;
|
| 109 |
+
color: rgba(255, 255, 255, 0.9);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.markdown-content :global(ul),
|
| 113 |
+
.markdown-content :global(ol) {
|
| 114 |
+
margin: 0.5rem 0;
|
| 115 |
+
padding-left: 1.5rem;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.markdown-content :global(li) {
|
| 119 |
+
margin: 0.25rem 0;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.markdown-content :global(blockquote) {
|
| 123 |
+
border-left: 3px solid rgba(255, 255, 255, 0.3);
|
| 124 |
+
padding-left: 0.75rem;
|
| 125 |
+
margin: 0.5rem 0;
|
| 126 |
+
color: rgba(255, 255, 255, 0.7);
|
| 127 |
+
font-style: italic;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.markdown-content :global(a) {
|
| 131 |
+
color: rgba(65, 105, 225, 0.9);
|
| 132 |
+
text-decoration: none;
|
| 133 |
+
border-bottom: 1px solid transparent;
|
| 134 |
+
transition: border-color 0.2s;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.markdown-content :global(a:hover) {
|
| 138 |
+
border-bottom-color: rgba(65, 105, 225, 0.5);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.markdown-content :global(strong) {
|
| 142 |
+
font-weight: 600;
|
| 143 |
+
color: rgba(255, 255, 255, 1);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.markdown-content :global(em) {
|
| 147 |
+
font-style: italic;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
.markdown-content :global(hr) {
|
| 151 |
+
border: none;
|
| 152 |
+
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
| 153 |
+
margin: 1rem 0;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.markdown-content :global(table) {
|
| 157 |
+
border-collapse: collapse;
|
| 158 |
+
width: 100%;
|
| 159 |
+
margin: 0.5rem 0;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.markdown-content :global(th),
|
| 163 |
+
.markdown-content :global(td) {
|
| 164 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 165 |
+
padding: 0.5rem;
|
| 166 |
+
text-align: left;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.markdown-content :global(th) {
|
| 170 |
+
background: rgba(0, 0, 0, 0.3);
|
| 171 |
+
font-weight: 600;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.cursor {
|
| 175 |
+
animation: blink 1s infinite;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
@keyframes blink {
|
| 179 |
+
0%,
|
| 180 |
+
50% {
|
| 181 |
+
opacity: 1;
|
| 182 |
+
}
|
| 183 |
+
51%,
|
| 184 |
+
100% {
|
| 185 |
+
opacity: 0;
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
</style>
|
src/lib/components/chat/ReasoningBlock.svelte
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
// Expand - quick but smooth
|
| 18 |
+
gsap.to(iconElement, {
|
| 19 |
+
rotation: 180,
|
| 20 |
+
duration: 0.15,
|
| 21 |
+
ease: "power2.out"
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
// Show content
|
| 25 |
+
gsap.set(contentElement, {
|
| 26 |
+
display: 'block'
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
gsap.fromTo(contentElement, {
|
| 30 |
+
opacity: 0,
|
| 31 |
+
maxHeight: 0,
|
| 32 |
+
y: -10
|
| 33 |
+
}, {
|
| 34 |
+
opacity: 1,
|
| 35 |
+
maxHeight: 500, // Large enough for content
|
| 36 |
+
y: 0,
|
| 37 |
+
duration: 0.2,
|
| 38 |
+
ease: "power2.out"
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
} else {
|
| 42 |
+
// Collapse - nearly instant
|
| 43 |
+
gsap.to(iconElement, {
|
| 44 |
+
rotation: 0,
|
| 45 |
+
duration: 0.1,
|
| 46 |
+
ease: "power2.in"
|
| 47 |
+
});
|
| 48 |
+
|
| 49 |
+
gsap.to(contentElement, {
|
| 50 |
+
opacity: 0,
|
| 51 |
+
maxHeight: 0,
|
| 52 |
+
y: -5,
|
| 53 |
+
duration: 0.1,
|
| 54 |
+
ease: "power2.in",
|
| 55 |
+
onComplete: () => {
|
| 56 |
+
gsap.set(contentElement, { display: 'none' });
|
| 57 |
+
}
|
| 58 |
+
});
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
onMount(() => {
|
| 63 |
+
if (iconElement) {
|
| 64 |
+
gsap.set(iconElement, {
|
| 65 |
+
transformOrigin: "center",
|
| 66 |
+
rotation: isExpanded ? 180 : 0
|
| 67 |
+
});
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
if (contentElement) {
|
| 71 |
+
gsap.set(contentElement, {
|
| 72 |
+
display: isExpanded ? 'block' : 'none',
|
| 73 |
+
opacity: isExpanded ? 1 : 0,
|
| 74 |
+
maxHeight: isExpanded ? 500 : 0,
|
| 75 |
+
y: isExpanded ? 0 : -10
|
| 76 |
+
});
|
| 77 |
+
}
|
| 78 |
+
});
|
| 79 |
+
</script>
|
| 80 |
+
|
| 81 |
+
<div class="reasoning-block">
|
| 82 |
+
<button
|
| 83 |
+
class="reasoning-toggle"
|
| 84 |
+
on:click={toggleExpanded}
|
| 85 |
+
title={isExpanded ? "Hide AI thinking" : "Show AI thinking"}
|
| 86 |
+
aria-expanded={isExpanded}
|
| 87 |
+
aria-label="Toggle thinking visibility"
|
| 88 |
+
>
|
| 89 |
+
<div class="toggle-content">
|
| 90 |
+
<span bind:this={iconElement} class="toggle-icon">
|
| 91 |
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
|
| 92 |
+
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
| 93 |
+
</svg>
|
| 94 |
+
</span>
|
| 95 |
+
<span class="toggle-label">
|
| 96 |
+
<span class="label-main">Thinking</span>
|
| 97 |
+
<span class="label-hint">{isExpanded ? "Click to hide" : "Click to view"}</span>
|
| 98 |
+
</span>
|
| 99 |
+
</div>
|
| 100 |
+
</button>
|
| 101 |
+
|
| 102 |
+
<div
|
| 103 |
+
bind:this={contentElement}
|
| 104 |
+
class="reasoning-content"
|
| 105 |
+
>
|
| 106 |
+
<div class="content-inner">
|
| 107 |
+
<pre class="reasoning-text">{reasoning}</pre>
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
<style>
|
| 113 |
+
.reasoning-block {
|
| 114 |
+
margin: 0.75rem 0;
|
| 115 |
+
border-radius: 8px;
|
| 116 |
+
background: linear-gradient(135deg, rgba(70, 70, 80, 0.15) 0%, rgba(60, 60, 70, 0.15) 100%);
|
| 117 |
+
border: 1px solid rgba(120, 120, 140, 0.15);
|
| 118 |
+
overflow: hidden;
|
| 119 |
+
transition: border-color 0.2s ease, background 0.2s ease;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.reasoning-block:hover {
|
| 123 |
+
border-color: rgba(140, 140, 160, 0.25);
|
| 124 |
+
background: linear-gradient(135deg, rgba(75, 75, 85, 0.2) 0%, rgba(65, 65, 75, 0.2) 100%);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.reasoning-toggle {
|
| 128 |
+
width: 100%;
|
| 129 |
+
padding: 0.75rem 1rem;
|
| 130 |
+
background: transparent;
|
| 131 |
+
border: none;
|
| 132 |
+
color: rgba(255, 255, 255, 0.7);
|
| 133 |
+
cursor: pointer;
|
| 134 |
+
font-family: inherit;
|
| 135 |
+
text-align: left;
|
| 136 |
+
transition: all 0.2s ease;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.reasoning-toggle:hover {
|
| 140 |
+
background: rgba(255, 255, 255, 0.03);
|
| 141 |
+
color: rgba(255, 255, 255, 0.9);
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.reasoning-toggle:focus-visible {
|
| 145 |
+
outline: 2px solid rgba(140, 140, 200, 0.5);
|
| 146 |
+
outline-offset: -2px;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.toggle-content {
|
| 150 |
+
display: flex;
|
| 151 |
+
align-items: center;
|
| 152 |
+
gap: 0.75rem;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.toggle-icon {
|
| 156 |
+
display: flex;
|
| 157 |
+
align-items: center;
|
| 158 |
+
justify-content: center;
|
| 159 |
+
width: 20px;
|
| 160 |
+
height: 20px;
|
| 161 |
+
border-radius: 4px;
|
| 162 |
+
background: rgba(255, 255, 255, 0.05);
|
| 163 |
+
color: rgba(255, 255, 255, 0.5);
|
| 164 |
+
transition: background 0.2s ease, color 0.2s ease;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.reasoning-toggle:hover .toggle-icon {
|
| 168 |
+
background: rgba(255, 255, 255, 0.08);
|
| 169 |
+
color: rgba(255, 255, 255, 0.7);
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.toggle-label {
|
| 173 |
+
display: flex;
|
| 174 |
+
flex-direction: column;
|
| 175 |
+
gap: 0.125rem;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
.label-main {
|
| 179 |
+
font-size: 0.875rem;
|
| 180 |
+
font-weight: 500;
|
| 181 |
+
letter-spacing: 0.01em;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.label-hint {
|
| 185 |
+
font-size: 0.7rem;
|
| 186 |
+
color: rgba(255, 255, 255, 0.4);
|
| 187 |
+
transition: color 0.2s ease;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
.reasoning-toggle:hover .label-hint {
|
| 191 |
+
color: rgba(255, 255, 255, 0.5);
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.reasoning-content {
|
| 195 |
+
border-top: 1px solid rgba(100, 100, 100, 0.15);
|
| 196 |
+
background: rgba(30, 30, 35, 0.3);
|
| 197 |
+
overflow: hidden;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.content-inner {
|
| 201 |
+
padding: 0.75rem 1rem;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.reasoning-text {
|
| 205 |
+
margin: 0;
|
| 206 |
+
padding: 0.75rem;
|
| 207 |
+
background: rgba(20, 20, 25, 0.4);
|
| 208 |
+
border-radius: 6px;
|
| 209 |
+
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
| 210 |
+
font-size: 0.75rem;
|
| 211 |
+
line-height: 1.5;
|
| 212 |
+
color: rgba(255, 255, 255, 0.6);
|
| 213 |
+
white-space: pre-wrap;
|
| 214 |
+
word-wrap: break-word;
|
| 215 |
+
max-height: 350px;
|
| 216 |
+
overflow-y: auto;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.reasoning-text::-webkit-scrollbar {
|
| 220 |
+
width: 8px;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.reasoning-text::-webkit-scrollbar-track {
|
| 224 |
+
background: rgba(30, 30, 35, 0.5);
|
| 225 |
+
border-radius: 4px;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.reasoning-text::-webkit-scrollbar-thumb {
|
| 229 |
+
background: rgba(100, 100, 110, 0.4);
|
| 230 |
+
border-radius: 4px;
|
| 231 |
+
transition: background 0.2s ease;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
.reasoning-text::-webkit-scrollbar-thumb:hover {
|
| 235 |
+
background: rgba(120, 120, 130, 0.5);
|
| 236 |
+
}
|
| 237 |
+
</style>
|
src/lib/components/chat/ToolCallDisplay.svelte
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts">
|
| 2 |
+
import { onMount } from "svelte";
|
| 3 |
+
import gsap from "gsap";
|
| 4 |
+
|
| 5 |
+
export let toolName: string;
|
| 6 |
+
export let parameters: any = null;
|
| 7 |
+
|
| 8 |
+
let element: HTMLDivElement;
|
| 9 |
+
|
| 10 |
+
onMount(() => {
|
| 11 |
+
gsap.fromTo(
|
| 12 |
+
element,
|
| 13 |
+
{ opacity: 0, scale: 0.9, y: -5 },
|
| 14 |
+
{ opacity: 1, scale: 1, y: 0, duration: 0.3, ease: "back.out(1.5)" }
|
| 15 |
+
);
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
const toolIcons: Record<string, string> = {
|
| 19 |
+
read_game_code: "📄",
|
| 20 |
+
edit_game_code: "✏️",
|
| 21 |
+
read_console: "📟",
|
| 22 |
+
clear_console: "🧹",
|
| 23 |
+
run_game: "▶️",
|
| 24 |
+
stop_game: "⏹",
|
| 25 |
+
default: "🔧",
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
$: icon = toolIcons[toolName] || toolIcons.default;
|
| 29 |
+
</script>
|
| 30 |
+
|
| 31 |
+
<div class="tool-call" bind:this={element}>
|
| 32 |
+
<span class="tool-icon">{icon}</span>
|
| 33 |
+
<span class="tool-name">{toolName}</span>
|
| 34 |
+
{#if parameters && Object.keys(parameters).length > 0}
|
| 35 |
+
<span class="tool-params">
|
| 36 |
+
{#each Object.entries(parameters) as [key, value]}
|
| 37 |
+
<span class="param">
|
| 38 |
+
<span class="param-key">{key}:</span>
|
| 39 |
+
<span class="param-value">{typeof value === 'string' && value.length > 20 ? value.substring(0, 20) + '...' : value}</span>
|
| 40 |
+
</span>
|
| 41 |
+
{/each}
|
| 42 |
+
</span>
|
| 43 |
+
{/if}
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<style>
|
| 47 |
+
.tool-call {
|
| 48 |
+
display: inline-flex;
|
| 49 |
+
align-items: center;
|
| 50 |
+
gap: 0.5rem;
|
| 51 |
+
background: rgba(65, 105, 225, 0.1);
|
| 52 |
+
border: 1px solid rgba(65, 105, 225, 0.3);
|
| 53 |
+
border-radius: 4px;
|
| 54 |
+
padding: 0.25rem 0.5rem;
|
| 55 |
+
margin: 0.25rem 0;
|
| 56 |
+
font-size: 0.8rem;
|
| 57 |
+
font-family: "Monaco", "Menlo", monospace;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.tool-icon {
|
| 61 |
+
font-size: 1rem;
|
| 62 |
+
line-height: 1;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.tool-name {
|
| 66 |
+
color: rgba(65, 105, 225, 1);
|
| 67 |
+
font-weight: 600;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.tool-params {
|
| 71 |
+
display: flex;
|
| 72 |
+
gap: 0.5rem;
|
| 73 |
+
color: rgba(255, 255, 255, 0.6);
|
| 74 |
+
font-size: 0.75rem;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.param {
|
| 78 |
+
display: flex;
|
| 79 |
+
gap: 0.25rem;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.param-key {
|
| 83 |
+
color: rgba(255, 255, 255, 0.5);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.param-value {
|
| 87 |
+
color: rgba(255, 210, 30, 0.7);
|
| 88 |
+
}
|
| 89 |
+
</style>
|
src/lib/components/chat/context.md
CHANGED
|
@@ -1,14 +1,19 @@
|
|
| 1 |
# Chat Context
|
| 2 |
|
| 3 |
-
AI
|
| 4 |
|
| 5 |
## Components
|
| 6 |
|
| 7 |
-
- `ChatPanel.svelte` -
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
## Features
|
| 10 |
|
| 11 |
-
-
|
| 12 |
-
-
|
| 13 |
-
-
|
| 14 |
-
-
|
|
|
|
|
|
|
|
|
| 1 |
# Chat Context
|
| 2 |
|
| 3 |
+
AI chat interface with markdown rendering and tool visualization.
|
| 4 |
|
| 5 |
## Components
|
| 6 |
|
| 7 |
+
- `ChatPanel.svelte` - Main chat UI with message display
|
| 8 |
+
- `ReasoningBlock.svelte` - Collapsible AI thinking viewer
|
| 9 |
+
- `MarkdownRenderer.svelte` - Markdown parser and renderer
|
| 10 |
+
- `ToolCallDisplay.svelte` - Visual badges for tool calls
|
| 11 |
|
| 12 |
## Features
|
| 13 |
|
| 14 |
+
- Real-time message streaming
|
| 15 |
+
- Markdown formatting for assistant messages
|
| 16 |
+
- Tool call visualization with icons
|
| 17 |
+
- Collapsible thinking blocks (0.1s collapse)
|
| 18 |
+
- Authentication integration
|
| 19 |
+
- GSAP animations
|
src/lib/components/layout/SplitView.svelte
CHANGED
|
@@ -1,355 +1,427 @@
|
|
| 1 |
<script lang="ts">
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
}
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
}
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
gsap.set(editorPaneEl, { flexGrow: userMainPaneSizes.editor, flexBasis: `${userMainPaneSizes.editor * 100}%` });
|
| 94 |
-
gsap.set(previewPaneEl, { flexGrow: userMainPaneSizes.preview, flexBasis: `${userMainPaneSizes.preview * 100}%` });
|
| 95 |
-
|
| 96 |
-
if (codePane && chatPane) {
|
| 97 |
-
gsap.set(codePane, { flexGrow: userEditorPaneSizes.code, flexBasis: `${userEditorPaneSizes.code * 100}%` });
|
| 98 |
-
gsap.set(chatPane, { flexGrow: userEditorPaneSizes.chat, flexBasis: `${userEditorPaneSizes.chat * 100}%` });
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
if (gamePane && consolePane) {
|
| 102 |
-
gsap.set(gamePane, { flexGrow: userPreviewPaneSizes.game, flexBasis: `${userPreviewPaneSizes.game * 100}%` });
|
| 103 |
-
gsap.set(consolePane, { flexGrow: userPreviewPaneSizes.console, flexBasis: `${userPreviewPaneSizes.console * 100}%` });
|
| 104 |
-
}
|
| 105 |
}
|
| 106 |
-
|
| 107 |
-
if (mode === 'preview') {
|
| 108 |
-
gsap.timeline({ ease: "power2.out" })
|
| 109 |
-
.to(editorPaneEl, {
|
| 110 |
-
opacity: 0,
|
| 111 |
-
duration: 0.15,
|
| 112 |
-
})
|
| 113 |
-
.to(consolePane, {
|
| 114 |
-
opacity: 0,
|
| 115 |
-
duration: 0.15,
|
| 116 |
-
}, "-=0.15")
|
| 117 |
-
.to(editorPaneEl, {
|
| 118 |
-
flexGrow: 0,
|
| 119 |
-
flexBasis: "0%",
|
| 120 |
-
duration: 0.2,
|
| 121 |
-
ease: "power2.inOut",
|
| 122 |
-
onComplete: () => {
|
| 123 |
-
editorPaneEl.style.display = 'none';
|
| 124 |
-
if (mainSplitter) mainSplitter.style.display = 'none';
|
| 125 |
-
}
|
| 126 |
-
}, "-=0.1")
|
| 127 |
-
.to(consolePane, {
|
| 128 |
-
flexGrow: 0,
|
| 129 |
-
flexBasis: "0%",
|
| 130 |
-
duration: 0.2,
|
| 131 |
-
ease: "power2.inOut",
|
| 132 |
-
onComplete: () => {
|
| 133 |
-
consolePane.style.display = 'none';
|
| 134 |
-
if (consoleSplitter) consoleSplitter.style.display = 'none';
|
| 135 |
-
}
|
| 136 |
-
}, "-=0.2")
|
| 137 |
-
.to(gamePane, {
|
| 138 |
-
flexGrow: 1,
|
| 139 |
-
flexBasis: "100%",
|
| 140 |
-
duration: 0.2,
|
| 141 |
-
ease: "power2.inOut",
|
| 142 |
-
}, "-=0.2")
|
| 143 |
-
.to(previewPaneEl, {
|
| 144 |
-
flexGrow: 1,
|
| 145 |
-
flexBasis: "100%",
|
| 146 |
-
duration: 0.2,
|
| 147 |
-
ease: "power2.inOut",
|
| 148 |
-
}, "-=0.2");
|
| 149 |
-
} else {
|
| 150 |
-
editorPaneEl.style.display = '';
|
| 151 |
-
if (mainSplitter) mainSplitter.style.display = '';
|
| 152 |
-
consolePane.style.display = '';
|
| 153 |
-
if (consoleSplitter) consoleSplitter.style.display = '';
|
| 154 |
-
|
| 155 |
-
gsap.timeline({ ease: "power2.out" })
|
| 156 |
-
.set(editorPaneEl, {
|
| 157 |
-
opacity: 0,
|
| 158 |
-
flexGrow: 0,
|
| 159 |
-
flexBasis: "0%"
|
| 160 |
-
})
|
| 161 |
-
.set(consolePane, {
|
| 162 |
-
opacity: 0,
|
| 163 |
-
flexGrow: 0,
|
| 164 |
-
flexBasis: "0%"
|
| 165 |
-
})
|
| 166 |
-
.to([editorPaneEl, previewPaneEl], {
|
| 167 |
-
flexGrow: (i) => i === 0 ? userMainPaneSizes.editor : userMainPaneSizes.preview,
|
| 168 |
-
flexBasis: (i) => i === 0 ? `${userMainPaneSizes.editor * 100}%` : `${userMainPaneSizes.preview * 100}%`,
|
| 169 |
-
duration: 0.2,
|
| 170 |
-
ease: "power2.inOut",
|
| 171 |
-
})
|
| 172 |
-
.to([codePane, chatPane], {
|
| 173 |
-
flexGrow: (i) => i === 0 ? userEditorPaneSizes.code : userEditorPaneSizes.chat,
|
| 174 |
-
flexBasis: (i) => i === 0 ? `${userEditorPaneSizes.code * 100}%` : `${userEditorPaneSizes.chat * 100}%`,
|
| 175 |
-
duration: 0.2,
|
| 176 |
-
ease: "power2.inOut",
|
| 177 |
-
}, "-=0.2")
|
| 178 |
-
.to([gamePane, consolePane], {
|
| 179 |
-
flexGrow: (i) => i === 0 ? userPreviewPaneSizes.game : userPreviewPaneSizes.console,
|
| 180 |
-
flexBasis: (i) => i === 0 ? `${userPreviewPaneSizes.game * 100}%` : `${userPreviewPaneSizes.console * 100}%`,
|
| 181 |
-
duration: 0.2,
|
| 182 |
-
ease: "power2.inOut",
|
| 183 |
-
}, "-=0.2")
|
| 184 |
-
.to(consolePane, {
|
| 185 |
-
opacity: 1,
|
| 186 |
-
duration: 0.15,
|
| 187 |
-
}, "-=0.15")
|
| 188 |
-
.to(editorPaneEl, {
|
| 189 |
-
opacity: 1,
|
| 190 |
-
duration: 0.15,
|
| 191 |
-
onComplete: () => {
|
| 192 |
-
window.dispatchEvent(new Event('resize'));
|
| 193 |
-
}
|
| 194 |
-
}, "-=0.15");
|
| 195 |
-
}
|
| 196 |
-
}
|
| 197 |
</script>
|
| 198 |
|
| 199 |
<div class="editor-layout">
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
</div>
|
| 227 |
|
| 228 |
<style>
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
</style>
|
|
|
|
| 1 |
<script lang="ts">
|
| 2 |
+
import { onMount } from "svelte";
|
| 3 |
+
import { Splitpanes, Pane } from "svelte-splitpanes";
|
| 4 |
+
import { uiStore } from "../../stores/ui";
|
| 5 |
+
import CodeEditor from "../editor/CodeEditor.svelte";
|
| 6 |
+
import GameCanvas from "../game/GameCanvas.svelte";
|
| 7 |
+
import ConsolePanel from "../console/ConsolePanel.svelte";
|
| 8 |
+
import ChatPanel from "../chat/ChatPanel.svelte";
|
| 9 |
+
import gsap from "gsap";
|
| 10 |
+
|
| 11 |
+
$: viewMode = $uiStore.viewMode;
|
| 12 |
+
|
| 13 |
+
let userMainPaneSizes = { editor: 0.4, preview: 0.6 };
|
| 14 |
+
let userEditorPaneSizes = { code: 0.5, chat: 0.5 };
|
| 15 |
+
let userPreviewPaneSizes = { game: 0.75, console: 0.25 };
|
| 16 |
+
|
| 17 |
+
onMount(() => {
|
| 18 |
+
gsap.fromTo(
|
| 19 |
+
".editor-pane",
|
| 20 |
+
{ opacity: 0, x: -20 },
|
| 21 |
+
{ opacity: 1, x: 0, duration: 0.6, delay: 0.1, ease: "power3.out" },
|
| 22 |
+
);
|
| 23 |
+
|
| 24 |
+
gsap.fromTo(
|
| 25 |
+
".preview-pane",
|
| 26 |
+
{ opacity: 0, x: 20 },
|
| 27 |
+
{ opacity: 1, x: 0, duration: 0.6, delay: 0.2, ease: "power3.out" },
|
| 28 |
+
);
|
| 29 |
+
|
| 30 |
+
const resetCursor = () => {
|
| 31 |
+
document.body.style.cursor = "";
|
| 32 |
+
};
|
| 33 |
+
document.addEventListener("mouseup", resetCursor);
|
| 34 |
+
|
| 35 |
+
return () => {
|
| 36 |
+
document.removeEventListener("mouseup", resetCursor);
|
| 37 |
+
};
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
$: if (viewMode) {
|
| 41 |
+
handleViewModeAnimation(viewMode);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
function handleViewModeAnimation(mode: "code" | "preview") {
|
| 45 |
+
const mainPanes = document
|
| 46 |
+
.querySelector(".main-splitpanes")
|
| 47 |
+
?.querySelectorAll(":scope > .splitpanes__pane");
|
| 48 |
+
if (!mainPanes || mainPanes.length < 2) return;
|
| 49 |
+
|
| 50 |
+
const editorPaneEl = mainPanes[0] as HTMLElement;
|
| 51 |
+
const previewPaneEl = mainPanes[1] as HTMLElement;
|
| 52 |
+
const mainSplitter = document.querySelector(
|
| 53 |
+
".main-splitpanes > .splitpanes__splitter",
|
| 54 |
+
) as HTMLElement;
|
| 55 |
+
|
| 56 |
+
if (!editorPaneEl || !previewPaneEl) return;
|
| 57 |
+
|
| 58 |
+
const editorSplitpanes = editorPaneEl.querySelectorAll(".splitpanes__pane");
|
| 59 |
+
const codePane = editorSplitpanes[0] as HTMLElement;
|
| 60 |
+
const chatPane = editorSplitpanes[1] as HTMLElement;
|
| 61 |
+
|
| 62 |
+
const previewSplitpanes = previewPaneEl.querySelectorAll(".splitpanes__pane");
|
| 63 |
+
const gamePane = previewSplitpanes[0] as HTMLElement;
|
| 64 |
+
const consolePane = previewSplitpanes[1] as HTMLElement;
|
| 65 |
+
const consoleSplitter = previewPaneEl.querySelector(".splitpanes__splitter") as HTMLElement;
|
| 66 |
+
|
| 67 |
+
if (mode === "preview") {
|
| 68 |
+
const editorWidth = editorPaneEl.offsetWidth;
|
| 69 |
+
const previewWidth = previewPaneEl.offsetWidth;
|
| 70 |
+
const totalWidth = editorWidth + previewWidth;
|
| 71 |
+
|
| 72 |
+
if (totalWidth > 0) {
|
| 73 |
+
userMainPaneSizes.editor = editorWidth / totalWidth;
|
| 74 |
+
userMainPaneSizes.preview = previewWidth / totalWidth;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
if (codePane && chatPane) {
|
| 78 |
+
const codeHeight = codePane.offsetHeight;
|
| 79 |
+
const chatHeight = chatPane.offsetHeight;
|
| 80 |
+
const totalEditorHeight = codeHeight + chatHeight;
|
| 81 |
+
|
| 82 |
+
if (totalEditorHeight > 0) {
|
| 83 |
+
userEditorPaneSizes.code = codeHeight / totalEditorHeight;
|
| 84 |
+
userEditorPaneSizes.chat = chatHeight / totalEditorHeight;
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
if (gamePane && consolePane) {
|
| 89 |
+
const gameHeight = gamePane.offsetHeight;
|
| 90 |
+
const consoleHeight = consolePane.offsetHeight;
|
| 91 |
+
const totalHeight = gameHeight + consoleHeight;
|
| 92 |
+
|
| 93 |
+
if (totalHeight > 0) {
|
| 94 |
+
userPreviewPaneSizes.game = gameHeight / totalHeight;
|
| 95 |
+
userPreviewPaneSizes.console = consoleHeight / totalHeight;
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
gsap.set(editorPaneEl, {
|
| 100 |
+
flexGrow: userMainPaneSizes.editor,
|
| 101 |
+
flexBasis: `${userMainPaneSizes.editor * 100}%`,
|
| 102 |
+
});
|
| 103 |
+
gsap.set(previewPaneEl, {
|
| 104 |
+
flexGrow: userMainPaneSizes.preview,
|
| 105 |
+
flexBasis: `${userMainPaneSizes.preview * 100}%`,
|
| 106 |
+
});
|
| 107 |
+
|
| 108 |
+
if (codePane && chatPane) {
|
| 109 |
+
gsap.set(codePane, {
|
| 110 |
+
flexGrow: userEditorPaneSizes.code,
|
| 111 |
+
flexBasis: `${userEditorPaneSizes.code * 100}%`,
|
| 112 |
+
});
|
| 113 |
+
gsap.set(chatPane, {
|
| 114 |
+
flexGrow: userEditorPaneSizes.chat,
|
| 115 |
+
flexBasis: `${userEditorPaneSizes.chat * 100}%`,
|
| 116 |
+
});
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
if (gamePane && consolePane) {
|
| 120 |
+
gsap.set(gamePane, {
|
| 121 |
+
flexGrow: userPreviewPaneSizes.game,
|
| 122 |
+
flexBasis: `${userPreviewPaneSizes.game * 100}%`,
|
| 123 |
+
});
|
| 124 |
+
gsap.set(consolePane, {
|
| 125 |
+
flexGrow: userPreviewPaneSizes.console,
|
| 126 |
+
flexBasis: `${userPreviewPaneSizes.console * 100}%`,
|
| 127 |
+
});
|
| 128 |
+
}
|
| 129 |
}
|
| 130 |
+
|
| 131 |
+
if (mode === "preview") {
|
| 132 |
+
gsap.timeline({ ease: "power2.out" })
|
| 133 |
+
.to(editorPaneEl, {
|
| 134 |
+
opacity: 0,
|
| 135 |
+
duration: 0.15,
|
| 136 |
+
})
|
| 137 |
+
.to(
|
| 138 |
+
consolePane,
|
| 139 |
+
{
|
| 140 |
+
opacity: 0,
|
| 141 |
+
duration: 0.15,
|
| 142 |
+
},
|
| 143 |
+
"-=0.15",
|
| 144 |
+
)
|
| 145 |
+
.to(
|
| 146 |
+
editorPaneEl,
|
| 147 |
+
{
|
| 148 |
+
flexGrow: 0,
|
| 149 |
+
flexBasis: "0%",
|
| 150 |
+
duration: 0.2,
|
| 151 |
+
ease: "power2.inOut",
|
| 152 |
+
onComplete: () => {
|
| 153 |
+
editorPaneEl.style.display = "none";
|
| 154 |
+
if (mainSplitter) mainSplitter.style.display = "none";
|
| 155 |
+
},
|
| 156 |
+
},
|
| 157 |
+
"-=0.1",
|
| 158 |
+
)
|
| 159 |
+
.to(
|
| 160 |
+
consolePane,
|
| 161 |
+
{
|
| 162 |
+
flexGrow: 0,
|
| 163 |
+
flexBasis: "0%",
|
| 164 |
+
duration: 0.2,
|
| 165 |
+
ease: "power2.inOut",
|
| 166 |
+
onComplete: () => {
|
| 167 |
+
consolePane.style.display = "none";
|
| 168 |
+
if (consoleSplitter) consoleSplitter.style.display = "none";
|
| 169 |
+
},
|
| 170 |
+
},
|
| 171 |
+
"-=0.2",
|
| 172 |
+
)
|
| 173 |
+
.to(
|
| 174 |
+
gamePane,
|
| 175 |
+
{
|
| 176 |
+
flexGrow: 1,
|
| 177 |
+
flexBasis: "100%",
|
| 178 |
+
duration: 0.2,
|
| 179 |
+
ease: "power2.inOut",
|
| 180 |
+
},
|
| 181 |
+
"-=0.2",
|
| 182 |
+
)
|
| 183 |
+
.to(
|
| 184 |
+
previewPaneEl,
|
| 185 |
+
{
|
| 186 |
+
flexGrow: 1,
|
| 187 |
+
flexBasis: "100%",
|
| 188 |
+
duration: 0.2,
|
| 189 |
+
ease: "power2.inOut",
|
| 190 |
+
},
|
| 191 |
+
"-=0.2",
|
| 192 |
+
);
|
| 193 |
+
} else {
|
| 194 |
+
editorPaneEl.style.display = "";
|
| 195 |
+
if (mainSplitter) mainSplitter.style.display = "";
|
| 196 |
+
consolePane.style.display = "";
|
| 197 |
+
if (consoleSplitter) consoleSplitter.style.display = "";
|
| 198 |
+
|
| 199 |
+
gsap.timeline({ ease: "power2.out" })
|
| 200 |
+
.set(editorPaneEl, {
|
| 201 |
+
opacity: 0,
|
| 202 |
+
flexGrow: 0,
|
| 203 |
+
flexBasis: "0%",
|
| 204 |
+
})
|
| 205 |
+
.set(consolePane, {
|
| 206 |
+
opacity: 0,
|
| 207 |
+
flexGrow: 0,
|
| 208 |
+
flexBasis: "0%",
|
| 209 |
+
})
|
| 210 |
+
.to([editorPaneEl, previewPaneEl], {
|
| 211 |
+
flexGrow: (i) =>
|
| 212 |
+
i === 0 ? userMainPaneSizes.editor : userMainPaneSizes.preview,
|
| 213 |
+
flexBasis: (i) =>
|
| 214 |
+
i === 0
|
| 215 |
+
? `${userMainPaneSizes.editor * 100}%`
|
| 216 |
+
: `${userMainPaneSizes.preview * 100}%`,
|
| 217 |
+
duration: 0.2,
|
| 218 |
+
ease: "power2.inOut",
|
| 219 |
+
})
|
| 220 |
+
.to(
|
| 221 |
+
[codePane, chatPane],
|
| 222 |
+
{
|
| 223 |
+
flexGrow: (i) =>
|
| 224 |
+
i === 0 ? userEditorPaneSizes.code : userEditorPaneSizes.chat,
|
| 225 |
+
flexBasis: (i) =>
|
| 226 |
+
i === 0
|
| 227 |
+
? `${userEditorPaneSizes.code * 100}%`
|
| 228 |
+
: `${userEditorPaneSizes.chat * 100}%`,
|
| 229 |
+
duration: 0.2,
|
| 230 |
+
ease: "power2.inOut",
|
| 231 |
+
},
|
| 232 |
+
"-=0.2",
|
| 233 |
+
)
|
| 234 |
+
.to(
|
| 235 |
+
[gamePane, consolePane],
|
| 236 |
+
{
|
| 237 |
+
flexGrow: (i) =>
|
| 238 |
+
i === 0 ? userPreviewPaneSizes.game : userPreviewPaneSizes.console,
|
| 239 |
+
flexBasis: (i) =>
|
| 240 |
+
i === 0
|
| 241 |
+
? `${userPreviewPaneSizes.game * 100}%`
|
| 242 |
+
: `${userPreviewPaneSizes.console * 100}%`,
|
| 243 |
+
duration: 0.2,
|
| 244 |
+
ease: "power2.inOut",
|
| 245 |
+
},
|
| 246 |
+
"-=0.2",
|
| 247 |
+
)
|
| 248 |
+
.to(
|
| 249 |
+
consolePane,
|
| 250 |
+
{
|
| 251 |
+
opacity: 1,
|
| 252 |
+
duration: 0.15,
|
| 253 |
+
},
|
| 254 |
+
"-=0.15",
|
| 255 |
+
)
|
| 256 |
+
.to(
|
| 257 |
+
editorPaneEl,
|
| 258 |
+
{
|
| 259 |
+
opacity: 1,
|
| 260 |
+
duration: 0.15,
|
| 261 |
+
onComplete: () => {
|
| 262 |
+
window.dispatchEvent(new Event("resize"));
|
| 263 |
+
},
|
| 264 |
+
},
|
| 265 |
+
"-=0.15",
|
| 266 |
+
);
|
| 267 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
</script>
|
| 270 |
|
| 271 |
<div class="editor-layout">
|
| 272 |
+
<Splitpanes class="main-splitpanes" theme="modern-theme">
|
| 273 |
+
<Pane minSize={20} size={40} class="editor-pane">
|
| 274 |
+
<div class="editor-panel">
|
| 275 |
+
<Splitpanes horizontal class="editor-splitpanes" theme="modern-theme">
|
| 276 |
+
<Pane minSize={30} size={userEditorPaneSizes.code * 100} class="code-pane">
|
| 277 |
+
<CodeEditor />
|
| 278 |
+
</Pane>
|
| 279 |
+
<Pane minSize={15} size={userEditorPaneSizes.chat * 100} class="chat-pane">
|
| 280 |
+
<ChatPanel />
|
| 281 |
+
</Pane>
|
| 282 |
+
</Splitpanes>
|
| 283 |
+
</div>
|
| 284 |
+
</Pane>
|
| 285 |
+
<Pane minSize={25} size={60} class="preview-pane">
|
| 286 |
+
<div class="preview-panel">
|
| 287 |
+
<Splitpanes horizontal class="preview-splitpanes" theme="modern-theme">
|
| 288 |
+
<Pane minSize={30} class="game-pane">
|
| 289 |
+
<GameCanvas />
|
| 290 |
+
</Pane>
|
| 291 |
+
<Pane minSize={15} size={25} class="console-pane">
|
| 292 |
+
<ConsolePanel />
|
| 293 |
+
</Pane>
|
| 294 |
+
</Splitpanes>
|
| 295 |
+
</div>
|
| 296 |
+
</Pane>
|
| 297 |
+
</Splitpanes>
|
| 298 |
</div>
|
| 299 |
|
| 300 |
<style>
|
| 301 |
+
.editor-layout {
|
| 302 |
+
flex: 1;
|
| 303 |
+
display: flex;
|
| 304 |
+
min-height: 0;
|
| 305 |
+
background: rgba(139, 115, 85, 0.02);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
:global(.main-splitpanes) {
|
| 309 |
+
width: 100%;
|
| 310 |
+
height: 100%;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
:global(.editor-splitpanes) {
|
| 314 |
+
width: 100%;
|
| 315 |
+
height: 100%;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
:global(.preview-splitpanes) {
|
| 319 |
+
width: 100%;
|
| 320 |
+
height: 100%;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
:global(.editor-pane) {
|
| 324 |
+
display: flex;
|
| 325 |
+
overflow: hidden;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
:global(.preview-pane) {
|
| 329 |
+
display: flex;
|
| 330 |
+
overflow: hidden;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
:global(.code-pane) {
|
| 334 |
+
display: flex;
|
| 335 |
+
overflow: hidden;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
:global(.game-pane) {
|
| 339 |
+
display: flex;
|
| 340 |
+
overflow: hidden;
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
:global(.console-pane) {
|
| 344 |
+
display: flex;
|
| 345 |
+
overflow: hidden;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
:global(.chat-pane) {
|
| 349 |
+
display: flex;
|
| 350 |
+
overflow: hidden;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
.editor-panel {
|
| 354 |
+
width: 100%;
|
| 355 |
+
height: 100%;
|
| 356 |
+
display: flex;
|
| 357 |
+
flex-direction: column;
|
| 358 |
+
background: rgba(11, 10, 9, 0.3);
|
| 359 |
+
min-width: 0;
|
| 360 |
+
overflow: hidden;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.preview-panel {
|
| 364 |
+
width: 100%;
|
| 365 |
+
height: 100%;
|
| 366 |
+
display: flex;
|
| 367 |
+
flex-direction: column;
|
| 368 |
+
background: rgba(11, 10, 9, 0.3);
|
| 369 |
+
min-width: 0;
|
| 370 |
+
overflow: hidden;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
:global(.splitpanes.modern-theme .splitpanes__splitter) {
|
| 374 |
+
background: rgba(139, 115, 85, 0.06);
|
| 375 |
+
position: relative;
|
| 376 |
+
transition: background 0.2s;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
:global(.splitpanes.modern-theme .splitpanes__splitter:hover) {
|
| 380 |
+
background: rgba(139, 115, 85, 0.12);
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
:global(.splitpanes.modern-theme .splitpanes__splitter:before) {
|
| 384 |
+
content: "";
|
| 385 |
+
position: absolute;
|
| 386 |
+
z-index: 1;
|
| 387 |
+
transition: opacity 0.2s;
|
| 388 |
+
background: rgba(124, 152, 133, 0.1);
|
| 389 |
+
opacity: 0;
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
:global(.splitpanes.modern-theme .splitpanes__splitter:hover:before) {
|
| 393 |
+
opacity: 1;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
:global(.splitpanes--vertical.modern-theme > .splitpanes__splitter) {
|
| 397 |
+
width: 1px;
|
| 398 |
+
cursor: col-resize;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
:global(.splitpanes--vertical.modern-theme > .splitpanes__splitter:before) {
|
| 402 |
+
left: -3px;
|
| 403 |
+
right: -3px;
|
| 404 |
+
height: 100%;
|
| 405 |
+
cursor: col-resize;
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
:global(.splitpanes--horizontal.modern-theme > .splitpanes__splitter) {
|
| 409 |
+
height: 1px;
|
| 410 |
+
cursor: row-resize;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
:global(.splitpanes--horizontal.modern-theme > .splitpanes__splitter:before) {
|
| 414 |
+
top: -3px;
|
| 415 |
+
bottom: -3px;
|
| 416 |
+
width: 100%;
|
| 417 |
+
cursor: row-resize;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
:global(body.splitpanes--dragging) {
|
| 421 |
+
cursor: inherit;
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
:global(body:not(.splitpanes--dragging) .splitpanes__splitter:not(:hover)) {
|
| 425 |
+
cursor: default;
|
| 426 |
+
}
|
| 427 |
</style>
|
src/lib/server/agent-config.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface AgentConfig {
|
| 2 |
+
model?: string;
|
| 3 |
+
maxIterations?: number;
|
| 4 |
+
temperature?: number;
|
| 5 |
+
maxTokens?: number;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export const DEFAULT_CONFIG: Required<AgentConfig> = {
|
| 9 |
+
model: "Qwen/Qwen3-Next-80B-A3B-Instruct",
|
| 10 |
+
maxIterations: 10,
|
| 11 |
+
temperature: 0.5,
|
| 12 |
+
maxTokens: 1200,
|
| 13 |
+
};
|
| 14 |
+
|
| 15 |
+
export const mergeConfig = (
|
| 16 |
+
config: AgentConfig = {},
|
| 17 |
+
): Required<AgentConfig> => {
|
| 18 |
+
return { ...DEFAULT_CONFIG, ...config };
|
| 19 |
+
};
|
src/lib/server/agent-runner.test.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { describe, expect, test, beforeAll } from "bun:test";
|
| 2 |
+
import { AgentRunner } from "./agent-runner";
|
| 3 |
+
import "../tools"; // Import to register tools
|
| 4 |
+
|
| 5 |
+
describe("AgentRunner Tool Execution", () => {
|
| 6 |
+
beforeAll(() => {
|
| 7 |
+
// Create runner instance for test context
|
| 8 |
+
new AgentRunner({
|
| 9 |
+
model: "test-model",
|
| 10 |
+
temperature: 0.7,
|
| 11 |
+
maxTokens: 1000,
|
| 12 |
+
});
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
describe("Tool Detection", () => {
|
| 16 |
+
test("should detect tool calls in response", async () => {
|
| 17 |
+
const { parseToolCalls } = await import("../tools");
|
| 18 |
+
|
| 19 |
+
// Test various agent responses
|
| 20 |
+
const responses = [
|
| 21 |
+
{
|
| 22 |
+
text: "I'll read the game code for you.\n\n[TOOL: read_game_code]\n\nAnalyzing...",
|
| 23 |
+
expected: ["read_game_code"],
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
text: "Let me check that. [TOOL: read_game_code] Now I can see the code.",
|
| 27 |
+
expected: ["read_game_code"],
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
text: "No tools needed for this response.",
|
| 31 |
+
expected: [],
|
| 32 |
+
},
|
| 33 |
+
];
|
| 34 |
+
|
| 35 |
+
for (const { text, expected } of responses) {
|
| 36 |
+
const calls = parseToolCalls(text);
|
| 37 |
+
expect(calls.map((c) => c.tool)).toEqual(expected);
|
| 38 |
+
}
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
test("should format tool results correctly", async () => {
|
| 42 |
+
const { formatToolResult } = await import("../tools");
|
| 43 |
+
|
| 44 |
+
const successResult = {
|
| 45 |
+
success: true,
|
| 46 |
+
data: { content: "<world>test</world>", language: "html" },
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
const formatted = formatToolResult("read_game_code", successResult);
|
| 50 |
+
expect(formatted).toContain("[TOOL_RESULT: read_game_code]");
|
| 51 |
+
expect(formatted).toContain("<world>test</world>");
|
| 52 |
+
expect(formatted).toContain("[/TOOL_RESULT]");
|
| 53 |
+
});
|
| 54 |
+
});
|
| 55 |
+
|
| 56 |
+
describe("System Prompt", () => {
|
| 57 |
+
test("should include tool instructions", async () => {
|
| 58 |
+
const { toolRegistry } = await import("../tools");
|
| 59 |
+
const descriptions = toolRegistry.getToolDescriptions();
|
| 60 |
+
|
| 61 |
+
// System prompt should mention the tool
|
| 62 |
+
expect(descriptions).toContain("read_game_code");
|
| 63 |
+
expect(descriptions).toContain(
|
| 64 |
+
"Get the current game code from the editor",
|
| 65 |
+
);
|
| 66 |
+
});
|
| 67 |
+
});
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
// Integration test that would require mocking HuggingFace API
|
| 71 |
+
describe("Full Tool Flow (Mock)", () => {
|
| 72 |
+
test("should execute tool when detected in response", async () => {
|
| 73 |
+
// This test demonstrates the expected flow
|
| 74 |
+
// In production, this would make actual API calls
|
| 75 |
+
|
| 76 |
+
const mockResponse = "I'll check the game code.\n\n[TOOL: read_game_code]";
|
| 77 |
+
const { parseToolCalls, toolRegistry } = await import("../tools");
|
| 78 |
+
|
| 79 |
+
// 1. Parse tool calls from response
|
| 80 |
+
const calls = parseToolCalls(mockResponse);
|
| 81 |
+
expect(calls).toHaveLength(1);
|
| 82 |
+
expect(calls[0].tool).toBe("read_game_code");
|
| 83 |
+
|
| 84 |
+
// 2. Execute the tool
|
| 85 |
+
const result = await toolRegistry.execute(
|
| 86 |
+
calls[0].tool,
|
| 87 |
+
calls[0].parameters,
|
| 88 |
+
);
|
| 89 |
+
expect(result.success).toBe(true);
|
| 90 |
+
expect(result.data).toBeDefined();
|
| 91 |
+
|
| 92 |
+
// 3. Format result for context
|
| 93 |
+
const { formatToolResult } = await import("../tools");
|
| 94 |
+
const formatted = formatToolResult(calls[0].tool, result);
|
| 95 |
+
expect(formatted).toContain("[TOOL_RESULT:");
|
| 96 |
+
|
| 97 |
+
// 4. In the real flow, this formatted result would be added to message history
|
| 98 |
+
// and a follow-up API call would be made to process it
|
| 99 |
+
});
|
| 100 |
+
});
|
src/lib/server/agent-runner.ts
CHANGED
|
@@ -1,32 +1,30 @@
|
|
| 1 |
import { HfInference } from "@huggingface/inference";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
export interface AgentMessage {
|
| 4 |
id: string;
|
| 5 |
role: "user" | "assistant" | "system";
|
| 6 |
content: string;
|
| 7 |
timestamp: number;
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
export interface AgentConfig {
|
| 11 |
-
model?: string;
|
| 12 |
-
maxIterations?: number;
|
| 13 |
-
temperature?: number;
|
| 14 |
-
maxTokens?: number;
|
| 15 |
}
|
| 16 |
|
| 17 |
export class AgentRunner {
|
| 18 |
private client: HfInference | null = null;
|
| 19 |
-
private config: AgentConfig
|
| 20 |
private messageHistory: AgentMessage[] = [];
|
|
|
|
| 21 |
|
| 22 |
constructor(config: AgentConfig = {}) {
|
| 23 |
-
this.config =
|
| 24 |
-
model: "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B",
|
| 25 |
-
maxIterations: 10,
|
| 26 |
-
temperature: 0.7,
|
| 27 |
-
maxTokens: 1000,
|
| 28 |
-
...config,
|
| 29 |
-
};
|
| 30 |
}
|
| 31 |
|
| 32 |
async initialize(hfToken?: string) {
|
|
@@ -35,94 +33,144 @@ export class AgentRunner {
|
|
| 35 |
"Hugging Face authentication required. Please sign in to continue.",
|
| 36 |
);
|
| 37 |
}
|
| 38 |
-
|
| 39 |
this.client = new HfInference(hfToken);
|
| 40 |
}
|
| 41 |
|
| 42 |
async processMessage(
|
| 43 |
message: string,
|
| 44 |
-
onStream?: (chunk: string) => void,
|
| 45 |
): Promise<string> {
|
| 46 |
if (!this.client) {
|
| 47 |
throw new Error("AgentRunner not initialized. Call initialize() first.");
|
| 48 |
}
|
| 49 |
|
| 50 |
-
|
| 51 |
-
id: this.generateId(),
|
| 52 |
-
role: "user",
|
| 53 |
-
content: message,
|
| 54 |
-
timestamp: Date.now(),
|
| 55 |
-
};
|
| 56 |
-
|
| 57 |
-
this.messageHistory.push(userMessage);
|
| 58 |
-
|
| 59 |
-
try {
|
| 60 |
-
const systemPrompt = `You are an AI assistant helping to build games with VibeGame.
|
| 61 |
-
You can read and modify game code using XML syntax.
|
| 62 |
-
Always validate changes and ensure the game remains playable.
|
| 63 |
-
Use small, incremental changes rather than large rewrites.`;
|
| 64 |
-
|
| 65 |
-
const messages = [
|
| 66 |
-
{ role: "system", content: systemPrompt },
|
| 67 |
-
...this.messageHistory.map((msg) => ({
|
| 68 |
-
role: msg.role,
|
| 69 |
-
content: msg.content,
|
| 70 |
-
})),
|
| 71 |
-
];
|
| 72 |
-
|
| 73 |
-
let fullResponse = "";
|
| 74 |
-
|
| 75 |
-
const stream = await this.client.chatCompletionStream({
|
| 76 |
-
model: this.config.model!,
|
| 77 |
-
messages: messages as Array<{ role: string; content: string }>,
|
| 78 |
-
temperature: this.config.temperature,
|
| 79 |
-
max_tokens: this.config.maxTokens,
|
| 80 |
-
});
|
| 81 |
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
const delta = chunk.choices[0].delta?.content;
|
| 85 |
-
if (delta) {
|
| 86 |
-
fullResponse += delta;
|
| 87 |
-
if (onStream) {
|
| 88 |
-
onStream(delta);
|
| 89 |
-
}
|
| 90 |
-
}
|
| 91 |
-
}
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
const assistantMessage: AgentMessage = {
|
| 95 |
-
id: this.generateId(),
|
| 96 |
-
role: "assistant",
|
| 97 |
-
content: fullResponse,
|
| 98 |
-
timestamp: Date.now(),
|
| 99 |
-
};
|
| 100 |
|
| 101 |
-
|
|
|
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
console.error("Error processing message:", error);
|
| 106 |
|
| 107 |
-
|
| 108 |
-
throw new Error(
|
| 109 |
-
"Hugging Face API rate limit exceeded. Please provide an API token or wait before retrying.",
|
| 110 |
-
);
|
| 111 |
-
}
|
| 112 |
|
| 113 |
-
|
|
|
|
|
|
|
| 114 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
}
|
| 116 |
|
| 117 |
getHistory(): AgentMessage[] {
|
| 118 |
return [...this.messageHistory];
|
| 119 |
}
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
}
|
| 124 |
|
| 125 |
private generateId(): string {
|
| 126 |
-
return `
|
| 127 |
}
|
| 128 |
}
|
|
|
|
| 1 |
import { HfInference } from "@huggingface/inference";
|
| 2 |
+
import { toolRegistry, toolExecutor } from "../tools";
|
| 3 |
+
import { documentationService } from "./documentation";
|
| 4 |
+
import {
|
| 5 |
+
buildSystemPrompt,
|
| 6 |
+
buildToolFollowUpPrompt,
|
| 7 |
+
formatDocumentation,
|
| 8 |
+
} from "./prompts";
|
| 9 |
+
import { mergeConfig, type AgentConfig } from "./agent-config";
|
| 10 |
+
import { UniversalReasoningExtractor } from "./reasoning-extractor";
|
| 11 |
|
| 12 |
export interface AgentMessage {
|
| 13 |
id: string;
|
| 14 |
role: "user" | "assistant" | "system";
|
| 15 |
content: string;
|
| 16 |
timestamp: number;
|
| 17 |
+
reasoning?: string;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
}
|
| 19 |
|
| 20 |
export class AgentRunner {
|
| 21 |
private client: HfInference | null = null;
|
| 22 |
+
private config: Required<AgentConfig>;
|
| 23 |
private messageHistory: AgentMessage[] = [];
|
| 24 |
+
private reasoningExtractor = new UniversalReasoningExtractor();
|
| 25 |
|
| 26 |
constructor(config: AgentConfig = {}) {
|
| 27 |
+
this.config = mergeConfig(config);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
| 29 |
|
| 30 |
async initialize(hfToken?: string) {
|
|
|
|
| 33 |
"Hugging Face authentication required. Please sign in to continue.",
|
| 34 |
);
|
| 35 |
}
|
|
|
|
| 36 |
this.client = new HfInference(hfToken);
|
| 37 |
}
|
| 38 |
|
| 39 |
async processMessage(
|
| 40 |
message: string,
|
| 41 |
+
onStream?: (chunk: string, reasoning?: string) => void,
|
| 42 |
): Promise<string> {
|
| 43 |
if (!this.client) {
|
| 44 |
throw new Error("AgentRunner not initialized. Call initialize() first.");
|
| 45 |
}
|
| 46 |
|
| 47 |
+
this.addUserMessage(message);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
+
const documentation = await this.loadDocumentation();
|
| 50 |
+
const systemPrompt = this.buildPrompt(documentation);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
+
this.reasoningExtractor.reset();
|
| 53 |
+
const response = await this.streamCompletion(systemPrompt, onStream);
|
| 54 |
|
| 55 |
+
const extracted = this.reasoningExtractor.extract(response);
|
| 56 |
+
this.addAssistantMessage(extracted.content, extracted.reasoning);
|
|
|
|
| 57 |
|
| 58 |
+
const toolResults = await this.executeToolsIfNeeded(extracted.content);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
+
if (toolResults.length > 0) {
|
| 61 |
+
this.addToolResults(toolResults);
|
| 62 |
+
return await this.handleToolFollowUp(documentation, onStream);
|
| 63 |
}
|
| 64 |
+
|
| 65 |
+
return extracted.content;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
clearHistory() {
|
| 69 |
+
this.messageHistory = [];
|
| 70 |
}
|
| 71 |
|
| 72 |
getHistory(): AgentMessage[] {
|
| 73 |
return [...this.messageHistory];
|
| 74 |
}
|
| 75 |
|
| 76 |
+
private async loadDocumentation(): Promise<string> {
|
| 77 |
+
const fullDocs = await documentationService.load();
|
| 78 |
+
return formatDocumentation(fullDocs || "");
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
private buildPrompt(documentation: string): string {
|
| 82 |
+
const tools = toolRegistry.getToolDescriptions();
|
| 83 |
+
return buildSystemPrompt(tools, documentation);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
private async streamCompletion(
|
| 87 |
+
systemPrompt: string,
|
| 88 |
+
onStream?: (chunk: string, reasoning?: string) => void,
|
| 89 |
+
): Promise<string> {
|
| 90 |
+
const messages = this.buildMessages(systemPrompt);
|
| 91 |
+
|
| 92 |
+
const stream = await this.client!.chatCompletionStream({
|
| 93 |
+
model: this.config.model,
|
| 94 |
+
messages: messages as Array<{ role: string; content: string }>,
|
| 95 |
+
temperature: this.config.temperature,
|
| 96 |
+
max_tokens: this.config.maxTokens,
|
| 97 |
+
});
|
| 98 |
+
|
| 99 |
+
let fullResponse = "";
|
| 100 |
+
for await (const chunk of stream) {
|
| 101 |
+
const delta = chunk.choices?.[0]?.delta?.content;
|
| 102 |
+
if (delta) {
|
| 103 |
+
fullResponse += delta;
|
| 104 |
+
|
| 105 |
+
const result = this.reasoningExtractor.stream(delta);
|
| 106 |
+
if (result.partial) {
|
| 107 |
+
onStream?.(result.partial, result.reasoning);
|
| 108 |
+
} else if (result.reasoning) {
|
| 109 |
+
onStream?.("", result.reasoning);
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
return fullResponse;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
private buildMessages(systemPrompt: string) {
|
| 118 |
+
return [
|
| 119 |
+
{ role: "system", content: systemPrompt },
|
| 120 |
+
...this.messageHistory.map((msg) => ({
|
| 121 |
+
role: msg.role,
|
| 122 |
+
content: msg.content,
|
| 123 |
+
})),
|
| 124 |
+
];
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
private async executeToolsIfNeeded(response: string) {
|
| 128 |
+
return toolExecutor.executeFromResponse(response);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
private addToolResults(results: { formatted: string }[]) {
|
| 132 |
+
for (const result of results) {
|
| 133 |
+
this.messageHistory.push({
|
| 134 |
+
id: this.generateId(),
|
| 135 |
+
role: "system",
|
| 136 |
+
content: result.formatted,
|
| 137 |
+
timestamp: Date.now(),
|
| 138 |
+
});
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
private async handleToolFollowUp(
|
| 143 |
+
documentation: string,
|
| 144 |
+
onStream?: (chunk: string, reasoning?: string) => void,
|
| 145 |
+
): Promise<string> {
|
| 146 |
+
const followUpPrompt = buildToolFollowUpPrompt(documentation);
|
| 147 |
+
this.reasoningExtractor.reset();
|
| 148 |
+
const response = await this.streamCompletion(followUpPrompt, onStream);
|
| 149 |
+
const extracted = this.reasoningExtractor.extract(response);
|
| 150 |
+
this.addAssistantMessage(extracted.content, extracted.reasoning);
|
| 151 |
+
return extracted.content;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
private addUserMessage(content: string) {
|
| 155 |
+
this.messageHistory.push({
|
| 156 |
+
id: this.generateId(),
|
| 157 |
+
role: "user",
|
| 158 |
+
content,
|
| 159 |
+
timestamp: Date.now(),
|
| 160 |
+
});
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
private addAssistantMessage(content: string, reasoning?: string) {
|
| 164 |
+
this.messageHistory.push({
|
| 165 |
+
id: this.generateId(),
|
| 166 |
+
role: "assistant",
|
| 167 |
+
content,
|
| 168 |
+
timestamp: Date.now(),
|
| 169 |
+
reasoning,
|
| 170 |
+
});
|
| 171 |
}
|
| 172 |
|
| 173 |
private generateId(): string {
|
| 174 |
+
return `msg-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
| 175 |
}
|
| 176 |
}
|
src/lib/server/api.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface WebSocketMessage {
|
|
| 8 |
content?: string;
|
| 9 |
role?: string;
|
| 10 |
chunk?: string;
|
|
|
|
| 11 |
error?: string;
|
| 12 |
processing?: boolean;
|
| 13 |
connected?: boolean;
|
|
@@ -102,10 +103,10 @@ class WebSocketManager {
|
|
| 102 |
|
| 103 |
const response = await connectionData.agent.processMessage(
|
| 104 |
userMessage,
|
| 105 |
-
(chunk: string) => {
|
| 106 |
this.sendMessage(ws, {
|
| 107 |
type: "stream",
|
| 108 |
-
payload: { chunk },
|
| 109 |
timestamp: Date.now(),
|
| 110 |
});
|
| 111 |
},
|
|
@@ -162,4 +163,7 @@ class WebSocketManager {
|
|
| 162 |
|
| 163 |
export const wsManager = new WebSocketManager();
|
| 164 |
|
| 165 |
-
export async function initializeAgent() {
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
content?: string;
|
| 9 |
role?: string;
|
| 10 |
chunk?: string;
|
| 11 |
+
reasoning?: string;
|
| 12 |
error?: string;
|
| 13 |
processing?: boolean;
|
| 14 |
connected?: boolean;
|
|
|
|
| 103 |
|
| 104 |
const response = await connectionData.agent.processMessage(
|
| 105 |
userMessage,
|
| 106 |
+
(chunk: string, reasoning?: string) => {
|
| 107 |
this.sendMessage(ws, {
|
| 108 |
type: "stream",
|
| 109 |
+
payload: { chunk, reasoning },
|
| 110 |
timestamp: Date.now(),
|
| 111 |
});
|
| 112 |
},
|
|
|
|
| 163 |
|
| 164 |
export const wsManager = new WebSocketManager();
|
| 165 |
|
| 166 |
+
export async function initializeAgent() {
|
| 167 |
+
// Initialize tools when server starts
|
| 168 |
+
await import("../tools");
|
| 169 |
+
}
|
src/lib/server/context.md
CHANGED
|
@@ -1,20 +1,25 @@
|
|
| 1 |
# Server Context
|
| 2 |
|
| 3 |
-
WebSocket server
|
| 4 |
|
| 5 |
## Components
|
| 6 |
|
| 7 |
-
- `
|
| 8 |
-
- `
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
## Architecture
|
| 11 |
|
| 12 |
-
|
| 13 |
|
| 14 |
-
-
|
| 15 |
-
-
|
| 16 |
-
-
|
|
|
|
| 17 |
|
| 18 |
## Integration
|
| 19 |
|
| 20 |
-
Frontend
|
|
|
|
| 1 |
# Server Context
|
| 2 |
|
| 3 |
+
WebSocket server and AI agent orchestration with reasoning extraction.
|
| 4 |
|
| 5 |
## Components
|
| 6 |
|
| 7 |
+
- `api.ts` - WebSocket connection management
|
| 8 |
+
- `agent-runner.ts` - Message flow orchestration with reasoning filtering
|
| 9 |
+
- `agent-config.ts` - Configuration constants
|
| 10 |
+
- `prompts.ts` - Prompt templates
|
| 11 |
+
- `documentation.ts` - Docs loading service
|
| 12 |
+
- `reasoning-extractor.ts` - Filters thinking tags from model output
|
| 13 |
|
| 14 |
## Architecture
|
| 15 |
|
| 16 |
+
Clean separation of concerns:
|
| 17 |
|
| 18 |
+
- Agent runner orchestrates message flow and filters reasoning
|
| 19 |
+
- Reasoning extractor handles streaming tag extraction
|
| 20 |
+
- Tools execute via registry pattern
|
| 21 |
+
- Prompts and config externalized
|
| 22 |
|
| 23 |
## Integration
|
| 24 |
|
| 25 |
+
Frontend via `src/lib/stores/agent.ts` store with reasoning support.
|
src/lib/server/documentation.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { readFile } from "fs/promises";
|
| 2 |
+
import { join } from "path";
|
| 3 |
+
|
| 4 |
+
export class DocumentationService {
|
| 5 |
+
private cache: string | null = null;
|
| 6 |
+
private readonly docsPath: string;
|
| 7 |
+
|
| 8 |
+
constructor(filename: string = "llms.txt") {
|
| 9 |
+
this.docsPath = join(process.cwd(), filename);
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
async load(): Promise<string | null> {
|
| 13 |
+
if (this.cache !== null) {
|
| 14 |
+
return this.cache;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
try {
|
| 18 |
+
this.cache = await readFile(this.docsPath, "utf-8");
|
| 19 |
+
return this.cache;
|
| 20 |
+
} catch {
|
| 21 |
+
return null;
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
clearCache(): void {
|
| 26 |
+
this.cache = null;
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
export const documentationService = new DocumentationService();
|
src/lib/server/hint.test.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { describe, expect, test } from "bun:test";
|
| 2 |
+
|
| 3 |
+
describe("Message Enhancement for Tool Usage", () => {
|
| 4 |
+
test("should detect requests to read game code", () => {
|
| 5 |
+
const testCases = [
|
| 6 |
+
{ input: "read the game code", shouldTrigger: true },
|
| 7 |
+
{ input: "show me the game", shouldTrigger: true },
|
| 8 |
+
{ input: "what's in the game?", shouldTrigger: true },
|
| 9 |
+
{ input: "check the code", shouldTrigger: true },
|
| 10 |
+
{ input: "view the editor", shouldTrigger: true },
|
| 11 |
+
{ input: "see the scene", shouldTrigger: true },
|
| 12 |
+
{ input: "display the game code", shouldTrigger: true },
|
| 13 |
+
{ input: "look at the code", shouldTrigger: true },
|
| 14 |
+
{ input: "get the game", shouldTrigger: true },
|
| 15 |
+
{ input: "game code please", shouldTrigger: true },
|
| 16 |
+
{ input: "hello there", shouldTrigger: false },
|
| 17 |
+
{ input: "add a box", shouldTrigger: false },
|
| 18 |
+
{ input: "change the color", shouldTrigger: false },
|
| 19 |
+
];
|
| 20 |
+
|
| 21 |
+
const pattern =
|
| 22 |
+
/\b(read|show|view|check|see|what's in|what is in|get|display|look at)\b.*\b(game|code|editor|scene)\b/i;
|
| 23 |
+
const reversePattern =
|
| 24 |
+
/\b(game|code|editor|scene)\b.*\b(read|show|view|check|see|please)\b/i;
|
| 25 |
+
|
| 26 |
+
for (const { input, shouldTrigger } of testCases) {
|
| 27 |
+
const matches = pattern.test(input) || reversePattern.test(input);
|
| 28 |
+
expect(matches).toBe(shouldTrigger);
|
| 29 |
+
}
|
| 30 |
+
});
|
| 31 |
+
});
|
src/lib/server/prompts.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const SYSTEM_PROMPTS = {
|
| 2 |
+
main: (
|
| 3 |
+
tools: string,
|
| 4 |
+
documentation: string,
|
| 5 |
+
) => `You are an expert VibeGame developer assistant. VibeGame is a declarative 3D game engine using XML-like syntax, similar to Roblox but with "vibe coding" - focusing on quick, creative game development.
|
| 6 |
+
|
| 7 |
+
ENGINE CONTEXT:
|
| 8 |
+
VibeGame is a custom engine not in your training data. It uses declarative XML syntax for scene definition with Entity-Component-System (ECS) architecture.
|
| 9 |
+
|
| 10 |
+
${documentation}
|
| 11 |
+
|
| 12 |
+
AVAILABLE TOOLS:
|
| 13 |
+
${tools}
|
| 14 |
+
|
| 15 |
+
TOOL USAGE:
|
| 16 |
+
- Execute tools with: [TOOL: tool_name] or [TOOL: tool_name {"param": "value"}]
|
| 17 |
+
- read_game_code: Get current editor content
|
| 18 |
+
- write_game_code: Update the editor with new code
|
| 19 |
+
- read_console_output: Check console for errors, logs, and game output
|
| 20 |
+
|
| 21 |
+
WORKFLOW:
|
| 22 |
+
1. Read the current code to understand the game
|
| 23 |
+
2. Make changes using write_game_code
|
| 24 |
+
3. Check console output for errors or success
|
| 25 |
+
4. Iterate based on feedback
|
| 26 |
+
|
| 27 |
+
Be helpful, accurate, and adapt to what the user needs. Focus on working code and practical solutions.`,
|
| 28 |
+
|
| 29 |
+
toolFollowUp: (
|
| 30 |
+
documentation: string,
|
| 31 |
+
) => `Based on the tool execution result, provide relevant analysis of what you found.
|
| 32 |
+
|
| 33 |
+
${documentation}`,
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
export const formatDocumentation = (fullDocs: string): string => {
|
| 37 |
+
return `VIBEGAME REFERENCE:
|
| 38 |
+
${fullDocs}`;
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
+
export const buildSystemPrompt = (
|
| 42 |
+
tools: string,
|
| 43 |
+
documentation: string,
|
| 44 |
+
): string => {
|
| 45 |
+
return SYSTEM_PROMPTS.main(tools, documentation);
|
| 46 |
+
};
|
| 47 |
+
|
| 48 |
+
export const buildToolFollowUpPrompt = (documentation: string): string => {
|
| 49 |
+
return SYSTEM_PROMPTS.toolFollowUp(documentation);
|
| 50 |
+
};
|
src/lib/server/reasoning-extractor.test.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { describe, test, expect } from "bun:test";
|
| 2 |
+
import { UniversalReasoningExtractor } from "./reasoning-extractor";
|
| 3 |
+
|
| 4 |
+
describe("UniversalReasoningExtractor", () => {
|
| 5 |
+
test("extracts Qwen thinking tags", () => {
|
| 6 |
+
const extractor = new UniversalReasoningExtractor();
|
| 7 |
+
const input = `<thinking>
|
| 8 |
+
Let me analyze this step by step:
|
| 9 |
+
1. First, I need to understand the problem
|
| 10 |
+
2. Then I'll formulate a solution
|
| 11 |
+
3. Finally, I'll provide the answer
|
| 12 |
+
</thinking>
|
| 13 |
+
|
| 14 |
+
The answer to your question is 42.`;
|
| 15 |
+
|
| 16 |
+
const result = extractor.extract(input);
|
| 17 |
+
expect(result.content).toBe("The answer to your question is 42.");
|
| 18 |
+
expect(result.reasoning).toContain("Let me analyze this step by step");
|
| 19 |
+
});
|
| 20 |
+
|
| 21 |
+
test("extracts DeepSeek R1 think tags", () => {
|
| 22 |
+
const extractor = new UniversalReasoningExtractor();
|
| 23 |
+
const input = `<think>
|
| 24 |
+
Hmm, this is an interesting problem.
|
| 25 |
+
I should consider multiple approaches.
|
| 26 |
+
</think>
|
| 27 |
+
|
| 28 |
+
Here's the solution to your problem.`;
|
| 29 |
+
|
| 30 |
+
const result = extractor.extract(input);
|
| 31 |
+
expect(result.content).toBe("Here's the solution to your problem.");
|
| 32 |
+
expect(result.reasoning).toContain("interesting problem");
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
test("handles interleaved reasoning tags", () => {
|
| 36 |
+
const extractor = new UniversalReasoningExtractor();
|
| 37 |
+
const input = `<thinking>
|
| 38 |
+
First reasoning block
|
| 39 |
+
</thinking>
|
| 40 |
+
Some content here.
|
| 41 |
+
<reasoning>
|
| 42 |
+
Second reasoning block
|
| 43 |
+
</reasoning>
|
| 44 |
+
Final answer.`;
|
| 45 |
+
|
| 46 |
+
const result = extractor.extract(input);
|
| 47 |
+
expect(result.content).toBe("Some content here.\n\nFinal answer.");
|
| 48 |
+
expect(result.reasoning).toContain("First reasoning block");
|
| 49 |
+
expect(result.reasoning).toContain("Second reasoning block");
|
| 50 |
+
});
|
| 51 |
+
|
| 52 |
+
test("streams content correctly", () => {
|
| 53 |
+
const extractor = new UniversalReasoningExtractor();
|
| 54 |
+
|
| 55 |
+
const chunks = [
|
| 56 |
+
"Hello, ",
|
| 57 |
+
"<think",
|
| 58 |
+
"ing>",
|
| 59 |
+
"This is my ",
|
| 60 |
+
"reasoning process",
|
| 61 |
+
"</thinking>",
|
| 62 |
+
" Here is the answer.",
|
| 63 |
+
];
|
| 64 |
+
|
| 65 |
+
let finalContent = "";
|
| 66 |
+
let finalReasoning = "";
|
| 67 |
+
|
| 68 |
+
for (const chunk of chunks) {
|
| 69 |
+
const result = extractor.stream(chunk);
|
| 70 |
+
if (result.partial) {
|
| 71 |
+
finalContent += result.partial;
|
| 72 |
+
}
|
| 73 |
+
if (result.reasoning) {
|
| 74 |
+
finalReasoning = result.reasoning;
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
expect(finalContent).toBe("Hello, Here is the answer.");
|
| 79 |
+
expect(finalReasoning).toBe("This is my reasoning process");
|
| 80 |
+
});
|
| 81 |
+
|
| 82 |
+
test("handles multiple reasoning blocks", () => {
|
| 83 |
+
const extractor = new UniversalReasoningExtractor();
|
| 84 |
+
const input = `<thinking>First thought</thinking>
|
| 85 |
+
Some content here.
|
| 86 |
+
<reasoning>Second thought</reasoning>
|
| 87 |
+
More content.`;
|
| 88 |
+
|
| 89 |
+
const result = extractor.extract(input);
|
| 90 |
+
expect(result.content).toBe("Some content here.\n\nMore content.");
|
| 91 |
+
expect(result.reasoning).toContain("First thought");
|
| 92 |
+
expect(result.reasoning).toContain("Second thought");
|
| 93 |
+
});
|
| 94 |
+
|
| 95 |
+
test("returns original text when no reasoning tags", () => {
|
| 96 |
+
const extractor = new UniversalReasoningExtractor();
|
| 97 |
+
const input = "This is just regular text without any reasoning tags.";
|
| 98 |
+
|
| 99 |
+
const result = extractor.extract(input);
|
| 100 |
+
expect(result.content).toBe(input);
|
| 101 |
+
expect(result.reasoning).toBe("");
|
| 102 |
+
});
|
| 103 |
+
|
| 104 |
+
test("reset clears internal state", () => {
|
| 105 |
+
const extractor = new UniversalReasoningExtractor();
|
| 106 |
+
|
| 107 |
+
extractor.stream("<thinking>Test");
|
| 108 |
+
extractor.reset();
|
| 109 |
+
|
| 110 |
+
const result = extractor.stream("Normal text");
|
| 111 |
+
expect(result.partial).toBe("Normal text");
|
| 112 |
+
expect(result.reasoning).toBeUndefined();
|
| 113 |
+
});
|
| 114 |
+
});
|
src/lib/server/reasoning-extractor.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface ExtractedContent {
|
| 2 |
+
reasoning: string;
|
| 3 |
+
content: string;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
export interface StreamResult {
|
| 7 |
+
partial: string;
|
| 8 |
+
reasoning?: string;
|
| 9 |
+
complete: boolean;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export interface ReasoningExtractor {
|
| 13 |
+
patterns: RegExp[];
|
| 14 |
+
extract(text: string): ExtractedContent;
|
| 15 |
+
stream(chunk: string): StreamResult;
|
| 16 |
+
reset(): void;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export class UniversalReasoningExtractor implements ReasoningExtractor {
|
| 20 |
+
patterns = [
|
| 21 |
+
/<thinking>(.*?)<\/thinking>/s,
|
| 22 |
+
/<think>(.*?)<\/think>/s,
|
| 23 |
+
/<reasoning>(.*?)<\/reasoning>/s,
|
| 24 |
+
];
|
| 25 |
+
|
| 26 |
+
private buffer = "";
|
| 27 |
+
private inReasoning = false;
|
| 28 |
+
private reasoningBuffer = "";
|
| 29 |
+
private currentTag: string | null = null;
|
| 30 |
+
|
| 31 |
+
extract(text: string): ExtractedContent {
|
| 32 |
+
let reasoning = "";
|
| 33 |
+
let cleanContent = text;
|
| 34 |
+
|
| 35 |
+
for (const pattern of this.patterns) {
|
| 36 |
+
const matches = Array.from(
|
| 37 |
+
text.matchAll(new RegExp(pattern.source, "gs")),
|
| 38 |
+
);
|
| 39 |
+
|
| 40 |
+
for (const match of matches) {
|
| 41 |
+
if (match[1]) {
|
| 42 |
+
reasoning += match[1].trim() + "\n";
|
| 43 |
+
cleanContent = cleanContent.replace(match[0], "");
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
return {
|
| 49 |
+
reasoning: reasoning.trim(),
|
| 50 |
+
content: cleanContent.trim(),
|
| 51 |
+
};
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
stream(chunk: string): StreamResult {
|
| 55 |
+
this.buffer += chunk;
|
| 56 |
+
|
| 57 |
+
const result: StreamResult = {
|
| 58 |
+
partial: "",
|
| 59 |
+
complete: false,
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
// Check for opening tags
|
| 63 |
+
if (!this.inReasoning) {
|
| 64 |
+
const openingMatch = this.buffer.match(/<(thinking|think|reasoning)>/);
|
| 65 |
+
if (openingMatch) {
|
| 66 |
+
const beforeTag = this.buffer.substring(0, openingMatch.index);
|
| 67 |
+
result.partial = beforeTag;
|
| 68 |
+
this.buffer = this.buffer.substring(
|
| 69 |
+
openingMatch.index! + openingMatch[0].length,
|
| 70 |
+
);
|
| 71 |
+
this.inReasoning = true;
|
| 72 |
+
this.currentTag = openingMatch[1];
|
| 73 |
+
return result;
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// Check for closing tags
|
| 78 |
+
if (this.inReasoning && this.currentTag) {
|
| 79 |
+
const closingTag = `</${this.currentTag}>`;
|
| 80 |
+
const closingIndex = this.buffer.indexOf(closingTag);
|
| 81 |
+
|
| 82 |
+
if (closingIndex !== -1) {
|
| 83 |
+
this.reasoningBuffer += this.buffer.substring(0, closingIndex);
|
| 84 |
+
result.reasoning = this.reasoningBuffer.trim();
|
| 85 |
+
this.reasoningBuffer = "";
|
| 86 |
+
this.buffer = this.buffer.substring(closingIndex + closingTag.length);
|
| 87 |
+
this.inReasoning = false;
|
| 88 |
+
this.currentTag = null;
|
| 89 |
+
|
| 90 |
+
// Process any remaining content
|
| 91 |
+
const nextResult = this.stream("");
|
| 92 |
+
result.partial = nextResult.partial;
|
| 93 |
+
if (nextResult.reasoning) {
|
| 94 |
+
result.reasoning =
|
| 95 |
+
(result.reasoning || "") + "\n" + nextResult.reasoning;
|
| 96 |
+
}
|
| 97 |
+
return result;
|
| 98 |
+
} else {
|
| 99 |
+
// Still in reasoning, buffer it
|
| 100 |
+
this.reasoningBuffer += this.buffer;
|
| 101 |
+
this.buffer = "";
|
| 102 |
+
return result;
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// No reasoning tags, output as content
|
| 107 |
+
if (!this.inReasoning && this.buffer.length > 0) {
|
| 108 |
+
// Check if we might be in the middle of a tag
|
| 109 |
+
const lastAngle = this.buffer.lastIndexOf("<");
|
| 110 |
+
if (lastAngle !== -1 && !this.buffer.substring(lastAngle).includes(">")) {
|
| 111 |
+
// Might be incomplete tag, output everything before it
|
| 112 |
+
result.partial = this.buffer.substring(0, lastAngle);
|
| 113 |
+
this.buffer = this.buffer.substring(lastAngle);
|
| 114 |
+
} else {
|
| 115 |
+
// Safe to output everything
|
| 116 |
+
result.partial = this.buffer;
|
| 117 |
+
this.buffer = "";
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
result.complete = !this.inReasoning && this.buffer.length === 0;
|
| 122 |
+
return result;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
reset(): void {
|
| 126 |
+
this.buffer = "";
|
| 127 |
+
this.inReasoning = false;
|
| 128 |
+
this.reasoningBuffer = "";
|
| 129 |
+
this.currentTag = null;
|
| 130 |
+
}
|
| 131 |
+
}
|
src/lib/stores/agent.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface ChatMessage {
|
|
| 7 |
content: string;
|
| 8 |
timestamp: number;
|
| 9 |
streaming?: boolean;
|
|
|
|
|
|
|
| 10 |
}
|
| 11 |
|
| 12 |
export interface AgentState {
|
|
@@ -95,6 +97,7 @@ function createAgentStore() {
|
|
| 95 |
processing?: boolean;
|
| 96 |
connected?: boolean;
|
| 97 |
chunk?: string;
|
|
|
|
| 98 |
role?: string;
|
| 99 |
content?: string;
|
| 100 |
error?: string;
|
|
@@ -129,6 +132,7 @@ function createAgentStore() {
|
|
| 129 |
content: newContent,
|
| 130 |
timestamp: Date.now(),
|
| 131 |
streaming: true,
|
|
|
|
| 132 |
};
|
| 133 |
return {
|
| 134 |
...state,
|
|
@@ -138,7 +142,11 @@ function createAgentStore() {
|
|
| 138 |
} else {
|
| 139 |
const messages = state.messages.map((msg) => {
|
| 140 |
if (msg.id === currentStreamId) {
|
| 141 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
}
|
| 143 |
return msg;
|
| 144 |
});
|
|
|
|
| 7 |
content: string;
|
| 8 |
timestamp: number;
|
| 9 |
streaming?: boolean;
|
| 10 |
+
reasoning?: string;
|
| 11 |
+
showReasoning?: boolean;
|
| 12 |
}
|
| 13 |
|
| 14 |
export interface AgentState {
|
|
|
|
| 97 |
processing?: boolean;
|
| 98 |
connected?: boolean;
|
| 99 |
chunk?: string;
|
| 100 |
+
reasoning?: string;
|
| 101 |
role?: string;
|
| 102 |
content?: string;
|
| 103 |
error?: string;
|
|
|
|
| 132 |
content: newContent,
|
| 133 |
timestamp: Date.now(),
|
| 134 |
streaming: true,
|
| 135 |
+
reasoning: message.payload.reasoning,
|
| 136 |
};
|
| 137 |
return {
|
| 138 |
...state,
|
|
|
|
| 142 |
} else {
|
| 143 |
const messages = state.messages.map((msg) => {
|
| 144 |
if (msg.id === currentStreamId) {
|
| 145 |
+
return {
|
| 146 |
+
...msg,
|
| 147 |
+
content: newContent,
|
| 148 |
+
reasoning: message.payload.reasoning || msg.reasoning,
|
| 149 |
+
};
|
| 150 |
}
|
| 151 |
return msg;
|
| 152 |
});
|
src/lib/tools/context.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Tools Context
|
| 2 |
+
|
| 3 |
+
AI agent tools for game manipulation and feedback.
|
| 4 |
+
|
| 5 |
+
## Structure
|
| 6 |
+
|
| 7 |
+
- `registry.ts` - Tool registration
|
| 8 |
+
- `parser.ts` - Tool call parsing
|
| 9 |
+
- `executor.ts` - Execution flow
|
| 10 |
+
- `read-game-code.ts` - Editor content reader
|
| 11 |
+
- `write-game-code.ts` - Editor content writer
|
| 12 |
+
- `read-console-output.ts` - Console message reader
|
| 13 |
+
- `index.ts` - Auto-registration
|
| 14 |
+
|
| 15 |
+
## Architecture
|
| 16 |
+
|
| 17 |
+
Clean separation:
|
| 18 |
+
|
| 19 |
+
- Registry handles registration only
|
| 20 |
+
- Parser extracts tool calls from responses
|
| 21 |
+
- Executor manages execution flow
|
| 22 |
+
- Individual tools implement business logic
|
| 23 |
+
|
| 24 |
+
## Pattern
|
| 25 |
+
|
| 26 |
+
Agent includes: `[TOOL: tool_name {"param": "value"}]`
|
| 27 |
+
Results formatted: `[TOOL_RESULT: name]` or `[TOOL_ERROR: name]`
|
| 28 |
+
|
| 29 |
+
## Available Tools
|
| 30 |
+
|
| 31 |
+
1. **read_game_code** - Returns editor content
|
| 32 |
+
2. **write_game_code** - Updates editor with new code
|
| 33 |
+
3. **read_console_output** - Reads console messages with filtering
|
| 34 |
+
|
| 35 |
+
## Workflow
|
| 36 |
+
|
| 37 |
+
Read code → Write changes → Check console → Iterate on feedback
|
src/lib/tools/executor.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { toolRegistry } from "./registry";
|
| 2 |
+
import { parseToolCalls, formatToolResult, type ToolCall } from "./parser";
|
| 3 |
+
|
| 4 |
+
export interface ToolExecutionResult {
|
| 5 |
+
toolName: string;
|
| 6 |
+
formatted: string;
|
| 7 |
+
raw: {
|
| 8 |
+
success: boolean;
|
| 9 |
+
data?: unknown;
|
| 10 |
+
error?: string;
|
| 11 |
+
};
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export class ToolExecutor {
|
| 15 |
+
async executeFromResponse(response: string): Promise<ToolExecutionResult[]> {
|
| 16 |
+
const toolCalls = parseToolCalls(response);
|
| 17 |
+
|
| 18 |
+
if (toolCalls.length === 0) {
|
| 19 |
+
return [];
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
return Promise.all(toolCalls.map((call) => this.executeSingle(call)));
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
private async executeSingle(call: ToolCall): Promise<ToolExecutionResult> {
|
| 26 |
+
const result = await toolRegistry.execute(call.tool, call.parameters);
|
| 27 |
+
|
| 28 |
+
return {
|
| 29 |
+
toolName: call.tool,
|
| 30 |
+
formatted: formatToolResult(call.tool, result),
|
| 31 |
+
raw: result,
|
| 32 |
+
};
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
export const toolExecutor = new ToolExecutor();
|
src/lib/tools/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export { toolRegistry } from "./registry";
|
| 2 |
+
export type { Tool, ToolResult } from "./registry";
|
| 3 |
+
|
| 4 |
+
export { parseToolCalls, formatToolResult } from "./parser";
|
| 5 |
+
export type { ToolCall } from "./parser";
|
| 6 |
+
|
| 7 |
+
export { toolExecutor } from "./executor";
|
| 8 |
+
export type { ToolExecutionResult } from "./executor";
|
| 9 |
+
|
| 10 |
+
import { toolRegistry } from "./registry";
|
| 11 |
+
import { readGameCodeTool } from "./read-game-code";
|
| 12 |
+
import { writeGameCodeTool } from "./write-game-code";
|
| 13 |
+
import { readConsoleOutputTool } from "./read-console-output";
|
| 14 |
+
|
| 15 |
+
toolRegistry.register(readGameCodeTool);
|
| 16 |
+
toolRegistry.register(writeGameCodeTool);
|
| 17 |
+
toolRegistry.register(readConsoleOutputTool);
|
src/lib/tools/parser.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface ToolCall {
|
| 2 |
+
tool: string;
|
| 3 |
+
parameters?: unknown;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
const TOOL_PATTERN = /\[TOOL:\s*(\w+)(?:\s+({[^}]*}))?\]/g;
|
| 7 |
+
|
| 8 |
+
export const parseToolCalls = (response: string): ToolCall[] => {
|
| 9 |
+
const toolCalls: ToolCall[] = [];
|
| 10 |
+
let match;
|
| 11 |
+
|
| 12 |
+
while ((match = TOOL_PATTERN.exec(response)) !== null) {
|
| 13 |
+
const toolName = match[1];
|
| 14 |
+
const paramsStr = match[2];
|
| 15 |
+
|
| 16 |
+
toolCalls.push({
|
| 17 |
+
tool: toolName,
|
| 18 |
+
parameters: paramsStr ? tryParseJson(paramsStr) : undefined,
|
| 19 |
+
});
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
return toolCalls;
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
const tryParseJson = (str: string): unknown => {
|
| 26 |
+
try {
|
| 27 |
+
return JSON.parse(str);
|
| 28 |
+
} catch {
|
| 29 |
+
return undefined;
|
| 30 |
+
}
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
export const formatToolResult = (
|
| 34 |
+
toolName: string,
|
| 35 |
+
result: { success: boolean; data?: unknown; error?: string },
|
| 36 |
+
): string => {
|
| 37 |
+
if (result.success) {
|
| 38 |
+
return `[TOOL_RESULT: ${toolName}]\n${JSON.stringify(result.data, null, 2)}\n[/TOOL_RESULT]`;
|
| 39 |
+
}
|
| 40 |
+
return `[TOOL_ERROR: ${toolName}]\n${result.error}\n[/TOOL_ERROR]`;
|
| 41 |
+
};
|
src/lib/tools/read-console-output.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { get } from "svelte/store";
|
| 2 |
+
import { consoleStore } from "../stores/console";
|
| 3 |
+
import type { Tool } from "./registry";
|
| 4 |
+
|
| 5 |
+
interface ReadConsoleParams {
|
| 6 |
+
limit?: number;
|
| 7 |
+
type?: "log" | "warn" | "error" | "info";
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export const readConsoleOutputTool: Tool = {
|
| 11 |
+
name: "read_console_output",
|
| 12 |
+
description: "Read messages from the game console",
|
| 13 |
+
execute: async (params: unknown) => {
|
| 14 |
+
const { limit = 10, type } = (params || {}) as ReadConsoleParams;
|
| 15 |
+
|
| 16 |
+
try {
|
| 17 |
+
const state = get(consoleStore);
|
| 18 |
+
let messages = state.messages;
|
| 19 |
+
|
| 20 |
+
if (type) {
|
| 21 |
+
messages = messages.filter((msg) => msg.type === type);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
if (limit > 0) {
|
| 25 |
+
messages = messages.slice(-limit);
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
return {
|
| 29 |
+
success: true,
|
| 30 |
+
data: {
|
| 31 |
+
messages: messages.map((msg) => ({
|
| 32 |
+
type: msg.type,
|
| 33 |
+
message: msg.message,
|
| 34 |
+
timestamp: msg.timestamp,
|
| 35 |
+
})),
|
| 36 |
+
total: messages.length,
|
| 37 |
+
},
|
| 38 |
+
};
|
| 39 |
+
} catch (error) {
|
| 40 |
+
return {
|
| 41 |
+
success: false,
|
| 42 |
+
error:
|
| 43 |
+
error instanceof Error ? error.message : "Failed to read console",
|
| 44 |
+
};
|
| 45 |
+
}
|
| 46 |
+
},
|
| 47 |
+
};
|
src/lib/tools/read-game-code.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { get } from "svelte/store";
|
| 2 |
+
import { editorStore } from "../stores/editor";
|
| 3 |
+
import type { Tool } from "./registry";
|
| 4 |
+
|
| 5 |
+
export const readGameCodeTool: Tool = {
|
| 6 |
+
name: "read_game_code",
|
| 7 |
+
description: "Get the current game code from the editor",
|
| 8 |
+
execute: async () => {
|
| 9 |
+
const state = get(editorStore);
|
| 10 |
+
return {
|
| 11 |
+
success: true,
|
| 12 |
+
data: {
|
| 13 |
+
content: state.content,
|
| 14 |
+
language: state.language,
|
| 15 |
+
},
|
| 16 |
+
};
|
| 17 |
+
},
|
| 18 |
+
};
|
src/lib/tools/registry.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface Tool {
|
| 2 |
+
name: string;
|
| 3 |
+
description: string;
|
| 4 |
+
execute: (params: unknown) => Promise<ToolResult>;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
export interface ToolResult {
|
| 8 |
+
success: boolean;
|
| 9 |
+
data?: unknown;
|
| 10 |
+
error?: string;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
class ToolRegistry {
|
| 14 |
+
private tools: Map<string, Tool> = new Map();
|
| 15 |
+
|
| 16 |
+
register(tool: Tool): void {
|
| 17 |
+
this.tools.set(tool.name, tool);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
async execute(toolName: string, params?: unknown): Promise<ToolResult> {
|
| 21 |
+
const tool = this.tools.get(toolName);
|
| 22 |
+
|
| 23 |
+
if (!tool) {
|
| 24 |
+
return {
|
| 25 |
+
success: false,
|
| 26 |
+
error: `Tool '${toolName}' not found. Available: ${this.getToolNames().join(", ")}`,
|
| 27 |
+
};
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
try {
|
| 31 |
+
return await tool.execute(params);
|
| 32 |
+
} catch (error) {
|
| 33 |
+
return {
|
| 34 |
+
success: false,
|
| 35 |
+
error: error instanceof Error ? error.message : "Unknown error",
|
| 36 |
+
};
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
getToolNames(): string[] {
|
| 41 |
+
return Array.from(this.tools.keys());
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
getToolDescriptions(): string {
|
| 45 |
+
return Array.from(this.tools.values())
|
| 46 |
+
.map((tool) => `- ${tool.name}: ${tool.description}`)
|
| 47 |
+
.join("\n");
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
export const toolRegistry = new ToolRegistry();
|
src/lib/tools/tools-integration.test.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { describe, it, expect, beforeEach } from "bun:test";
|
| 2 |
+
import { get } from "svelte/store";
|
| 3 |
+
import { toolRegistry } from "./registry";
|
| 4 |
+
import { editorStore } from "../stores/editor";
|
| 5 |
+
import { consoleStore } from "../stores/console";
|
| 6 |
+
import "./index"; // Import to register tools
|
| 7 |
+
|
| 8 |
+
describe("Tool Integration", () => {
|
| 9 |
+
beforeEach(() => {
|
| 10 |
+
// Reset stores
|
| 11 |
+
editorStore.setContent("");
|
| 12 |
+
consoleStore.reset();
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
describe("write_game_code", () => {
|
| 16 |
+
it("should update editor content", async () => {
|
| 17 |
+
const testCode = `<world canvas="#game-canvas">
|
| 18 |
+
<static-part pos="0 0 0" shape="box" size="10 1 10" color="#90ee90"></static-part>
|
| 19 |
+
</world>`;
|
| 20 |
+
|
| 21 |
+
const result = await toolRegistry.execute("write_game_code", {
|
| 22 |
+
content: testCode,
|
| 23 |
+
});
|
| 24 |
+
|
| 25 |
+
expect(result.success).toBe(true);
|
| 26 |
+
expect(get(editorStore).content).toBe(testCode);
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
it("should handle invalid input", async () => {
|
| 30 |
+
const result = await toolRegistry.execute("write_game_code", {
|
| 31 |
+
content: 123, // Invalid type
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
expect(result.success).toBe(false);
|
| 35 |
+
expect(result.error).toContain("Content must be a string");
|
| 36 |
+
});
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
describe("read_game_code", () => {
|
| 40 |
+
it("should read current editor content", async () => {
|
| 41 |
+
const testCode = "<world>Test</world>";
|
| 42 |
+
editorStore.setContent(testCode);
|
| 43 |
+
|
| 44 |
+
const result = await toolRegistry.execute("read_game_code");
|
| 45 |
+
|
| 46 |
+
expect(result.success).toBe(true);
|
| 47 |
+
expect(result.data).toEqual({
|
| 48 |
+
content: testCode,
|
| 49 |
+
language: "html",
|
| 50 |
+
});
|
| 51 |
+
});
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
describe("read_console_output", () => {
|
| 55 |
+
it("should read console messages", async () => {
|
| 56 |
+
consoleStore.addMessage("log", "Game started");
|
| 57 |
+
consoleStore.addMessage("error", "Entity not found");
|
| 58 |
+
consoleStore.addMessage("warn", "Low performance");
|
| 59 |
+
|
| 60 |
+
const result = await toolRegistry.execute("read_console_output", {
|
| 61 |
+
limit: 2,
|
| 62 |
+
});
|
| 63 |
+
|
| 64 |
+
expect(result.success).toBe(true);
|
| 65 |
+
const data = result.data as { messages: Array<{ message: string }> };
|
| 66 |
+
expect(data.messages).toHaveLength(2);
|
| 67 |
+
expect(data.messages[1].message).toBe("Low performance");
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
it("should filter by type", async () => {
|
| 71 |
+
consoleStore.addMessage("log", "Info 1");
|
| 72 |
+
consoleStore.addMessage("error", "Error 1");
|
| 73 |
+
consoleStore.addMessage("log", "Info 2");
|
| 74 |
+
|
| 75 |
+
const result = await toolRegistry.execute("read_console_output", {
|
| 76 |
+
type: "error",
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
+
expect(result.success).toBe(true);
|
| 80 |
+
const data = result.data as { messages: Array<{ message: string }> };
|
| 81 |
+
expect(data.messages).toHaveLength(1);
|
| 82 |
+
expect(data.messages[0].message).toBe("Error 1");
|
| 83 |
+
});
|
| 84 |
+
});
|
| 85 |
+
|
| 86 |
+
describe("Tool workflow", () => {
|
| 87 |
+
it("should support a complete edit cycle", async () => {
|
| 88 |
+
// 1. Write some code
|
| 89 |
+
const code = `<world canvas="#game-canvas">
|
| 90 |
+
<dynamic-part pos="0 5 0" shape="sphere" size="1" color="#ff0000"></dynamic-part>
|
| 91 |
+
</world>`;
|
| 92 |
+
|
| 93 |
+
await toolRegistry.execute("write_game_code", { content: code });
|
| 94 |
+
|
| 95 |
+
// 2. Read it back
|
| 96 |
+
const readResult = await toolRegistry.execute("read_game_code");
|
| 97 |
+
expect((readResult.data as { content: string }).content).toBe(code);
|
| 98 |
+
|
| 99 |
+
// 3. Simulate console output
|
| 100 |
+
consoleStore.addMessage("log", "Entity created: dynamic-part");
|
| 101 |
+
consoleStore.addMessage("info", "Physics initialized");
|
| 102 |
+
|
| 103 |
+
// 4. Read console
|
| 104 |
+
const consoleResult = await toolRegistry.execute("read_console_output");
|
| 105 |
+
expect(consoleResult.success).toBe(true);
|
| 106 |
+
const messages = (consoleResult.data as { messages: unknown[] }).messages;
|
| 107 |
+
expect(messages.length).toBeGreaterThan(0);
|
| 108 |
+
});
|
| 109 |
+
});
|
| 110 |
+
});
|
src/lib/tools/write-game-code.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { editorStore } from "../stores/editor";
|
| 2 |
+
import type { Tool } from "./registry";
|
| 3 |
+
|
| 4 |
+
interface WriteGameCodeParams {
|
| 5 |
+
content: string;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export const writeGameCodeTool: Tool = {
|
| 9 |
+
name: "write_game_code",
|
| 10 |
+
description: "Update the game code in the editor",
|
| 11 |
+
execute: async (params: unknown) => {
|
| 12 |
+
const { content } = params as WriteGameCodeParams;
|
| 13 |
+
|
| 14 |
+
if (typeof content !== "string") {
|
| 15 |
+
return {
|
| 16 |
+
success: false,
|
| 17 |
+
error: "Content must be a string",
|
| 18 |
+
};
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
try {
|
| 22 |
+
editorStore.setContent(content);
|
| 23 |
+
|
| 24 |
+
return {
|
| 25 |
+
success: true,
|
| 26 |
+
data: {
|
| 27 |
+
message: "Code updated successfully",
|
| 28 |
+
length: content.length,
|
| 29 |
+
},
|
| 30 |
+
};
|
| 31 |
+
} catch (error) {
|
| 32 |
+
return {
|
| 33 |
+
success: false,
|
| 34 |
+
error: error instanceof Error ? error.message : "Failed to update code",
|
| 35 |
+
};
|
| 36 |
+
}
|
| 37 |
+
},
|
| 38 |
+
};
|
tsconfig.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
| 18 |
"noUnusedParameters": true,
|
| 19 |
"noImplicitReturns": true,
|
| 20 |
"noFallthroughCasesInSwitch": true,
|
| 21 |
-
"types": ["vite/client", "node"]
|
| 22 |
},
|
| 23 |
"include": ["src/**/*", "src/**/*.svelte", "vite.config.ts"],
|
| 24 |
"exclude": ["node_modules", "dist"]
|
|
|
|
| 18 |
"noUnusedParameters": true,
|
| 19 |
"noImplicitReturns": true,
|
| 20 |
"noFallthroughCasesInSwitch": true,
|
| 21 |
+
"types": ["vite/client", "node", "bun"]
|
| 22 |
},
|
| 23 |
"include": ["src/**/*", "src/**/*.svelte", "vite.config.ts"],
|
| 24 |
"exclude": ["node_modules", "dist"]
|