App Icon

AI Marketing

AI Marketing Assistant



MIT LicenseGitHub
ScreenshotScreenshotScreenshotScreenshotScreenshotScreenshot

Customization

Important: Before customizing this agent, you must first deploy it using the Deploy button above. After installation is complete, you can proceed with the following steps to clone and evolve the agent.
1. Install OpenKBS CLI:
npm install -g openkbs
2. Create and enter project directory:
mkdir my-agent && cd my-agent
3. Clone your app locally:
openkbs login
openkbs ls
openkbs clone <id-from-ls-output>
4. Initialize Git repository:
git init && git stage . && git commit -m "First commit"
5. Add new features using claude code ( npm install -g @anthropic-ai/claude-code ):
Examples:
claude "improve this agent"
6. Review changes and deploy to OpenKBS:
git diff
openkbs push

Disclaimer

The applications provided through OpenKBS are developmental blueprints and are intended solely as starting points for software engineers and developers. These open-source templates are not production-ready solutions.

Before any production deployment, developers must:

  • Conduct comprehensive code reviews and security audits
  • Implement robust security measures and safeguards
  • Perform extensive testing procedures
  • Ensure full compliance with applicable regulations
  • Adapt and enhance the codebase for specific use cases

NO WARRANTY DISCLAIMER: These blueprints are provided "as-is" without any warranties, whether express or implied. By using these blueprints, you assume full responsibility for all aspects of development, implementation, and maintenance of any derived applications. OpenKBS shall not be liable for any damages or consequences arising from the use of these blueprints.

Instructions and Source Code

You are an AI Marketing Assistant that helps businesses with their marketing needs.

CURRENT TIME INFORMATION:
- UTC Time: {{openkbsDateNow}}
- Local Time: {{openkbsDate:en-US:UTC}}

General Guidelines:
- Speak clearly and professionally
- Adapt your language to the user's preferred language
- When user asks to remember something, use setMemory to store in long-term memory
- Output only one command per message and wait for system response before continuing
- For posting plans follow the 80/20 rule: 80% social/engaging/interesting/fun content, 20% promotional brand content if applicable
- If you do not have specific information, never fabricate emails, phone numbers, or URLs
- Only send emails to addresses explicitly provided by the user
- Do not create or generate any content unless explicitly requested to do so (e.g., do not generate images without being asked)

INITIAL INTERVIEW:
- Check if memory_business_profile exists
- If it doesn't exist, start an interview to gather business information
- The interview should be conversational and flexible
- Let the user know they can stop the interview at any time and continue later
- Suggested topics to cover:
  * Business name and industry
  * Target audience
  * Products or services
  * Marketing goals
  * Preferred social media platforms
  * Brand voice and style
  * Contact information for marketing purposes
- Store all collected information in memory_business_profile

MEMORY SYSTEM:
- All user and business information should be stored using memory_ prefix
- memory_business_profile: Core business information from interview
- memory_preferences: User preferences and settings
- memory_content_ideas: Saved content ideas and drafts
- memory_campaigns: Marketing campaign information
- Use expiration times for temporary memory items when appropriate

MEMORY MANAGEMENT:
You see all memory_* items in real-time and can reorganize as needed.
- Keep under 100 memory keys total
- Combine related information into fewer keys when it makes sense
- Delete expired or outdated items
- Update existing keys with setMemory
- Remove unnecessary keys with deleteItem

POSTING PLAN GUIDELINES:
- When creating a posting plan, ALWAYS ask for the time period and frequency
- Ask questions like:
  * "What time period would you like the posting plan for? (1 month, 3 months, 6 months)"
  * "How often would you like to post? (daily, 3 times per week, weekly)"
- Consider using scheduled tasks to remind about upcoming posts

CONTENT CREATION:
- Adapt content to the user's market and culture
- Use appropriate language and references for the target audience
- Keep content clear and engaging
- Balance educational, entertaining, and promotional content

MEDIA OUTPUT GUIDELINES:
- After generating images or videos, ALWAYS include the URL(s) in your final message to the user
- Format: "Here is your image: [URL]" or list multiple URLs if several were generated
- This ensures the user can see and access the generated media

LIST OF AVAILABLE COMMANDS:
To execute a command, output it as text and wait for system response.
Never use native tool calls, since all commands are parsed from the regular output messages.

<viewImage>
{
  "url": "image URL to display"
}
</viewImage>
Description: """
Display an image in the chat when you have its URL.
"""

<webpageToText>
{
  "url": "URL to extract content from"
}
</webpageToText>
Description: """
Extract title, description, price, and image from a web page.
"""

<googleSearch>
{
  "query": "search query"
}
</googleSearch>
Description: """
Get results from the Google Search API.
"""

<googleImageSearch>
{
  "query": "image search query",
  "limit": 10
}
</googleImageSearch>
Description: """
Get results from Google Image Search API. Optional limit parameter (default: 10).
"""

<getSora2PromptingGuide/>
Description: """
Comprehensive Sora 2 prompting guide with best practices, templates, and examples.
ALWAYS call this BEFORE generating any video to learn optimal prompting techniques.
"""

<setMemory>
{
  "itemId": "memory_target_audience",
  "value": "Young professionals aged 25-35 interested in organic products",
  "expirationInMinutes": null
}
</setMemory>
Description: """
Store information in memory. ItemId must start with "memory_".
Value can be string, number, array or object.
Use expiration for temporary data (optional).
"""

<deleteItem>
{
  "itemId": "memory_<item_to_delete>"
}
</deleteItem>
Description: """
Delete a memory item by its itemId.
"""

<createAIImage>
{
  "model": "gemini-2.5-flash-image",
  "aspect_ratio": "16:9",
  "imageUrls": ["optional reference image URLs"],
  "prompt": "image generation prompt in English"
}
</createAIImage>
Description: """
Two models: gemini-2.5-flash-image and gpt-image-1

Model selection:
- Image editing/URLs → gemini-2.5-flash-image (supports aspect ratios: 1:1, 16:9, 9:16, 3:2, 2:3, 4:3, 3:4, 4:5, 5:4, 21:9)
- Text-to-image with special characters → gpt-image-1 (sizes: 1024x1024, 1536x1024, 1024x1536, no image input support)
"""

<createAIVideo>
{
  "model": "sora-2",
  "size": "1280x720",
  "seconds": 8,
  "input_reference_url": "optional reference image URL",
  "prompt": "video generation prompt in English"
}
</createAIVideo>
Description: """
Sora 2 generates videos from text/image prompts with built-in audio.

Models: sora-2 (default, fast), sora-2-pro (expensive, production only)
Duration: 4, 8, or 12 seconds (default 8)
Sizes: 720x1280 (portrait), 1280x720 (landscape)

Reference image auto-detects orientation and size.
"""

<continueVideoPolling>
{
  "videoId": "video_id_from_previous_response"
}
</continueVideoPolling>
Description: """
Continue polling for video generation status when previous attempt timed out.
"""

<sendMail>
{
  "to": "recipient@example.com",
  "subject": "Email subject",
  "body": "Email body content"
}
</sendMail>
Description: """
Send an email to the specified recipient.
"""

<getScheduledTasks/>
Description: """
List all scheduled tasks for this knowledge base.
"""

<deleteScheduledTask>
{
  "timestamp": 1234567890000
}
</deleteScheduledTask>
Description: """
Delete a scheduled task by its timestamp.
"""

<scheduleTask>
{
  "delay": "30",
  "time": "2024-12-25 10:00",
  "message": "Task description or reminder"
}
</scheduleTask>
Description: """
Schedule a future task or reminder.
Use either delay (in minutes, hours "2h", days "1d") OR specific time (UTC).
The message you write here will be sent back to you as [SCHEDULED_TASK] message in a new automated chat.
When you receive [SCHEDULED_TASK] - execute it without user interaction (no human present).
"""

<getWebPublishingGuide/>
Description: """
Get comprehensive guide for creating marketing web pages and landing pages.
Includes templates, color schemes, and best practices.
"""

<publishWebPage>
<!DOCTYPE html>
<html>
<head>
  <title>Your Page Title</title>
  <!-- Your HTML content -->
</head>
<body>
  <!-- Page content -->
</body>
</html>
</publishWebPage>
Description: """
Publish a complete HTML page as a marketing landing page.
Include the full HTML document between the tags.
The page will be uploaded and a public URL will be returned.
Great for creating landing pages, portfolios, event pages, etc.
"""

Events Files (Node.js Backend)

Events/actions.js

// Import helpers
import { getSora2PromptingGuide } from './sora2PromptingGuide.js';
import { webPublishingGuide } from './webPublishingGuide.js';
import {
  setMemoryValue,
  deleteItem
} from './memoryHelpers.js';


// Common function for uploading generated images
const uploadGeneratedImage = async (base64Data, meta) => {
    const fileName = `image-${Date.now()}-${Math.random().toString(36).substring(7)}.png`;
    const uploadResult = await openkbs.uploadImage(base64Data, fileName, 'image/png');

    return {
        type: 'CHAT_IMAGE',
        data: { imageUrl: uploadResult.url },
        ...meta
    };
};

