App Icon

AI Banner Maker

Creates HTML5 banners using generative AI


Try Now
MIT LicenseGitHub Repository
ScreenshotScreenshotScreenshotScreenshotScreenshotScreenshot

Customization

1. Install OpenKBS CLI:
npm install -g openkbs
2. Create and enter project directory:
mkdir woo-agent && cd woo-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:
Examples:
openkbs modify "Implement new command getBitcoinPrice(days) that returns price history from CoinGecko API" src/Events/actions.js
openkbs modify "Add instructions for getBitcoinPrice" src/Events/actions.js app/instructions.txt
6. Review changes and deploy to OpenKBS:
git diff
openkbs push
7. Test your customization:
Go to chat and ask: "Analyze the price of Bitcoin"

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 HTML banner maker, assist users with their inquiries and tasks efficiently.

Guidelines:
- Create banners in HTML format
- Use the provided image or generate one with /textToImage("image prompt") if none is available
- To initiate an API request first output the command
- Execute one action/command per message and wait for the response
- Users can upload their own images to be used in the banner creation process
- Users can ask you to generate images using the textToImage command
- When you generate an image, wait for the response before you start writing the HTML
- When creating an HTML banner, for proper system rendering, enclose it within:
```html
<html>{htmlContent}</html>
```
- Always generate a title tag within the htmlContent
- When generating HTML forms, always use this public API to provide backend persistence:
Step 1. load the axios and sweetalert in the HTML:
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>

Step 2. Make call to the items service using kbId {{kbId}} and generate required attributes/fields.

Example:
        Swal.fire({ title: 'Loading...', didOpen: () => Swal.showLoading(), allowOutsideClick: false, showConfirmButton: false });
        axios.post('https://chat.openkbs.com/publicAPIRequest',  {
          action: "createItem",
          kbId: "{{kbId}}",
          itemType: "myFormName",
          attributes: [
            // Ensure `attrType` numbers increase sequentially for repeated types.
            { attrType: "keyword1", attrName: "name", encrypted: true },
            { attrType: "keyword2", attrName: "email", encrypted: true },
            { attrType: "integer1", attrName: "age", encrypted: false },
            { attrType: "float1", attrName: "rating", encrypted: false },
            { attrType: "date1", attrName: "datetime", encrypted: false },
            { attrType: "boolean1", attrName: "active", encrypted: false },
          ],
          item: { name: "Joe", company: "none",  age: 32, rating: 4.5, datetime: "2023-11-17T19:32", active: true}
        })
        .then(() => Swal.fire('Success', 'Success!', 'success'))
        .catch(() => Swal.fire('Error', 'Failed!', 'error'));

- When generating HTML forms, keep the submit button always on new line
- Only generate an HTML form if prompted by the user

Here are other commands you can use to assist users:

/webpageToText("URL")
Description: """
Use this API to extract the title, description, price, and image from a webpage.
"""

/textToImage("image prompt", "negative image prompt")
Description: """
Generates images by text prompt (English only supported) and returns the image url
Negative image prompt is optional, it excludes unwanted elements from the images

Concise Prompt Guide for Text-to-Image Models:

A good prompt needs to be detailed and specific, a good process is to look through a list of keyword components and decide whether you want to use any of them

Key Components of a Good Prompt:
1. Subject: Clearly define the main focus, including details like age, gender, and distinctive features. Example: "A powerful sorceress casting lightning magic."
2. Medium: Specify the artistic medium, such as digital art, oil painting, or photography. Example: "Digital art"
3. Style: Indicate the artistic style, like hyperrealistic, fantasy, or surrealist. Example: "Fantasy"
4. Composition: Describe the arrangement, pose, and framing of the subject. Example: "Sitting on a rock with a castle in the background"
5. Lighting: Define the type and quality of light, such as studio lighting or golden hour. Example: "Studio lighting"
6. Color Palette: Specify dominant colors or a color scheme. Example: "Iridescent gold"
7. Resolution and Detail: Indicate desired sharpness and detail level. Example: "Highly detailed"
8. Mood/Atmosphere: Convey the emotional tone or ambiance. Example: "Dark and mysterious"
9. Additional Elements: Include supporting details or background information. Example: "Detailed leather clothing with gemstones"
10. Artistic References: Reference specific artists or styles to guide the output. Example: "In the style of Vincent van Gogh"
11. Website Influence: Mention websites like Artstation to influence style. Example: "Artstation"

Prompt Crafting Techniques
- Be Specific and Descriptive: Provide detailed information to guide the model.
- Blend Concepts: Combine different ideas for unique images.
- Use Contrast: Create striking images by contrasting elements.
- Incorporate Mood: Describe the emotional tone for desired feel.

Tips for Optimal Results
- Iterate and Refine: Generate multiple images and refine prompts.
- Balance Detail and Freedom: Guide the model while allowing creative interpretation.
- Use Natural Language: Write clear, descriptive prompts.
- Explore Diverse Themes: Experiment with various subjects.

Common Pitfalls to Avoid
- Overloading the Prompt: Avoid too many conflicting ideas.
- Neglecting Composition: Guide overall composition, not just elements.
- Ignoring Lighting and Atmosphere: These greatly influence mood and realism.
- Being Too Vague: General prompts may lead to generic results.

Tips to Create Realistic Photos
- Structure Your Prompts: Subject, description of the subject {hairstyle, skin, cloth, props, poses}, environment, {other important features}, composition, lighting, camera {angles, camera properties}, by [photographer], quality keywords.
- Incorporate keywords: Incorporate keywords like (DSLR camera, RAW photo, analog style) to guide the model. Including specific camera brands such as "Fujifilm" or "Sony"
- Use Effective Negative Prompts: Including terms like doll,anime,animation,cartoon,render,artwork,semi-realistic,CGI,3d,sketch,drawing and etc.
- Define the Lighting: Terms like (e.g., dramatic lighting,soft sunlight,golden hour,bokeh and etc.) can guide the model to create images with the desired ambiance
- Photorealistic Example: "... shot on a professional DSLR camera, ultra-high definition, sharp focus on ..."
"""

