@ -0,0 +1,75 @@ |
|||
# Generative AI for Odoo 18 |
|||
|
|||
[](https://www.odoo.com) |
|||
[](https://opensource.org/licenses/MIT) |
|||
|
|||
## Overview |
|||
|
|||
The Generative AI for Odoo 18 module allows users to generate website snippets using AI-powered text prompts directly within the Odoo Website Editor. It seamlessly integrates with OpenAI and OpenRouter to enhance content creation and streamline web development workflows. |
|||
|
|||
## Features |
|||
|
|||
- 💾 Instantly generate custom website snippets from text prompts using OpenAI or OpenRouter. |
|||
- 🧠 Create, preview, and insert AI-generated snippets directly within Odoo’s website editor. |
|||
- ⚙️ Easily configure AI provider, model, and token limits from Odoo settings for full control. |
|||
|
|||
## Screenshots |
|||
|
|||
Here are some glimpses of Generative AI |
|||
|
|||
### User Interface of Website module |
|||
|
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/Screenshot1.png" alt="Screenshot 1" width="500" style="border: none;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/Screenshot1.png" alt="Screenshot 2" width="500" style="border: none;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/Screenshot1.png" alt="Screenshot 3" width="500" style="border: none;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/Screenshot1.png" alt="Screenshot 4" width="500" style="border: none;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/Screenshot1.png" alt="Screenshot 5" width="500" style="border: none;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/Screenshot1.png" alt="Screenshot 6" width="500" style="border: none;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
|
|||
|
|||
|
|||
## Prerequisites |
|||
|
|||
Before you begin, ensure you have the following installed: |
|||
|
|||
- An active Odoo Community/Enterprise Edition instance (local or hosted) |
|||
|
|||
## Configuration |
|||
- Ensure the OpenAI Python package is installed. |
|||
|
|||
@ -0,0 +1,23 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
|||
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from . import controllers |
|||
from . import models |
|||
@ -0,0 +1,64 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
|||
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
{ |
|||
'name': 'Generative AI In Snippet Creation', |
|||
'version': '18.0.1.0.0', |
|||
'category': 'Generative AI/Generative AI', |
|||
'summary': 'To integrate AI tools in website snippet creation.', |
|||
'description': """This module contains details about Generative AI in website.""", |
|||
'author': 'Cybrosys Techno Solutions', |
|||
'company': 'Cybrosys Techno Solutions', |
|||
'maintainer': 'Cybrosys Techno Solutions', |
|||
'website': 'https://www.cybrosys.com', |
|||
'depends': [ |
|||
'base', 'website', 'web', 'web_editor' |
|||
], |
|||
'external_dependencies': { |
|||
'python': [ |
|||
'openai', |
|||
], |
|||
}, |
|||
'data': [ |
|||
'security/ir.model.access.csv', |
|||
'data/test_data.xml', |
|||
'views/res_config_settings_views.xml', |
|||
'views/snippets/s_snippet_group_with_ai_content.xml', |
|||
'views/snippets.xml', |
|||
], |
|||
'assets': { |
|||
'web_editor.assets_wysiwyg': [ |
|||
'generative_ai/static/src/xml/website_dialogue_box.xml', |
|||
'generative_ai/static/src/js/ai_button_action.js', |
|||
('include', 'web._assets_helpers'), |
|||
'generative_ai/static/src/js/title.js', |
|||
], |
|||
'web.assets_backend': [ |
|||
'generative_ai/static/src/img/placeholder.png', |
|||
'generative_ai/static/src/css/snippets.css', |
|||
], |
|||
}, |
|||
'images': ['static/description/banner.jpg'], |
|||
'license': 'AGPL-3', |
|||
'installable': True, |
|||
'auto_install': False, |
|||
'application': True, |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
|||
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from . import main |
|||
@ -0,0 +1,400 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
|||
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
import hashlib |
|||
import re |
|||
import requests |
|||
from markupsafe import Markup |
|||
from openai import OpenAI |
|||
from odoo import http |
|||
from odoo.http import request |
|||
|
|||
|
|||
def preprocess_ai_output(text): |
|||
""" |
|||
Preprocesses AI-generated text by carefully extracting and closing CSS rules. |
|||
Args: |
|||
text (str): Input text containing HTML and CSS |
|||
Returns: |
|||
str: Preprocessed text with properly closed CSS |
|||
""" |
|||
text = re.sub(r'^```(html|css|plaintext)?\s*', '', text.strip(), flags=re.MULTILINE) |
|||
text = re.sub(r'\s*```$', '', text, flags=re.MULTILINE) |
|||
# Check if style tags already exist |
|||
style_match = re.search(r'<style>(.*?)</style>', text, re.DOTALL) |
|||
if style_match: |
|||
return text |
|||
css_lines = [] |
|||
html_lines = [] |
|||
current_rule = [] |
|||
in_css_block = False |
|||
for line in text.split('\n'): |
|||
stripped_line = line.strip() |
|||
if re.match(r'^(\.|#|[a-zA-Z])[\w\s,#>:.()-]+\s*{', stripped_line): |
|||
in_css_block = True |
|||
current_rule = [line] |
|||
elif in_css_block: |
|||
current_rule.append(line) |
|||
if stripped_line.endswith('}'): |
|||
css_lines.extend(current_rule) |
|||
current_rule = [] |
|||
in_css_block = False |
|||
else: |
|||
if stripped_line: |
|||
html_lines.append(line) |
|||
if current_rule: |
|||
if not current_rule[-1].strip().endswith('}'): |
|||
current_rule[-1] = current_rule[-1] + '\n}' |
|||
css_lines.extend(current_rule) |
|||
if css_lines: |
|||
css_output = "<style>\n" + "\n".join(css_lines) + "\n</style>" |
|||
else: |
|||
css_output = "" |
|||
html_output = "\n".join(html_lines) |
|||
final_output = f"{css_output}\n{html_output}" if css_output else html_output |
|||
return final_output.strip() |
|||
|
|||
|
|||
class SnippetGenerator(http.Controller): |
|||
|
|||
|
|||
def _generate_unique_id(self, snippet_name): |
|||
"""Generate a unique ID for the snippet to avoid CSS conflicts""" |
|||
return hashlib.md5(f"{snippet_name}_{request.session.sid}".encode()).hexdigest()[ |
|||
:8] |
|||
|
|||
def _generate_openai_response(self, prompt, api_key, model_id, max_tokens, |
|||
snippet_name): |
|||
"""Helper method for OpenAI API calls""" |
|||
response = OpenAI(api_key=api_key).chat.completions.create( |
|||
model=model_id, |
|||
messages=[ |
|||
{"role": "system", |
|||
"content": "You are a helpful assistant that generates website snippets similar to Odoo's inbuilt website snippets. " |
|||
"Create responsive snippets using Bootstrap classes, ensuring proper container and row/column structure. " |
|||
"Use placeholder images from 'https://via.placeholder.com/' with appropriate sizes. " |
|||
"Return only clean HTML and CSS without any wrapper divs or preview elements. " |
|||
"Do not include any code block markers (```). " |
|||
"Focus on creating functional, beautiful snippets that work well in Odoo's website builder. " |
|||
"Use modern design principles with good typography, spacing, and colors." |
|||
}, |
|||
{"role": "user", "content": prompt} |
|||
], |
|||
max_tokens=int(max_tokens), |
|||
temperature=0.7 |
|||
) |
|||
return response.choices[0].message.content.strip() |
|||
|
|||
def _generate_openrouter_response(self, prompt, api_key, model_id, max_tokens, |
|||
snippet_name): |
|||
"""Helper method for OpenRouter API calls with improved error handling""" |
|||
url = "https://openrouter.ai/api/v1/chat/completions" |
|||
headers = { |
|||
"Authorization": f"Bearer {api_key}", |
|||
"HTTP-Referer": request.env['ir.config_parameter'].sudo().get_param( |
|||
'web.base.url', ''), |
|||
"X-Title": "Odoo Integration" |
|||
} |
|||
payload = { |
|||
"model": model_id, |
|||
"messages": [ |
|||
{"role": "system", |
|||
"content": "You are a helpful assistant that generates website snippets similar to Odoo's inbuilt website snippets. " |
|||
"Create responsive snippets using Bootstrap classes, ensuring proper container and row/column structure. " |
|||
"Use placeholder images from 'https://via.placeholder.com/' with appropriate sizes. " |
|||
"Return only clean HTML and CSS without any wrapper divs or preview elements. " |
|||
"Do not include any code block markers (```). " |
|||
"Focus on creating functional, beautiful snippets that work well in Odoo's website builder. " |
|||
"Use modern design principles with good typography, spacing, and colors." |
|||
}, |
|||
{"role": "user", "content": prompt} |
|||
], |
|||
"max_tokens": int(max_tokens), |
|||
"temperature": 0.7 |
|||
} |
|||
response = requests.post(url, headers=headers, json=payload) |
|||
if response.status_code != 200: |
|||
raise Exception( |
|||
f"OpenRouter API error: {response.status_code} - {response.text}") |
|||
json_response = response.json() |
|||
if 'choices' not in json_response or not json_response['choices']: |
|||
raise Exception("Invalid response format from OpenRouter API") |
|||
choice = json_response['choices'][0] |
|||
if 'message' not in choice: |
|||
raise Exception("No message in OpenRouter API response") |
|||
message = choice['message'] |
|||
content = message.get('content', '') |
|||
reasoning = message.get('reasoning', '') |
|||
partial_content = content.strip() if content else reasoning.strip() |
|||
if choice.get('finish_reason') == 'length' and partial_content: |
|||
completion_prompt = f""" |
|||
You previously started generating a website snippet but it was cut off. |
|||
Please complete the snippet based on this partial content: |
|||
{partial_content} |
|||
Continue from where it was cut off and make sure the HTML/CSS is complete and valid. |
|||
""" |
|||
completion_payload = { |
|||
"model": model_id, |
|||
"messages": [ |
|||
{"role": "system", |
|||
"content": "You are completing a partially generated website snippet. " |
|||
"Return only HTML/CSS code to complete the snippet, don't start over."}, |
|||
{"role": "user", "content": completion_prompt} |
|||
], |
|||
"max_tokens": int(max_tokens), |
|||
"temperature": 0.7 |
|||
} |
|||
try: |
|||
completion_response = requests.post(url, headers=headers, |
|||
json=completion_payload) |
|||
completion_response.raise_for_status() |
|||
completion_json = completion_response.json() |
|||
if 'choices' in completion_json and completion_json['choices']: |
|||
completion_message = completion_json['choices'][0]['message'] |
|||
completion_content = completion_message.get('content', '') |
|||
if completion_content: |
|||
cleaned_completion = re.sub(r"^```(html|css)?\s*", "", |
|||
completion_content.strip(), |
|||
flags=re.MULTILINE) |
|||
cleaned_completion = re.sub(r"\s*```$", "", cleaned_completion, |
|||
flags=re.MULTILINE) |
|||
combined_content = partial_content + "\n" + cleaned_completion |
|||
return combined_content |
|||
return partial_content |
|||
except Exception: |
|||
return partial_content + "\n<!-- Note: This snippet is incomplete. The system attempted to complete it but encountered an error. -->" |
|||
if content: |
|||
return content.strip() |
|||
elif reasoning: |
|||
return reasoning.strip() |
|||
else: |
|||
raise Exception( |
|||
"Empty response from AI model. Please try again with a " |
|||
"different prompt or model.") |
|||
|
|||
@http.route('/website/generate_snippet', type='json', auth='user', website=True) |
|||
def generate_snippet(self, **kwargs): |
|||
"""Single route to handle both OpenAI and OpenRouter snippet generation""" |
|||
prompt = kwargs.get('prompt') |
|||
snippet_name = kwargs.get('name') |
|||
if not prompt: |
|||
return {'error': "Prompt is required"} |
|||
try: |
|||
api_key = request.env['ir.config_parameter'].sudo().get_param('api_key') |
|||
ai_system = request.env['ir.config_parameter'].sudo().get_param( |
|||
'generative_ai_systems') |
|||
max_tokens = request.env['ir.config_parameter'].sudo().get_param('max_token') |
|||
if not api_key: |
|||
return {'error': "API key not configured"} |
|||
unique_id = self._generate_unique_id(snippet_name) |
|||
try: |
|||
if ai_system == 'openai': |
|||
model_id = request.env['ir.config_parameter'].sudo().get_param( |
|||
'model_id') |
|||
if not model_id: |
|||
return {'error': "No OpenAI model selected"} |
|||
snippet_text = self._generate_openai_response(prompt, api_key, |
|||
model_id, max_tokens, |
|||
snippet_name) |
|||
elif ai_system == 'openrouter': |
|||
model_id = request.env['ir.config_parameter'].sudo().get_param( |
|||
'openrouter_model_id') |
|||
if not model_id or model_id == 'none': |
|||
return {'error': "No OpenRouter model selected"} |
|||
snippet_text = self._generate_openrouter_response(prompt, api_key, |
|||
model_id, |
|||
max_tokens, |
|||
snippet_name) |
|||
else: |
|||
return {'error': "Invalid AI system selected"} |
|||
except Exception as api_error: |
|||
return {'error': f"API Error: {str(api_error)}", 'retry': True} |
|||
# Check if we got a valid response |
|||
if not snippet_text or snippet_text.strip() == "": |
|||
return { |
|||
'error': "AI did not generate any content. Please try again with a different prompt or model.", |
|||
'retry': True |
|||
} |
|||
processed_snippet_text = preprocess_ai_output(snippet_text) |
|||
preview_style = f""" |
|||
<style> |
|||
.snippet-preview-{unique_id} {{ |
|||
position: relative !important; |
|||
min-width: 600px !important; |
|||
min-height: 400px !important; |
|||
width: 100% !important; |
|||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%) !important; |
|||
border-radius: 12px !important; |
|||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1) !important; |
|||
overflow: visible !important; |
|||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; |
|||
display: flex !important; |
|||
flex-direction: column !important; |
|||
align-items: center !important; |
|||
justify-content: center !important; |
|||
padding: 20px !important; |
|||
box-sizing: border-box !important; |
|||
cursor: pointer !important; |
|||
}} |
|||
|
|||
.snippet-preview-{unique_id}:hover {{ |
|||
transform: translateY(-5px) !important; |
|||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15) !important; |
|||
}} |
|||
|
|||
.snippet-preview-{unique_id}::before {{ |
|||
content: '' !important; |
|||
position: absolute !important; |
|||
top: 0 !important; |
|||
left: 0 !important; |
|||
right: 0 !important; |
|||
height: 4px !important; |
|||
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%) !important; |
|||
z-index: 1 !important; |
|||
}} |
|||
|
|||
.snippet-name-{unique_id} {{ |
|||
position: absolute !important; |
|||
top: -10px !important; |
|||
left: 50% !important; |
|||
transform: translateX(-50%) !important; |
|||
background: rgba(255, 255, 255, 0.95) !important; |
|||
backdrop-filter: blur(10px) !important; |
|||
-webkit-backdrop-filter: blur(10px) !important; |
|||
color: #2d3748 !important; |
|||
padding: 12px 20px !important; |
|||
border-radius: 25px !important; |
|||
font-size: 16px !important; |
|||
font-weight: 600 !important; |
|||
letter-spacing: 0.5px !important; |
|||
opacity: 0 !important; |
|||
visibility: hidden !important; |
|||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; |
|||
z-index: 1000 !important; |
|||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15) !important; |
|||
border: 1px solid rgba(255, 255, 255, 0.3) !important; |
|||
white-space: nowrap !important; |
|||
pointer-events: none !important; |
|||
min-width: 120px !important; |
|||
text-align: center !important; |
|||
}} |
|||
|
|||
.snippet-preview-{unique_id}:hover .snippet-name-{unique_id} {{ |
|||
opacity: 1 !important; |
|||
visibility: visible !important; |
|||
transform: translateX(-50%) translateY(-8px) !important; |
|||
pointer-events: auto !important; |
|||
}} |
|||
|
|||
/* Alternative hover trigger - also trigger on any child hover */ |
|||
.snippet-preview-{unique_id} *:hover ~ .snippet-name-{unique_id}, |
|||
.snippet-preview-{unique_id} .snippet-content-{unique_id}:hover ~ .snippet-name-{unique_id} {{ |
|||
opacity: 1 !important; |
|||
visibility: visible !important; |
|||
transform: translateX(-50%) translateY(-8px) !important; |
|||
}} |
|||
|
|||
.snippet-content-{unique_id} {{ |
|||
width: 100% !important; |
|||
max-width: 100% !important; |
|||
z-index: 2 !important; |
|||
position: relative !important; |
|||
background: white !important; |
|||
border-radius: 8px !important; |
|||
padding: 20px !important; |
|||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08) !important; |
|||
margin-top: 30px !important; |
|||
}} |
|||
|
|||
/* Ensure content doesn't overflow */ |
|||
.snippet-content-{unique_id} * {{ |
|||
max-width: 100% !important; |
|||
box-sizing: border-box !important; |
|||
}} |
|||
|
|||
/* Style for cards and common elements */ |
|||
.snippet-content-{unique_id} .card {{ |
|||
border: none !important; |
|||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1) !important; |
|||
border-radius: 8px !important; |
|||
transition: transform 0.2s ease !important; |
|||
}} |
|||
|
|||
.snippet-content-{unique_id} .card:hover {{ |
|||
transform: translateY(-2px) !important; |
|||
}} |
|||
|
|||
.snippet-content-{unique_id} .btn {{ |
|||
border-radius: 6px !important; |
|||
font-weight: 500 !important; |
|||
transition: all 0.2s ease !important; |
|||
}} |
|||
|
|||
.snippet-content-{unique_id} .btn-primary {{ |
|||
background: linear-gradient(45deg, #667eea, #764ba2) !important; |
|||
border: none !important; |
|||
}} |
|||
|
|||
.snippet-content-{unique_id} .btn-primary:hover {{ |
|||
background: linear-gradient(45deg, #5a67d8, #6b46c1) !important; |
|||
transform: translateY(-1px) !important; |
|||
}} |
|||
|
|||
/* Fallback JavaScript-free hover detection */ |
|||
.snippet-preview-{unique_id}:focus-within .snippet-name-{unique_id} {{ |
|||
opacity: 1 !important; |
|||
visibility: visible !important; |
|||
transform: translateX(-50%) translateY(-8px) !important; |
|||
}} |
|||
</style>""" |
|||
|
|||
# Wrap content with unique scoped classes |
|||
wrapped_content = f""" |
|||
{preview_style} |
|||
<div class="snippet-preview-{unique_id}" title="{snippet_name}"> |
|||
<div class="snippet-name-{unique_id}">{snippet_name}</div> |
|||
<div class="snippet-content-{unique_id}"> |
|||
{processed_snippet_text} |
|||
</div> |
|||
</div> |
|||
""" |
|||
if not wrapped_content or wrapped_content.strip() == "": |
|||
return { |
|||
'error': "Could not process AI output into a valid snippet. Please try again.", |
|||
'retry': True |
|||
} |
|||
snippet = request.env['website.snippet.data'].sudo().create({ |
|||
'name': snippet_name, |
|||
'content': Markup(wrapped_content), |
|||
'image_url': '/generative_ai/static/src/img/placeholder.png', |
|||
'is_ai_generated': True, |
|||
}) |
|||
request.env['ir.qweb'].clear_caches() |
|||
request.env['ir.ui.view'].clear_caches() |
|||
return { |
|||
'success': True, |
|||
'snippet': snippet, |
|||
'snippet_id': snippet.id, |
|||
'content': snippet.content, |
|||
'image_url': '/generative_ai/static/src/img/placeholder.png' |
|||
} |
|||
except Exception as e: |
|||
return {'error': str(e), 'retry': True} |
|||
@ -0,0 +1,17 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<odoo noupdate="1"> |
|||
<data> |
|||
<record id="default_ai_snippet" model="website.snippet.data"> |
|||
<field name="name">Default AI Snippet</field> |
|||
<field name="content"><![CDATA[ |
|||
<div class="default-ai-snippet" style="background-color: #f4f4f4; padding: 20px; text-align: center; border-radius: 10px;"> |
|||
<h1 style="color: #007bff;">Welcome to AI Generated Snippets</h1> |
|||
<img src="generative_ai/static/src/img/placeholder.png" alt="AI Snippet Image" style="width: 350px; height: auto; margin: 10px auto; display: block;"> |
|||
<p style="color: #333; font-size: 45px;">This is a default snippet that showcases AI capabilities.</p> |
|||
</div> |
|||
]]></field> |
|||
<field name="image_url">generative_ai/static/src/img/placeholder.png</field> |
|||
<field name="is_ai_generated">True</field> |
|||
</record> |
|||
</data> |
|||
</odoo> |
|||
@ -0,0 +1,6 @@ |
|||
## Module <generative_ai> |
|||
|
|||
#### 21.07.2025 |
|||
#### Version 18.0.1.0.0 |
|||
##### ADD |
|||
- Initial commit for Generative AI. |
|||
@ -0,0 +1,23 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
|||
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from . import res_config_settings |
|||
from . import website_snippet_data |
|||
@ -0,0 +1,141 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
|||
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
import requests |
|||
from odoo import api, fields, models |
|||
|
|||
|
|||
class ResConfigSettings(models.TransientModel): |
|||
_inherit = 'res.config.settings' |
|||
|
|||
generative_ai_systems = fields.Selection([ |
|||
('openai', 'Open AI'), |
|||
('openrouter', 'OpenRouter') |
|||
], string='Generative AI Systems') |
|||
api_key = fields.Char('API Key', copy=False) |
|||
max_token = fields.Char(string='Maximum tokens', |
|||
default=4096, config_parameter='generative_ai.max_token') |
|||
model_id = fields.Selection(selection=[ |
|||
('gpt-3.5-turbo', 'gpt-3.5-turbo'), |
|||
('gpt-4-turbo', 'gpt-4-turbo'), |
|||
('gpt-4o', 'gpt-4o'), |
|||
('gpt-4o-mini', 'gpt-4o-mini'), |
|||
('gpt-4-0613', 'gpt-4-0613'), |
|||
]) |
|||
openrouter_model_id = fields.Selection(selection='_get_model_selection', |
|||
string="AI Model") |
|||
|
|||
@api.model |
|||
def default_get(self, fields): |
|||
"""Override default_get to set a default value for 'max_token' field |
|||
when it is included in the fields to fetch.""" |
|||
res = super(ResConfigSettings, self).default_get(fields) |
|||
if 'max_token' in fields: |
|||
res['max_token'] = "4096" |
|||
return res |
|||
|
|||
@api.model |
|||
def get_values(self): |
|||
"""Override get_values to load saved configuration parameters from the system settings. |
|||
These values are shown on the settings UI.""" |
|||
res = super(ResConfigSettings, self).get_values() |
|||
res['generative_ai_systems'] = self.env['ir.config_parameter'].sudo().get_param( |
|||
'generative_ai_systems') |
|||
res['api_key'] = self.env['ir.config_parameter'].sudo().get_param('api_key') |
|||
res['model_id'] = self.env['ir.config_parameter'].sudo().get_param('model_id') |
|||
res['openrouter_model_id'] = self.env['ir.config_parameter'].sudo().get_param( |
|||
'openrouter_model_id') |
|||
res['max_token'] = self.env['ir.config_parameter'].sudo().get_param('max_token') |
|||
return res |
|||
|
|||
@api.onchange('generative_ai_systems') |
|||
def _onchange_generative_ai_systems(self): |
|||
"""Clear model selection when system or API key changes""" |
|||
if not self.generative_ai_systems or not self.api_key: |
|||
self.model_id = False |
|||
self.openrouter_model_id = False |
|||
|
|||
def _get_model_selection(self): |
|||
"""Return list of tuples for model selection""" |
|||
try: |
|||
config_system = self.env['ir.config_parameter'].sudo().get_param( |
|||
'generative_ai_systems') |
|||
config_key = self.env['ir.config_parameter'].sudo().get_param('api_key') |
|||
ai_system = self.generative_ai_systems or config_system |
|||
api_key = self.api_key or config_key |
|||
if ai_system and api_key: |
|||
if ai_system == 'openrouter': |
|||
url = "https://openrouter.ai/api/v1/models" |
|||
# Required headers for OpenRouter API |
|||
headers = { |
|||
'Authorization': f'Bearer {api_key}', |
|||
'HTTP-Referer': self.env['ir.config_parameter'].sudo().get_param( |
|||
'web.base.url', ''), |
|||
'X-Title': 'Odoo Integration' |
|||
} |
|||
response = requests.get(url, headers=headers, timeout=10) |
|||
if response.status_code == 200: |
|||
models = response.json().get('data', |
|||
[]) # OpenRouter returns models in 'data' field |
|||
if models: |
|||
model_list = [(str(model['id']), str(model['name'])) for model |
|||
in models] |
|||
return model_list |
|||
else: |
|||
return [('none', 'No Models Available')] |
|||
else: |
|||
return [('none', f'API Error: {response.status_code}')] |
|||
except requests.exceptions.RequestException: |
|||
return [('none', 'Connection Error')] |
|||
except Exception: |
|||
return [('none', 'Error Loading Models')] |
|||
return [('none', 'No Models Available')] |
|||
|
|||
def set_values(self): |
|||
"""Save settings and fetch models""" |
|||
super(ResConfigSettings, self).set_values() |
|||
self.env['ir.config_parameter'].sudo().set_param('generative_ai_systems', |
|||
self.generative_ai_systems) |
|||
self.env['ir.config_parameter'].sudo().set_param('api_key', self.api_key) |
|||
self.env['ir.config_parameter'].sudo().set_param('max_token', self.max_token) |
|||
if self.generative_ai_systems == 'openrouter': |
|||
self.env['ir.config_parameter'].sudo().set_param('openrouter_model_id', |
|||
self.openrouter_model_id) |
|||
self.env['ir.config_parameter'].sudo().set_param('model_id', |
|||
'') |
|||
else: |
|||
self.env['ir.config_parameter'].sudo().set_param('model_id', self.model_id) |
|||
if self.generative_ai_systems and self.api_key: |
|||
models = self._get_model_selection() |
|||
if models and models != [('none', 'No Models Available')]: |
|||
# Only save model_id if it's in the list of available models |
|||
if self.openrouter_model_id and any( |
|||
self.openrouter_model_id == model[0] for model in models): |
|||
self.env['ir.config_parameter'].sudo().set_param( |
|||
'openrouter_model_id', self.openrouter_model_id) |
|||
else: |
|||
# If current model_id is not in available models, set to first available model |
|||
self.env['ir.config_parameter'].sudo().set_param( |
|||
'openrouter_model_id', models[0][0]) |
|||
self.openrouter_model_id = models[0][0] |
|||
else: |
|||
self.env['ir.config_parameter'].sudo().set_param('openrouter_model_id', '') |
|||
self.openrouter_model_id = False |
|||
@ -0,0 +1,37 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################# |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
|||
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>) |
|||
# |
|||
# You can modify it under the terms of the GNU AFFERO |
|||
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
|||
# (AGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################# |
|||
from markupsafe import Markup |
|||
from odoo import fields, models |
|||
|
|||
|
|||
class WebsiteSnippetData(models.Model): |
|||
_name = 'website.snippet.data' |
|||
_description = 'AI Generated Website Snippet' |
|||
|
|||
name = fields.Char(string="Snippet Name", required=True) |
|||
content = fields.Text(string="Snippet Content", required=True) |
|||
image_url = fields.Char(string="Image URL") |
|||
create_date = fields.Datetime(string="Created Date", readonly=True) |
|||
is_ai_generated = fields.Boolean(string="AI Generated") |
|||
|
|||
def get_content(self): |
|||
return Markup(self.content) |
|||
|
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 628 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 495 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 624 B |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 310 B |
|
After Width: | Height: | Size: 929 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 542 B |
|
After Width: | Height: | Size: 576 B |
|
After Width: | Height: | Size: 733 B |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 383 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 911 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 600 B |
|
After Width: | Height: | Size: 673 B |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 926 B |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 878 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 653 B |
|
After Width: | Height: | Size: 800 B |
|
After Width: | Height: | Size: 905 B |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 839 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 427 B |
|
After Width: | Height: | Size: 627 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 988 B |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 875 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 544 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 486 KiB |
|
After Width: | Height: | Size: 529 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 880 KiB |
|
After Width: | Height: | Size: 728 KiB |
|
After Width: | Height: | Size: 372 KiB |
|
After Width: | Height: | Size: 44 KiB |
@ -0,0 +1,857 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8"/> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
|||
<title>Generative AI</title> |
|||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" |
|||
rel="stylesheet"/> |
|||
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap" |
|||
rel="stylesheet"> |
|||
<link rel="stylesheet" |
|||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"/> |
|||
<link rel="stylesheet" |
|||
href="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/assets/owl.carousel.min.css"/> |
|||
<style> |
|||
:root { |
|||
--primary-color: #7f54b3; |
|||
--bg-white: #fff; |
|||
--text-color: #121212; |
|||
--text-color-light: #64728f; |
|||
} |
|||
|
|||
body { |
|||
font-family: "Montserrat", sans-serif; |
|||
} |
|||
|
|||
.nav-tabs .nav-item.show .nav-link, .nav-tabs .nav-link.active { |
|||
color: #121212; |
|||
font-family: Montserrat; |
|||
font-size: 16px !important; |
|||
font-weight: 500 !important; |
|||
border-radius: 30px; |
|||
line-height: normal; |
|||
text-transform: capitalize; |
|||
background-color: #F5F5F5; |
|||
border: none; |
|||
margin-bottom: 0; |
|||
padding: 12px 24px; |
|||
} |
|||
|
|||
.nav-tabs .nav-link:focus, .nav-tabs .nav-link:hover { |
|||
border-color: transparent; |
|||
isolation: isolate; |
|||
} |
|||
|
|||
.nav-tabs .nav-link:focus-visible { |
|||
border-color: transparent; |
|||
box-shadow: none; |
|||
} |
|||
|
|||
/* owl-carosel */ |
|||
.owl-carousel .owl-nav { |
|||
position: absolute; |
|||
top: 42%; |
|||
width: 100%; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
transform: translateY(-42%); |
|||
} |
|||
|
|||
.owl-carousel .owl-nav button.owl-prev { |
|||
position: absolute; |
|||
right: -36px; |
|||
font-size: 28px; |
|||
background-color: #e4e4e4; |
|||
border-radius: 20px; |
|||
width: 40px; |
|||
height: 40px; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
|||
|
|||
.owl-carousel .owl-nav button.owl-next { |
|||
position: absolute; |
|||
left: -36px; |
|||
font-size: 28px; |
|||
background-color: #e4e4e4; |
|||
border-radius: 20px; |
|||
width: 40px; |
|||
height: 40px; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
|
|||
} |
|||
|
|||
</style> |
|||
</head> |
|||
<body> |
|||
<!-- overview --> |
|||
<div class="container"> |
|||
<div class="my-5"> |
|||
<!-- button tab --> |
|||
<!-- --> |
|||
<!-- version support --> |
|||
<div class="my-3 d-flex align-items-center justify-content-end"> |
|||
<div class="text-center" |
|||
style="background-color:#017E84 !important; font-size:0.8rem !important; color:#fff !important; font-weight:500 !important; padding:4px !important; margin:0 3px !important; border-radius:50px !important; min-width:120px !important"> |
|||
Community |
|||
</div> |
|||
<div class="text-center" |
|||
style="background-color:#875A7B !important; color:#fff !important; font-size:0.8rem !important; font-weight:500 !important; padding:4px !important; margin:0 3px !important; border-radius:50px !important; min-width:120px !important"> |
|||
Enterprise |
|||
</div> |
|||
<div class="text-center" |
|||
style="background-color:#7C7BAD !important; color:#fff !important; font-size:0.8rem !important; font-weight:500 !important; padding:4px !important; margin:0 3px !important; border-radius:50px !important; min-width:120px !important"> |
|||
Odoo.sh |
|||
</div> |
|||
</div> |
|||
<div class="tab-content" id="myTabContent"> |
|||
<!-- description --> |
|||
<div class="tab-pane fade show active" id="home" role="tabpanel" |
|||
aria-labelledby="home-tab"> |
|||
<div class="position-relative" |
|||
style="border-radius: 16px; background: #f8f8f8; padding: 20px 0;"> |
|||
<div class="row " style=" |
|||
padding: 2rem 0rem 0 !important; |
|||
"> |
|||
<div class="col-lg-8 mx-auto gap-4 d-flex flex-column align-items-center"> |
|||
<p class="my-1 text-center text-uppercase" |
|||
style=" |
|||
letter-spacing: 4px !important; |
|||
color: #7f54b3; |
|||
font-weight: bold; |
|||
text-align: center; |
|||
font-size: 14px; |
|||
font-weight: 600; |
|||
line-height: 15.96px; |
|||
text-transform: uppercase; |
|||
"> |
|||
Generates responsive website snippets from text prompts using AI. |
|||
</p> |
|||
<h1 class="text-center text-uppercase my-0" |
|||
style=" |
|||
color: #121212; |
|||
font-size: 46px; |
|||
font-weight: 700; |
|||
line-height: normal; |
|||
">Generative AI</span> |
|||
</h1> |
|||
</div> |
|||
<div class="col-lg-12 d-flex justify-content-center align-items-center" |
|||
style="margin: 3rem 0;"> |
|||
<img src="./assets/icons/brand-pair.svg" |
|||
width="100%" |
|||
height="auto" |
|||
style="width: 50%" |
|||
class="img-responsive"/> |
|||
</div> |
|||
<div class="col-md-12 text-center"> |
|||
<a href="mailto:odoo@cybrosys.com" |
|||
target="_blank" |
|||
style="background-color: transparent;border-radius: 35px; |
|||
font-family: Montserrat; |
|||
display: inline-block; |
|||
padding: 7px 33px; |
|||
border: 1px solid #7f54b3; |
|||
color: #7f54b3; |
|||
text-decoration: none; |
|||
" |
|||
class="mx-1 mb-2 deep-1 deep_hover"> |
|||
<img class="img" |
|||
style="width: 24px" |
|||
src="./assets/icons/mail.svg"/> |
|||
<span |
|||
class="pl-2" |
|||
style=" font-size: 16px; vertical-align: middle" |
|||
>Email Us</span |
|||
> |
|||
</a> |
|||
<a href="skype:cybroopenerp?chat" |
|||
target="_blank" |
|||
style=" |
|||
background-color: #7f289b; |
|||
font-family: Montserrat; |
|||
display: inline-block; |
|||
padding: 7px 33px; |
|||
border: 1px solid #7f289b; |
|||
border-radius: 35px; |
|||
text-decoration: none; |
|||
" |
|||
class="mx-1 mb-2 deep-1 deep_hover"> |
|||
<img |
|||
class="img" |
|||
style="width: 24px" |
|||
src="./assets/icons/skype-fill.svg" |
|||
/> |
|||
<span |
|||
class="pl-2" |
|||
style="color: #fff; font-size: 16px; vertical-align: middle" |
|||
>Skype Us</span |
|||
> |
|||
</a> |
|||
</div> |
|||
<!-- <div class="d-flex justify-content-center mt-2">--> |
|||
<!-- <img src="./assets/icons/hero.gif"--> |
|||
<!-- class="w-100"--> |
|||
<!-- style="z-index: 3; height: auto;">--> |
|||
<!-- </div>--> |
|||
</div> |
|||
<div class="position-absolute bottom-0" |
|||
style="z-index: 1; width: 100%;"> |
|||
<img src="./assets/icons/banner-bg.svg" |
|||
class="img-fluid w-100"> |
|||
</div> |
|||
<div class="position-absolute bottom-0 end-0" |
|||
style=" z-index: 2;"> |
|||
<img src="./assets/icons/patter.svg"> |
|||
</div> |
|||
</div> |
|||
<!-- key-highlight --> |
|||
<div class="" style="border-radius: 16px; |
|||
padding: 60px 40px; |
|||
border: 1px solid #EBEEF2; |
|||
background: #F5F5F7; |
|||
box-shadow: 0px 5px 20px -11px rgba(0, 0, 0, 0.25); "> |
|||
<div class="row"> |
|||
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center"> |
|||
<h2 style=" color: #121212; |
|||
text-align: center; |
|||
font-size: 40px; |
|||
font-weight: 700; |
|||
text-transform: uppercase; padding-bottom: 50px;">Key |
|||
Highlights</h2> |
|||
</div> |
|||
<div class="col-lg-4"> |
|||
<div class="mb-4 d-flex flex-column justify-content-center gap-3" |
|||
style="border-radius: 12px; border: 1px solid #B6BCCD; |
|||
background: #FFF;padding:32px "> |
|||
<div class="d-flex justify-content-center align-items-center" |
|||
style="background-color:#7847D9 !important; border-radius:8px !important; height:42px; width:42px"> |
|||
<img src="./assets/icons/feature-icon.svg" |
|||
class="img-responsive" height="26px" |
|||
width="26px"> |
|||
</div> |
|||
<h5 class="m-0" |
|||
style="color:#000 !important; font-weight:bold"> |
|||
Easy to set up. |
|||
</h5> |
|||
<p class="m-0" |
|||
style="font-size:0.9rem; color:#64728f; font-size: 16px; font-weight: 400;"> |
|||
</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-lg-4"> |
|||
<div class="mb-4 d-flex flex-column justify-content-center gap-3" |
|||
style="border-radius: 12px; |
|||
border: 1px solid #B6BCCD; |
|||
background: #FFF;padding:32px "> |
|||
<div class="d-flex justify-content-center align-items-center" |
|||
style="background-color:#7847D9 !important; border-radius:8px !important; height:42px; width:42px"> |
|||
<img src="./assets/icons/feature-icon.svg" |
|||
class="img-responsive" height="26px" |
|||
width="26px"> |
|||
</div> |
|||
<h5 class="m-0" |
|||
style="color:#000 !important; font-weight:bold"> |
|||
AI-powered snippet creation. |
|||
</h5> |
|||
<p class="m-0" |
|||
style="font-size:0.9rem; color:#64728f; font-size: 16px; font-weight: 400;"> |
|||
</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-lg-4"> |
|||
<div class="mb-4 d-flex flex-column justify-content-center gap-3" |
|||
style="border-radius: 12px; |
|||
border: 1px solid #B6BCCD; |
|||
background: #FFF;padding:32px "> |
|||
<div class="d-flex justify-content-center align-items-center" |
|||
style="background-color:#7847D9 !important; border-radius:8px !important; height:42px; width:42px"> |
|||
<img src="./assets/icons/feature-icon.svg" |
|||
class="img-responsive" height="26px" |
|||
width="26px"> |
|||
</div> |
|||
<h5 class="m-0" |
|||
style="color:#000 !important; font-weight:bold"> |
|||
Unique preview & styling. |
|||
</h5> |
|||
<p class="m-0" |
|||
style="font-size:0.9rem; color:#64728f; font-size: 16px; font-weight: 400;"> |
|||
|
|||
</p> |
|||
</div> |
|||
</div> |
|||
|
|||
</div> |
|||
</div> |
|||
<!--code --> |
|||
<div class="my-5"> |
|||
<div class="position-relative" style=" padding: 5rem 4rem 5rem 4rem; background-color: #0A1425; border-radius: 12px;"> |
|||
<div class="d-flex flex-column gap-4"> |
|||
<span class="wrapper-subtitle" |
|||
style="font-size: 40px; font-weight: 700; color: #fff;line-height: 60px; text-transform: capitalize; width: 450px; font-family: Montserrat;">Generative AI</span> |
|||
<h3 class="wrapper-details" |
|||
style="font-size: 20px; font-weight: 400; color: #fff; line-height: 32px; "> |
|||
Are you ready to make your business more |
|||
organized? |
|||
<br> Improve now! |
|||
</h3> |
|||
<div class="d-flex gap-3"> |
|||
<a href="mailto:odoo@cybrosys.com" |
|||
class="shop-btn" style="cursor: pointer; border-radius: 16px; display: flex; justify-content: center; align-items: center; gap: 7px; |
|||
border: 1px solid #ffffff33; |
|||
background-color: #ffffff14; |
|||
backdrop-filter: blur(10px); color: #fff; padding: 12px 16px 12px 16px; text-decoration: none;"> |
|||
<span style="border-radius: 12px; |
|||
background-color: #ffffff1a; |
|||
backdrop-filter: blur(6px);padding: 12px; "> |
|||
<img src="./assets/icons/banner-mail.svg"> |
|||
</span> |
|||
<span style="font-weight: 500;font-family: Montserrat;">odoo@cybrosys.com</span> |
|||
</a> |
|||
<a href="tel:+91 9074270811" class="shop-btn" |
|||
style="cursor: pointer; border-radius: 16px; display: flex; justify-content: center; align-items: center; gap: 7px; |
|||
border: 1px solid #ffffff33; |
|||
background-color: #ffffff14; |
|||
backdrop-filter: blur(10px); color: #fff; padding: 12px 22px 12px 18px; text-decoration: none;"> |
|||
<span style="border-radius: 12px; |
|||
background-color: #ffffff1a; |
|||
backdrop-filter: blur(6px);padding: 12px;"> |
|||
<img src="./assets/icons/banner-call.svg"> |
|||
</span> |
|||
<span style="font-weight: 500;font-family: Montserrat;">+91 9074270811</span> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
<div class="position-absolute bottom-0 end-0"> |
|||
<img src="./assets/icons/banner-pattern.svg" |
|||
style="width: 540px;"> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<!-- end-code --> |
|||
<!-- --> |
|||
<!-- screenshot and other --> |
|||
|
|||
<div class="mb-4 bg-white" |
|||
style=" border: 1px solid #EBEEF2; border-radius: 6px; box-shadow: 0px 8px 20px -4px rgba(0, 0, 0, 0.10); border: 1px solid #EBEEF2;"> |
|||
<div> |
|||
<ul class="nav nav-tabs justify-content-center bg-white py-2" |
|||
id="myTab" role="tablist" |
|||
style="border-radius: 6px 6px 0 0;"> |
|||
<li class="nav-item"> |
|||
<a aria-controls="overview" |
|||
aria-bs-selected="true" |
|||
class="nav-link active" data-bs-toggle="tab" |
|||
href="#overview" id="overview-tab" role="tab" |
|||
style="color:#121212; font-weight:500; font-size:16px"> |
|||
Screenshots</a> |
|||
</li> |
|||
<li class="nav-item"> |
|||
<a aria-controls="feature" |
|||
aria-bs-selected="false" |
|||
class="nav-link py-2" data-bs-toggle="tab" |
|||
href="#feature" id="feature-tab" role="tab" |
|||
style="color:#121212; font-weight:500; font-size:16px">Features</a> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
<div class="tab-content p-md-5 p-2 py-3" id="myTabContent"> |
|||
<div aria-labelledby="overview-tab" |
|||
class="tab-pane fade show active" id="overview" |
|||
role="tabpanel"> |
|||
<div class="position-relative mb-4" |
|||
style="border-radius:10px"> |
|||
<img alt="acc_bg" |
|||
class="w-100 h-100 position-absolute img-fluid left_0" |
|||
loading="lazy" |
|||
src="//apps.odoocdn.com/apps/assets/17.0/ks_dashboard_ninja/ai-img/o3.png?007008f" |
|||
style=""> |
|||
</div> |
|||
<!-- screenshots section--> |
|||
<div class="position-relative mb-4" |
|||
style="border-radius:10px; background-color:#f4f4f4"> |
|||
<div class="p-md-5 p-3 position-relative"> |
|||
<div class="row"> |
|||
<div class="col-md-12"> |
|||
<h1 style="font-weight:bold; font-size:calc(1.1rem + 1vw); line-height:120%; text-align:center; text-transform:capitalize; font-size: 40px; |
|||
font-weight: 700;"> |
|||
<span style="color:#121212; font-size:calc(1.1rem + 1vw)"> Navigate to the |
|||
</span> |
|||
<span style="color: #7f54b3; font-size:calc(1.1rem + 1vw)">AI tools section.</span> |
|||
</h1> |
|||
</div> |
|||
<div class="col-md-12 mb-4"> |
|||
<p style="font-weight:400; font-size:16px; line-height:150%; text-align:center; color:#64728f"> |
|||
In Settings → Website → Configuration → AI tools section, |
|||
select your AI system (OpenAI or OpenRouter), enter your API key, |
|||
set maximum tokens and choose your preferred AI model. |
|||
</p> |
|||
</div> |
|||
<div class="col-md-12 text-center"> |
|||
<div class="d-inline-block p-3 shadow-sm" |
|||
style="background-color:#fff; border-radius:10px"> |
|||
<img alt="" class="img-fluid" |
|||
loading="lazy" |
|||
src="./assets/screenshots/Screenshot1.png" |
|||
style="min-height: 1px;"> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="position-relative mb-4" |
|||
style="border-radius:10px; background-color:#f4f4f4"> |
|||
<div class="p-md-5 p-3 position-relative"> |
|||
<div class="row"> |
|||
<div class="col-md-12"> |
|||
<h1 style="font-weight:bold; font-size:calc(1.1rem + 1vw); line-height:120%; text-align:center; text-transform:capitalize; font-size: 40px; |
|||
font-weight: 700;"> |
|||
<span style="color:#121212; font-size:calc(1.1rem + 1vw)">Click |
|||
</span> |
|||
<span style="color: #7f54b3; font-size:calc(1.1rem + 1vw)"> Edit Button.</span> |
|||
</h1> |
|||
</div> |
|||
<div class="col-md-12 mb-4"> |
|||
<p style="font-weight:400; font-size:16px; line-height:150%; text-align:center; color:#64728f"> |
|||
Click the “Edit” button to open the Website Editor Sidebar. |
|||
|
|||
</p> |
|||
</div> |
|||
<div class="col-md-12 text-center"> |
|||
<div class="d-inline-block p-3 shadow-sm" |
|||
style="background-color:#fff; border-radius:10px"> |
|||
<img alt="" class="img-fluid" |
|||
loading="lazy" |
|||
src="./assets/screenshots/Screenshot2.png" |
|||
style="min-height: 1px;"> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="position-relative mb-4" |
|||
style="border-radius:10px; background-color:#f4f4f4"> |
|||
<div class="p-md-5 p-3 position-relative"> |
|||
<div class="row"> |
|||
<div class="col-md-12"> |
|||
<h1 style="font-weight:bold; font-size:calc(1.1rem + 1vw); line-height:120%; text-align:center; text-transform:capitalize; font-size: 40px; |
|||
font-weight: 700;"> |
|||
<span style="color:#121212; font-size:calc(1.1rem + 1vw)">Open |
|||
</span> |
|||
<span style="color: #7f54b3; font-size:calc(1.1rem + 1vw)">Add Snippet Dialog. </span> |
|||
</h1> |
|||
</div> |
|||
<div class="col-md-12 mb-4"> |
|||
<p style="font-weight:400; font-size:16px; line-height:150%; text-align:center; color:#64728f"> |
|||
Click AI Generated to view the Add Snippet Dialog. |
|||
</p> |
|||
</div> |
|||
<div class="col-md-12 text-center"> |
|||
<div class="d-inline-block p-3 shadow-sm" |
|||
style="background-color:#fff; border-radius:10px"> |
|||
<img alt="" class="img-fluid" |
|||
loading="lazy" |
|||
src="./assets/screenshots/Screenshot3.png" |
|||
style="min-height: 1px;"> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="position-relative mb-4" |
|||
style="border-radius:10px; background-color:#f4f4f4"> |
|||
<div class="p-md-5 p-3 position-relative"> |
|||
<div class="row"> |
|||
<div class="col-md-12"> |
|||
<h1 style="font-weight:bold; font-size:calc(1.1rem + 1vw); line-height:120%; text-align:center; text-transform:capitalize; font-size: 40px; |
|||
font-weight: 700;"> |
|||
<span style="color:#121212; font-size:calc(1.1rem + 1vw)">Click |
|||
|
|||
</span> |
|||
<span style="color:#7f54b3; font-size:calc(1.1rem + 1vw)">Generate with AI.</span> |
|||
</h1> |
|||
</div> |
|||
<div class="col-md-12 mb-4"> |
|||
<p style="font-weight:400; font-size:16px; line-height:150%; text-align:center; color:#64728f"> |
|||
Click Generate with AI to open the Generate Snippet with AI dialog box. |
|||
</p> |
|||
</div> |
|||
<div class="col-md-12 text-center"> |
|||
<div class="d-inline-block p-3 shadow-sm" |
|||
style="background-color:#fff; border-radius:10px"> |
|||
<img alt="" class="img-fluid" |
|||
loading="lazy" |
|||
src="./assets/screenshots/Screenshot4.png" |
|||
style="min-height: 1px;"> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="position-relative mb-4" |
|||
style="border-radius:10px; background-color:#f4f4f4"> |
|||
<div class="p-md-5 p-3 position-relative"> |
|||
<div class="row"> |
|||
<div class="col-md-12"> |
|||
<h1 style="font-weight:bold; font-size:calc(1.1rem + 1vw); line-height:120%; text-align:center; text-transform:capitalize; font-size: 40px; |
|||
font-weight: 700;"> |
|||
<span style="color:#121212; font-size:calc(1.1rem + 1vw)">Generate |
|||
|
|||
</span> |
|||
<span style="color:#7f54b3; font-size:calc(1.1rem + 1vw)">Snippet.</span> |
|||
</h1> |
|||
</div> |
|||
<div class="col-md-12 mb-4"> |
|||
<p style="font-weight:400; font-size:16px; line-height:150%; text-align:center; color:#64728f"> |
|||
Enter the snippet name and prompt, and click Generate. |
|||
</p> |
|||
</div> |
|||
<div class="col-md-12 text-center"> |
|||
<div class="d-inline-block p-3 shadow-sm" |
|||
style="background-color:#fff; border-radius:10px"> |
|||
<img alt="" class="img-fluid" |
|||
loading="lazy" |
|||
src="./assets/screenshots/Screenshot5.png" |
|||
style="min-height: 1px;"> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-12 mb-4 pt-5"> |
|||
<p style="font-weight:400; font-size:16px; line-height:150%; text-align:center; color:#64728f"> |
|||
You can view the newly created snippet in the Add Snippet dialog box. |
|||
</p> |
|||
</div> |
|||
<div class="col-md-12 text-center"> |
|||
<div class="d-inline-block p-3 shadow-sm" |
|||
style="background-color:#fff; border-radius:10px"> |
|||
<img alt="" class="img-fluid" |
|||
loading="lazy" |
|||
src="./assets/screenshots/Screenshot6.png" |
|||
style="min-height: 1px;"> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div aria-labelledby="feature-tab" |
|||
class="tab-pane fade show py-1" id="feature" |
|||
role="tabpanel"> |
|||
<div class="row py-4"> |
|||
<!-- Features Section --> |
|||
<div class="col-md-6 col-sm-12 p-3"> |
|||
<div class="d-flex flex-column align-items-start h-100" |
|||
style="padding:30px; border-radius:12px; background-color:#faf8ff"> |
|||
<div class="d-flex align-items-center justify-content-center"> |
|||
<div class="d-flex align-items-center justify-content-center " |
|||
style="width:36px; height:36px; border-radius:50%; background-color:#7847D9 ; margin-right:10px"> |
|||
<i class="fa fa-star " |
|||
style="color:#fff; font-size:14px"></i> |
|||
</div> |
|||
<p style="color:#1A202C; font-weight:600; font-size:1.2rem; margin-bottom:2px"> |
|||
Instantly generate custom website snippets from text prompts using |
|||
OpenAI or OpenRouter. |
|||
</p> |
|||
</div> |
|||
|
|||
</div> |
|||
</div> |
|||
<div class="col-md-6 col-sm-12 p-3"> |
|||
<div class="d-flex flex-column align-items-start h-100" |
|||
style="padding:30px; border-radius:12px; background-color:#faf8ff"> |
|||
<div class="d-flex align-items-center justify-content-center"> |
|||
<div class="d-flex align-items-center justify-content-center " |
|||
style="width:36px; height:36px; border-radius:50%; background-color:#7847D9 ; margin-right:10px"> |
|||
<i class="fa fa-star " |
|||
style="color:#fff; font-size:14px"></i> |
|||
</div> |
|||
<p style="color:#1A202C; font-weight:600; font-size:1.2rem; margin-bottom:2px"> |
|||
Create, preview, and insert AI-generated snippets directly within |
|||
Odoo’s website editor. |
|||
</p> |
|||
</div> |
|||
|
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<!-- --> |
|||
<!-- related post --> |
|||
<!-- --> |
|||
<section class="oe_container mt32"> |
|||
<h2 style="color: #091E42;font-family: "Montserrat";text-align: center;margin: 25px auto;text-transform: uppercase;" class="oe_slogan"> |
|||
<b>Related Products</b> |
|||
</h2> |
|||
<div id="demo" class="row carousel slide mt64 mb32" data-bs-ride="carousel"> |
|||
<!-- The slideshow --> |
|||
<div class="carousel-inner"> |
|||
<div class="carousel-item active"> |
|||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float: left; padding: 10px;"> |
|||
<a href="https://apps.odoo.com/apps/modules/18.0/hide_menu_user" target="_blank" style="color: #000; text-decoration: none;"> |
|||
<div style="border-radius: 6px; padding: 16px; border: 1px solid #cbcbcb;" class="shadow-sm"> |
|||
<img class="img img-responsive center-block" style=" max-width: 100%;" src="./assets/modules/b1.png" /> |
|||
<h4 class="mt0 text-truncate" style="text-align:center;width:100% margin-bottom: 8px; font-weight: 600; padding-top: 16px; text-decoration:none;font-size: 18px; padding-bottom: 8px; margin-bottom: 0px"> |
|||
Hide Any Menu User Wise</h4> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float: left; padding: 10px;"> |
|||
<a href="https://apps.odoo.com/apps/modules/18.0/web_login_styles" target="_blank" style="color: #000; text-decoration: none;"> |
|||
<div style="border-radius: 6px; padding: 16px; border: 1px solid #cbcbcb;" class="shadow-sm"> |
|||
<img class="img img-responsive center-block" style=" max-width: 100%;" src="./assets/modules/b2.png" /> |
|||
<h4 class="mt0 text-truncate" style="text-align:center;width:100% margin-bottom: 8px; font-weight: 600; padding-top: 16px; text-decoration:none;font-size: 18px; padding-bottom: 8px; margin-bottom: 0px"> |
|||
Customize Login Page Style</h4> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float: left; padding: 10px;"> |
|||
<a href="https://apps.odoo.com/apps/modules/18.0/whatsapp_mail_messaging" target="_blank" style="color: #000; text-decoration: none;"> |
|||
<div style="border-radius: 6px;padding: 16px; border: 1px solid #cbcbcb;" class="shadow-sm"> |
|||
<img class="img img-responsive center-block" style=" max-width: 100%;" src="./assets/modules/b3.png"/> |
|||
<h4 class="mt0 text-truncate" style="text-align:center;width:100% margin-bottom: 8px; font-weight: 600; padding-top: 16px; text-decoration:none;font-size: 18px; padding-bottom: 8px; margin-bottom: 0px"> |
|||
Odoo Whatsapp Connector</h4> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
<div class="carousel-item"> |
|||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float: left; padding: 10px;"> |
|||
<a href="https://apps.odoo.com/apps/modules/18.0/login_user_detail" target="_blank" style="color: #000; text-decoration: none;"> |
|||
<div style="border-radius: 6px; padding: 16px; border: 1px solid #cbcbcb;" class="shadow-sm"> |
|||
<img class="img img-responsive center-block" style=" max-width: 100%;" src="./assets/modules/b4.png" /> |
|||
<h4 class="mt0 text-truncate" style="text-align:center;width:100% margin-bottom: 8px; font-weight: 600; padding-top: 16px; text-decoration:none;font-size: 18px; padding-bottom: 8px; margin-bottom: 0px"> |
|||
User Log Details</h4> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float: left; padding: 10px;"> |
|||
<a href="https://apps.odoo.com/apps/modules/18.0/report_attachment_preview" target="_blank" style="color: #000; text-decoration: none;"> |
|||
<div style="border-radius: 6px; padding: 16px; border: 1px solid #cbcbcb;" class="shadow-sm"> |
|||
<img class="img img-responsive center-block" style=" max-width: 100%;" src="./assets/modules/b5.png" /> |
|||
<h4 class="mt0 text-truncate" style="text-align:center;width:100% margin-bottom: 8px; font-weight: 600; padding-top: 16px; text-decoration:none;font-size: 18px; padding-bottom: 8px; margin-bottom: 0px"> |
|||
Reports and Attachments Preview in Browser</h4> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float: left; padding: 10px;"> |
|||
<a href="https://apps.odoo.com/apps/modules/18.0/project_dashboard_odoo" target="_blank" style="color: #000; text-decoration: none;"> |
|||
<div style="border-radius: 6px; padding: 16px; border: 1px solid #cbcbcb;" class="shadow-sm"> |
|||
<img class="img img-responsive center-block" style=" max-width: 100%;" src="./assets/modules/b6.png" /> |
|||
<h4 class="mt0 text-truncate" style="text-align:center;width:100% margin-bottom: 8px; font-weight: 600; padding-top: 16px; text-decoration:none;font-size: 18px; padding-bottom: 8px; margin-bottom: 0px"> |
|||
Project Dashboard</h4> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<!-- Left and right controls --> |
|||
<a class="carousel-control-prev" href="#demo" data-bs-slide="prev" style="margin-left: -30px;width: 35px;color: #000;"> |
|||
<span class="carousel-control-prev-icon"> |
|||
<i class="fa fa-chevron-left" style="font-size:24px"></i> |
|||
</span> |
|||
</a> |
|||
<a class="carousel-control-next" href="#demo" data-bs-slide="next" style="margin-right: -30px;width: 35px;color: #000;"> |
|||
<span class="carousel-control-next-icon"> |
|||
<i class="fa fa-chevron-right" style="font-size:24px"></i> |
|||
</span> |
|||
</a> |
|||
</div> |
|||
</section> |
|||
<!-- service-section --> |
|||
|
|||
<section id="services" class="mt-5" style="border-radius: 16px; |
|||
border: 1px solid #EBEEF2; |
|||
background: var(--Neutral-N0, #FFF); |
|||
padding: 60px 40px; |
|||
box-shadow: 0px 5px 20px -11px rgba(0, 0, 0, 0.25);"> |
|||
<div class="text-center mt-4"><h3 class="mb-0" style="color: #000; |
|||
text-align: center; |
|||
font-family: Montserrat; |
|||
font-size: 40px; |
|||
font-style: normal; |
|||
font-weight: 700; |
|||
line-height: normal; |
|||
text-transform: uppercase; |
|||
padding-bottom: 50px;"> |
|||
Our Services</h3></div> |
|||
<div class="row mt-3"> |
|||
<div class="col-lg-3 col-sm-12 mb-3"> |
|||
<a href="#" style="text-decoration:none"> |
|||
<div class="btn-lg btn-block p-4 mb-2 d-flex flex-column justify-content-center align-items-center" |
|||
style="font-size:25px; font-weight:bold;background-color:#FFE2E5; margin:auto; gap: 16px; border-radius: 8px;"> |
|||
<div class="d-flex justify-content-center align-items-center" |
|||
style="background-color:#FA5A7D; border-radius:50%; height:56px; width:56px"> |
|||
<img src="./assets/icons/gear.svg" |
|||
class="img-responsive" |
|||
height="28px" width="28px"> |
|||
</div> |
|||
<span style="font-size: 18px; |
|||
color: var(--text-color); |
|||
font-weight: 600;"> Odoo Customization</span> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-lg-3 col-sm-12 mb-3"> |
|||
<a href="#" style="text-decoration:none"> |
|||
<div class="btn-lg btn-block p-4 mb-2 d-flex flex-column justify-content-center align-items-center" |
|||
style="font-size:25px; font-weight:bold;background-color:#FFF4DE; margin:auto; gap: 16px; border-radius: 8px;"> |
|||
<div class="d-flex justify-content-center align-items-center" |
|||
style="background-color:#FF947A; border-radius:50%; height:56px; width:56px"> |
|||
<img src="./assets/icons/wrench-icon.svg" |
|||
class="img-responsive" |
|||
height="28px" width="28px"> |
|||
</div> |
|||
<span style="font-size: 18px; |
|||
color: var(--text-color); |
|||
font-weight: 600;"> Odoo Implementation</span> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-lg-3 col-sm-12 mb-3"> |
|||
<a href="#" style="text-decoration:none"> |
|||
<div class="btn-lg btn-block p-4 mb-2 d-flex flex-column justify-content-center align-items-center" |
|||
style="font-size:25px; font-weight:bold;background-color:#DCFCE7; margin:auto; gap: 16px; border-radius: 8px;"> |
|||
<div class="d-flex justify-content-center align-items-center" |
|||
style="background-color:#3CD856; border-radius:50%; height:56px; width:56px"> |
|||
<img src="./assets/icons/life-ring-icon.svg" |
|||
class="img-responsive" |
|||
height="28px" width="28px"> |
|||
</div> |
|||
<span style="font-size: 18px; |
|||
color: var(--text-color); |
|||
font-weight: 600;">Odoo Support</span> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-lg-3 col-sm-12 mb-3"> |
|||
<a href="#" style="text-decoration:none"> |
|||
<div class="btn-lg btn-block p-4 mb-2 d-flex flex-column justify-content-center align-items-center" |
|||
style="font-size:25px; font-weight:bold;background-color:#F3E8FF; margin:auto; gap: 16px; border-radius: 8px;"> |
|||
<div class="d-flex justify-content-center align-items-center" |
|||
style="background-color:#BF83FF; border-radius:50%; height:56px; width:56px"> |
|||
<img src="./assets/icons/arrows-repeat.svg" |
|||
class="img-responsive" |
|||
height="28px" width="28px"> |
|||
</div> |
|||
<span style="font-size: 18px; |
|||
color: var(--text-color); |
|||
font-weight: 600;">Odoo Migration</span> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-lg-3 col-sm-12 mb-3"> |
|||
<a href="#" style="text-decoration:none"> |
|||
<div class="btn-lg btn-block p-4 mb-2 d-flex flex-column justify-content-center align-items-center" |
|||
style="font-size:25px; font-weight:bold;background-color:#F1F9FF; margin:auto; gap: 16px; border-radius: 8px;"> |
|||
<div class="d-flex justify-content-center align-items-center" |
|||
style="background-color:#01649C; border-radius:50%; height:56px; width:56px"> |
|||
<img src="./assets/icons/puzzle-piece-icon.svg" |
|||
class="img-responsive" |
|||
height="28px" width="28px"> |
|||
</div> |
|||
<span style="font-size: 18px; |
|||
color: var(--text-color); |
|||
font-weight: 600;">Odoo integration</span> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-lg-3 col-sm-12 mb-3"> |
|||
<a href="#" style="text-decoration:none"> |
|||
<div class="btn-lg btn-block p-4 mb-2 d-flex flex-column justify-content-center align-items-center" |
|||
style="font-size:25px; font-weight:bold;background-color:#EDF8ED; margin:auto; gap: 16px; border-radius: 8px;"> |
|||
|
|||
<div class="d-flex justify-content-center align-items-center" |
|||
style="background-color:#69CC70; border-radius:50%; height:56px; width:56px"> |
|||
<img src="./assets/icons/odoo-consultancy.svg" |
|||
class="img-responsive" |
|||
height="28px" width="28px"> |
|||
</div> |
|||
<span style="font-size: 18px; |
|||
color: var(--text-color); |
|||
font-weight: 600;">Odoo Consultancy</span> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-lg-3 col-sm-12 mb-3"> |
|||
<a href="#" style="text-decoration:none"> |
|||
<div class="btn-lg btn-block p-4 mb-2 d-flex flex-column justify-content-center align-items-center" |
|||
style="font-size:25px; font-weight:bold;background-color:#F1F6FF; margin:auto; gap: 16px; border-radius: 8px;"> |
|||
|
|||
<div class="d-flex justify-content-center align-items-center" |
|||
style="background-color:#2E4556; border-radius:50%; height:56px; width:56px"> |
|||
<img src="./assets/icons/odoo-licencing.svg" |
|||
class="img-responsive" |
|||
height="28px" width="28px"> |
|||
</div> |
|||
<span style="font-size: 18px; |
|||
color: var(--text-color); |
|||
font-weight: 600;">Odoo Licensing</span> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-lg-3 col-sm-12 mb-3"> |
|||
<a href="#" style="text-decoration:none"> |
|||
<div class="btn-lg btn-block p-4 mb-2 d-flex flex-column justify-content-center align-items-center" |
|||
style="font-size:25px; font-weight:bold;background-color:#FAF6EA; margin:auto; gap: 16px; border-radius: 8px;"> |
|||
|
|||
<div class="d-flex justify-content-center align-items-center" |
|||
style="background-color:#FCD12C; border-radius:50%; height:56px; width:56px"> |
|||
<img src="./assets/icons/hire-odoo.svg" |
|||
class="img-responsive" |
|||
height="28px" width="28px"> |
|||
</div> |
|||
<span style="font-size: 18px; |
|||
color: var(--text-color); |
|||
font-weight: 600;">Hire Odoo Developer</span> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- licence --> |
|||
<div class="tab-pane fade" id="profile" role="tabpanel" |
|||
aria-labelledby="profile-tab"> |
|||
<div class="px-5"> |
|||
.... |
|||
</div> |
|||
</div> |
|||
<!-- --> |
|||
</div> |
|||
</section> |
|||
<!-- --> |
|||
</div> |
|||
</div> |
|||
</body> |
|||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"></script> |
|||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" |
|||
integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" |
|||
crossorigin="anonymous" referrerpolicy="no-referrer"></script> |
|||
<script src="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/owl.carousel.min.js"></script> |
|||
<script> |
|||
$('.owl-carousel').owlCarousel({ |
|||
rtl: true, |
|||
loop: true, |
|||
margin: 10, |
|||
nav: true, |
|||
responsive: { |
|||
0: { |
|||
items: 1 |
|||
}, |
|||
600: { |
|||
items: 3 |
|||
}, |
|||
1000: { |
|||
items: 3 |
|||
} |
|||
} |
|||
}) |
|||
</script> |
|||
</html> |
|||
@ -0,0 +1,3 @@ |
|||
.s_ai_snippet .loading-spinner { |
|||
min-height: 100px; |
|||
} |
|||
|
After Width: | Height: | Size: 12 KiB |
@ -0,0 +1,117 @@ |
|||
/** @odoo-module **/ |
|||
|
|||
import { patch } from "@web/core/utils/patch"; |
|||
import { useService } from "@web/core/utils/hooks"; |
|||
import { rpc } from "@web/core/network/rpc"; |
|||
import { Component, xml, useRef, useState } from "@odoo/owl"; |
|||
import { AddSnippetDialog } from "@web_editor/js/editor/add_snippet_dialog"; |
|||
|
|||
|
|||
// Define a custom Owl component for the AI Prompt Modal
|
|||
class AIPromptModal extends Component { |
|||
setup() { |
|||
this.orm = useService("orm"); |
|||
this.notification = useService("notification"); |
|||
this.textAreaRef = useRef('textAreaRef'); |
|||
this.nameRef = useRef('snippetNameRef'); |
|||
this.state = useState({ |
|||
isLoading: false, |
|||
}); |
|||
} |
|||
// Close the modal (disabled if a request is loading)
|
|||
onClose() { |
|||
if (this.state.isLoading) { |
|||
return; |
|||
} |
|||
this.props.close(); |
|||
} |
|||
async onSubmit() { |
|||
const promptValue = this.textAreaRef.el.value; |
|||
const snippetName = this.nameRef.el.value; |
|||
|
|||
if (!promptValue || !snippetName) { |
|||
this.notification.add( |
|||
"Please fill in both the prompt and snippet name", |
|||
{ type: "warning" } |
|||
); |
|||
return; |
|||
} |
|||
try { |
|||
this.state.isLoading = true; |
|||
const result = await rpc('/website/generate_snippet', { |
|||
prompt: promptValue, |
|||
name: snippetName |
|||
}); |
|||
if (result.error) { |
|||
this.notification.add(result.error, { type: "danger" }); |
|||
} else { |
|||
this.notification.add( |
|||
"Snippet generated successfully! Refreshing snippet panel...", |
|||
{ type: "success" } |
|||
); |
|||
window.location.reload(); |
|||
} |
|||
} catch (error) { |
|||
this.notification.add( |
|||
"Failed to generate snippet: " + error, |
|||
{ type: "danger" } |
|||
); |
|||
} finally { |
|||
this.state.isLoading = false; |
|||
} |
|||
this.props.close(); |
|||
} |
|||
} |
|||
|
|||
// Define the template for the modal
|
|||
AIPromptModal.template = xml` |
|||
<div class="modal o_technical_modal d-block" tabindex="-1" role="dialog"> |
|||
<div class="modal-dialog" role="document"> |
|||
<div class="modal-content"> |
|||
<div class="modal-header"> |
|||
<h5 class="modal-title">Generate Snippet with AI</h5> |
|||
<button type="button" class="btn-close" t-att-disabled="state.isLoading" t-on-click="onClose" aria-label="Close"/> |
|||
</div> |
|||
<div class="modal-body"> |
|||
<div class="form-group mb-3"> |
|||
<label for="snippet-name" class="form-label">Snippet Name</label> |
|||
<input type="text" id="snippet-name" class="form-control" t-ref="snippetNameRef" |
|||
t-att-disabled="state.isLoading" placeholder="Enter a name for your snippet"/> |
|||
</div> |
|||
<div class="form-group"> |
|||
<label for="ai-prompt" class="form-label">Enter your prompt here...</label> |
|||
<textarea id="ai-prompt" class="form-control" rows="3" t-ref="textAreaRef" |
|||
t-att-disabled="state.isLoading"/> |
|||
</div> |
|||
</div> |
|||
<div class="modal-footer"> |
|||
<button type="button" class="btn btn-secondary" t-att-disabled="state.isLoading" t-on-click="onClose">Cancel</button> |
|||
<button type="button" class="btn btn-primary" t-att-disabled="state.isLoading" t-on-click="onSubmit"> |
|||
<t t-if="state.isLoading"> |
|||
<span class="fa fa-spinner fa-spin me-1"></span> |
|||
Generating... |
|||
</t> |
|||
<t t-else="">Generate</t> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
`;
|
|||
|
|||
// Patch the AddSnippetDialog to add the AI prompt dialog on click
|
|||
const originalSetup = AddSnippetDialog.prototype.setup; |
|||
patch(AddSnippetDialog.prototype, { |
|||
setup() { |
|||
originalSetup.call(this); |
|||
this.dialog = useService("dialog"); |
|||
}, |
|||
async _onGenerateClick() { |
|||
this.dialog.add(AIPromptModal, { |
|||
onClose: () => { |
|||
// Refresh the snippets panel after closing
|
|||
this.render(); |
|||
} |
|||
}); |
|||
} |
|||
}); |
|||
@ -0,0 +1,241 @@ |
|||
/** @odoo-module **/ |
|||
|
|||
import { patch } from "@web/core/utils/patch"; |
|||
import { _t } from "@web/core/l10n/translation"; |
|||
import { AddSnippetDialog } from "@web_editor/js/editor/add_snippet_dialog"; |
|||
|
|||
|
|||
/** |
|||
* Patch AddSnippetDialog to: |
|||
* - add custom tooltip on hover showing snippet name |
|||
*/ |
|||
patch(AddSnippetDialog.prototype, { |
|||
/** |
|||
* Override default snippet insertion. |
|||
* Inserts snippets into two columns, filters by search, handles custom snippets, etc. |
|||
*/ |
|||
async insertSnippets() { |
|||
const insertSnippetsCallID = ++this.currentInsertSnippetsCallID; |
|||
let snippetsToDisplay = [...this.props.snippets.values()].filter(snippet => { |
|||
return !snippet.excluded && snippet.group; |
|||
}); |
|||
if (this.state.search) { |
|||
const search = this.state.search; |
|||
const selectorSearch = /^s_[\w-]*$/.test(search) && `[class^="${search}"], [class*=" ${search}"]`; |
|||
const lowerCasedSearch = search.toLowerCase(); |
|||
const strMatches = str => str.toLowerCase().includes(lowerCasedSearch); |
|||
snippetsToDisplay = snippetsToDisplay.filter(snippet => { |
|||
return selectorSearch && ( |
|||
snippet.baseBody.matches(selectorSearch) |
|||
|| snippet.baseBody.querySelector(selectorSearch) |
|||
) |
|||
|| strMatches(snippet.category.text) |
|||
|| strMatches(snippet.displayName) |
|||
|| strMatches(snippet.data.oeKeywords || ''); |
|||
}); |
|||
if (selectorSearch) { |
|||
snippetsToDisplay.sort((snippetA, snippetB) => { |
|||
if (snippetA.data.snippet === search) { |
|||
return -1; |
|||
} |
|||
if (snippetB.data.snippet === search) { |
|||
return 1; |
|||
} |
|||
const aHasExactClassOnRoot = snippetA.baseBody.classList.contains(search); |
|||
const bHasExactClassOnRoot = snippetB.baseBody.classList.contains(search); |
|||
if (aHasExactClassOnRoot !== bHasExactClassOnRoot) { |
|||
return aHasExactClassOnRoot ? -1 : 1; |
|||
} |
|||
const aHasPartialClassOnRoot = snippetA.baseBody.matches(selectorSearch); |
|||
const bHasPartialClassOnRoot = snippetB.baseBody.matches(selectorSearch); |
|||
if (aHasPartialClassOnRoot !== bHasPartialClassOnRoot) { |
|||
return aHasPartialClassOnRoot ? -1 : 1; |
|||
} |
|||
|
|||
return 0; |
|||
}); |
|||
} |
|||
} else { |
|||
snippetsToDisplay = snippetsToDisplay.filter(snippet => { |
|||
return snippet.group === this.state.groupSelected; |
|||
}); |
|||
} |
|||
this.iframeDocument.body.scrollTop = 0; |
|||
const rowEl = document.createElement("div"); |
|||
rowEl.classList.add("row", "g-0", "o_snippets_preview_row"); |
|||
rowEl.style.setProperty("direction", this.props.frontendDirection); |
|||
const leftColEl = document.createElement("div"); |
|||
leftColEl.classList.add("col-lg-6"); |
|||
rowEl.appendChild(leftColEl); |
|||
const rightColEl = document.createElement("div"); |
|||
rightColEl.classList.add("col-lg-6"); |
|||
rowEl.appendChild(rightColEl); |
|||
this.iframeDocument.body.appendChild(rowEl); |
|||
const BIG_CHUNK_SIZE = 6; |
|||
const SMALL_CHUNK_SIZE = 3; |
|||
const chunks = [snippetsToDisplay.splice(0, BIG_CHUNK_SIZE)]; |
|||
while (snippetsToDisplay.length) { |
|||
chunks.push(snippetsToDisplay.splice(0, SMALL_CHUNK_SIZE)); |
|||
} |
|||
let leftColSize = 0; |
|||
let rightColSize = 0; |
|||
for (const chunk of chunks) { |
|||
const itemEls = await Promise.all(chunk.map(snippet => { |
|||
let containerEl = null; |
|||
let clonedSnippetEl; |
|||
let originalSnippet; |
|||
if (snippet.isCustom) { |
|||
originalSnippet = [...this.props.snippets.values()].filter(snip => |
|||
!snip.isCustom && snip.name === snippet.name |
|||
)[0]; |
|||
if (originalSnippet.baseBody.querySelector(".s_dialog_preview") |
|||
|| originalSnippet.imagePreview |
|||
|| originalSnippet.name === "s_countdown") { |
|||
clonedSnippetEl = originalSnippet.baseBody.cloneNode(true); |
|||
} |
|||
} |
|||
if (!clonedSnippetEl) { |
|||
clonedSnippetEl = snippet.baseBody.cloneNode(true); |
|||
} |
|||
clonedSnippetEl.classList.remove("oe_snippet_body"); |
|||
const snippetPreviewWrapEl = document.createElement("div"); |
|||
snippetPreviewWrapEl.classList.add("o_snippet_preview_wrap", "position-relative"); |
|||
snippetPreviewWrapEl.dataset.snippetId = snippet.name; |
|||
snippetPreviewWrapEl.dataset.snippetKey = snippet.key; |
|||
snippetPreviewWrapEl.dataset.snippetName = snippet.displayName; |
|||
snippetPreviewWrapEl.appendChild(clonedSnippetEl); |
|||
this.__onSnippetPreviewClick = this._onSnippetPreviewClick.bind(this); |
|||
snippetPreviewWrapEl.addEventListener("click", this.__onSnippetPreviewClick); |
|||
snippetPreviewWrapEl.addEventListener("mouseenter", this.__onSnippetPreviewHover); |
|||
containerEl = snippetPreviewWrapEl; |
|||
if (snippet.installable) { |
|||
snippetPreviewWrapEl.classList.add("o_snippet_preview_install"); |
|||
clonedSnippetEl.dataset.moduleId = snippet.moduleId; |
|||
const installBtnEl = document.createElement("button"); |
|||
installBtnEl.classList.add("o_snippet_preview_install_btn", "btn", "text-white", "rounded-1", "mx-auto", "p-2", "bottom-50"); |
|||
installBtnEl.innerText = _t("Install %s", snippet.displayName); |
|||
snippetPreviewWrapEl.appendChild(installBtnEl); |
|||
} |
|||
// Image preview
|
|||
const imagePreview = snippet.imagePreview || originalSnippet?.imagePreview; |
|||
if (imagePreview) { |
|||
clonedSnippetEl.style.setProperty("padding", "0", "important"); |
|||
const previewImgDivEl = document.createElement("div"); |
|||
previewImgDivEl.classList.add("s_dialog_preview", "s_dialog_preview_image"); |
|||
const previewImgEl = document.createElement("img"); |
|||
previewImgEl.src = imagePreview; |
|||
previewImgDivEl.appendChild(previewImgEl); |
|||
clonedSnippetEl.innerHTML = ""; |
|||
clonedSnippetEl.appendChild(previewImgDivEl); |
|||
} |
|||
|
|||
clonedSnippetEl.classList.remove("o_dynamic_empty"); |
|||
|
|||
if (snippet.isCustom) { |
|||
const editCustomSnippetEl = document.createElement("div"); |
|||
editCustomSnippetEl.classList.add("d-grid", "mt-2", "mx-5", "gap-2", |
|||
"d-md-flex", "justify-content-md-end", "o_custom_snippet_edit"); |
|||
const spanEl = document.createElement("span"); |
|||
spanEl.classList.add("w-100"); |
|||
spanEl.textContent = snippet.displayName; |
|||
const renameBtnEl = document.createElement("button"); |
|||
renameBtnEl.classList.add("btn", "fa", "fa-pencil", "me-md-2"); |
|||
renameBtnEl.type = "button"; |
|||
|
|||
const removeBtnEl = document.createElement("button"); |
|||
removeBtnEl.classList.add("btn", "fa", "fa-trash"); |
|||
removeBtnEl.type = "button"; |
|||
|
|||
editCustomSnippetEl.appendChild(spanEl); |
|||
editCustomSnippetEl.appendChild(renameBtnEl); |
|||
editCustomSnippetEl.appendChild(removeBtnEl); |
|||
|
|||
const customSnippetWrapEl = document.createElement("div"); |
|||
customSnippetWrapEl.classList.add("o_custom_snippet_wrap"); |
|||
customSnippetWrapEl.appendChild(snippetPreviewWrapEl); |
|||
customSnippetWrapEl.appendChild(editCustomSnippetEl); |
|||
containerEl = customSnippetWrapEl; |
|||
|
|||
this.__onRenameCustomBtnClick = this._onRenameCustomBtnClick.bind(this); |
|||
renameBtnEl.addEventListener("click", this.__onRenameCustomBtnClick); |
|||
this.__onDeleteCustomBtnClick = this._onDeleteCustomBtnClick.bind(this); |
|||
removeBtnEl.addEventListener("click", this.__onDeleteCustomBtnClick); |
|||
} |
|||
containerEl.classList.add("invisible"); |
|||
leftColEl.appendChild(containerEl); |
|||
|
|||
// preload images
|
|||
const imageEls = snippetPreviewWrapEl.querySelectorAll("img"); |
|||
// TODO: move onceAllImagesLoaded in web_editor and to use it here
|
|||
return Promise.all(Array.from(imageEls).map(imgEl => { |
|||
imgEl.setAttribute("loading", "eager"); |
|||
return new Promise(resolve => { |
|||
if (imgEl.complete) { |
|||
resolve(); |
|||
} else { |
|||
imgEl.onload = () => resolve(); |
|||
imgEl.onerror = () => resolve(); |
|||
} |
|||
}); |
|||
})).then(() => containerEl); |
|||
})); |
|||
if (this.currentInsertSnippetsCallID !== insertSnippetsCallID) { |
|||
return; |
|||
} |
|||
// Balance items into two columns
|
|||
const leftColElements = []; |
|||
const rightColElements = []; |
|||
for (const itemEl of itemEls) { |
|||
const size = itemEl.getBoundingClientRect().height; |
|||
if (leftColSize <= rightColSize) { |
|||
leftColElements.push(itemEl); |
|||
leftColSize += size; |
|||
} else { |
|||
rightColElements.push(itemEl); |
|||
rightColSize += size; |
|||
} |
|||
} |
|||
for (const [colEl, colItemEls] of [ |
|||
[leftColEl, leftColElements], |
|||
[rightColEl, rightColElements], |
|||
]) { |
|||
for (const el of colItemEls) { |
|||
colEl.appendChild(el); |
|||
el.classList.remove("invisible"); |
|||
} |
|||
} |
|||
// Remove previous content
|
|||
while (rowEl.previousSibling) { |
|||
rowEl.previousSibling.remove(); |
|||
} |
|||
} |
|||
this._updateSnippetContent(this.iframeDocument); |
|||
}, |
|||
/** |
|||
* Show snippet name on snippet hover. |
|||
*/ |
|||
__onSnippetPreviewHover(ev) { |
|||
const previewEl = ev.currentTarget; |
|||
const title = previewEl.firstElementChild?.getAttribute("title"); |
|||
if (!title || previewEl.querySelector(".snippet-hover-title-tooltip")) { |
|||
return; |
|||
} |
|||
const tooltip = document.createElement("div"); |
|||
tooltip.className = "snippet-hover-title-tooltip position-absolute top-0 start-50 translate-middle-x bg-dark text-white px-5 py-3 rounded shadow-lg"; |
|||
Object.assign(tooltip.style, { |
|||
zIndex: "1000", |
|||
pointerEvents: "none", |
|||
fontSize: "2rem", |
|||
fontWeight: "900", |
|||
border: "2px solid white", |
|||
boxShadow: "0 4px 20px rgba(0, 0, 0, 0.3)", |
|||
}); |
|||
tooltip.textContent = title; |
|||
previewEl.appendChild(tooltip); |
|||
const removeTooltip = () => { |
|||
tooltip.remove(); |
|||
previewEl.removeEventListener("mouseleave", removeTooltip); |
|||
}; |
|||
previewEl.addEventListener("mouseleave", removeTooltip, { once: true }); |
|||
}, |
|||
}); |
|||