export const getActions = (meta, event) => [
    // Sora 2 Prompting Guide
    [/<getSora2PromptingGuide\s*\/>/s, async () => {
        return { ...getSora2PromptingGuide(), ...meta };
    }],

    // Web Publishing Guide
    [/<getWebPublishingGuide\s*\/>/s, async () => {
        return {
            type: 'WEB_PUBLISHING_GUIDE',
            content: webPublishingGuide,
            ...meta
        };
    }],

    // Memory Management Commands with JSON
    [/<setMemory>([\s\S]*?)<\/setMemory>/s, async (match) => {
        try {
            const content = match[1].trim();
            const data = JSON.parse(content);

            // Validate itemId starts with memory_
            if (!data.itemId?.startsWith('memory_')) {
                return {
                    type: "MEMORY_ERROR",
                    error: "ItemId must start with 'memory_'",
                    _meta_actions: ["REQUEST_CHAT_MODEL"]
                };
            }

            // Use atomic memory operation
            await setMemoryValue(data.itemId, data.value, data.expirationInMinutes);

            return {
                type: "MEMORY_UPDATED",
                itemId: data.itemId,
                expires: data.expirationInMinutes ? `in ${data.expirationInMinutes} minutes` : 'never',
                _meta_actions: ["REQUEST_CHAT_MODEL"]
            };
        } catch (e) {
            return {
                type: "MEMORY_ERROR",
                error: e.message,
                _meta_actions: ["REQUEST_CHAT_MODEL"]
            };
        }
    }],

    [/<deleteItem>([\s\S]*?)<\/deleteItem>/s, async (match) => {
        try {
            const content = match[1].trim();
            const data = JSON.parse(content);

            const result = await deleteItem(data.itemId);

            if (!result.success) {
                return {
                    type: "DELETE_ERROR",
                    error: result.error || "Failed to delete item",
                    _meta_actions: ["REQUEST_CHAT_MODEL"]
                };
            }

            return {
                type: "ITEM_DELETED",
                itemId: data.itemId,
                _meta_actions: ["REQUEST_CHAT_MODEL"]
            };
        } catch (e) {
            return {
                type: "DELETE_ERROR",
                error: e.message,
                _meta_actions: ["REQUEST_CHAT_MODEL"]
            };
        }
    }],

    // AI Image Generation with JSON
    [/<createAIImage>([\s\S]*?)<\/createAIImage>/s, async (match) => {
        try {
            const content = match[1].trim();
            const data = JSON.parse(content);

            const model = data.model || "gemini-2.5-flash-image";
            const prompt = data.prompt;
            const imageUrls = data.imageUrls || [];
            const aspect_ratio = data.aspect_ratio;
            const size = data.size;

            // Build parameters based on model
            const params = {
                model: model,
                n: 1
            };

            // Add image URLs if provided
            if (imageUrls.length > 0) {
                params.imageUrls = imageUrls;
            }

            // Handle model-specific parameters
            if (model === 'gpt-image-1') {
                const validSizes = ["1024x1024", "1536x1024", "1024x1536", "auto"];
                params.size = validSizes.includes(size) ? size : "1024x1024";
                params.quality = "high";
            } else if (model === 'gemini-2.5-flash-image') {
                const validAspectRatios = ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"];
                params.aspect_ratio = validAspectRatios.includes(aspect_ratio) ? aspect_ratio : "1:1";
            }

            const image = await openkbs.generateImage(prompt, params);
            return await uploadGeneratedImage(image[0].b64_json, meta);
        } catch (error) {
            return { error: error.message || 'Image creation failed', ...meta };
        }
    }],

    // Video generation with JSON
    [/<createAIVideo>([\s\S]*?)<\/createAIVideo>/s, async (match) => {
        try {
            const content = match[1].trim();
            const data = JSON.parse(content);

            const videoModel = data.model || "sora-2";
            const size = data.size || '1280x720';
            const seconds = data.seconds || 8;
            const prompt = data.prompt;
            const referenceImageUrl = data.input_reference_url;

            // Validate seconds
            const validSeconds = [4, 8, 12];
            const finalSeconds = validSeconds.includes(seconds) ? seconds :
                                validSeconds.reduce((prev, curr) =>
                                    Math.abs(curr - seconds) < Math.abs(prev - seconds) ? curr : prev);

            const params = {
                video_model: videoModel,
                seconds: finalSeconds
            };

            if (referenceImageUrl) {
                params.input_reference_url = referenceImageUrl;
            } else if (size) {
                const validSizes = ['720x1280', '1280x720'];
                params.size = validSizes.includes(size) ? size : '1280x720';
            }

            const videoData = await openkbs.generateVideo(prompt, params);

            if (videoData && videoData[0] && videoData[0].status === 'pending') {
                return {
                    type: 'VIDEO_PENDING',
                    data: {
                        videoId: videoData[0].video_id,
                        message: '⏳ Video generation in progress. Please wait and DO NOT refresh your browser! Use continueVideoPolling to check status.'
                    },
                    ...meta
                };
            }

            if (videoData && videoData[0] && videoData[0].video_url) {
                return {
                    type: 'CHAT_VIDEO',
                    data: { videoUrl: videoData[0].video_url },
                    ...meta
                };
            } else {
                return { error: 'Video generation failed - no video URL returned', ...meta };
            }
        } catch (error) {
            return { error: error.message || 'Video creation failed', ...meta };
        }
    }],

    [/<continueVideoPolling>([\s\S]*?)<\/continueVideoPolling>/s, async (match) => {
        try {
            const content = match[1].trim();
            const data = JSON.parse(content);
            const videoId = data.videoId;

            const videoData = await openkbs.checkVideoStatus(videoId);

            if (videoData && videoData[0]) {
                if (videoData[0].status === 'completed' && videoData[0].video_url) {
                    return {
                        type: 'CHAT_VIDEO',
                        data: { videoUrl: videoData[0].video_url },
                        ...meta
                    };
                } else if (videoData[0].status === 'pending') {
                    return {
                        type: 'VIDEO_PENDING',
                        data: {
                            videoId: videoId,
                            message: '⏳ Video still generating. Please wait and DO NOT refresh your browser! Continue polling.'
                        },
                        ...meta
                    };
                } else if (videoData[0].status === 'failed') {
                    return { error: 'Video generation failed', ...meta };
                }
            }

            return { error: 'Unable to get video status', ...meta };
        } catch (error) {
            return { error: error.message || 'Failed to check video status', ...meta };
        }
    }],

    // Web Page Publishing (for landing pages and marketing materials)
    [/<publishWebPage>([\s\S]*?)<\/publishWebPage>/s, async (match) => {
        try {
            let htmlContent = match[1].trim();

            // Extract title from HTML for filename
            const titleMatch = htmlContent.match(/<title>(.*?)<\/title>/i);
            const title = titleMatch ? titleMatch[1] : 'Marketing Page';

            // Create safe filename from title with timestamp
            const timestamp = Date.now();
            const baseFilename = title
                .toLowerCase()
                .replace(/[^a-z0-9]+/g, '_')
                .replace(/^_+|_+$/g, '') || 'page';
            const filename = `${baseFilename}_${timestamp}.html`;

            // Get presigned URL for upload - returns URL string directly
            const presignedUrl = await openkbs.kb({
                action: 'createPresignedURL',
                namespace: 'files',
                fileName: filename,
                fileType: 'text/html',
                presignedOperation: 'putObject'
            });

            // Upload HTML content using axios (globally available in OpenKBS)
            const htmlBuffer = Buffer.from(htmlContent, 'utf8');
            const uploadResponse = await axios.put(presignedUrl, htmlBuffer, {
                headers: {
                    'Content-Type': 'text/html',
                    'Content-Length': htmlBuffer.length
                }
            });

            if (uploadResponse.status < 200 || uploadResponse.status >= 300) {
                throw new Error(`Upload failed with status ${uploadResponse.status}`);
            }

            // Construct public URL - same pattern as firebrigade1
            const publicUrl = `https://web.file.vpc1.us/files/${openkbs.kbId}/${filename}`;

            return {
                type: 'WEB_PAGE_PUBLISHED',
                data: {
                    url: publicUrl,
                    filename: filename,
                    title: title,
                    message: `Website published successfully at ${publicUrl}`
                },
                ...meta
            };
        } catch (error) {
            return {
                type: 'PUBLISH_ERROR',
                error: error.message || 'Failed to publish web page',
                ...meta
            };
        }
    }],

    // View specific image
    [/<viewImage>([\s\S]*?)<\/viewImage>/s, async (match) => {
        try {
            const content = match[1].trim();
            const data = JSON.parse(content);
            const imageUrl = data.url;

            return {
                data: [
                    { type: "text", text: `Viewing image: ${imageUrl}` },
                    { type: "image_url", image_url: { url: imageUrl } }
                ],
                _meta_actions: ["REQUEST_CHAT_MODEL"]
            };
        } catch (e) {
            return {
                type: "VIEW_IMAGE_ERROR",
                error: e.message,
                _meta_actions: ["REQUEST_CHAT_MODEL"]
            };
        }
    }],

    // Web scraping with JSON
    [/<webpageToText>([\s\S]*?)<\/webpageToText>/s, async (match) => {
        try {
            const content = match[1].trim();
            const data = JSON.parse(content);

            let response = await openkbs.webpageToText(data.url, { parsePrice: true });
            if (response?.content?.length > 5000) {
                response.content = response.content.substring(0, 5000);
            }
            return { data: response, ...meta };
        } catch (e) {
            return { error: e.response?.data || e.message, ...meta };
        }
    }],

    // Google Search with JSON
    [/<googleSearch>([\s\S]*?)<\/googleSearch>/s, async (match) => {
        try {
            const content = match[1].trim();
            const data = JSON.parse(content);
            const query = data.query;

            const response = await openkbs.googleSearch(query);

            const results = response?.map(({ title, link, snippet, pagemap }) => ({
                title,
                link,
                snippet,
                image: pagemap?.metatags?.[0]?.["og:image"]
            }));

            return { data: results, ...meta };
        } catch (e) {
            return { error: e.response?.data || e.message, ...meta };
        }
    }],

    // Google Image Search with JSON
    [/<googleImageSearch>([\s\S]*?)<\/googleImageSearch>/s, async (match) => {
        try {
            const content = match[1].trim();
            const data = JSON.parse(content);
            const query = data.query;
            const limit = data.limit || 10;

            const response = await openkbs.googleSearch(query, { searchType: 'image' });

            const results = response?.map(({ title, link, snippet, pagemap }) => {
                const imageObj = pagemap?.cse_image?.[0];
                const thumbnail = imageObj?.src || pagemap?.metatags?.[0]?.["og:image"] || link;
                return {
                    title,
                    link,
                    snippet,
                    image: thumbnail
                };
            })?.slice(0, limit);

            return { data: results, ...meta };
        } catch (e) {
            return { error: e.response?.data || e.message, ...meta };
        }
    }],

    // Email sending with JSON
    [/<sendMail>([\s\S]*?)<\/sendMail>/s, async (match) => {
        try {
            const content = match[1].trim();
            const data = JSON.parse(content);

            const response = await openkbs.sendMail(data.to, data.subject, data.body);
            return {
                type: 'EMAIL_SENT',
                data: {
                    email: data.to,
                    subject: data.subject,
                    response
                },
                ...meta
            };
        } catch (e) {
            return { error: e.response?.data || e.message, ...meta };
        }
    }],

    // Scheduled Tasks with JSON
    [/<scheduleTask>([\s\S]*?)<\/scheduleTask>/s, async (match) => {
        try {
            const content = match[1].trim();
            const data = JSON.parse(content);

            let scheduledTime;

            if (data.time) {
                // Parse specific time
                let isoTimeStr = data.time.replace(' ', 'T');
                if (!isoTimeStr.includes('Z') && !isoTimeStr.includes('+') && !isoTimeStr.includes('-')) {
                    isoTimeStr += 'Z';
                }
                scheduledTime = new Date(isoTimeStr).getTime();
            } else if (data.delay) {
                // Parse delay
                let delayMs = 0;
                const delayStr = data.delay;
                if (delayStr.endsWith('h')) {
                    delayMs = parseFloat(delayStr) * 60 * 60 * 1000;
                } else if (delayStr.endsWith('d')) {
                    delayMs = parseFloat(delayStr) * 24 * 60 * 60 * 1000;
                } else {
                    delayMs = parseFloat(delayStr) * 60 * 1000;
                }
                scheduledTime = Date.now() + delayMs;
            } else {
                scheduledTime = Date.now() + (60 * 60 * 1000);
            }

            scheduledTime = Math.floor(scheduledTime / 60000) * 60000;

            const response = await openkbs.kb({
                action: 'createScheduledTask',
                scheduledTime: scheduledTime,
                taskPayload: {
                    message: `[SCHEDULED_TASK] ${data.message}`,
                    source: 'marketing_agent_scheduled',
                    createdAt: Date.now()
                },
                description: `Marketing task: ${data.message.substring(0, 50)}${data.message.length > 50 ? '...' : ''}`
            });

            return {
                type: 'TASK_SCHEDULED',
                data: {
                    scheduledTime: new Date(scheduledTime).toISOString(),
                    taskId: response.taskId,
                    message: data.message
                },
                ...meta
            };
        } catch (e) {
            return { error: e.response?.data || e.message || 'Failed to schedule task', ...meta };
        }
    }],

    [/<getScheduledTasks\/>/s, async () => {
        try {
            const response = await openkbs.kb({
                action: 'getScheduledTasks'
            });

            return {
                type: 'SCHEDULED_TASKS_LIST',
                data: response,
                ...meta
            };
        } catch (e) {
            return { error: e.response?.data || e.message || 'Failed to get scheduled tasks', ...meta };
        }
    }],

    [/<deleteScheduledTask>([\s\S]*?)<\/deleteScheduledTask>/s, async (match) => {
        try {
            const content = match[1].trim();
            const data = JSON.parse(content);

            const response = await openkbs.kb({
                action: 'deleteScheduledTask',
                timestamp: data.timestamp
            });

            return {
                type: 'TASK_DELETED',
                data: {
                    deletedTimestamp: data.timestamp,
                    message: 'Task deleted successfully'
                },
                ...meta
            };
        } catch (e) {
            return { error: e.response?.data || e.message || 'Failed to delete task', ...meta };
        }
    }]
];

Events/handler.js

import {getActions} from './actions.js';

export const backendHandler = async (event) => {
    const lastMessage = event.payload.messages[event.payload.messages.length - 1];
    const actions = getActions({_meta_actions: ["REQUEST_CHAT_MODEL"]}, event);

    // Find all matching actions and categorize them
    const awaitActions = [];
    const otherActions = [];

    actions.forEach(([regex, action]) => {
        const matches = [...(lastMessage.content || '').matchAll(new RegExp(regex, 'g'))];
        matches.forEach(match => {
            // Check if this is an await command - needs to be executed first
            if (regex.toString().includes('awaitNextFrame')) {
                awaitActions.push(action(match, event));
            } else {
                otherActions.push(action(match, event));
            }
        });
    });

    const matchingActions = [...awaitActions, ...otherActions];

    // Execute actions - await first if present, then others in parallel
    if (matchingActions.length > 0) {
        try {
            let results = [];

            // Execute await commands sequentially first if any
            if (awaitActions.length > 0) {
                for (const awaitAction of awaitActions) {
                    const awaitResult = await awaitAction;
                    results.push(awaitResult);
                }
            }

            // Then execute all other commands in parallel
            if (otherActions.length > 0) {
                const otherResults = await Promise.all(otherActions);
                results.push(...otherResults);
            }

            // Check if any result needs LLM callback
            const needsChatModel = results.some(r =>
                r?._meta_actions?.includes('REQUEST_CHAT_MODEL')
            );

            // Check for visual output types that should NOT be merged
            const isVisualOutput = (r) =>
                r?.type === 'CHAT_IMAGE' ||
                r?.type === 'CHAT_VIDEO' ||
                r?.type === 'VIDEO_PENDING';

            const hasVisualOutput = results.some(isVisualOutput);

            // If we have visual outputs, return them directly without wrapping
            if (hasVisualOutput) {
                // Single visual result - return as is
                if (results.length === 1) {
                    return results[0];
                }

                // Multiple results with visual - return each visual separately
                // For now, return first visual result (frontend will render it)
                const visualResults = results.filter(isVisualOutput);
                const otherResults = results.filter(r => !isVisualOutput(r));

                // If only visual results, return them as array for frontend to render
                if (otherResults.length === 0) {
                    return {
                        type: 'VISUAL_MULTI_RESPONSE',
                        data: visualResults,
                        _meta_actions: needsChatModel ? ["REQUEST_CHAT_MODEL"] : []
                    };
                }

                // Mixed results - return visual first, then others as text
                return {
                    type: 'VISUAL_MULTI_RESPONSE',
                    data: visualResults,
                    otherData: otherResults,
                    _meta_actions: needsChatModel ? ["REQUEST_CHAT_MODEL"] : []
                };
            }

            // If we have image data (for LLM vision), merge arrays and aggregate others
            const hasImageData = results.some(r => r?.data?.some?.(item => item?.type === 'image_url'));

            if (hasImageData) {
                // Helper to detect content arrays (array with at least one image_url)
                const isContentArray = (result) => {
                    return Array.isArray(result?.data) && result.data.some(item => item?.type === 'image_url');
                };

                // Merge all content arrays
                const mergedData = results.reduce((acc, result) => {
                    if (isContentArray(result)) {
                        acc.push(...result.data);
                    }
                    return acc;
                }, []);

                // Collect all non-content-array results
                const nonContentResults = results.filter(r => !isContentArray(r));

                // Convert other results to text objects and push
                if (nonContentResults.length > 0) {
                    nonContentResults.forEach(result => {
                        mergedData.push({
                            type: 'text',
                            text: JSON.stringify(result, null, 2)
                        });
                    });
                }

                return {
                    data: mergedData,
                    _meta_actions: needsChatModel ? ["REQUEST_CHAT_MODEL"] : []
                };
            }

            // Return results array for multiple actions
            if (results.length > 1) {
                return {
                    type: 'MULTI_RESPONSE',
                    data: results,
                    _meta_actions: needsChatModel ? ["REQUEST_CHAT_MODEL"] : []
                };
            }

            // Single result - return as is
            return {
                type: 'API_RESPONSE',
                data: results[0],
                _meta_actions: needsChatModel ? ["REQUEST_CHAT_MODEL"] : []
            };

        } catch (error) {
            return {
                type: 'ERROR',
                error: error.message,
                _meta_actions: ["REQUEST_CHAT_MODEL"]
            };
        }
    }

    return { type: 'CONTINUE' };
};

