@ -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 }); |
||||
|
}, |
||||
|
}); |
||||