$InputLabel = """Halloween Bar Event"""
$InputValue = """
/textToImage("Extreme close-up of a photorealistic whiskey glass positioned on the far right side of a wooden bar counter. The glass has visible condensation droplets on its surface. It contains ice cubes and an amber-colored liquid. Above the glass, a blurred neon sign reads 'Your Brand Here'. On the bar surface, subtle Halloween-themed bar decorations such as a miniature skull shot glass, a small black cat figurine, and a tiny witch's hat. Dark, moody lighting emphasizes the glow of the drink. The left two-thirds of the image is intentionally darker to allow for text overlay. High-resolution, photorealistic rendering with attention to condensation, reflections, and realistic textures.")

All text content on left side
Event name, Event Date: 31st October 2024, Location
"Reserve Your Spot" call-to-action button
Halloween-style fonts
Orange and white color scheme
Responsive design
"""

$InputLabel = """Halloween Night Club Event"""
$InputValue = """
/textToImage("Extreme close-up of a vibrant cocktail glass positioned on the far left side of a sleek, illuminated bar counter in a night club. The glass contains a glowing, neon-blue drink with wisps of dry ice vapor. Visible condensation droplets on the glass surface catch the colorful lights. Above the glass, a blurred neon sign reads 'Your Brand Here'. In the background, pulsating, multicolored strobe lights create an empty dance floor atmosphere. On the bar surface, subtle Halloween decorations like mini pumpkins and fake spider webs. Dynamic lighting with streaks of electric blue, purple, and orange creates an energetic club atmosphere. High-resolution, photorealistic rendering with attention to light reflections, glass textures, and the vibrant club energy mixed with Halloween elements.")

Text content on right side
Event name, Event Date: 31st October 2024, Location
"Reserve Your Spot" call-to-action button
Halloween-style fonts
Orange and white color scheme
Responsive design
"""

$InputLabel = """Travel Agency Promotion"""
$InputValue = """
/textToImage("Photorealistic panoramic view of a beautiful tropical beach with white sand, text says 'Your Brand Here' casually written in the sand, turquoise water, and a colorful sunset sky with orange hues. Palm trees in the foreground, distant islands in the background. High resolution, sharp focus.")

Use generated background image to create a travel agency banner.
The banner should evoke a sense of adventure and wanderlust.
Generate photorealistic background image of popular travel destinations beaches.
Incorporate a color scheme of turquoise and sunset orange to create a warm and inviting feel.
Include promotional text about special offers or packages.
Use a playful yet readable font to convey excitement and fun.
"""

$InputLabel = """Fruit Shop"""
$InputValue = """
/textToImage("Overhead view of a rustic wooden table covered with an array of fresh, vibrant fruits and vegetables. Scattered across the table are whole and sliced produce including tomatoes, lettuce leaves, carrots, berries, apples, and herbs. In the center, the words 'Your Brand' are artfully spelled out using colorful fruit and vegetable pieces - letters formed from cucumber slices, carrot sticks, berry arrangements, and leafy greens. The arrangement looks natural and abundant, with soft, warm lighting giving a fresh, organic feel. High detail, vivid natural colors, and appetizing textures.")

Make a fruit shop banner with a title "Fresh & Delicious Fruits" and big "Shop Now" button
"""

$InputLabel = """Corporate Event"""
$InputValue = """
/textToImage("Modern city skyline at dusk, tall skyscrapers, blue hour, professional and elegant, high resolution")

Use generated background image to create a corporate event banner.
The banner should include the company logo, event name, date, and location.
Use a professional color scheme such as navy blue and white.
The design should be clean and modern, with a focus on elegance.
Include a high-resolution background image of a city skyline in the background to convey a sense of sophistication and success.
Ensure the text is legible and stands out against the background.
"""

$InputLabel = """Charity Fundraiser Gala"""
$InputValue = """
/textToImage("Luxurious ballroom with crystal chandeliers and twinkling lights, deep purple and gold color scheme, elegant and sophisticated atmosphere, high resolution")

Design an elegant banner for a charity fundraiser gala.
The banner should convey a sense of sophistication and compassion.
Use a color scheme of deep purple and gold for a regal feel.
Include the gala's name, beneficiary cause, date, and venue.
Add subtle graphical elements like gold ribbons or champagne glasses.
Use an elegant, script font for the main title with big 3d text shadow and a clean sans-serif for other details.
Ensure the design balances grandeur with the seriousness of the charitable cause.
"""

$InputLabel = """Music Festival"""
$InputValue = """Design a banner for an upcoming music festival. The banner should capture the energy and excitement of live music. Use vibrant colors like electric blue and neon pink. Generate a background image of a lively concert scene with a crowd and stage lights. Include the festival name, dates, and location. Use a bold and dynamic font to convey the thrill of the event. Ensure the text is prominent and easy to read."""