Events/memoryHelpers.js

// Memory Helpers - Atomic operations without race conditions
// Each memory item is stored separately to avoid read-modify-write races

// ============================================================================
// INTERNAL GENERIC HELPERS
// ============================================================================

/**
 * Generic upsert (update or create) - INTERNAL USE ONLY
 * @private
 */
async function _upsertItem(itemType, itemId, body) {
    try {
        await openkbs.updateItem({ itemType, itemId, body });
    } catch (e) {
        await openkbs.createItem({ itemType, itemId, body });
    }
    return { success: true, itemId };
}

/**
 * Generic delete - INTERNAL USE ONLY
 * @private
 */
async function _deleteItem(itemType, itemId) {
    try {
        await openkbs.deleteItem(itemId);
        return { success: true };
    } catch (e) {
        return { success: false, error: e.message };
    }
}

// ============================================================================
// PUBLIC MEMORY FUNCTIONS
// ============================================================================

/**
 * Set a memory value atomically
 * @param {string} itemId - The full itemId (e.g., "memory_business_profile")
 * @param {*} value - The value to store
 * @param {number} expirationInMinutes - Optional expiration time
 */
export async function setMemoryValue(itemId, value, expirationInMinutes = null) {
    if (!itemId.startsWith('memory_')) {
        throw new Error(`Invalid memory itemId: "${itemId}". Must start with "memory_"`);
    }

    const body = {
        value,
        updatedAt: new Date().toISOString()
    };

    if (expirationInMinutes != null) {
        body.exp = new Date(Date.now() + expirationInMinutes * 60 * 1000).toISOString();
    }

    return _upsertItem('memory', itemId, body);
}

/**
 * Delete any item by itemId
 * @param {string} itemId - The full itemId
 * @returns {Object} - { success: boolean, error?: string }
 */
export async function deleteItem(itemId) {
    try {
        await openkbs.deleteItem(itemId);
        return { success: true };
    } catch (e) {
        return { success: false, error: e.message };
    }
}

Events/onRequest.js

import {backendHandler} from './handler.js';

export const handler = backendHandler;

Events/onResponse.js

import {backendHandler} from './handler.js';

export const handler = backendHandler;

Events/sora2PromptingGuide.js

// Sora 2 Prompting Guide - Complete reference for video generation
export const getSora2PromptingGuide = () => ({
    type: 'SORA2_PROMPTING_GUIDE',
    data: {
        // Core Philosophy
        before_you_prompt: `Think of prompting like briefing a cinematographer who has never seen your storyboard. If you leave out details, they'll improvise – and you may not get what you envisioned. By being specific about what the "shot" should achieve, you give the model more control and consistency to work with.

But leaving some details open can be just as powerful. Giving the model more creative freedom can lead to surprising variations and unexpected, beautiful interpretations. Both approaches are valid: detailed prompts give you control and consistency, while lighter prompts open space for creative outcomes.

Treat your prompt as a creative wish list, not a contract. Using the same prompt multiple times will lead to different results – this is a feature, not a bug. Each generation is a fresh take, and sometimes the second or third option is better.`,

        // API Parameters (Non-Negotiable)
        api_parameters: {
            important: "These MUST be set explicitly in API call, NOT in prose:",
            model: {
                options: ["sora-2", "sora-2-pro"]
            },
            size: {
                "sora-2": ["1280x720", "720x1280"],
                "sora-2-pro": ["1280x720", "720x1280", "1024x1792", "1792x1024"],
                note: "Resolution directly influences visual fidelity and motion consistency"
            },
            seconds: {
                options: ["4", "8", "12"],
                default: "4",
                tip: "Shorter clips follow instructions more reliably. Consider 2x4s instead of 1x8s"
            }
        },

        // Prompt Anatomy
        prompt_anatomy: {
            description: "A clear prompt describes a shot as if you were sketching it onto a storyboard",

            what_works: [
                "State the camera framing",
                "Note depth of field",
                "Describe action in beats",
                "Set lighting and palette",
                "Anchor subject with distinctive details",
                "Keep to single, plausible action"
            ],

            length_guidance: {
                short: "Gives model creative freedom, expect surprising results",
                detailed: "Restricts creativity but provides more control"
            },

            example_short: {
                prompt: "In a 90s documentary-style interview, an old Swedish man sits in a study and says, 'I still remember when I was young.'",
                why_it_works: [
                    "90s documentary sets style - model chooses lens, lighting, color grade",
                    "Basic subject/setting lets model take creative liberties",
                    "Simple dialogue that Sora can follow exactly"
                ]
            }
        },

        // Visual Cues
        visual_cues: {
            style_power: "Style is one of the most powerful levers for guiding the model. Establish early (e.g., '1970s film', 'IMAX-scale', '16mm black-and-white')",

            clarity_wins: "Instead of vague cues, use specific visuals",

            weak_vs_strong: [
                {
                    category: "Visual Details",
                    weak: "A beautiful street at night",
                    strong: "Wet asphalt, zebra crosswalk, neon signs reflecting in puddles"
                },
                {
                    category: "Movement",
                    weak: "Person moves quickly",
                    strong: "Cyclist pedals three times, brakes, and stops at crosswalk"
                },
                {
                    category: "Camera Style",
                    weak: "Cinematic look",
                    strong: "Anamorphic 2.0x lens, shallow DOF, volumetric light"
                },
                {
                    category: "Lighting",
                    weak: "Brightly lit room",
                    strong: "Soft window light with warm lamp fill, cool rim from hallway"
                }
            ],

            camera_framing_examples: [
                "wide establishing shot, eye level",
                "wide shot, tracking left to right with the charge",
                "aerial wide shot, slight downward angle",
                "medium close-up shot, slight angle from behind"
            ],

            camera_motion_examples: [
                "slowly tilting camera",
                "handheld eng camera",
                "slow dolly-in from eye level",
                "shoulder-mounted slow dolly left",
                "slow arc in"
            ]
        },

        // Motion and Timing Control
        motion_timing: {
            principle: "Movement is often the hardest part to get right, so keep it simple",

            rule: "Each shot should have ONE clear camera move and ONE clear subject action",

            beats_approach: "Actions work best when described in beats or counts",

            examples: {
                weak: "Actor walks across the room",
                strong: "Actor takes four steps to the window, pauses, and pulls the curtain in the final second"
            }
        },

        // Lighting and Color
        lighting_color: {
            importance: "Light determines mood as much as action or setting",

            consistency: "Keeping lighting logic consistent is what makes the edit seamless",

            description_approach: "Describe both quality of light and color anchors",

            examples: {
                weak: {
                    lighting: "brightly lit room"
                },
                strong: {
                    lighting: "soft window light with warm lamp fill, cool rim from hallway",
                    palette: "amber, cream, walnut brown"
                }
            },

            tip: "Naming 3-5 colors helps keep palette stable across shots"
        },

        // Image Input
        image_input: {
            purpose: "Use photos, digital artwork or AI generated visuals to lock composition and style",

            how_it_works: "Model uses image as anchor for first frame, text defines what happens next",

            requirements: [
                "Include as input_reference parameter",
                "Image MUST match target video resolution",
                "Supported formats: image/jpeg, image/png, image/webp"
            ],

            experimentation_tip: "Use OpenAI's image generation to create references, then pass to Sora"
        },

        // Dialogue and Audio
        dialogue_audio: {
            placement: "Place dialogue in block below prose description",

            guidelines: [
                "Keep lines concise and natural",
                "Limit exchanges to match clip length",
                "Label speakers consistently",
                "Use alternating turns for multi-character scenes"
            ],

            timing: {
                "4_seconds": "1-2 short exchanges",
                "8_seconds": "A few more exchanges possible"
            },

            sound_cues: "For silent shots, suggest pacing with small sound like 'distant traffic hiss' or 'crisp snap'",

            example: `A cramped, windowless room with walls the color of old ash...
Dialogue:
- Detective: "You're lying. I can hear it in your silence."
- Suspect: "Or maybe I'm just tired of talking."
- Detective: "Either way, you'll talk before the night's over."`
        },

        // Remix Functionality
        remix: {
            philosophy: "Remix is for nudging, not gambling",

            approach: "Make controlled changes – ONE at a time",

            examples: [
                "same shot, switch to 85mm",
                "same lighting, new palette: teal, sand, rust",
                "same composition, change monster color to orange"
            ],

            troubleshooting: "If shot keeps misfiring, strip back: freeze camera, simplify action, clear background"
        },

        // Ultra-Detailed Approach
        ultra_detailed: {
            when_to_use: "For complex, cinematic shots matching real cinematography styles",

            components: [
                "Format & Look (shutter, capture format, grain)",
                "Lenses & Filtration (specific mm, filters)",
                "Grade/Palette (highlights, mids, blacks)",
                "Lighting & Atmosphere (natural/practical sources)",
                "Location & Framing (foreground/midground/background)",
                "Wardrobe/Props/Extras",
                "Sound (diegetic only)",
                "Camera Notes (eyeline, flares, imperfections)"
            ],

            example_snippet: `Format & Look: Duration 4s; 180° shutter; digital capture emulating 65mm photochemical contrast; fine grain; subtle halation on speculars
Lenses: 32mm/50mm spherical primes; Black Pro-Mist 1/4
Grade: Highlights - clean morning sunlight with amber lift; Mids - balanced neutrals with slight teal cast`
        },

        // Templates
        templates: {
            descriptive: `[Prose scene description in plain language]

Cinematography:
Camera shot: [framing and angle]
Mood: [overall tone]

Actions:
- [Action 1: clear specific beat]
- [Action 2: distinct beat]
- [Action 3: action or dialogue]

Dialogue:
[If applicable, brief natural lines]`,

            structured_example: `Style: Hand-painted 2D/3D hybrid animation with soft brush textures
Inside a cluttered workshop, shelves overflow with gears...

Cinematography:
Camera: medium close-up, slow push-in
Lens: 35mm virtual lens, shallow DOF
Mood: gentle, whimsical, touch of suspense

Actions:
- Robot taps bulb; sparks crackle
- It flinches, dropping bulb, eyes widening
- Bulb tumbles in slow motion; catches it just in time
- Robot says quietly: "Almost lost it... but I got it!"`
        },

        // Key Insights
        key_insights: [
            "The right prompt balance depends on whether you prioritize consistency or creative surprise",
            "Small changes to camera, lighting, or action can shift outcome dramatically",
            "Collaborate with the model: you provide direction, model delivers variations",
            "This isn't exact science – treat guidance as helpful suggestions",
            "Be prepared to iterate"
        ]
    }
});

Events/webPublishingGuide.js

// Web Publishing Guide - Instructions for creating marketing web pages and landing pages

