import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { writeFile, mkdir } from 'fs/promises';
import { join } from 'path';
import { homedir, tmpdir } from 'os';
import { spawn } from 'child_process';
import { mkdirSync, rmSync } from 'fs';
import type { MorphFileFormat } from '$lib/types/morph';
import { createMorphFile } from '$lib/types/morph';
const PANELS_DIR = join(homedir(), 'morphbox', 'panels');
interface PanelMetadata {
id: string;
name: string;
description: string;
promptHistory: Array<{
prompt: string;
timestamp: string;
type: 'create' | 'morph';
}>;
version: string;
createdAt: string;
updatedAt: string;
}
function generatePanelId(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '') +
'-' +
Date.now().toString(36);
}
const DEFAULT_SYSTEM_PROMPT = `Create a vanilla JavaScript panel for MorphBox with the following requirements:
Panel Name: {name}
Description: {description}
Generate a complete HTML/CSS/JavaScript panel that:
1. Uses vanilla JavaScript (no frameworks)
2. Has access to these variables: panelId, data, websocketUrl
3. Can connect to WebSocket for real-time data: const ws = new WebSocket(websocketUrl)
4. Uses MorphBox CSS variables for theming (--bg-primary, --text-primary, --border-color, etc.)
5. Implements the functionality described above
6. Uses proper error handling and loading states where applicable
7. IMPORTANT: The JavaScript code will be automatically wrapped in an onMount() function, so you can safely access DOM elements directly without waiting for DOMContentLoaded
8. Is responsive and works well on mobile
WebSocket Access:
- Connect using: new WebSocket(websocketUrl)
- Listen for 'OUTPUT' events (terminal output)
- Listen for 'context_update' events (Claude Code context tracking)
- See full API: https://github.com/instant-unicorn/morphbox/blob/main/docs/CUSTOM_PANELS.md
IMPORTANT: Return ONLY the HTML code starting with
tags. Do not include any markdown formatting, code blocks, or explanations. Just the raw HTML/CSS/JavaScript code.
The panel should follow this structure:
Make it fully functional and production-ready. Use modern JavaScript features.`;
async function generatePanelWithClaude(name: string, description: string, customPrompt?: string): Promise
{
return new Promise((resolve, reject) => {
console.log('=== Starting Claude Panel Generation ===');
console.log('Panel Name:', name);
console.log('Description:', description);
console.log('Using custom prompt:', !!customPrompt);
// Use custom prompt or default, replacing placeholders
const promptTemplate = customPrompt || DEFAULT_SYSTEM_PROMPT;
const prompt = promptTemplate
.replace(/\{name\}/g, name)
.replace(/\{description\}/g, description);
console.log('Prompt length:', prompt.length);
// Use Claude CLI in a dedicated temporary directory
const startTime = Date.now();
console.log('Executing Claude CLI in dedicated temp directory...');
// Create a temporary directory for this Claude session
const tempDir = join(tmpdir(), `claude-session-${Date.now()}`);
mkdirSync(tempDir, { recursive: true });
console.log('Created temp directory:', tempDir);
const claude = spawn('claude', ['-p', prompt], {
stdio: ['pipe', 'pipe', 'pipe'],
cwd: tempDir, // Run Claude in the temp directory
env: { ...process.env }
});
console.log('Claude process spawned, PID:', claude.pid);
console.log('Command:', `claude -p [prompt] (in ${tempDir})`);
let output = '';
let error = '';
let isResolved = false;
// Set a timeout (2 minutes for complex panel generation)
const timeout = setTimeout(() => {
if (!isResolved) {
console.error('=== Claude Process Timeout ===');
console.error('Process took longer than 120 seconds');
console.error('Output received so far:', output.length, 'characters');
console.error('Error output:', error);
claude.kill('SIGTERM');
reject(new Error('Claude process timed out after 120 seconds'));
}
}, 120000);
// Handle stdout
claude.stdout.on('data', (data) => {
const chunk = data.toString();
output += chunk;
console.log(`Received stdout chunk: ${chunk.length} characters`);
console.log('Chunk preview:', chunk.substring(0, 200));
});
// Handle stderr
claude.stderr.on('data', (data) => {
const chunk = data.toString();
error += chunk;
console.error('Received stderr:', chunk);
});
// Handle errors
claude.on('error', (err) => {
clearTimeout(timeout);
isResolved = true;
console.error('Claude spawn error:', err);
reject(err);
});
// Handle exit
claude.on('exit', (code) => {
clearTimeout(timeout);
isResolved = true;
// Clean up temp directory
try {
rmSync(tempDir, { recursive: true, force: true });
console.log('Cleaned up temp directory');
} catch (e) {
console.warn('Failed to clean up temp directory:', e);
}
const duration = Date.now() - startTime;
console.log(`=== Claude Process Completed ===`);
console.log(`Exit code: ${code}`);
console.log(`Duration: ${duration}ms`);
console.log(`Total output: ${output.length} characters`);
console.log(`Total error: ${error.length} characters`);
if (code !== 0) {
console.error('Claude process failed with code:', code);
console.error('Error output:', error);
console.error('Standard output preview:', output.substring(0, 500));
reject(new Error(`Claude process exited with code ${code}: ${error}`));
} else {
console.log('Claude completed successfully');
console.log('Full output preview (first 1000 chars):', output.substring(0, 1000));
// Try multiple patterns to extract the component
// 1. Look for content between code blocks (most common)
const codeBlockPatterns = [
/```(?:html|svelte|xml|javascript)?\s*\n([\s\S]*?)```/,
/```\s*\n([\s\S]*?)```/
];
for (const pattern of codeBlockPatterns) {
const match = output.match(pattern);
if (match && match[1]) {
console.log('Found code block match with pattern:', pattern);
resolve(match[1].trim());
return;
}
}
// 2. Look for complete HTML structure with metadata
const fullMatch = output.match(/