$InputLabel = """Tech Conference"""
$InputValue = """Design a banner for a tech conference. The banner should reflect innovation and cutting-edge technology. Use a sleek color scheme of silver and black. Generate a futuristic background image of a digital cityscape or abstract technology patterns. Include the conference name, dates, and location. Use a modern and clean font to convey professionalism. Ensure the text is clear and stands out against the background."""

$InputLabel = """Placeholder Banner"""
$InputValue = """Create a simple gray rectangle with 'Placeholder Banner' text in black, without images."""

Events Files (Node.js Backend)

Events/actions.js

export const getActions = (meta) => [
    [/\/?textToImage\("([^"]*)"(?:,\s*"([^"]*)")?\)/, async (match) => {
        const hasDefaultImageToTextModel = !'{{variables.defaultImageToTextModel}}'.startsWith('{{')
        const response = await openkbs.textToImage(match[1], {
            negative_prompt: match[2],
            serviceId: hasDefaultImageToTextModel ? '{{variables.defaultImageToTextModel}}' : undefined
        });
        const imageSrc = `data:${response.ContentType};base64,${response.base64Data}`;
        return { type: 'SAVED_CHAT_IMAGE', imageSrc, ...meta };
    }],
    [/\/?webpageToText\("(.*)"\)/, async (match) => {
        try {
            let response = await openkbs.webpageToText(match[1], { 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, ...meta };
        }
    }]
];

Events/onPublicAPIRequest.js

const handler = async ({ payload }) => {
    const { item, attributes, itemType, action } = payload;

    if (action === 'createItem') {
        const myItem = {};

        myItem.contry = openkbs.clientHeaders['cloudfront-viewer-country-name']
        myItem.ip = openkbs.clientHeaders['x-forwarded-for']

        for (const attribute of attributes) {
            const { attrName, encrypted } = attribute;
            if (encrypted && item[attrName] !== undefined) {
                myItem[attrName] = await openkbs.encrypt(item[attrName]);
            } else {
                myItem[attrName] = item[attrName];
            }
        }

        return await openkbs.items({ action, itemType, attributes, item: myItem });
    }
}

module.exports = { handler }

Events/onRequest.js

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

export const handler = async (event) => {
    const actions = getActions({_meta_actions: ["REQUEST_CHAT_MODEL"]});

    for (let [regex, action] of actions) {
        const lastMessage = event.payload.messages[event.payload.messages.length - 1].content;        
        const match = lastMessage?.match(regex);        
        if (match) return await action(match);            
    }

    return { type: 'CONTINUE' }
};

Events/onResponse.js

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

export const handler = async (event) => {
    const actions = getActions({_meta_actions: ["REQUEST_CHAT_MODEL"]});

    for (let [regex, action] of actions) {
        const lastMessage = event.payload.messages[event.payload.messages.length - 1].content;        
        const match = lastMessage?.match(regex);        
        if (match) return await action(match);            
    }

    return { type: 'CONTINUE' }
};

Frontend Files (React Frontend)

Frontend/blocks.js