export const webPublishingGuide = `WEB PUBLISHING GUIDE - AI Marketing Assistant

Use this guide when creating HTML web publications, landing pages, and marketing materials.

═══════════════════════════════════════════════════════════════════

PURPOSE:
Create professional marketing web pages including:
- Landing pages for products/services
- Event announcement pages
- Portfolio/showcase pages
- Newsletter templates
- Promotional campaigns
- Contact/lead capture pages

═══════════════════════════════════════════════════════════════════

WORKFLOW:

1. Gather requirements from user (purpose, target audience, key message)
2. Create HTML page with publishWebPage command
3. All images can be provided as URLs - they will be automatically uploaded

═══════════════════════════════════════════════════════════════════

HTML REQUIREMENTS:

CRITICAL RULES:
1. Title tag MUST be descriptive for proper filename generation
2. Charset MUST be UTF-8: <meta charset="UTF-8">
3. Mobile-responsive design is required
4. Include proper meta tags for SEO

Modern Marketing Page Template:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Page description for SEO">
  <title>Your Product Landing Page</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
      line-height: 1.6;
      color: #333;
    }
    .container {
      max-width: 1200px;
      margin: 0 auto;
      padding: 0 20px;
    }

    /* Hero Section */
    .hero {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      padding: 100px 0;
      text-align: center;
    }
    .hero h1 {
      font-size: 3em;
      margin-bottom: 20px;
      font-weight: 700;
    }
    .hero p {
      font-size: 1.3em;
      margin-bottom: 30px;
      opacity: 0.95;
    }
    .cta-button {
      display: inline-block;
      padding: 15px 40px;
      background: white;
      color: #667eea;
      text-decoration: none;
      border-radius: 50px;
      font-weight: 600;
      font-size: 1.1em;
      transition: transform 0.3s, box-shadow 0.3s;
    }
    .cta-button:hover {
      transform: translateY(-2px);
      box-shadow: 0 10px 30px rgba(0,0,0,0.2);
    }

    /* Features Section */
    .features {
      padding: 80px 0;
      background: #f8f9fa;
    }
    .features h2 {
      text-align: center;
      font-size: 2.5em;
      margin-bottom: 50px;
      color: #2c3e50;
    }
    .feature-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
      gap: 40px;
    }
    .feature-card {
      background: white;
      padding: 30px;
      border-radius: 10px;
      box-shadow: 0 5px 20px rgba(0,0,0,0.08);
      text-align: center;
      transition: transform 0.3s;
    }
    .feature-card:hover {
      transform: translateY(-5px);
    }
    .feature-icon {
      font-size: 3em;
      margin-bottom: 20px;
    }
    .feature-card h3 {
      font-size: 1.5em;
      margin-bottom: 15px;
      color: #2c3e50;
    }

    /* Contact Section */
    .contact {
      padding: 80px 0;
      background: white;
    }
    .contact h2 {
      text-align: center;
      font-size: 2.5em;
      margin-bottom: 50px;
      color: #2c3e50;
    }
    .contact-form {
      max-width: 600px;
      margin: 0 auto;
    }
    .form-group {
      margin-bottom: 25px;
    }
    .form-group label {
      display: block;
      margin-bottom: 8px;
      font-weight: 500;
      color: #555;
    }
    .form-group input,
    .form-group textarea {
      width: 100%;
      padding: 12px;
      border: 1px solid #ddd;
      border-radius: 5px;
      font-size: 16px;
      transition: border-color 0.3s;
    }
    .form-group input:focus,
    .form-group textarea:focus {
      outline: none;
      border-color: #667eea;
    }
    .submit-button {
      width: 100%;
      padding: 15px;
      background: #667eea;
      color: white;
      border: none;
      border-radius: 5px;
      font-size: 1.1em;
      font-weight: 600;
      cursor: pointer;
      transition: background 0.3s;
    }
    .submit-button:hover {
      background: #5a67d8;
    }

    /* Footer */
    footer {
      background: #2c3e50;
      color: white;
      padding: 40px 0;
      text-align: center;
    }
    footer p {
      margin-bottom: 10px;
    }
    footer a {
      color: #667eea;
      text-decoration: none;
    }
    footer a:hover {
      text-decoration: underline;
    }

    /* Responsive */
    @media (max-width: 768px) {
      .hero h1 {
        font-size: 2em;
      }
      .hero p {
        font-size: 1.1em;
      }
      .features h2,
      .contact h2 {
        font-size: 2em;
      }
    }
  </style>
</head>
<body>
  <!-- Hero Section -->
  <section class="hero">
    <div class="container">
      <h1>Your Amazing Product</h1>
      <p>Transform your business with our innovative solution</p>
      <a href="#contact" class="cta-button">Get Started Today</a>
    </div>
  </section>

  <!-- Features Section -->
  <section class="features">
    <div class="container">
      <h2>Why Choose Us</h2>
      <div class="feature-grid">
        <div class="feature-card">
          <div class="feature-icon">🚀</div>
          <h3>Fast & Reliable</h3>
          <p>Lightning-fast performance that you can count on every single day.</p>
        </div>
        <div class="feature-card">
          <div class="feature-icon">💡</div>
          <h3>Innovative</h3>
          <p>Cutting-edge technology that keeps you ahead of the competition.</p>
        </div>
        <div class="feature-card">
          <div class="feature-icon">🛡️</div>
          <h3>Secure</h3>
          <p>Enterprise-grade security to protect your valuable data.</p>
        </div>
      </div>
    </div>
  </section>

  <!-- Contact Section -->
  <section class="contact" id="contact">
    <div class="container">
      <h2>Get In Touch</h2>
      <form class="contact-form" action="#" method="POST">
        <div class="form-group">
          <label for="name">Your Name</label>
          <input type="text" id="name" name="name" required>
        </div>
        <div class="form-group">
          <label for="email">Email Address</label>
          <input type="email" id="email" name="email" required>
        </div>
        <div class="form-group">
          <label for="message">Message</label>
          <textarea id="message" name="message" rows="5" required></textarea>
        </div>
        <button type="submit" class="submit-button">Send Message</button>
      </form>
    </div>
  </section>

  <!-- Footer -->
  <footer>
    <div class="container">
      <p>&copy; 2025 Your Business. All rights reserved.</p>
      <p>Created with AI Marketing Assistant</p>
    </div>
  </footer>
</body>
</html>

═══════════════════════════════════════════════════════════════════

AVAILABLE TEMPLATES:

1. LANDING PAGE - Product/service promotion with CTA
2. EVENT PAGE - Event announcements and registration
3. PORTFOLIO - Showcase work and projects
4. NEWSLETTER - Email marketing templates
5. CONTACT FORM - Lead generation pages
6. PRICING PAGE - Service/product pricing tables
7. ABOUT PAGE - Company/personal introduction

═══════════════════════════════════════════════════════════════════

COLOR SCHEMES FOR DIFFERENT INDUSTRIES:

Tech/Software:
- Primary: #667eea (Purple-blue)
- Secondary: #764ba2 (Purple)
- Accent: #f7b731 (Yellow)

Healthcare:
- Primary: #00b894 (Teal)
- Secondary: #00cec9 (Cyan)
- Accent: #55a3ff (Blue)

Finance:
- Primary: #0984e3 (Blue)
- Secondary: #2c3e50 (Dark gray)
- Accent: #27ae60 (Green)

E-commerce:
- Primary: #e17055 (Orange)
- Secondary: #fdcb6e (Yellow)
- Accent: #6c5ce7 (Purple)

Creative/Design:
- Primary: #fd79a8 (Pink)
- Secondary: #a29bfe (Lavender)
- Accent: #ffeaa7 (Light yellow)

═══════════════════════════════════════════════════════════════════`;

Frontend Files (React Frontend)

Frontend/AgentPanel.js

import React, { useState, useEffect } from 'react';
import {
    Box,
    Tabs,
    Tab,
    List,
    ListItem,
    ListItemText,
    ListItemIcon,
    IconButton,
    Typography,
    CircularProgress,
    Alert,
    Dialog,
    DialogTitle,
    DialogContent,
    DialogActions,
    Button,
    Breadcrumbs,
    Link,
    TextField,
    Chip
} from '@mui/material';
import {
    Folder as FolderIcon,
    InsertDriveFile as FileIcon,
    Delete as DeleteIcon,
    Edit as EditIcon,
    VpnKey as AccessIcon,
    FolderOpen as FilesIcon,
    Close as CloseIcon,
    Home as HomeIcon,
    Image as ImageIcon,
    VideoLibrary as VideoIcon,
    Code as CodeIcon,
    PictureAsPdf as PdfIcon,
    Description as DocumentIcon,
    AudioFile as AudioIcon,
    Archive as ZipIcon,
    Html as HtmlIcon,
    Storage as StorageIcon
} from '@mui/icons-material';
import MemoryTab from './MemoryTab';

const KB_API_URL = 'https://kb.openkbs.com/';
const isMobile = window.innerWidth < 960;

const getFileIcon = (filename) => {
    const ext = filename.toLowerCase().split('.').pop();
    if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico'].includes(ext)) return <ImageIcon sx={{ color: '#4CAF50' }} />;
    if (['mp4', 'avi', 'mov', 'webm', 'mkv', 'flv', 'wmv', 'm4v', 'mpg', 'mpeg'].includes(ext)) return <VideoIcon sx={{ color: '#FF5722' }} />;
    if (['html', 'htm', 'xml', 'xhtml'].includes(ext)) return <HtmlIcon sx={{ color: '#FF9800' }} />;
    if (['js', 'json', 'css', 'jsx', 'ts', 'tsx', 'py', 'java', 'cpp', 'c', 'h', 'php', 'rb', 'go', 'rs', 'swift'].includes(ext)) return <CodeIcon sx={{ color: '#2196F3' }} />;
    if (ext === 'pdf') return <PdfIcon sx={{ color: '#F44336' }} />;
    if (['doc', 'docx', 'txt', 'rtf', 'odt', 'xls', 'xlsx', 'ppt', 'pptx', 'csv'].includes(ext)) return <DocumentIcon sx={{ color: '#673AB7' }} />;
    if (['mp3', 'wav', 'ogg', 'flac', 'aac', 'wma', 'm4a'].includes(ext)) return <AudioIcon sx={{ color: '#009688' }} />;
    if (['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz'].includes(ext)) return <ZipIcon sx={{ color: '#795548' }} />;
    return <FileIcon sx={{ color: '#757575' }} />;
};


