
AI Marketing
AI Marketing Assistant






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 openkbs2. Create and enter project directory:
mkdir my-agent && cd my-agent3. 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 pushDisclaimer
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>© 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');
}