export const addBlocks = ({editor, KB}) => {
    const bm = editor.BlockManager;
    const category = {
        id: 'Top Picks',
        label: 'Top Picks',
        order: 1,
        open: true,
    }
    // Image Top Picks Block
    bm.add('image-top-picks', {
        ...bm?.get('image')?.attributes,
        category,
        media: `
        <svg viewBox="0 0 64 64" width="40" height="40" xmlns="http://www.w3.org/2000/svg">
        <rect x="8" y="8" width="48" height="48" rx="4" ry="4" fill="none" stroke="currentColor" stroke-width="4"/>
        <circle cx="20" cy="20" r="4" fill="currentColor"/>
        <path d="M8 56 L24 40 L34 50 L56 28 L56 56 Z" fill="currentColor"/>
        </svg>
    `
    });

    // Promotion Banner Block
    bm.add('promo-banner', {
        label: 'Promo Banner',
        category,
        media: `
        <svg viewBox="0 0 64 64" width="40" height="40" xmlns="http://www.w3.org/2000/svg">
            <rect x="4" y="16" width="56" height="32" fill="currentColor" rx="4" ry="4"/>   
            <circle cx="22" cy="26" r="8" fill="#fff"/>
            <circle cx="42" cy="38" r="8" fill="#fff"/>
            <line x1="16" y1="44" x2="48" y2="20" stroke="#fff" stroke-width="4"/>
        </svg>
    `,
        content: `
        ${commonStyles}
        <div class="kb-block promo-banner">
            <h2>Special Offer!</h2>
            <p>Get 50% off on all items</p>
        </div>
    `,
    });

    // Call-To-Action Block
    bm.add('action-button', {
        label: 'Action Button',
        category,
        media: `
        <svg viewBox="0 0 64 64" width="40" height="40" xmlns="http://www.w3.org/2000/svg">
            <rect x="4" y="16" width="56" height="32" fill="currentColor" rx="16" ry="16"/>
            <path d="M26 26 L38 32 L26 38 Z" fill="#fff"/>
            <line x1="20" y1="32" x2="12" y2="32" stroke="#fff" stroke-width="2" stroke-linecap="round"/>
            <line x1="52" y1="32" x2="44" y2="32" stroke="#fff" stroke-width="2" stroke-linecap="round"/>
        </svg>
    `,
        content: `
        ${commonStyles}
        <button class="kb-block action-button">Act Now</button>
    `,
    });

    // Quote Block (using existing quote block)
    bm.add('quote-top-picks', {
        ...bm?.get('quote')?.attributes,
        category,
        label: 'Quote',
        media: `
        <svg viewBox="0 0 24 24" width="40" height="40" xmlns="http://www.w3.org/2000/svg">
            <path fill="currentColor" d="M6,17h3l2-4V7H5v6h3L6,17z M14,17h3l2-4V7h-6v6h3L14,17z"/>
        </svg>
    `
    });

    // Text Block (using existing text block)
    bm.add('text-box-top-picks', {
        ...bm?.get('text-basic')?.attributes,
        category,
        label: 'Text Box',
    });

    // Bullet List Block (custom)
    bm.add('bullet-list', {
        label: 'Bullet List',
        category,
        media: `
        <svg viewBox="0 0 24 24" width="40" height="40" xmlns="http://www.w3.org/2000/svg">
            <path fill="currentColor" d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/>
        </svg>
    `,
        content: `
        ${commonStyles}
        <ul class="kb-block bullet-list">
            <li>First item</li>
            <li>Second item</li>
            <li>Third item</li>
        </ul>
    `,
    });

    // Columns Block (using existing column block)
    bm.add('columns3-top-picks', {
        ...bm?.get('column3')?.attributes,
        category,
        label: 'Columns3',
    });

    // Columns Block (using existing column block)
    bm.add('columns1-top-picks', {
        ...bm?.get('column1')?.attributes,
        category,
        label: 'Columns1',
    });

    // Subscribe Form Block
    bm.add('subscribe-form', {
        label: 'Subscribe Form',
        category,
        media: `
            <svg viewBox="0 0 24 24" width="40" height="40">
                <path fill="none" d="M0 0h24v24H0z"/>
                <path fill="currentColor" d="M21 8v11.5a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 3 19.5V8l9 6 9-6zm-9 4L3 6h18l-9 6z"/>
            </svg>
        `,
        content: `
            ${commonStyles}
            <div class="kb-block subscribe-form">
                <h2>Subscribe for Updates</h2>
                <form id="subscribe-form">
                    <input type="text" id="name" name="name" placeholder="Your Name" required>
                    <input type="email" id="email" name="email" placeholder="Your Email" required>
                    <button type="submit">Subscribe</button>
                </form>
            </div>
            <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
            <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
            <script>
                document.getElementById('subscribe-form').addEventListener('submit', function(event) {
                    event.preventDefault();
                    const formData = new FormData(event.target);
                    const data = {
                        action: "createItem",
                        kbId: "${KB.kbId}",
                        itemType: "subscribeForm",
                        attributes: [
                            { attrType: "keyword1", attrName: "name", encrypted: true },
                            { attrType: "keyword2", attrName: "email", encrypted: true }
                        ],
                        item: {
                            name: formData.get('name'),
                            email: formData.get('email')
                        }
                    };
                    ${loadingJS}
                    axios.post('https://chat.openkbs.com/publicAPIRequest', data)
                        .then(response => {
                            Swal.fire('Success', 'Subscription successful!', 'success');
                        })
                        .catch(error => {
                            Swal.fire('Error', 'Subscription failed. Please try again.', 'error');
                        });
                });
            </script>
        `,
    });

    // Contact Us Form Block
    bm.add('contact-us-form', {
        label: 'Contact Us Form',
        category,
        media: `
            <svg viewBox="0 0 24 24" width="40" height="40">
                <path fill="none" d="M0 0h24v24H0z"/>
                <path fill="currentColor" d="M21 6.5a1 1 0 0 1 .993.883L22 7.5v9a1 1 0 0 1-.883.993L21 17.5h-2a1 1 0 0 1-.993-.883L18 16.5v-9a1 1 0 0 1 .883-.993L19 6.5h2zm-6 1v2H5v-2h10zm0 4v2H5v-2h10zm0 4v2H5v-2H5z"/>
            </svg>
        `,
        content: `
            ${commonStyles}
            <div class="kb-block contact-us-form">
                <h2>Contact Us</h2>
                <form id="contact-us-form">
                    <input type="text" id="name" name="name" placeholder="Your Name" required>
                    <input type="email" id="email" name="email" placeholder="Your Email" required>
                    <textarea id="message" name="message" placeholder="Your Message" rows="4" required></textarea>
                    <button type="submit">Send Message</button>
                </form>
            </div>
            <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
            <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
            <script>
                document.getElementById('contact-us-form').addEventListener('submit', function(event) {
                    event.preventDefault();
                    const formData = new FormData(event.target);
                    const data = {
                        action: "createItem",
                        kbId: "${KB.kbId}",
                        itemType: "contactForm",
                        attributes: [
                            { attrType: "keyword1", attrName: "name", encrypted: true },
                            { attrType: "keyword2", attrName: "email", encrypted: true },
                            { attrType: "text1", attrName: "message", encrypted: true }
                        ],
                        item: {
                            name: formData.get('name'),
                            email: formData.get('email'),
                            message: formData.get('message')
                        }
                    };
                    ${loadingJS}
                    axios.post('https://chat.openkbs.com/publicAPIRequest', data)
                        .then(response => {
                            Swal.fire('Success', 'Message sent successfully!', 'success');
                        })
                        .catch(error => {
                            Swal.fire('Error', 'Failed to send message. Please try again.', 'error');
                        });
                });
            </script>
        `,
    });

    // Feedback Form Block
    bm.add('feedback-form', {
        label: 'Feedback Form',
        category,
        media: `
            <svg viewBox="0 0 24 24" width="40" height="40" xmlns="http://www.w3.org/2000/svg">
              <path fill="currentColor" d="M2 2v16h4v4l4-4h12V2H2zm14 9H6V9h10v2zm0-3H6V6h10v2z"/>
            </svg>
        `,
        content: `
            ${commonStyles}
            <div class="kb-block feedback-form">
                <h2>Feedback</h2>
                <form id="feedback-form">
                    <input type="text" id="name" name="name" placeholder="Your Name" required>
                    <textarea id="feedback" name="feedback" placeholder="Your Feedback" rows="4" required></textarea>
                    <button type="submit">Submit Feedback</button>
                </form>
            </div>
            <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
            <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
            <script>
                document.getElementById('feedback-form').addEventListener('submit', function(event) {
                    event.preventDefault();
                    const formData = new FormData(event.target);
                    const data = {
                        action: "createItem",
                        kbId: "${KB.kbId}",
                                                itemType: "feedbackForm",
                        attributes: [
                            { attrType: "keyword1", attrName: "name", encrypted: true },
                            { attrType: "text1", attrName: "feedback", encrypted: true }
                        ],
                        item: {
                            name: formData.get('name'),
                            feedback: formData.get('feedback')
                        }
                    };
                    ${loadingJS}
                    axios.post('https://chat.openkbs.com/publicAPIRequest', data)
                        .then(response => {
                            Swal.fire('Thank you!', 'Your feedback has been submitted.', 'success');
                        })
                        .catch(error => {
                            Swal.fire('Error', 'Failed to submit feedback. Please try again.', 'error');
                        });
                });
            </script>
        `,
    });

    // Appointment Booking Form Block
    bm.add('appointment-form', {
        label: 'Appointment Booking Form',
        category,
        media: `
            <svg viewBox="0 0 24 24" width="40" height="40">
                <path fill="none" d="M0 0h24v24H0z"/>
                <path fill="currentColor" d="M19 3h-1V1h-2v2H8V1H6v2H5a2.006 2.006 0 0 0-2 2v14a2.006 2.006 0 0 0 2 2h14a2.006 2.006 0 0 0 2-2V5a2.006 2.006 0 0 0-2-2zm0 16H5V8h14z"/>
            </svg>
        `,
        content: `
            ${commonStyles}
            <div class="kb-block appointment-form">
                <h2>Book an Appointment</h2>
                <form id="appointment-form">
                    <input type="text" id="name" name="name" placeholder="Your Name" required>
                    <input type="email" id="email" name="email" placeholder="Your Email" required>
                    <input type="datetime-local" id="datetime" name="datetime" required>
                    <button type="submit">Book Now</button>
                </form>
            </div>
            <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
            <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
            <script>
                document.getElementById('appointment-form').addEventListener('submit', function(event) {
                    event.preventDefault();
                    const formData = new FormData(event.target);
                    const data = {
                        action: "createItem",
                        kbId: "${KB.kbId}",
                        itemType: "appointmentForm",
                        attributes: [
                            { attrType: "keyword1", attrName: "name", encrypted: true },
                            { attrType: "keyword2", attrName: "email", encrypted: true },
                            { attrType: "date1", attrName: "datetime", encrypted: false }
                        ],
                        item: {
                            name: formData.get('name'),
                            email: formData.get('email'),
                            datetime: formData.get('datetime')
                        }
                    };
                    ${loadingJS}
                    axios.post('https://chat.openkbs.com/publicAPIRequest', data)
                        .then(response => {
                            Swal.fire('Success', 'Your appointment has been booked!', 'success');
                        })
                        .catch(error => {
                            Swal.fire('Error', 'Failed to book appointment. Please try again.', 'error');
                        });
                });
            </script>
        `,
    });

    // Survey Form Block
    bm.add('survey-form', {
        label: 'Survey Form',
        category,
        media: `
            <svg viewBox="0 0 24 24" width="40" height="40">
                <path fill="none" d="M0 0h24v24H0z"/>
                <path fill="currentColor" d="M17 2H7a2.002 2.002 0 0 0-2 2v16l7-3 7 3V4a2.002 2.002 0 0 0-2-2z"/>
            </svg>
        `,
        content: `
            ${commonStyles}
            <div class="kb-block survey-form">
                <h2>Customer Satisfaction Survey</h2>
                <form id="survey-form">
                    <input type="text" id="name" name="name" placeholder="Your Name" required>
                    <select id="satisfaction" name="satisfaction" required>
                        <option value="">How satisfied are you with our service?</option>
                        <option value="Very Satisfied">Very Satisfied</option>
                        <option value="Satisfied">Satisfied</option>
                        <option value="Neutral">Neutral</option>
                        <option value="Dissatisfied">Dissatisfied</option>
                        <option value="Very Dissatisfied">Very Dissatisfied</option>
                    </select>
                    <textarea id="comments" name="comments" placeholder="Additional Comments" rows="4"></textarea>
                    <button type="submit">Submit Survey</button>
                </form>
            </div>
            <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
            <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
            <script>
                document.getElementById('survey-form').addEventListener('submit', function(event) {
                    event.preventDefault();
                    const formData = new FormData(event.target);
                    const data = {
                        action: "createItem",
                        kbId: "${KB.kbId}",
                        itemType: "surveyForm",
                        attributes: [
                            { attrType: "keyword1", attrName: "name", encrypted: true },
                            { attrType: "keyword2", attrName: "satisfaction", encrypted: false },
                            { attrType: "text1", attrName: "comments", encrypted: true }
                        ],
                        item: {
                            name: formData.get('name'),
                            satisfaction: formData.get('satisfaction'),
                            comments: formData.get('comments')
                        }
                    };
                    ${loadingJS}
                    axios.post('https://chat.openkbs.com/publicAPIRequest', data)
                        .then(response => {
                            Swal.fire('Thank you!', 'Your feedback has been recorded.', 'success');
                        })
                        .catch(error => {
                            Swal.fire('Error', 'Failed to submit survey. Please try again.', 'error');
                        });
                });
            </script>
        `,
    });

    // Registration Form Block
    bm.add('registration-form', {
        label: 'Registration Form',
        category,
        media: `
            <svg viewBox="0 0 24 24" width="40" height="40">
                <path fill="none" d="M0 0h24v24H0z"/>
                <path fill="currentColor" d="M12 12c2.206 0 4-1.794 4-4s-1.794-4-4-4S8 5.794 8 8s1.794 4 4 4zm0 2c-2.674 0-8 1.336-8 4v2h16v-2c0-2.664-5.326-4-8-4z"/>
            </svg>
        `,
        content: `
            ${commonStyles}
            <div class="kb-block registration-form">
                <h2>Register</h2>
                <form id="registration-form">
                    <input type="text" id="username" name="username" placeholder="Username" required>
                    <input type="email" id="email" name="email" placeholder="Email" required>
                    <input type="password" id="password" name="password" placeholder="Password" required>
                    <button type="submit">Sign Up</button>
                </form>
            </div>
            <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
            <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
            <script>
                document.getElementById('registration-form').addEventListener('submit', function(event) {
                    event.preventDefault();
                    const formData = new FormData(event.target);
                    const data = {
                        action: "createItem",
                        kbId: "${KB.kbId}",
                        itemType: "registrationForm",
                        attributes: [
                            { attrType: "keyword1", attrName: "username", encrypted: true },
                            { attrType: "keyword2", attrName: "email", encrypted: true },
                            { attrType: "text1", attrName: "password", encrypted: true }
                        ],
                        item: {
                            username: formData.get('username'),
                            email: formData.get('email'),
                            password: formData.get('password')
                        }
                    };
                    ${loadingJS}
                    axios.post('https://chat.openkbs.com/publicAPIRequest', data)
                        .then(response => {
                            Swal.fire('Welcome!', 'You have registered successfully.', 'success');
                        })
                        .catch(error => {
                            Swal.fire('Error', 'Registration failed. Please try again.', 'error');
                        });
                });
            </script>
        `,
    });
}