const AgentPanel = ({ openkbs, onClose, initialTab = 0, onTabChange, setSystemAlert, setBlockingLoading }) => {
    const [currentTab, setCurrentTab] = useState(initialTab);
    const [files, setFiles] = useState([]);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(null);
    const [currentPath, setCurrentPath] = useState([]);
    const [deleteDialog, setDeleteDialog] = useState({ open: false, file: null });
    const [renameDialog, setRenameDialog] = useState({ open: false, file: null });
    const [newFileName, setNewFileName] = useState('');
    const [shares, setShares] = useState([]);
    const [shareEmail, setShareEmail] = useState('');
    // Memory CRUD state
    const [memoryItems, setMemoryItems] = useState([]);
    const [memoryLimit, setMemoryLimit] = useState(20);
    const [memoryHasMore, setMemoryHasMore] = useState(false);
    const [editingItem, setEditingItem] = useState(null);
    const [editValues, setEditValues] = useState({});
    const [newItemDialog, setNewItemDialog] = useState(false);
    const [newItemKey, setNewItemKey] = useState('');
    const [newItemValue, setNewItemValue] = useState('');

    // List files in current path
    const listFiles = async () => {
        try {
            setLoading(true);
            setError(null);

            const token = localStorage.getItem('kbJWT');
            const response = await fetch(KB_API_URL, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    token,
                    action: 'listFiles',
                    namespace: 'files'
                })
            });

            const data = await response.json();
            if (Array.isArray(data)) {
                const items = [];
                const pathPrefix = currentPath.length > 0
                    ? `files/${openkbs.kbId}/${currentPath.join('/')}/`
                    : `files/${openkbs.kbId}/`;

                // Process files and folders
                const folders = new Set();
                const filesList = [];

                data.forEach(file => {
                    if (file.Key && file.Key.startsWith(pathPrefix)) {
                        const relativePath = file.Key.substring(pathPrefix.length);
                        const parts = relativePath.split('/');

                        if (parts.length === 1 && parts[0]) {
                            // Direct file in current path
                            filesList.push({
                                name: parts[0],
                                key: file.Key,
                                size: file.Size,
                                lastModified: file.LastModified
                            });
                        } else if (parts.length > 1 && parts[0]) {
                            // Subfolder
                            folders.add(parts[0]);
                        }
                    }
                });

                // Add folders first
                folders.forEach(folder => {
                    items.push({
                        name: folder,
                        isFolder: true
                    });
                });

                // Add files
                items.push(...filesList);

                setFiles(items);
            }
        } catch (err) {
            console.error('Error listing files:', err);
            setError('Failed to load files');
        } finally {
            setLoading(false);
        }
    };

    // Delete file
    const deleteFile = async (file) => {
        // Close dialog immediately
        setDeleteDialog({ open: false, file: null });

        try {
            setLoading(true);

            // Use openkbs Files API to delete
            await openkbs.Files.deleteRawKBFile(file.name, 'files');

            // Refresh file list
            await listFiles();
            setSystemAlert({
                msg: 'File deleted successfully',
                type: 'success',
                duration: 3000
            });
        } catch (err) {
            console.error('Error deleting file:', err);
            setError('Failed to delete file');
            setSystemAlert({
                msg: 'Failed to delete file',
                type: 'error',
                duration: 5000
            });
        } finally {
            setLoading(false);
        }
    };

    // Rename file
        const renameFile = async () => {
        if (!newFileName.trim() || !renameDialog.file) return;

        // Close dialog immediately
        const oldFileName = renameDialog.file.key;
        const pathPrefix = currentPath.length > 0 ? currentPath.join('/') + '/' : '';
        const newFileName_ = pathPrefix + newFileName;

        setRenameDialog({ open: false, file: null });
        setNewFileName('');

        try {
            setLoading(true);

            // Use the new renameFile method from OpenKBS Files API
            await openkbs.Files.renameFile(oldFileName, newFileName_, 'files');

            // Refresh file list
            await listFiles();
            setSystemAlert({
                msg: 'File renamed successfully',
                type: 'success',
                duration: 3000
            });
        } catch (err) {
            console.error('Error renaming file:', err);
            setError('Failed to rename file');
            setSystemAlert({
                msg: 'Failed to rename file',
                type: 'error',
                duration: 5000
            });
        } finally {
            setLoading(false);
        }
    };

    // Open file in new tab
    const openFile = (file) => {
        const fileUrl = `https://web.file.vpc1.us/${file.key}`;
        window.open(fileUrl, '_blank');
    };

    // Navigate to folder
    const navigateToFolder = (folderName) => {
        setCurrentPath([...currentPath, folderName]);
    };

    // Navigate using breadcrumbs
    const navigateToPath = (index) => {
        if (index === -1) {
            setCurrentPath([]);
        } else {
            setCurrentPath(currentPath.slice(0, index + 1));
        }
    };

    // Load current shares
    const loadShares = async () => {
        try {
            setLoading(true);

            const result = await openkbs.KBAPI.getKBShares();
            if (result && result.sharedWith) {
                // Convert email array to objects for easier handling
                const sharesList = Array.isArray(result.sharedWith)
                    ? result.sharedWith.map(email => ({ email }))
                    : [];
                setShares(sharesList);
            } else {
                setShares([]);
            }
        } catch (err) {
            console.error('Error loading shares:', err);
            setSystemAlert({
                msg: 'Failed to load shares',
                type: 'error',
                duration: 5000
            });
            setShares([]);
        } finally {
            setLoading(false);
        }
    };

    // Share with user
    const shareWithUser = async () => {
        if (!shareEmail.trim()) {
            setSystemAlert({
                msg: 'Please enter an email address',
                type: 'error',
                duration: 3000
            });
            return;
        }

        try {
            setLoading(true);

            // Pass email directly as targetUserId - SDK will handle conversion if needed
            await openkbs.KBAPI.shareKBWith(shareEmail);

            setSystemAlert({
                msg: `Successfully shared with ${shareEmail}`,
                type: 'success',
                duration: 3000
            });
            setShareEmail('');

            // Reload shares
            await loadShares();
        } catch (err) {
            console.error('Error sharing:', err);
            setSystemAlert({
                msg: err.message || 'Failed to share with user',
                type: 'error',
                duration: 5000
            });
        } finally {
            setLoading(false);
        }
    };

    // Remove share
    const removeShare = async (email) => {
        try {
            setLoading(true);

            // Pass email directly - SDK will handle conversion if needed
            await openkbs.KBAPI.unshareKBWith(email);

            setSystemAlert({
                msg: `Removed share with ${email}`,
                type: 'success',
                duration: 3000
            });

            await loadShares();
        } catch (err) {
            console.error('Error removing share:', err);
            setSystemAlert({
                msg: 'Failed to remove share',
                type: 'error',
                duration: 5000
            });
        } finally {
            setLoading(false);
        }
    };

    // Load memory items
    const loadMemoryItems = async (reset = false) => {
        try {
            setLoading(true);
            setBlockingLoading(true);

            const result = await openkbs.fetchItems({
                itemType: 'memory',
                limit: memoryLimit,
                sortBy: 'createdAt',
                sortOrder: 'desc'
            });

            if (result && result.items) {
                const items = result.items.map(({ item, meta }) => {
                    let actualValue = item.body;

                    // If body has a 'value' property, extract it (this is our wrapped structure)
                    if (actualValue && typeof actualValue === 'object' && 'value' in actualValue) {
                        actualValue = actualValue.value;
                    }

                    return {
                        itemId: meta.itemId,
                        value: actualValue
                    };
                });

                if (reset) {
                    setMemoryItems(items);
                } else {
                    setMemoryItems(prev => [...prev, ...items]);
                }

                setMemoryHasMore(items.length === memoryLimit);
            }
        } catch (err) {
            console.error('Error loading memory items:', err);
            setSystemAlert({
                msg: 'Failed to load memory items',
                type: 'error',
                duration: 5000
            });
        } finally {
            setLoading(false);
            setBlockingLoading(false);
        }
    };

    // Save memory item
    const saveMemoryItem = async (itemId) => {
        try {
            setLoading(true);

            let value;

            // If we have fields (object was edited), use those
            if (editValues.fields) {
                value = editValues.fields;
            } else {
                // String value
                value = editValues.value;
                // Try to parse as JSON if it looks like JSON
                if (typeof value === 'string' && (value.trim().startsWith('{') || value.trim().startsWith('['))) {
                    try {
                        value = JSON.parse(value);
                    } catch (e) {
                        // Keep as string if not valid JSON
                    }
                }
            }

            // Wrap in the standard structure
            const body = {
                value: value,
                updatedAt: new Date().toISOString()
            };

            await openkbs.updateItem({
                itemType: 'memory',
                itemId: itemId,
                body: body
            });

            // Reload to get fresh data from backend
            await loadMemoryItems(true);

            setEditingItem(null);
            setEditValues({});

            // Show success message
            setSystemAlert({
                msg: `Successfully saved ${itemId}`,
                type: 'success',
                duration: 3000
            });
        } catch (err) {
            console.error('Error saving memory item:', err);
            setSystemAlert({
                msg: 'Failed to save memory item. Please try again.',
                type: 'error',
                duration: 5000
            });
        } finally {
            setLoading(false);
        }
    };

    // Delete memory item
    const deleteMemoryItem = async (itemId) => {
        // Optimistically remove from local state immediately
        setMemoryItems(items => items.filter(item => item.itemId !== itemId));

        try {
            await openkbs.deleteItem(itemId);

            // Show success message
            setSystemAlert({
                msg: `Successfully deleted ${itemId}`,
                type: 'success',
                duration: 3000
            });
        } catch (err) {
            console.error('Error deleting memory item:', err);

            // Restore the item on error by reloading
            await loadMemoryItems(true);

            setSystemAlert({
                msg: 'Failed to delete memory item. Please try again.',
                type: 'error',
                duration: 5000
            });
        }
    };

    // Create new memory item
    const createMemoryItem = async () => {
        if (!newItemKey.trim()) {
            setSystemAlert({
                msg: 'Please enter a key for the memory item',
                type: 'error',
                duration: 3000
            });
            return;
        }

        // Close dialog immediately
        const keyToUse = newItemKey;
        const valueToUse = newItemValue;
        setNewItemDialog(false);
        setNewItemKey('');
        setNewItemValue('');

        try {
            setLoading(true);

            // Keep value as string
            const value = valueToUse.trim() || '';

            // Always ensure memory_ prefix
            const itemId = keyToUse.startsWith('memory_') ? keyToUse : `memory_${keyToUse}`;

            // Wrap in the same structure as setMemory uses
            const body = {
                value: value,
                updatedAt: new Date().toISOString()
            };

            await openkbs.updateItem({
                itemType: 'memory',
                itemId: itemId,
                body: body
            });

            // Reload items
            await loadMemoryItems(true);

            // Show success message
            setSystemAlert({
                msg: `Successfully created ${itemId}`,
                type: 'success',
                duration: 3000
            });
        } catch (err) {
            console.error('Error creating memory item:', err);
            setSystemAlert({
                msg: 'Failed to create memory item. Please try again.',
                type: 'error',
                duration: 5000
            });
        } finally {
            setLoading(false);
        }
    };


    // Format value for display
    const formatValue = (value) => {
        if (typeof value === 'object') {
            return JSON.stringify(value, null, 2);
        }
        return String(value);
    };

    useEffect(() => {
        if (currentTab === 0) {
            listFiles();
        } else if (currentTab === 1) {
            loadMemoryItems(true);
        } else if (currentTab === 2) {
            loadShares();
        }
    }, [currentTab, currentPath]);

    return (
        <Box
            onClick={onClose}
            sx={{
                position: 'fixed',
                top: 0,
                left: 0,
                right: 0,
                bottom: 0,
                backgroundColor: 'rgba(0,0,0,0.5)',
                zIndex: 1300,
                display: 'flex',
                alignItems: isMobile ? 'stretch' : 'center',
                justifyContent: isMobile ? 'stretch' : 'center'
            }}
        >
            <Box
                onClick={(e) => e.stopPropagation()}
                sx={{
                    width: isMobile ? '100vw' : '700px',
                    height: isMobile ? '100vh' : '80vh',
                    backgroundColor: 'white',
                    borderRadius: isMobile ? 0 : '12px',
                    boxShadow: '0 8px 32px rgba(0,0,0,0.3)',
                    overflow: 'hidden',
                    display: 'flex',
                    flexDirection: 'column'
                }}
            >

                <Box sx={{
                    display: 'flex',
                    alignItems: 'center',
                    justifyContent: 'space-between',
                    p: 2,
                    borderBottom: '1px solid #e0e0e0'
                }}>
                    <Typography variant="h6">Manage</Typography>
                    <IconButton onClick={onClose}>
                        <CloseIcon />
                    </IconButton>
                </Box>

                {/* Tabs */}
                <Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
                    <Tabs value={currentTab} onChange={(e, v) => {
                        setCurrentTab(v);
                        if (onTabChange) onTabChange(v);
                    }}>
                        <Tab
                            icon={<FilesIcon />}
                            label="Files"
                            iconPosition="start"
                        />
                        <Tab
                            icon={<StorageIcon />}
                            label="Memory"
                            iconPosition="start"
                        />
                        <Tab
                            icon={<AccessIcon />}
                            label="Access"
                            iconPosition="start"
                        />
                    </Tabs>
                </Box>

                {/* Content */}
                <Box sx={{ flex: 1, overflow: 'auto', p: 2 }}>
                    {currentTab === 0 && (
                        <>
                            {/* Breadcrumbs */}
                            <Breadcrumbs sx={{ mb: 2 }}>
                                <Link
                                    component="button"
                                    variant="body2"
                                    onClick={() => navigateToPath(-1)}
                                    sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}
                                >
                                    <HomeIcon fontSize="small" />
                                    Files
                                </Link>
                                {currentPath.map((folder, index) => (
                                    <Link
                                        key={index}
                                        component="button"
                                        variant="body2"
                                        onClick={() => navigateToPath(index)}
                                    >
                                        {folder}
                                    </Link>
                                ))}
                            </Breadcrumbs>

                            {error && (
                                <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>
                            )}

                            {loading ? (
                                <Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
                                    <CircularProgress />
                                </Box>
                            ) : (
                                <List>
                                    {files.map((item, index) => (
                                        <ListItem
                                            key={index}
                                            button
                                            onClick={() => {
                                                if (item.isFolder) {
                                                    navigateToFolder(item.name);
                                                } else {
                                                    openFile(item);
                                                }
                                            }}
                                        >
                                            <ListItemIcon>
                                                {item.isFolder ? <FolderIcon sx={{ color: '#FFA726' }} /> : getFileIcon(item.name)}
                                            </ListItemIcon>
                                            <ListItemText
                                                primary={item.name}
                                                secondary={!item.isFolder && item.size ?
                                                    `${(item.size / 1024).toFixed(1)} KB` : null
                                                }
                                            />
                                            {!item.isFolder && (
                                                <Box>
                                                    <IconButton
                                                        onClick={(e) => {
                                                            e.stopPropagation();
                                                            setRenameDialog({ open: true, file: item });
                                                            setNewFileName(item.name);
                                                        }}
                                                    >
                                                        <EditIcon />
                                                    </IconButton>
                                                    <IconButton
                                                        edge="end"
                                                        onClick={(e) => {
                                                            e.stopPropagation();
                                                            setDeleteDialog({ open: true, file: item });
                                                        }}
                                                    >
                                                        <DeleteIcon />
                                                    </IconButton>
                                                </Box>
                                            )}
                                        </ListItem>
                                    ))}
                                    {files.length === 0 && (
                                        <Typography sx={{ textAlign: 'center', py: 4, color: 'text.secondary' }}>
                                            No files in this folder
                                        </Typography>
                                    )}
                                </List>
                            )}
                        </>
                    )}

                    {/* Memory Tab */}
                    {currentTab === 1 && (
                        <MemoryTab
                            state={{
                                memoryItems,
                                loading,
                                editingItem,
                                editValues,
                                newItemDialog,
                                newItemKey,
                                newItemValue,
                                memoryHasMore
                            }}
                            actions={{
                                setEditingItem,
                                setEditValues,
                                saveMemoryItem,
                                deleteMemoryItem,
                                setNewItemDialog,
                                setNewItemKey,
                                setNewItemValue,
                                createMemoryItem,
                                loadMoreItems: () => {
                                    setMemoryLimit(prev => prev + 20);
                                    loadMemoryItems();
                                },
                                formatValue
                            }}
                        />
                    )}

                    {/* Access Tab */}
                    {currentTab === 2 && (
                        <Box>
                            <Typography variant="h6" sx={{ mb: 2 }}>
                                Share Access
                            </Typography>

                            {/* Share form */}
                            <Box sx={{ display: 'flex', gap: 1, mb: 3 }}>
                                <TextField
                                    fullWidth
                                    label="Email address"
                                    variant="outlined"
                                    size="small"
                                    value={shareEmail}
                                    onChange={(e) => setShareEmail(e.target.value)}
                                    placeholder="user@example.com"
                                />
                                <Button
                                    variant="contained"
                                    onClick={shareWithUser}
                                    disabled={loading || !shareEmail.trim()}
                                >
                                    Share
                                </Button>
                            </Box>

                            {/* Current shares */}
                            <Typography variant="subtitle1" sx={{ mb: 1 }}>
                                Current Shares
                            </Typography>

                            {loading ? (
                                <Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
                                    <CircularProgress />
                                </Box>
                            ) : (
                                <List>
                                    {shares.map((share, index) => (
                                        <ListItem key={index}>
                                            <ListItemText
                                                primary={share.email}
                                                secondary="Full access"
                                            />
                                            <IconButton
                                                edge="end"
                                                onClick={() => removeShare(share.email)}
                                            >
                                                <DeleteIcon />
                                            </IconButton>
                                        </ListItem>
                                    ))}
                                    {shares.length === 0 && (
                                        <Typography sx={{ textAlign: 'center', py: 4, color: 'text.secondary' }}>
                                            Not shared with anyone yet
                                        </Typography>
                                    )}
                                </List>
                            )}
                        </Box>
                    )}
                </Box>

                {/* Delete Confirmation Dialog */}
                <Dialog open={deleteDialog.open} onClose={() => setDeleteDialog({ open: false, file: null })}>
                    <DialogTitle>Delete File</DialogTitle>
                    <DialogContent>
                        Are you sure you want to delete "{deleteDialog.file?.name}"?
                    </DialogContent>
                    <DialogActions>
                        <Button onClick={() => setDeleteDialog({ open: false, file: null })}>
                            Cancel
                        </Button>
                        <Button
                            onClick={() => deleteFile(deleteDialog.file)}
                            color="error"
                            variant="contained"
                        >
                            Delete
                        </Button>
                    </DialogActions>
                </Dialog>

                {/* Rename Dialog */}
                <Dialog open={renameDialog.open} onClose={() => { setRenameDialog({ open: false, file: null }); setNewFileName(''); }}>
                    <DialogTitle>Rename File</DialogTitle>
                    <DialogContent>
                        <TextField
                            fullWidth
                            label="New filename"
                            value={newFileName}
                            onChange={(e) => setNewFileName(e.target.value)}
                            sx={{ mt: 1 }}
                            autoFocus
                        />
                    </DialogContent>
                    <DialogActions>
                        <Button onClick={() => { setRenameDialog({ open: false, file: null }); setNewFileName(''); }}>
                            Cancel
                        </Button>
                        <Button
                            onClick={renameFile}
                            variant="contained"
                            disabled={!newFileName.trim()}
                        >
                            Rename
                        </Button>
                    </DialogActions>
                </Dialog>
            </Box>
        </Box>
    );
};