const commonStyles = `
        <style>
            .kb-block {
                background-color: #f8f9fa;
                border-radius: 10px;
                box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
                padding: 30px;
                max-width: 500px;
                margin: 20px auto;
                font-family: 'Arial', sans-serif;
            }
            .kb-block h2 {
                color: #2c3e50;
                font-size: 24px;
                margin-bottom: 20px;
                text-align: center;
            }
            .kb-block input[type="text"],
            .kb-block input[type="email"],
            .kb-block input[type="password"],
            .kb-block input[type="datetime-local"],
            .kb-block textarea,
            .kb-block select {
                width: 100%;
                padding: 12px;
                margin: 8px 0;
                border: 1px solid #bdc3c7;
                border-radius: 5px;
                font-size: 16px;
                transition: border-color 0.3s ease;
            }
            .kb-block input:focus,
            .kb-block textarea:focus,
            .kb-block select:focus {
                border-color: #3498db;
                outline: none;
            }
            .kb-block button {
                width: 100%;
                padding: 12px;
                background-color: #3498db;
                color: #ffffff;
                border: none;
                border-radius: 5px;
                font-size: 18px;
                cursor: pointer;
                transition: background-color 0.3s ease;
            }
            .kb-block button:hover {
                background-color: #2980b9;
            }
            .kb-block .bullet-list {
                list-style-type: disc;
                padding-left: 20px;
            }
            .kb-block .bullet-list li {
                margin-bottom: 10px;
            }
            @keyframes spin {
                0% { transform: rotate(0deg); }
                100% { transform: rotate(360deg); }
            }
        </style>
    `;

const loadingJS = `Swal.fire({ title: 'Loading...', didOpen: () => Swal.showLoading(), allowOutsideClick: false, showConfirmButton: false });`;

Frontend/contentRender.js

import React, { useEffect, useRef, useState, useCallback } from 'react';
import 'grapesjs/dist/css/grapes.min.css';
import { addBlocks } from "./blocks";
import {
    extractHTMLContent,
    isContentHTML,
    getFullHtml,
    generateFilename,
    getBaseURL
} from "./utils";
import { LinearProgress, IconButton, Tooltip } from '@mui/material';
import { OpenWith, Launch as Preview } from '@mui/icons-material';

const debounce = (func, wait) => {
    let timeout;
    return (...args) => {
        clearTimeout(timeout);
        timeout = setTimeout(() => func(...args), wait);
    };
};


const GrapesJSEditor = ({ htmlContent, params }) => {
    const editorContainerRef = useRef(null);
    const editorRef = useRef(null);
    const [grapesjs, setGrapesjs] = useState(null);
    const { msgIndex, messages, setMessages, chatAPI, KB, uploadFileAPI, setBlockingLoading, blockAutoscroll,
    setInputValue, sendButtonRippleRef } = params;

    const currentHTMLContentRef = useRef(htmlContent);
    const isLocalContentUpdate = useRef(false);
    const [isSaving, setIsSaving] = useState(false);
    const [dragMode, setDragMode] = useState(undefined);

    async function uploadAsset(file) {
        const res = await uploadFileAPI(file, 'files')
        return getBaseURL(KB) + decodeURIComponent(res?.config.url.split('/').pop().split('?')[0]);
    }

    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' });
            await uploadAsset(file)
        } catch (e) {
            console.error('Error during upload:', e);
        }
    }, [uploadFileAPI]);

    const save = async (updatedHTMLContent) => {
        blockAutoscroll();
        const html = extractHTMLContent(updatedHTMLContent);
        if (!html) return;
        try {
            setIsSaving(true);
            isLocalContentUpdate.current = true;
            await Promise.all([
                chatAPI.chatEditMessage(window.openkbs.parseChatId(), messages[msgIndex].msgId, updatedHTMLContent),
                uploadHTMLContent(html)
            ]);
            setMessages(prevMessages => prevMessages.map((msg, index) =>
                index === msgIndex ? { ...msg, content: updatedHTMLContent } : msg
            ));
            currentHTMLContentRef.current = html;

        } catch (e) {
            console.error(e);
        } finally {
            setIsSaving(false);
        }
    }

    const handleSave = useCallback(save, [chatAPI, messages, msgIndex, setMessages, uploadHTMLContent]);
    const debouncedHandleSave = useRef(debounce(handleSave, 300)).current;

    useEffect(() => {
        const loadGrapesJS = async () => {
            const grapesjsInstance = (await import('grapesjs')).default;
            const grapesjsPresetWebpage = (await import('grapesjs-preset-webpage')).default;
            const grapesjsBlocksBasic = (await import('grapesjs-blocks-basic')).default;
            setGrapesjs({ grapesjs: grapesjsInstance, grapesjsPresetWebpage, grapesjsBlocksBasic });
        };

        loadGrapesJS();
    }, []);

    const initializeEditor = useCallback((content) => {
        if (grapesjs && editorContainerRef.current) {
            const { grapesjs: grapesjsInstance, grapesjsPresetWebpage, grapesjsBlocksBasic } = grapesjs;

            const editor = grapesjsInstance.init({
                dragMode: dragMode,
                container: editorContainerRef.current,
                fromElement: false,
                components: content,
                width: 'auto',
                storageManager: false,
                plugins: [grapesjsPresetWebpage, grapesjsBlocksBasic],
                pluginsOpts: {
                    grapesjsPresetWebpage: {},
                },
                parser: {
                    optionsHtml: { allowScripts: true },
                },
                colorPicker: {   appendTo: 'parent',   offset: { top: 26, left: -166, },   },
                assetManager: {
                    upload: false,
                    uploadFile: async (event) => {
                        setBlockingLoading(true)
                        const files = event.dataTransfer ? event.dataTransfer.files : event.target.files;
                        const toAdd = [];
                        for (const file of files) {
                            try {
                                toAdd.push({ src: await uploadAsset(file) });
                            } catch (error) {
                                console.error('Error uploading file:', error);
                            }
                        }
                        setTimeout(() => {
                            editor.AssetManager.add(toAdd)
                            setBlockingLoading(false)
                        }, 1000)

                    }
                }
            });

            editor.on('asset:add', (asset) => {
                const imageComponent = editor.getSelected();
                if (imageComponent && imageComponent.is('image')) {
                    imageComponent.addStyle({
                        width: '256px',
                        height: 'auto',
                    });
                }
            });

            if (window.openkbs.hasActivity()) uploadHTMLContent(content)

            editor.Panels.removeButton('options', 'export-template');
            editor.Panels.removeButton('options', 'sw-visibility');
            editor.Panels.removeButton('devices-c', 'set-device-desktop');
            editor.Panels.removeButton('devices-c', 'set-device-tablet');
            editor.Panels.removeButton('devices-c', 'set-device-mobile');
            editor.Panels.removeButton('options', 'preview');

            if (window.openkbs.isMobile) {
                console.log(editor.Panels)
                editor.Panels.removeButton('views', 'open-sm');
                editor.Panels.removeButton('views', 'open-tm');
                editor.Panels.removeButton('views', 'open-layers');
                editor.Panels.removeButton('views', 'open-blocks');
                document.documentElement.style.setProperty('--gjs-left-width', '0%');
                document.documentElement.style.setProperty('--gjs-right-width', '0%');
                editor.Panels.removeButton('options', 'fullscreen');
                editor.Panels.removeButton('options', 'gjs-open-import-webpage');
            }
            editorRef.current = editor;
            window.editor = editor;

            addBlocks({ editor, KB });

            editor.on('load', () => {
                const bm = editor.BlockManager;
                const categories = bm.getCategories();

                categories.each(category => {
                    if (category.get('id') === 'Top Picks') {
                        category.set('open', true);
                        category.set('order', 1);
                    } else {
                        category.set('open', false);
                        const currentOrder = category.get('order') || 0;
                        category.set('order', 10 + currentOrder);
                    }
                });

                bm.render();
            });

            editor.on('block:drag:stop', (component, block) => {
                if (block && component && !block.get('id')?.startsWith('image')) {
                    blockAutoscroll(0);
                    const msg = `Make the "${block.get('label')}" a natural part of this banner in terms of style, colors, content and design.`;
                    setInputValue(prev => prev ? prev + msg : msg )
                    setTimeout(() => sendButtonRippleRef?.current?.pulsate(), 100)
                }
            });

            editor.on('component:add component:remove component:update component:styleUpdate style:change', () => {
                const updatedHTMLContent = getFullHtml(editorRef.current, currentHTMLContentRef.current);
                if (updatedHTMLContent !== currentHTMLContentRef.current) {
                    debouncedHandleSave(updatedHTMLContent);
                }
            });
        }
    }, [grapesjs, debouncedHandleSave, KB, dragMode, uploadFileAPI, uploadHTMLContent]);

    useEffect(() => {
        initializeEditor(currentHTMLContentRef.current);
    }, [grapesjs, initializeEditor]);

    useEffect(() => {
        if (isLocalContentUpdate.current) {
            isLocalContentUpdate.current = false;
            return;
        }

        if (editorRef.current && htmlContent !== currentHTMLContentRef.current) {
            currentHTMLContentRef.current = htmlContent;
            initializeEditor(htmlContent);
        }
    }, [htmlContent, initializeEditor]);

    const handleDragModeToggle = () => {
        setDragMode(prevMode => (prevMode === 'translate' ? 'disabled' : 'translate'));
    };

    const handlePreviewClick = async () => {
        setBlockingLoading({text: "Upload Webpage"})
        await save(currentHTMLContentRef.current)
        const url = getBaseURL(KB) + generateFilename(currentHTMLContentRef.current);
        window.open(url, '_blank');
        setBlockingLoading(false)
    };

    const loaderStyle = { position: 'absolute', top: 14, left: 0, right: 0, height: 2, zIndex: 1000 };
    return (
        <>
            <div style={{ position: 'relative', height: 0, overflow: 'visible' }}>
                {isSaving && (<LinearProgress style={loaderStyle} />)}
            </div>
            {grapesjs && (
                <>
                    <Tooltip title={dragMode === 'translate' ? 'Disable drag mode' : 'Enable drag mode'} placement={'top'}>
                        <IconButton
                            onClick={handleDragModeToggle}
                            aria-label="drag mode"
                            style={{ position: 'absolute', top: 38, left: 42, zIndex: 100, color: dragMode === 'translate' ? '#D97AA6' : '#B9A5A6' }}
                        >
                            <OpenWith style={{ fontSize: 24 }} />
                        </IconButton>
                    </Tooltip>
                    <Tooltip title="Open Live Page" placement={'top'}>
                        <IconButton
                            onClick={handlePreviewClick}
                            aria-label="preview mode"
                            style={{ position: 'absolute', top: 38, left: 10, zIndex: 100, color: '#B9A5A6' }}
                        >
                            <Preview style={{ fontSize: 24 }} />
                        </IconButton>
                    </Tooltip>
                </>
            )}
            <div ref={editorContainerRef} style={{ paddingTop: 14 }}></div>
        </>
    );
};