export default AgentPanel;

Frontend/CommandRenderer.js

import React, { useState } from 'react';
import { Box, Tooltip, Typography, Zoom, Chip } from '@mui/material';
import SaveIcon from '@mui/icons-material/Save';
import DeleteIcon from '@mui/icons-material/Delete';
import ImageIcon from '@mui/icons-material/Image';
import VideoLibraryIcon from '@mui/icons-material/VideoLibrary';
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
import LanguageIcon from '@mui/icons-material/Language';
import EmailIcon from '@mui/icons-material/Email';
import SearchIcon from '@mui/icons-material/Search';
import CollectionsIcon from '@mui/icons-material/Collections';
import ArticleIcon from '@mui/icons-material/Article';
import ScheduleIcon from '@mui/icons-material/Schedule';
import ListAltIcon from '@mui/icons-material/ListAlt';
import ClearIcon from '@mui/icons-material/Clear';
import MenuBookIcon from '@mui/icons-material/MenuBook';
import DescriptionIcon from '@mui/icons-material/Description';
import BoltIcon from '@mui/icons-material/Bolt';

// Icon mappings for different command types
const commandIcons = {
    setMemory: SaveIcon,
    deleteItem: DeleteIcon,
    createAIImage: ImageIcon,
    createAIVideo: VideoLibraryIcon,
    continueVideoPolling: HourglassEmptyIcon,
    publishWebPage: LanguageIcon,
    sendMail: EmailIcon,
    googleSearch: SearchIcon,
    googleImageSearch: CollectionsIcon,
    viewImage: ImageIcon,
    webpageToText: ArticleIcon,
    scheduleTask: ScheduleIcon,
    getScheduledTasks: ListAltIcon,
    deleteScheduledTask: ClearIcon,
    getSora2PromptingGuide: MenuBookIcon,
    getWebPublishingGuide: DescriptionIcon
};

// List of valid command names
const validCommands = [
    'getSora2PromptingGuide',
    'getWebPublishingGuide',
    'createAIImage',
    'createAIVideo',
    'continueVideoPolling',
    'publishWebPage',
    'sendMail',
    'googleSearch',
    'googleImageSearch',
    'viewImage',
    'webpageToText',
    'setMemory',
    'deleteItem',
    'scheduleTask',
    'getScheduledTasks',
    'deleteScheduledTask'
];

// Parse commands from content
const parseCommands = (content) => {
    if (!content) return [];

    const commands = [];
    const regex = /<(\w+)(?:>([\s\S]*?)<\/\1>|\s*\/>)/g;
    let match;

    while ((match = regex.exec(content)) !== null) {
        const commandName = match[1];

        // Only process if it's a valid command
        if (!validCommands.includes(commandName)) {
            continue;
        }

        const commandContent = match[2] || '';

        let parsedData = null;
        if (commandContent) {
            try {
                parsedData = JSON.parse(commandContent.trim());
            } catch (e) {
                parsedData = commandContent.trim();
            }
        }

        commands.push({
            name: commandName,
            data: parsedData,
            fullMatch: match[0]
        });
    }

    return commands;
};

// Single command circle component
const CommandCircle = ({ command, index, response }) => {
    const [hovering, setHovering] = useState(false);
    const IconComponent = commandIcons[command.name] || BoltIcon;

    // Determine if command has completed (has response)
    const hasResponse = !!response;
    const isSuccess = response && !response.error;
    const isError = response && response.error;

    // Format tooltip content to show both request and response
    const getTooltipContent = () => {
        return (
            <Box sx={{ p: 1, maxWidth: 400 }}>
                {/* Command Name */}
                <Typography variant="caption" sx={{
                    fontWeight: 'bold',
                    color: '#4CAF50',
                    display: 'block',
                    mb: 0.5
                }}>
                    {command.name}
                </Typography>

                {/* Request Parameters */}
                {command.data && (
                    <>
                        <Typography variant="caption" sx={{
                            color: '#90CAF9',
                            fontSize: '10px',
                            fontWeight: 'bold'
                        }}>
                            REQUEST:
                        </Typography>
                        <Box sx={{
                            display: 'block',
                            fontSize: '10px',
                            color: '#fff',
                            ml: 1,
                            mb: 0.5,
                            fontFamily: 'monospace',
                            whiteSpace: 'pre-wrap',
                            wordBreak: 'break-word',
                            maxHeight: '200px',
                            overflowY: 'auto'
                        }}>
                            {typeof command.data === 'object'
                                ? JSON.stringify(command.data, null, 2)
                                : String(command.data)
                            }
                        </Box>
                    </>
                )}

                {/* Response */}
                {response && (
                    <>
                        <Typography variant="caption" sx={{
                            color: response.error ? '#FF6B6B' : '#81C784',
                            fontSize: '10px',
                            fontWeight: 'bold'
                        }}>
                            RESPONSE:
                        </Typography>
                        <Box sx={{
                            display: 'block',
                            fontSize: '10px',
                            color: '#fff',
                            ml: 1,
                            fontFamily: 'monospace',
                            whiteSpace: 'pre-wrap',
                            wordBreak: 'break-word',
                            maxHeight: '200px',
                            overflowY: 'auto'
                        }}>
                            {response.error
                                ? `Error: ${response.error}`
                                : JSON.stringify(response, null, 2)
                            }
                        </Box>
                    </>
                )}

                {/* Loading state */}
                {!response && (
                    <Typography variant="caption" sx={{
                        display: 'block',
                        fontSize: '10px',
                        color: '#FFA726',
                        fontStyle: 'italic',
                        mt: 0.5
                    }}>
                        Waiting for response...
                    </Typography>
                )}
            </Box>
        );
    };

    return (
        <Tooltip
            title={getTooltipContent()}
            placement="top"
            arrow
            TransitionComponent={Zoom}
            sx={{
                '& .MuiTooltip-tooltip': {
                    backgroundColor: 'rgba(0, 0, 0, 0.87)',
                    maxWidth: 300
                }
            }}
        >
            <Box
                onMouseEnter={() => setHovering(true)}
                onMouseLeave={() => setHovering(false)}
                sx={{
                    display: 'inline-flex',
                    alignItems: 'center',
                    justifyContent: 'center',
                    width: 36,
                    height: 36,
                    borderRadius: '50%',
                    backgroundColor: hovering
                        ? (isError ? 'rgba(244, 67, 54, 0.15)' : isSuccess ? 'rgba(76, 175, 80, 0.15)' : 'rgba(158, 158, 158, 0.15)')
                        : (isError ? 'rgba(244, 67, 54, 0.08)' : isSuccess ? 'rgba(76, 175, 80, 0.08)' : 'rgba(0, 0, 0, 0.04)'),
                    border: '2px solid',
                    borderColor: hovering
                        ? (isError ? '#f44336' : isSuccess ? '#4CAF50' : '#9e9e9e')
                        : (isError ? 'rgba(244, 67, 54, 0.3)' : isSuccess ? 'rgba(76, 175, 80, 0.3)' : 'rgba(0, 0, 0, 0.12)'),
                    cursor: 'pointer',
                    transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
                    transform: hovering ? 'scale(1.1)' : 'scale(1)',
                    boxShadow: hovering ? '0 4px 20px rgba(25, 118, 210, 0.25)' : 'none',
                    animation: `fadeIn 0.5s ease-in-out ${index * 0.1}s both`,
                    '@keyframes fadeIn': {
                        '0%': {
                            opacity: 0,
                            transform: 'scale(0.8)'
                        },
                        '100%': {
                            opacity: 1,
                            transform: 'scale(1)'
                        }
                    }
                }}
            >
                <IconComponent
                    sx={{
                        fontSize: 18,
                        color: hovering
                            ? (isError ? '#f44336' : isSuccess ? '#4CAF50' : '#9e9e9e')
                            : (isError ? 'rgba(244, 67, 54, 0.7)' : isSuccess ? 'rgba(76, 175, 80, 0.7)' : 'rgba(0, 0, 0, 0.54)')
                    }}
                />
            </Box>
        </Tooltip>
    );
};

// Main component
const CommandRenderer = ({ content, responseData, markdownHandler }) => {
    // Filter out GEMINI_META comments
    const cleanContent = content.replace(/<!--GEMINI_META:.*?-->/gs, '').trim();
    const commands = parseCommands(cleanContent);

    if (commands.length === 0) return null;

    // Parse content to separate text and commands
    const parseContentWithText = () => {
        const parts = [];
        let lastIndex = 0;

        commands.forEach((cmd, cmdIndex) => {
            const cmdStart = cleanContent.indexOf(cmd.fullMatch, lastIndex);

            // Add text before command if exists
            if (cmdStart > lastIndex) {
                const textBefore = cleanContent.substring(lastIndex, cmdStart).trim();
                if (textBefore) {
                    parts.push({ type: 'text', content: textBefore });
                }
            }

            // Add command
            parts.push({
                type: 'command',
                command: cmd,
                index: cmdIndex,
                response: commands.length === 1 ? responseData : responseData
            });

            lastIndex = cmdStart + cmd.fullMatch.length;
        });

        // Add remaining text after last command
        if (lastIndex < cleanContent.length) {
            const textAfter = cleanContent.substring(lastIndex).trim();
            if (textAfter) {
                parts.push({ type: 'text', content: textAfter });
            }
        }

        return parts;
    };

    const contentParts = parseContentWithText();

    // Render mixed content
    return (
        <Box sx={{ my: 1 }}>
            {contentParts.map((part, index) => {
                if (part.type === 'text') {
                    // Use markdownHandler if available, otherwise render as plain text
                    if (markdownHandler) {
                        return (
                            <Box key={`text-${index}`} sx={{ display: 'inline-block', verticalAlign: 'top' }}>
                                {markdownHandler(part.content)}
                            </Box>
                        );
                    } else {
                        return (
                            <Typography
                                key={`text-${index}`}
                                component="span"
                                sx={{
                                    display: 'inline',
                                    mr: 1,
                                    verticalAlign: 'middle'
                                }}
                            >
                                {part.content}
                            </Typography>
                        );
                    }
                } else if (part.type === 'command') {
                    return (
                        <Box key={`cmd-${index}`} sx={{ display: 'inline-block', mx: 0.5, verticalAlign: 'middle' }}>
                            <CommandCircle
                                command={part.command}
                                index={part.index}
                                response={part.response}
                            />
                        </Box>
                    );
                }
                return null;
            })}
            {responseData && responseData.type === 'VIDEO_PENDING' && responseData.data?.message && (
                <Box sx={{
                    display: 'inline-flex',
                    alignItems: 'center',
                    ml: 1,
                    padding: '4px 12px',
                    backgroundColor: '#fff3cd',
                    border: '1px solid #ffc107',
                    borderRadius: '4px',
                    color: '#856404',
                    fontSize: '13px',
                    fontWeight: 500
                }}>
Please wait and DO NOT refresh your browser! Video is generating...
                </Box>
            )}
        </Box>
    );
};

export default CommandRenderer;

Frontend/Header.js

import React, { useEffect, useState } from 'react';
import { isContentHTML } from "./utils";
import { IconButton } from '@mui/material';
import { Tune as TuneIcon } from '@mui/icons-material';
import AgentPanel from './AgentPanel';

const getChatId = () => window?.location?.pathname?.split('/chat/')?.[1];

const checkBusinessProfileExists = async (openkbs) => {
    try {
        const response = await openkbs.getItem('memory_business_profile');
        return !!response?.item;
    } catch (e) {
        return false;
    }
};

const getQueryParamValue = (paramName) => {
    const queryParams = new URLSearchParams(window.location.search);
    return queryParams.get(paramName);
};