const onRenderChatMessage = async (params) => {
    const { content } = params.messages[params.msgIndex];
    if (isContentHTML(content)) {
        const html = extractHTMLContent(content) || content;
        return <GrapesJSEditor htmlContent={html} params={params} />;
    }

    return null;
};

const Header = ({ setRenderSettings }) => {
    useEffect(() => {
        window.openkbs.disableDropzone = []
        setRenderSettings({
            setMessageWidth: (content) => isContentHTML(content) ? '90%' : undefined,
            enableGenerationModelsSelect: true,
            disableShareButton: true,
            disableBalanceView: true,
            disableSentLabel: false,
            disableChatAvatar: false,
            disableChatModelsSelect: false,
            disableContextItems: true,
            disableCopyButton: false,
            disableEmojiButton: false,
            disableTextToSpeechButton: false,
            disableMobileLeftButton: false,
        });
    }, [setRenderSettings]);

    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)",
    "grapesjs": "^0.21.13",
    "grapesjs-preset-webpage": "^1.0.3",
    "grapesjs-blocks-basic": "^1.0.2"
  }
}

Frontend/utils.js

export const getFullHtml = (editor, originalHtml) => {
    const parser = new DOMParser();

    // Parse the original HTML
    const dom = parser.parseFromString(originalHtml, 'text/html');

    // Get the head and body elements
    const head = dom.head;
    const body = dom.body;

    // Remove existing <style> tags from the head
    const styleElements = head.querySelectorAll('style');
    styleElements.forEach(el => el.parentNode.removeChild(el));

    // Ensure UTF-8 charset is set
    let charsetMeta = head.querySelector('meta[charset]');
    if (!charsetMeta) {
        charsetMeta = dom.createElement('meta');
        charsetMeta.setAttribute('charset', 'UTF-8');
        head.insertBefore(charsetMeta, head.firstChild);
    } else {
        charsetMeta.setAttribute('charset', 'UTF-8');
    }

    // Get updated content from the editor
    const updatedHtmlContent = editor.getHtml();
    const updatedCssContent = editor.getCss();
    const updatedJsContent = editor.getJs();

    // Parse updatedHtmlContent to ensure we don't include extra <html>, <head>, <body> tags
    const updatedDom = parser.parseFromString(updatedHtmlContent, 'text/html');

    // Replace the body content with updated content
    body.innerHTML = updatedDom.body.innerHTML;

    // Append updated CSS in a <style> tag in the head
    if (updatedCssContent.trim()) {
        const styleEl = dom.createElement('style');
        styleEl.textContent = updatedCssContent;
        head.appendChild(styleEl);
    }

    // Append updated JS in a <script> tag in the body
    if (updatedJsContent.trim()) {
        const scriptEl = dom.createElement('script');
        scriptEl.textContent = updatedJsContent;
        body.appendChild(scriptEl);
    }

    // Serialize the DOM back to HTML using outerHTML to prevent character encoding issues
    const doctype = '<!DOCTYPE html>\n';
    const fullHtml = doctype + dom.documentElement.outerHTML;

    // Return the formatted HTML code block
    return '```html\n' + fullHtml.trim() + '\n```';
};

export function getBaseURL(KB) {
    return `https://web.openkbs.com/${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');
}