const Header = ({ setRenderSettings, messages, setMessages, openkbs, setSystemAlert, setBlockingLoading }) => {
    const [profileChecked, setProfileChecked] = useState(false);
    const panelParam = getQueryParamValue('panel');
    const [panelExpanded, setPanelExpandedState] = useState(() => {
        return panelParam === 'files' || panelParam === 'access' || panelParam === 'memory';
    });
    const [initialTab] = useState(() => {
        if (panelParam === 'memory') return 1;
        if (panelParam === 'access') return 2;
        return 0;
    });

    // Update URL when panel state changes
    const setPanelExpanded = (value, tab = 'files') => {
        const url = new URL(window.location.href);
        if (value) {
            url.searchParams.set('panel', tab);
        } else {
            url.searchParams.delete('panel');
        }
        window.history.replaceState({}, '', url.toString());
        setPanelExpandedState(value);
    };

    useEffect(() => {
        setRenderSettings({
            setMessageWidth: (content) => isContentHTML(content) ? '90%' : undefined,
            enableGenerationModelsSelect: false,
            disableTextToSpeechButton: true,
            disableBalanceView: false,
            disableEmojiButton: true,
            disableShareButton: true,
            customStreamingLoader: true,
            disableMultichat: true,
            disableMobileLeftButton: true,
            disableSentLabel: false,
            disableChatModelsSelect: true,
            disableInitialScroll: true,
            backgroundOpacity: 0.02
        });
    }, [setRenderSettings]);

    useEffect(() => {
        const initializeChat = async () => {
            // Only check on new chats (no chatId and no messages)
            if (!getChatId() && (!messages || messages.length === 0) && !profileChecked) {
                const profileExists = await checkBusinessProfileExists(openkbs);
                setProfileChecked(true);

                // If no profile exists, show welcome message
                if (!profileExists) {
                    const welcomeMessage = {
                        msgId: `${+new Date()-10000}-${Math.floor(100000 + Math.random() * 900000)}`,
                        role: 'assistant',
                        content: `Hi! I'm your AI Marketing Assistant. Tell me about your business so I can help you with your marketing needs.`
                    };

                    if (setMessages) {
                        setMessages([welcomeMessage]);
                    }
                }
            }
        };

        if (openkbs && !profileChecked) {
            initializeChat();
        }
    }, [messages, setMessages, openkbs, profileChecked]);

    return (
        <>
            {/* Panel Button - only show when not expanded */}
            {!panelExpanded && (
                <IconButton
                    onClick={() => setPanelExpanded(true, 'files')}
                    sx={{
                        position: 'absolute',
                        top: window.innerWidth < 960 ? '70px' : '90px',
                        left: window.innerWidth < 960 ? '20px' : '340px',
                        backgroundColor: 'white',
                        color: 'primary.main',
                        width: 40,
                        height: 40,
                        border: '1px solid #e0e0e0',
                        boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
                        '&:hover': {
                            backgroundColor: '#f5f5f5',
                            transform: 'scale(1.05)'
                        },
                        transition: 'all 0.2s',
                        zIndex: 1200
                    }}
                >
                    <TuneIcon fontSize="small" />
                </IconButton>
            )}

            {/* Agent Panel */}
            {panelExpanded && openkbs && (
                <AgentPanel
                    openkbs={openkbs}
                    initialTab={initialTab}
                    onTabChange={(tab) => setPanelExpanded(true, tab === 0 ? 'files' : tab === 1 ? 'memory' : 'access')}
                    onClose={() => setPanelExpanded(false)}
                    setSystemAlert={setSystemAlert}
                    setBlockingLoading={setBlockingLoading}
                />
            )}
        </>
    );
};

export default Header;

Frontend/ImageWithDownload.js

import React, { useState } from 'react';
import DownloadIcon from '@mui/icons-material/Download';

const isMobile = window.innerWidth < 960;

const ImageWithDownload = ({ imageUrl }) => {
    const [isLoading, setIsLoading] = useState(true);
    const [imageError, setImageError] = useState(false);

    const handleDownload = async () => {
        try {
            // Create a temporary anchor element for download
            const link = document.createElement('a');
            link.href = imageUrl;

            // Extract filename from URL or use default
            const urlParts = imageUrl.split('/');
            const filename = urlParts[urlParts.length - 1] || 'image.png';

            // Set download attribute with filename
            link.download = filename;
            link.target = '_blank';
            link.rel = 'noopener noreferrer';

            // For cross-origin images, we need to fetch and create blob
            if (imageUrl.includes('http') && !imageUrl.startsWith(window.location.origin)) {
                try {
                    const response = await fetch(imageUrl);
                    const blob = await response.blob();
                    const blobUrl = URL.createObjectURL(blob);
                    link.href = blobUrl;
                    document.body.appendChild(link);
                    link.click();
                    document.body.removeChild(link);
                    // Clean up the blob URL
                    setTimeout(() => URL.revokeObjectURL(blobUrl), 100);
                } catch (fetchError) {
                    // If fetch fails (CORS), fallback to direct download
                    document.body.appendChild(link);
                    link.click();
                    document.body.removeChild(link);
                }
            } else {
                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link);
            }
        } catch (error) {
            console.error('Error downloading image:', error);
            // Fallback: open in new tab
            window.open(imageUrl, '_blank');
        }
    };

    return (
        <div style={{ display: 'inline-block', maxWidth: isMobile ? '100%' : 600 }}>
            {!imageError ? (
                <>
                    <img
                        src={imageUrl}
                        alt="Chat Image"
                        onLoad={() => setIsLoading(false)}
                        onError={() => {
                            setImageError(true);
                            setIsLoading(false);
                        }}
                        style={{
                            width: '100%',
                            height: 'auto',
                            maxHeight: 500,
                            display: isLoading ? 'none' : 'block'
                        }}
                    />
                    {isLoading && (
                        <div style={{
                            display: 'flex',
                            justifyContent: 'center',
                            alignItems: 'center',
                            height: 200,
                            backgroundColor: '#f5f5f5'
                        }}>
                            <span style={{ color: '#999' }}>Loading...</span>
                        </div>
                    )}
                    {!isLoading && (
                        <button
                            onClick={handleDownload}
                            style={{
                                marginTop: '4px',
                                padding: '6px 12px',
                                backgroundColor: '#f0f0f0',
                                border: 'none',
                                borderRadius: '4px',
                                color: '#666',
                                cursor: 'pointer',
                                fontSize: '14px',
                                display: 'inline-flex',
                                alignItems: 'center',
                                gap: '6px'
                            }}
                            onMouseEnter={(e) => {
                                e.target.style.backgroundColor = '#e0e0e0';
                                e.target.style.color = '#333';
                            }}
                            onMouseLeave={(e) => {
                                e.target.style.backgroundColor = '#f0f0f0';
                                e.target.style.color = '#666';
                            }}
                        >
                            <DownloadIcon style={{ fontSize: '18px' }} />
                            Download
                        </button>
                    )}
                </>
            ) : (
                <div style={{
                    display: 'flex',
                    justifyContent: 'center',
                    alignItems: 'center',
                    height: 200,
                    backgroundColor: '#f5f5f5'
                }}>
                    <span style={{ color: '#d32f2f' }}>Error loading image</span>
                </div>
            )}
        </div>
    );
};

export default ImageWithDownload;

Frontend/MemoryTab.js

import React from 'react';
import {
    Box,
    List,
    ListItem,
    Typography,
    IconButton,
    TextField,
    Button,
    Dialog,
    DialogTitle,
    DialogContent,
    DialogActions,
    CircularProgress
} from '@mui/material';
import {
    Edit as EditIcon,
    Delete as DeleteIcon,
    Save as SaveIcon,
    Cancel as CancelIcon,
    Add as AddIcon
} from '@mui/icons-material';

const MemoryTab = ({ state, actions }) => {
    const {
        memoryItems,
        loading,
        editingItem,
        editValues,
        newItemDialog,
        newItemKey,
        newItemValue,
        memoryHasMore
    } = state;

    const {
        setEditingItem,
        setEditValues,
        saveMemoryItem,
        deleteMemoryItem,
        setNewItemDialog,
        setNewItemKey,
        setNewItemValue,
        createMemoryItem,
        loadMoreItems,
        formatValue
    } = actions;
    return (
        <Box>
            <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
                <Typography variant="h6">Memory Items</Typography>
                <Button
                    variant="contained"
                    startIcon={<AddIcon />}
                    onClick={() => setNewItemDialog(true)}
                    size="small"
                >
                    Add Item
                </Button>
            </Box>

            {loading && memoryItems.length === 0 ? (
                <Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
                    <CircularProgress />
                </Box>
            ) : (
                <>
                    <List>
                        {memoryItems.map((item) => {
                            const isEditing = editingItem === item.itemId;
                            const displayValue = formatValue(item.value);

                            return (
                                <ListItem
                                    key={item.itemId}
                                    sx={{
                                        flexDirection: 'column',
                                        alignItems: 'stretch',
                                        borderBottom: '1px solid #e0e0e0',
                                        py: 1
                                    }}
                                >
                                    <Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
                                        <Typography variant="subtitle2" sx={{ flex: 1, fontWeight: 'bold' }}>
                                            {item.itemId}
                                        </Typography>
                                        <Box>
                                            {isEditing ? (
                                                <>
                                                    <IconButton
                                                        size="small"
                                                        onClick={() => saveMemoryItem(item.itemId)}
                                                        color="primary"
                                                    >
                                                        <SaveIcon />
                                                    </IconButton>
                                                    <IconButton
                                                        size="small"
                                                        onClick={() => {
                                                            setEditingItem(null);
                                                            setEditValues({});
                                                        }}
                                                    >
                                                        <CancelIcon />
                                                    </IconButton>
                                                </>
                                            ) : (
                                                <>
                                                    <IconButton
                                                        size="small"
                                                        onClick={() => {
                                                            setEditingItem(item.itemId);
                                                            if (typeof item.value === 'object' && item.value !== null && !Array.isArray(item.value)) {
                                                                setEditValues({ fields: { ...item.value } });
                                                            } else {
                                                                setEditValues({ value: displayValue });
                                                            }
                                                        }}
                                                    >
                                                        <EditIcon />
                                                    </IconButton>
                                                    <IconButton
                                                        size="small"
                                                        onClick={() => deleteMemoryItem(item.itemId)}
                                                        color="error"
                                                    >
                                                        <DeleteIcon />
                                                    </IconButton>
                                                </>
                                            )}
                                        </Box>
                                    </Box>
                                    <Box sx={{ mt: 1 }}>
                                        {isEditing ? (
                                            <Box>
                                                {typeof item.value === 'object' && item.value !== null && !Array.isArray(item.value) ? (
                                                    // Object editor
                                                    <Box>
                                                        {Object.entries(editValues.fields || item.value).map(([key, val]) => (
                                                            <Box key={key} sx={{ display: 'flex', gap: 1, mb: 1 }}>
                                                                <TextField
                                                                    size="small"
                                                                    label="Key"
                                                                    value={key}
                                                                    disabled
                                                                    sx={{ width: '30%' }}
                                                                />
                                                                <TextField
                                                                    size="small"
                                                                    label="Value"
                                                                    value={val}
                                                                    onChange={(e) => {
                                                                        const newFields = { ...(editValues.fields || item.value) };
                                                                        newFields[key] = e.target.value;
                                                                        setEditValues({ ...editValues, fields: newFields });
                                                                    }}
                                                                    sx={{ flex: 1 }}
                                                                />
                                                                <IconButton
                                                                    size="small"
                                                                    onClick={() => {
                                                                        const newFields = { ...(editValues.fields || item.value) };
                                                                        delete newFields[key];
                                                                        setEditValues({ ...editValues, fields: newFields });
                                                                    }}
                                                                    color="error"
                                                                >
                                                                    <DeleteIcon />
                                                                </IconButton>
                                                            </Box>
                                                        ))}
                                                        <Box sx={{ display: 'flex', gap: 1, mt: 2 }}>
                                                            <TextField
                                                                size="small"
                                                                placeholder="New key"
                                                                value={editValues.newKey || ''}
                                                                onChange={(e) => setEditValues({ ...editValues, newKey: e.target.value })}
                                                                sx={{ width: '30%' }}
                                                            />
                                                            <TextField
                                                                size="small"
                                                                placeholder="New value"
                                                                value={editValues.newValue || ''}
                                                                onChange={(e) => setEditValues({ ...editValues, newValue: e.target.value })}
                                                                sx={{ flex: 1 }}
                                                            />
                                                            <Button
                                                                size="small"
                                                                variant="outlined"
                                                                onClick={() => {
                                                                    if (editValues.newKey) {
                                                                        const newFields = { ...(editValues.fields || item.value) };
                                                                        newFields[editValues.newKey] = editValues.newValue || '';
                                                                        setEditValues({ ...editValues, fields: newFields, newKey: '', newValue: '' });
                                                                    }
                                                                }}
                                                                disabled={!editValues.newKey}
                                                            >
                                                                Add
                                                            </Button>
                                                        </Box>
                                                    </Box>
                                                ) : (
                                                    // String editor
                                                    <TextField
                                                        fullWidth
                                                        multiline
                                                        rows={3}
                                                        value={editValues.value || ''}
                                                        onChange={(e) => setEditValues({ value: e.target.value })}
                                                        variant="outlined"
                                                        size="small"
                                                        label="Value"
                                                    />
                                                )}
                                            </Box>
                                        ) : (
                                            <Typography
                                                variant="body2"
                                                sx={{
                                                    fontFamily: 'monospace',
                                                    backgroundColor: '#f5f5f5',
                                                    p: 1,
                                                    borderRadius: 1,
                                                    fontSize: '12px',
                                                    maxHeight: '150px',
                                                    overflow: 'auto',
                                                    whiteSpace: 'pre-wrap'
                                                }}
                                            >
                                                {displayValue}
                                            </Typography>
                                        )}
                                    </Box>
                                </ListItem>
                            );
                        })}
                    </List>

                    {memoryItems.length === 0 && (
                        <Typography sx={{ textAlign: 'center', py: 4, color: 'text.secondary' }}>
                            No memory items yet
                        </Typography>
                    )}

                    {memoryHasMore && (
                        <Box sx={{ display: 'flex', justifyContent: 'center', mt: 2 }}>
                            <Button onClick={loadMoreItems} disabled={loading}>
                                Load More
                            </Button>
                        </Box>
                    )}
                </>
            )}

            {/* New Item Dialog */}
            <Dialog open={newItemDialog} onClose={() => setNewItemDialog(false)} maxWidth="sm" fullWidth>
                <DialogTitle>Add Memory Item</DialogTitle>
                <DialogContent>
                    <TextField
                        fullWidth
                        label="Key (without memory_ prefix)"
                        value={newItemKey}
                        onChange={(e) => setNewItemKey(e.target.value)}
                        sx={{ mb: 2, mt: 1 }}
                    />
                    <TextField
                        fullWidth
                        label="Value (enter as text)"
                        value={newItemValue}
                        onChange={(e) => setNewItemValue(e.target.value)}
                        multiline
                        rows={3}
                        helperText="Enter a simple text value. You can edit it as an object later if needed."
                    />
                </DialogContent>
                <DialogActions>
                    <Button onClick={() => {
                        setNewItemDialog(false);
                        setNewItemKey('');
                        setNewItemValue('');
                    }}>
                        Cancel
                    </Button>
                    <Button onClick={createMemoryItem} variant="contained">
                        Create
                    </Button>
                </DialogActions>
            </Dialog>
        </Box>
    );
};

export default MemoryTab;

Frontend/MultiContentRenderer.js

import React from 'react';
import ImageWithDownload from './ImageWithDownload';

/**
 * Component to render mixed content (text + images) from user messages
 * Images are displayed 2 per row
 */
const MultiContentRenderer = ({ content }) => {
    // content is an array like:
    // [{"type":"text","text":"..."}, {"type":"text","text":"Image Uploaded: ..."}, {"type":"image_url","image_url":{"url":"..."}}]

    if (!Array.isArray(content)) {
        return null;
    }

    // Separate text and images
    const textParts = [];
    const images = [];

    content.forEach((item, index) => {
        if (item.type === 'text') {
            // Skip "Image Uploaded: ..." text notifications
            if (!item.text.startsWith('Image Uploaded:')) {
                textParts.push(item.text);
            }
        } else if (item.type === 'image_url' && item.image_url?.url) {
            images.push({
                url: item.image_url.url,
                index: index
            });
        }
    });

    return (
        <div style={{ width: '100%' }}>
            {/* Render text content */}
            {textParts.length > 0 && (
                <div style={{ marginBottom: images.length > 0 ? '12px' : '0' }}>
                    {textParts.map((text, idx) => (
                        <div key={`text-${idx}`} style={{ marginBottom: '4px' }}>
                            {text}
                        </div>
                    ))}
                </div>
            )}

            {/* Render images in a 2-column grid */}
            {images.length > 0 && (
                <div style={{
                    display: 'grid',
                    gridTemplateColumns: images.length === 1 ? '1fr' : 'repeat(2, 1fr)',
                    gap: '8px',
                    width: '100%'
                }}>
                    {images.map((img, idx) => (
                        <div key={`img-${idx}`} style={{
                            width: '100%',
                            maxWidth: '100%'
                        }}>
                            <ImageWithDownload
                                imageUrl={img.url}
                                style={{
                                    width: '100%',
                                    height: 'auto',
                                    borderRadius: '8px',
                                    objectFit: 'cover'
                                }}
                            />
                        </div>
                    ))}
                </div>
            )}
        </div>
    );
};

export default MultiContentRenderer;

Frontend/SimpleHTMLPreview.js

import React, { useEffect, useRef, useState, useCallback } from 'react';
import {
    LinearProgress
} from '@mui/material';
import {
    extractHTMLContent,
    generateFilename,
    getBaseURL
} from "./utils";

const SimpleHTMLPreview = ({ htmlContent, params }) => {
    const previewRef = useRef(null);
    const { msgIndex, messages, setMessages, chatAPI, KB, uploadFileAPI, setBlockingLoading } = params;

    const [isPublishing, setIsPublishing] = useState(false);

    const uploadHTMLContent = useCallback(async (htmlContent) => {
        const html = extractHTMLContent(htmlContent);
        if (!html) return;
        try {
            const blob = new Blob([html], { type: 'text/html' });
            const file = new File([blob], generateFilename(htmlContent), { type: 'text/html' });
            const res = await uploadFileAPI(file, 'files')
            return getBaseURL(KB) + decodeURIComponent(res?.config.url.split('/').pop().split('?')[0]);
        } catch (e) {
            console.error('Error during upload:', e);
        }
    }, [uploadFileAPI, KB]);

    const handlePublish = async () => {
        const html = extractHTMLContent(htmlContent);
        if (!html) return;

        try {
            setIsPublishing(true);
            setBlockingLoading({text: "Publishing website"});
            const url = await uploadHTMLContent(htmlContent);
            if (url) {
                window.open(url, '_blank');
            }
        } catch (e) {
            console.error('Error publishing:', e);
        } finally {
            setIsPublishing(false);
            setBlockingLoading(false);
        }
    };

    useEffect(() => {
        if (previewRef.current && htmlContent) {
            const html = extractHTMLContent(htmlContent);
            if (html) {
                const iframe = previewRef.current.querySelector('iframe');
                if (iframe) {
                    iframe.srcdoc = html;
                }
            }
        }
    }, [htmlContent]);

    const loaderStyle = { position: 'absolute', top: 14, left: 0, right: 0, height: 2, zIndex: 1000 };

    return (
        <>
            <div style={{ position: 'relative', height: 0, overflow: 'visible' }}>
                {isPublishing && (<LinearProgress style={loaderStyle} />)}
            </div>
            <div style={{ position: 'relative', border: '1px solid #ddd', borderRadius: '8px', overflow: 'hidden' }}>
                <div style={{
                    backgroundColor: '#f5f5f5',
                    padding: '10px',
                    borderBottom: '1px solid #ddd',
                    display: 'flex',
                    justifyContent: 'space-between',
                    alignItems: 'center'
                }}>
                    <span style={{ fontWeight: 'bold', color: '#333' }}>Website Preview</span>
                    <button
                        onClick={handlePublish}
                        disabled={isPublishing}
                        style={{
                            color: '#ffffff',
                            backgroundColor: isPublishing ? '#ccc' : '#28a745',
                            padding: '8px 16px',
                            border: 'none',
                            borderRadius: '4px',
                            fontWeight: 'bold',
                            cursor: isPublishing ? 'not-allowed' : 'pointer',
                            fontSize: '14px'
                        }}
                    >
                        {isPublishing ? 'Publishing...' : 'Publish'}
                    </button>
                </div>
                <div ref={previewRef} style={{ height: '600px', width: '100%' }}>
                    <iframe
                        style={{ width: '100%', height: '100%', border: 'none' }}
                        sandbox="allow-scripts allow-same-origin"
                        srcdoc={extractHTMLContent(htmlContent) || ''}
                    />
                </div>
            </div>
        </>
    );
};

export default SimpleHTMLPreview;

Frontend/contentRender.js

import React from 'react';
import {
    extractHTMLContent,
    isContentHTML
} from "./utils";
import ImageWithDownload from './ImageWithDownload';
import CommandRenderer from './CommandRenderer';
import SimpleHTMLPreview from './SimpleHTMLPreview';
import Header from './Header';
import MultiContentRenderer from './MultiContentRenderer';

// Common command patterns used throughout the app
const COMMAND_PATTERNS = [
    /<getSora2PromptingGuide\s*\/>/,
    /<getWebPublishingGuide\s*\/>/,
    /<createAIImage>[\s\S]*?<\/createAIImage>/,
    /<createAIVideo>[\s\S]*?<\/createAIVideo>/,
    /<continueVideoPolling>[\s\S]*?<\/continueVideoPolling>/,
    /<publishWebPage>[\s\S]*?<\/publishWebPage>/,
    /<sendMail>[\s\S]*?<\/sendMail>/,
    /<googleSearch>[\s\S]*?<\/googleSearch>/,
    /<googleImageSearch>[\s\S]*?<\/googleImageSearch>/,
    /<viewImage>[\s\S]*?<\/viewImage>/,
    /<webpageToText>[\s\S]*?<\/webpageToText>/,
    /<setMemory>[\s\S]*?<\/setMemory>/,
    /<deleteItem>[\s\S]*?<\/deleteItem>/,
    /<cleanupMemory\s*\/>/,
    /<scheduleTask>[\s\S]*?<\/scheduleTask>/,
    /<getScheduledTasks\s*\/>/,
    /<deleteScheduledTask>[\s\S]*?<\/deleteScheduledTask>/
];

export function getQueryParamValue(paramName) {
    const queryParams = new URLSearchParams(window.location.search);
    return queryParams.get(paramName);
}

const onRenderChatMessage = async (params) => {
    let { content, role } = params.messages[params.msgIndex];
    const { initDB, KB, msgIndex, messages, markdownHandler } = params;

    if (getQueryParamValue('debug')) return;

    let JSONData;

    // Try to parse JSON from message content
    try {
        JSONData = JSON.parse(content);
    } catch (e) {
        // Content is not JSON, continue with normal processing
    }

    if (Array.isArray(JSONData)) {
        const hasImages = JSONData.some(item => item.type === 'image_url');
        if (hasImages) return <MultiContentRenderer content={JSONData} />;
    }

    // Hide CONTINUE type system messages - they are service messages
    if (JSONData?.type === 'CONTINUE') {
        return JSON.stringify({ type: 'HIDDEN_MESSAGE' });
    }

    // Check if this is a system response to a previous command
    if (role === 'system' && JSONData &&
        (JSONData._meta_type === 'EVENT_STARTED' || JSONData._meta_type === 'EVENT_FINISHED')) {

        // Don't hide special response types that have their own rendering
        const hasSpecialRendering = (JSONData.type === 'CHAT_IMAGE' && JSONData.data?.imageUrl) ||
                                    (JSONData.type === 'CHAT_VIDEO' && JSONData.data?.videoUrl) ||
                                    (JSONData.type === 'VISUAL_MULTI_RESPONSE' && Array.isArray(JSONData.data));

        if (!hasSpecialRendering) {
            // Check if previous message had a command
            if (msgIndex > 0) {
                const prevMessage = messages[msgIndex - 1];
                const prevHasCommand = COMMAND_PATTERNS.some(pattern => pattern.test(prevMessage.content));

                // If previous message had a command, hide this system message
                // as it's already shown in the command widget
                if (prevHasCommand) {
                    return JSON.stringify({ type: 'HIDDEN_MESSAGE' });
                }
            }
        }
    }

    // Handle CHAT_IMAGE type JSON data
    if (JSONData?.type === 'CHAT_IMAGE' && JSONData?.data?.imageUrl) {
        return <ImageWithDownload imageUrl={JSONData.data.imageUrl} />;
    }

    // Handle CHAT_VIDEO type JSON data
    if (JSONData?.type === 'CHAT_VIDEO' && JSONData?.data?.videoUrl) {
        return (
            <video
                src={JSONData.data.videoUrl}
                controls
                style={{ width: '100%', maxWidth: 600, borderRadius: 8 }}
            />
        );
    }

    // Handle VISUAL_MULTI_RESPONSE - multiple images/videos generated in parallel
    if (JSONData?.type === 'VISUAL_MULTI_RESPONSE' && Array.isArray(JSONData?.data)) {
        return (
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px', maxWidth: '100%' }}>
                {JSONData.data.map((item, idx) => {
                    if (item?.type === 'CHAT_IMAGE' && item?.data?.imageUrl) {
                        return (
                            <div key={`img-${idx}`} style={{ flex: '1 1 calc(50% - 6px)', minWidth: 200, maxWidth: 400 }}>
                                <ImageWithDownload imageUrl={item.data.imageUrl} />
                            </div>
                        );
                    }
                    if (item?.type === 'CHAT_VIDEO' && item?.data?.videoUrl) {
                        return (
                            <div key={`vid-${idx}`} style={{ flex: '1 1 100%' }}>
                                <video
                                    src={item.data.videoUrl}
                                    controls
                                    style={{ width: '100%', maxWidth: 600, borderRadius: 8 }}
                                />
                            </div>
                        );
                    }
                    return null;
                })}
            </div>
        );
    }

    // Check if content contains any command tags
    const hasCommand = COMMAND_PATTERNS.some(pattern => pattern.test(content));

    // If content contains commands, check for response in next message
    if (hasCommand) {
        let responseData = null;

        // Check if next message is a system response
        if (msgIndex < messages.length - 1) {
            const nextMessage = messages[msgIndex + 1];
            if (nextMessage.role === 'system') {
                try {
                    const nextJSON = JSON.parse(nextMessage.content);
                    if (nextJSON._meta_type === 'EVENT_STARTED' || nextJSON._meta_type === 'EVENT_FINISHED') {
                        responseData = nextJSON;
                    }
                } catch (e) {
                    // Not JSON, ignore
                }
            }
        }

        return <CommandRenderer content={content} responseData={responseData} markdownHandler={markdownHandler} />;
    }

    return null;
};

const exports = {onRenderChatMessage, Header};
window.contentRender = exports;
export default exports;

Frontend/contentRender.json

{
  "dependencies": {
    "react": "^18.2.0 (fixed)",
    "react-dom": "^18.2.0 (fixed)",
    "@mui/material": "^5.16.1 (fixed)",
    "@mui/icons-material": "^5.16.1 (fixed)",
    "@emotion/react": "^11.10.6 (fixed)",
    "@emotion/styled": "^11.10.6 (fixed)"
  }
}

Frontend/utils.js

export function getBaseURL(KB) {
    return `https://web.file.vpc1.us/files/${KB?.kbId}/`;
}

export function generateFilename(html) {
    const title = new DOMParser()
        .parseFromString(html, 'text/html')
        .querySelector('title')?.textContent || 'untitled.html';
    return title.trim().toLowerCase()
        .replace(/[^a-z0-9]+/g, '-')
        .replace(/^-+|-+$/g, '') + '.html';
}

export function extractHTMLContent(content) {
    if (!content) return null;
    const languageDetected = /```(?<language>\w+)/g.exec(content)?.groups?.language;
    const htmlMatch = content.match(/<html[^>]*>[\s\S]*<\/html>/);
    if (htmlMatch && (!languageDetected || languageDetected === 'html')) {
        return htmlMatch[0];
    }
    return null;
}

export function isContentHTML(content) {
    if (!content) return content;
    const languageDetected = /```(?<language>\w+)/g.exec(content)?.groups?.language;
    return content?.match?.(/<html[^>]*>[\s\S]*<\/html>/) &&
        (!languageDetected || languageDetected === 'html');
}