@ -0,0 +1,119 @@ |
|||
# Meeting Summarizer for Odoo 18 |
|||
|
|||
[](https://www.odoo.com) |
|||
[](https://opensource.org/licenses/MIT) |
|||
|
|||
## Overview |
|||
|
|||
The Meeting Summarizer module transcribes Discuss meetings and saves the |
|||
transcript along with a summary. |
|||
|
|||
## Features |
|||
|
|||
- Features |
|||
- 📄 Download the transcription summary file. |
|||
- 📁 Access and download the full transcription data. |
|||
- ✉️ Automatically send transcription and summary files |
|||
to selected users. |
|||
|
|||
|
|||
## Screenshots |
|||
|
|||
Here are some glimpses of Json Widget: |
|||
|
|||
### User Interface |
|||
|
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/1.png" alt="Feature 1" width="500" style="border: none;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/2.png" alt="Feature 1" width="500" style="border: none;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/3.png" alt="Feature 1" width="500" style="border: none;"/> |
|||
</td> |
|||
</tr> |
|||
</div> |
|||
<div> |
|||
<tr> |
|||
<td align="center"> |
|||
<img src="static/description/assets/screenshots/4.png" alt="Feature 1" 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. |
|||
|
|||
## Installation |
|||
|
|||
Follow these steps to set up and run the app: |
|||
|
|||
1. **Clone the Repository** |
|||
|
|||
```git clone https://github.com/cybrosystech/Meeting-Summarize.git``` |
|||
|
|||
2. **Add the module to addons** |
|||
|
|||
```cd Meeting-Summarize``` |
|||
|
|||
## Contributing |
|||
|
|||
We welcome contributions! Currently, this feature is supported only in Google Chrome. |
|||
You’re welcome to contribute and help extend compatibility to other browsers. |
|||
|
|||
To get started: |
|||
|
|||
1. Fork the repository. |
|||
|
|||
2. Create a new branch: |
|||
``` |
|||
git checkout -b feature/your-feature-name |
|||
``` |
|||
3. Make changes and commit: |
|||
``` |
|||
git commit -m "Add your message here" |
|||
``` |
|||
4. Push your changes: |
|||
``` |
|||
git push origin feature/your-feature-name |
|||
``` |
|||
5. Create a Pull Request on GitHub. |
|||
|
|||
--- |
|||
- Submit a pull request with a clear description of your changes. |
|||
|
|||
## License |
|||
|
|||
This project is licensed under the AGPL-3. Feel free to use, modify, and distribute it as needed. |
|||
|
|||
|
|||
## Contact |
|||
|
|||
* Mail Contact : odoo@cybrosys.com |
|||
* Website : https://cybrosys.com |
|||
|
|||
Maintainer |
|||
========== |
|||
 |
|||
https://cybrosys.com |
|||
|
|||
This module is maintained by Cybrosys Technologies. |
|||
For support and more information, please visit https://www.cybrosys.com |
|||
@ -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 controller |
|||
from .import models |
|||
@ -0,0 +1,61 @@ |
|||
# -*- 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': 'Meeting Summarizer', |
|||
'version': '18.0.1.0.0', |
|||
'category': 'Extra Tools', |
|||
'summary': """Transcribes Discuss meetings and |
|||
saves the text with a summary.""", |
|||
'description': """This module transcribes the Discuss meeting and |
|||
saves the transcription in a file. It also generates a summary of the meeting content.""", |
|||
'author': 'Cybrosys Techno Solutions', |
|||
'company': 'Cybrosys Techno Solutions', |
|||
'maintainer': 'Cybrosys Techno Solutions', |
|||
'website': 'https://www.cybrosys.com', |
|||
'depends':['mail', 'web'], |
|||
'data':[ |
|||
'security/ir.model.access.csv', |
|||
'data/send_transcription_template.xml', |
|||
'views/res_config_settings.xml', |
|||
'views/send_mail_transcription.xml', |
|||
], |
|||
"assets": { |
|||
'web.assets_backend': [ |
|||
"meeting_summarizer/static/src/js/call_action_list.js", |
|||
"meeting_summarizer/static/src/js/attachment_list.js", |
|||
"meeting_summarizer/static/src/xml/attachment_list.xml", |
|||
], |
|||
'mail.assets_public': [ |
|||
"meeting_summarizer/static/src/js/call_action_list.js", |
|||
"meeting_summarizer/static/src/js/attachment_list.js", |
|||
"meeting_summarizer/static/src/xml/attachment_list.xml", |
|||
], |
|||
}, |
|||
'external_dependencies': { |
|||
'python': ['openai'], |
|||
}, |
|||
'images': ['static/description/banner.jpg'], |
|||
'license': 'AGPL-3', |
|||
'installable': True, |
|||
'auto_install': False, |
|||
'application': True, |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
# -*- 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/>. |
|||
# |
|||
############################################################################# |
|||
""" |
|||
This module handles the transcription-related functionalities for the application. |
|||
|
|||
It includes features for transcribing meeting recordings, processing audio data, |
|||
and generating transcription files. Additionally, the module may interface with |
|||
external APIs or services to improve transcription accuracy. |
|||
|
|||
The 'transcription' module is imported to manage specific transcription tasks. |
|||
""" |
|||
from . import transcription |
|||
@ -0,0 +1,273 @@ |
|||
# -*- 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/>. |
|||
# |
|||
############################################################################# |
|||
""" |
|||
This module defines HTTP controllers for handling transcription-related requests |
|||
in the Meeting Summarizer module, including interactions with the OpenAI API. |
|||
""" |
|||
import base64 |
|||
import json |
|||
import openai |
|||
|
|||
from odoo import _ |
|||
from odoo import http |
|||
from odoo.exceptions import ValidationError |
|||
from odoo.http import request |
|||
|
|||
|
|||
class TranscriptionController(http.Controller): |
|||
"""Transcription controllers""" |
|||
@http.route('/get/transcription_data', type='json', auth='public') |
|||
def get_transcription_file(self, **kwargs): |
|||
"""Controller is used to store the transcription data""" |
|||
transcription_id = kwargs['id'] |
|||
if not transcription_id: |
|||
return {'error': 'No ID provided'} |
|||
cache_key = f"transcription_id_{transcription_id}" |
|||
stored_data = request.env['ir.config_parameter'].sudo().get_param( |
|||
cache_key) |
|||
if stored_data: |
|||
transcription_list = json.loads(stored_data) |
|||
else: |
|||
transcription_list = [] |
|||
transcription_list.append(kwargs) |
|||
request.env['ir.config_parameter'].sudo().set_param(cache_key, |
|||
json.dumps( |
|||
transcription_list)) |
|||
return {'message': 'Data stored successfully', 'cache_key': cache_key} |
|||
|
|||
@http.route('/create/transcription_file_summary', type='json', auth='user') |
|||
def get_cached_transcription_file(self, **kwargs): |
|||
"""Get the data from ir_config parameter and create |
|||
transcription file and summary file in ir_attachment""" |
|||
transcription_id = kwargs.get('kwargs', {}).get('id') |
|||
if not transcription_id: |
|||
return {'error': 'No ID provided'} |
|||
cache_key = f"transcription_id_{transcription_id}" |
|||
cached_data = request.env['ir.config_parameter'].sudo().get_param( |
|||
cache_key) |
|||
if not cached_data: |
|||
return {'error': 'No cached data found'} |
|||
cached_data = json.loads(cached_data) |
|||
text_content = "\n".join(item["data"] for item in cached_data) |
|||
api_key = request.env['ir.config_parameter'].sudo().get_param( |
|||
"meeting_summarizer.open_api_key") |
|||
if not api_key: |
|||
raise ValidationError( |
|||
_("Please Enter a valid api key in settings..")) |
|||
client = openai.OpenAI(api_key=api_key) |
|||
def create_summary(content): |
|||
response = client.chat.completions.create( |
|||
model="gpt-4-turbo", |
|||
messages=[ |
|||
{"role": "system", |
|||
"content": "Summarize the following meeting transcript."}, |
|||
{"role": "user", "content": content} |
|||
], |
|||
temperature=0.7, |
|||
max_tokens=500 |
|||
) |
|||
return response |
|||
summary_data = create_summary(text_content) |
|||
summary_text = summary_data.choices[0].message.content # Corrected response extraction |
|||
file_data = base64.b64encode(summary_text.encode("utf-8")) |
|||
file_name = f"transcription_id_{transcription_id}.txt" |
|||
summary_file_name = f"summary_id_{transcription_id}.txt" |
|||
new_file_content = text_content.encode('utf-8') |
|||
new_file_base64 = base64.b64encode(new_file_content).decode('utf-8') |
|||
def get_attachment(attachment_name): |
|||
attachment_detail = request.env['ir.attachment'].sudo().search([ |
|||
('name', '=', attachment_name), |
|||
('res_model', '=', 'ir.attachment'), |
|||
('res_id', '=', transcription_id) |
|||
], limit=1) |
|||
return attachment_detail |
|||
attachment = get_attachment(file_name) |
|||
summary_attachment = get_attachment(summary_file_name) |
|||
|
|||
if attachment or summary_attachment: |
|||
existing_summary_content = ( |
|||
base64.b64decode(summary_attachment.datas).decode('utf-8')) |
|||
updated_summary_content = ( |
|||
existing_summary_content + "\n" + text_content) # Append new data |
|||
|
|||
attachment.sudo().write({ |
|||
'datas': base64.b64encode( |
|||
text_content.encode('utf-8')).decode('utf-8') |
|||
}) |
|||
summary_data_update = ( |
|||
create_summary(updated_summary_content)) |
|||
summary_text = ( |
|||
summary_data_update.choices[0].message.content) # Corrected response extraction |
|||
summary_attachment.sudo().write({ |
|||
'datas': base64.b64encode(summary_text.encode('utf-8')).decode( |
|||
'utf-8') |
|||
}) |
|||
else: |
|||
def create_attachment(filename, datas): |
|||
file = request.env['ir.attachment'].sudo().create({ |
|||
'name': filename, |
|||
'datas': datas, |
|||
'res_model': 'ir.attachment', |
|||
'res_id': transcription_id, |
|||
'type': 'binary', |
|||
'mimetype': 'text/plain', |
|||
}) |
|||
return file |
|||
attachment = create_attachment(file_name, new_file_base64) |
|||
summary_attachment = create_attachment(summary_file_name, file_data) |
|||
return {'success': True, 'attachment_id': attachment.id, 'summary': summary_attachment.id} |
|||
|
|||
@http.route('/get/transcription_data/summary', type='json', auth='public') |
|||
def get_transcription_data_summary(self, **kwargs): |
|||
"""Controller for return the specific transcription and summary file""" |
|||
transcription_id = False |
|||
summary_id = False |
|||
channel_id = kwargs['kwargs'].get('channelId', False) |
|||
attachments = request.env['ir.attachment'].sudo().search( |
|||
[('res_id', '=', int(channel_id)), |
|||
('res_model', '=', 'ir.attachment')]) |
|||
|
|||
for attachment in attachments: |
|||
if attachment.name == f"transcription_id_{channel_id}.txt": |
|||
transcription_id = attachment.id |
|||
else: |
|||
summary_id = attachment.id |
|||
return {'transcriptionId': transcription_id, |
|||
'summaryId': summary_id} |
|||
|
|||
@http.route('/create/send_transcription/record', type='json', auth='public') |
|||
def get_send_transcription_id(self, **kwargs): |
|||
"""create a record in send_mail_transcription using partner_ids,subject |
|||
email_body,transcription_attachment_ids, summary_attachment_ids and |
|||
return the corresponding record id""" |
|||
subject = kwargs['kwargs'].get('subject') |
|||
email_body = kwargs['kwargs'].get('email_body') |
|||
partner_ids = kwargs['kwargs'].get('partnerIds') |
|||
transcription_id = kwargs['kwargs'].get('transcriptionId') |
|||
summary_id = kwargs['kwargs'].get('summaryId') |
|||
transcription_id = request.env['ir.attachment'].browse(transcription_id) |
|||
summary_id = request.env['ir.attachment'].browse(summary_id) |
|||
send_mail_transcription_id = request.env['send.mail.transcription'].create( |
|||
{'partner_ids': partner_ids, |
|||
'subject': subject, |
|||
'email_body': email_body, |
|||
'transcription_attachment_ids': transcription_id, |
|||
'summary_attachment_ids': summary_id}) |
|||
return send_mail_transcription_id.id |
|||
|
|||
@http.route('/check/auto_mail_send', type='json', auth='public') |
|||
def check_auto_mail_send(self, **kwargs): |
|||
"""Here checking the configuration settings that auto_mail_send_option |
|||
enable or not and also get all the values from that and return the |
|||
specific participant details""" |
|||
channel_id = kwargs['kwargs'].get('channelId', False) |
|||
auto_mail_send = request.env['ir.config_parameter'].sudo().get_param( |
|||
"meeting_summarizer.auto_mail_send") |
|||
|
|||
select_users = request.env['ir.config_parameter'].sudo().get_param( |
|||
"meeting_summarizer.select_user") |
|||
|
|||
participants = [] |
|||
if auto_mail_send and select_users: |
|||
partner_details = request.env['discuss.channel.member'].sudo().search( |
|||
[('channel_id', '=', channel_id)] |
|||
) |
|||
host = request.env['discuss.channel'].browse(channel_id) |
|||
for rec in partner_details: |
|||
user = request.env['res.users'].search( |
|||
[('partner_id', '=', rec.partner_id.id)] |
|||
) |
|||
if user: |
|||
if select_users == 'host': |
|||
participants.append({ |
|||
'partner_id': host.create_uid.partner_id.id, |
|||
'email': host.create_uid.email |
|||
}) |
|||
break # Exit the loop after adding the host, no need for further logic |
|||
if user.has_group('base.group_user'): |
|||
participants.append({ |
|||
'partner_id': rec.partner_id.id, |
|||
'email': rec.partner_id.email |
|||
}) |
|||
return participants |
|||
|
|||
@http.route('/send/auto_email', type='json', auth='public') |
|||
def send_auto_mail(self, **kwargs): |
|||
"""Here sending automatic mail for selected users in settings.""" |
|||
transcription_id = kwargs['kwargs'].get('transcriptionId') |
|||
summary_id = kwargs['kwargs'].get('summaryId') |
|||
email_body = kwargs['kwargs'].get('email_body') |
|||
subject = kwargs['kwargs'].get('subject') |
|||
partners_email = kwargs['kwargs'].get('partners_email', |
|||
[]) |
|||
if not partners_email: |
|||
return { |
|||
'error': 'No valid email addresses found for the selected partners.'} |
|||
from_mail = request.env.user.email |
|||
attachment_ids = [] |
|||
if transcription_id and summary_id: |
|||
attachment_ids.append((4, transcription_id)) |
|||
attachment_ids.append((4, summary_id)) |
|||
email_values = { |
|||
'email_from': from_mail, |
|||
'email_to': ','.join(partners_email), # Convert email list to string |
|||
'subject': subject, |
|||
'body_html': email_body, |
|||
'attachment_ids': attachment_ids, # Attach files if available |
|||
} |
|||
email = request.env['mail.mail'].sudo().create(email_values) |
|||
email.send() |
|||
return True |
|||
|
|||
@http.route('/get/Meeting/creator', type='json', auth='public') |
|||
def get_meeting_creator(self, **kwargs): |
|||
"""Return the channel creator""" |
|||
channel_id = kwargs['kwargs'].get('channelId', False) |
|||
channel_details = request.env['discuss.channel'].sudo().browse(channel_id) |
|||
return channel_details.create_uid.id |
|||
|
|||
@http.route('/attach/transcription_data/summary', type='json', |
|||
auth='public') |
|||
def attach_transcription_data_summary(self, **kwargs): |
|||
"""Controller for return the specific transcription and summary file""" |
|||
transcription_id = False |
|||
summary_id = False |
|||
channel_id = kwargs['kwargs'].get('channelId', False) |
|||
attachments = request.env['ir.attachment'].sudo().search( |
|||
[('res_id', '=', int(channel_id)), |
|||
('res_model', '=', 'ir.attachment')]) |
|||
for attachment in attachments: |
|||
if attachment.name == f"transcription_id_{channel_id}.txt": |
|||
transcription_id = attachment.id |
|||
else: |
|||
summary_id = attachment.id |
|||
|
|||
channel = request.env['discuss.channel'].sudo().browse(int(channel_id)) |
|||
attachment_ids = list(filter(None, [transcription_id, summary_id])) |
|||
odoo_bot_user = request.env.ref('base.user_root') |
|||
channel.with_user(odoo_bot_user).message_post( |
|||
body="📝 Meeting transcription and summary are now available.", |
|||
message_type='comment', |
|||
subtype_xmlid='mail.mt_comment', |
|||
attachment_ids=attachment_ids |
|||
) |
|||
return True |
|||
@ -0,0 +1,22 @@ |
|||
<?xml version="1.0" encoding="UTF-8" ?> |
|||
<odoo> |
|||
<record id="email_template_transcription" model="mail.template"> |
|||
<!--create a new email template for meeting summarizer details--> |
|||
<field name="name">Account Report email template</field> |
|||
<field name="model_id" ref="meeting_summarizer.model_send_mail_transcription"/> |
|||
<field name="body_html" type="html"> |
|||
<div> |
|||
<div style="margin: 0px; padding: 0px;"> |
|||
<p style="margin: 0px; padding: 0px; font-size: 12px;"> |
|||
Hello, |
|||
<br/> |
|||
<span t-esc="object.email_body"/> |
|||
</p> |
|||
</div> |
|||
Regards, |
|||
<br/> |
|||
<span t-esc="object.create_uid.name"/> |
|||
</div> |
|||
</field> |
|||
</record> |
|||
</odoo> |
|||
@ -0,0 +1,8 @@ |
|||
## Module <meeting_summarizer> |
|||
|
|||
#### 25.07.2025 |
|||
#### Version 18.0.1.0.0 |
|||
#### ADD |
|||
|
|||
- Initial commit for Meeting Summarizer |
|||
- |
|||
@ -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 send_mail_transcription |
|||
@ -0,0 +1,53 @@ |
|||
# -*- 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/>. |
|||
# |
|||
############################################################################# |
|||
""" |
|||
This module extends Odoo's ResConfigSettings to provide configuration options |
|||
for the Meeting Summarizer module, including Open API settings and email options. |
|||
""" |
|||
from odoo import models, fields |
|||
|
|||
|
|||
class ResConfigSettings(models.TransientModel): |
|||
""" |
|||
This class extends the res.config.settings model to add configuration options |
|||
for the Meeting Summarizer module. It allows users to configure: |
|||
|
|||
- Whether the Open API feature is enabled. |
|||
- The API key to use with the Open API. |
|||
- Whether to automatically send transcription emails. |
|||
- Who should receive the emails (host or all logged-in users). |
|||
|
|||
All fields are stored as system-wide configuration parameters. |
|||
""" |
|||
_inherit = "res.config.settings" |
|||
|
|||
open_api_value = fields.Boolean(string="Open API Key", |
|||
config_parameter="meeting_summarizer.open_api_value" ) |
|||
open_api_key = fields.Char(string="Open API Value", |
|||
config_parameter="meeting_summarizer.open_api_key" ) |
|||
auto_mail_send = fields.Boolean(string="Automatically Send Mail", |
|||
config_parameter="meeting_summarizer.auto_mail_send") |
|||
select_user = fields.Selection( |
|||
selection=[ |
|||
('host', 'Host'), |
|||
('all_attendees', 'All Attendees'), |
|||
], string="Select Recipients", config_parameter="meeting_summarizer.select_user") |
|||
@ -0,0 +1,82 @@ |
|||
# -*- 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/>. |
|||
# |
|||
############################################################################# |
|||
""" |
|||
This module defines a transient model for sending transcription files via email in Odoo. |
|||
|
|||
The `SendMailTranscription` model facilitates the creation of a wizard where users can: |
|||
- Specify the recipient's email address. |
|||
- Enter the subject and body of the email. |
|||
- Choose whether to attach a transcription file. |
|||
|
|||
This module also includes the necessary logic for sending the email, potentially |
|||
using predefined email templates. |
|||
""" |
|||
from odoo import fields, models |
|||
|
|||
|
|||
class SendMailTranscription(models.TransientModel): |
|||
"""Created a new transient model for send mail""" |
|||
_name = 'send.mail.transcription' |
|||
_description = "Display send mail transcription wizard details" |
|||
|
|||
partner_ids = fields.Many2many('res.partner', string='Recipients', |
|||
help="Select multiple recipients") |
|||
subject = fields.Char(string='Subject', required=True, |
|||
help="Subject of the email") |
|||
email_body = fields.Html(required=True, String="Content", |
|||
help="Body of the email") |
|||
transcription_attachment_ids = fields.Many2many( |
|||
'ir.attachment','transcription_attachment_rel', string='Transcription', readonly=True, |
|||
help="Transcription attachment ") |
|||
summary_attachment_ids = fields.Many2many( |
|||
'ir.attachment','summary_attachment_rel', string='Summary', readonly=True, |
|||
help="Summary attachment ") |
|||
|
|||
def action_send_transcription(self): |
|||
"""Button action to send mail to the corresponding users that select in recipient mail""" |
|||
transcription_id = self.id |
|||
email_list = [','.join(self.partner_ids.mapped( |
|||
'email'))] # Collect emails from all partners |
|||
from_mail = self.env.user.email |
|||
mail_template = (self.env.ref( |
|||
'meeting_summarizer.email_template_transcription')) |
|||
attachment_ids = [] |
|||
|
|||
for attachment in self.transcription_attachment_ids: |
|||
attachment_ids.append( |
|||
(4, attachment.id)) |
|||
|
|||
# Loop through summary attachments and add them |
|||
for attachment in self.summary_attachment_ids: |
|||
attachment_ids.append( |
|||
(4, attachment.id)) # (4, attachment_id) adds attachment |
|||
|
|||
email_values = { |
|||
'email_from': from_mail, |
|||
'email_to': ','.join(email_list), |
|||
'subject': self.subject, |
|||
'body_html': self.email_body, |
|||
'attachment_ids': attachment_ids, # Attach the files here |
|||
} |
|||
# Send the email using the template |
|||
mail_template.send_mail(transcription_id, email_values=email_values, |
|||
force_send=True) |
|||
|
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 28 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: 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: 90 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 785 KiB |
|
After Width: | Height: | Size: 56 KiB |
@ -0,0 +1,890 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8"/> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
|||
<title>Meeting Summarizer</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> |
|||
<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; |
|||
"> |
|||
This module helps to generate summaries of meetings by transcribing and processing conversations in real time. </p> |
|||
<h1 class="text-center text-uppercase my-0" |
|||
style=" |
|||
color: #121212; |
|||
font-size: 46px; |
|||
font-weight: 700; |
|||
line-height: normal; |
|||
">Meeting Summarizer</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/screenshots/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"> |
|||
Retrieve and download the transcription data file. |
|||
</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"> |
|||
Get and download the transcription summary file. |
|||
</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"> |
|||
Send the transcription and summary files automatically to the selected users. |
|||
</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"> |
|||
Send the transcription and summary files manually to the selected users as well. |
|||
</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;">Meeting Summarizer</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> |
|||
<li class="nav-item"> |
|||
<a aria-controls="faq" aria-bs-selected="false" |
|||
class="nav-link" data-bs-toggle="tab" |
|||
href="#faq" id="faq-tab" role="tab" |
|||
style="color:#121212; font-weight:500; font-size:16px"> |
|||
FAQs</a> |
|||
</li> |
|||
<li class="nav-item"> |
|||
<a aria-controls="releases" |
|||
aria-bs-selected="false" class="nav-link" |
|||
data-bs-toggle="tab" href="#releases" |
|||
id="releases-tab" role="tab" |
|||
style="color:#121212; font-weight:500; font-size:16px">Releases</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)">Add Open API Key |
|||
</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"> |
|||
First, set the OpenAPI key to enable transcription summary creation.</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/1.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)">Manually send the transcription and summary files. |
|||
</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"> |
|||
When a new meeting starts with participants and the call end button is clicked, a wizard will automatically open with the transcription and summary files attached. The meeting description will be set as the subject. |
|||
These files can be sent to multiple participants by clicking the Send Mail button. |
|||
</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/2.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)"> |
|||
</span>Automatically send the transcription and summary files. |
|||
<span style="color: #7f54b3; font-size:calc(1.1rem + 1vw)"></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">When the 'Automatically Send Mail' feature is enabled in General Settings, |
|||
the system can automatically send the files based on the selected option. If 'Host' is selected, the files will be sent to the person who created the meeting. If 'All Attendees' is selected, |
|||
the files will be sent to all participants who attended the meeting, excluding public users.</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/3.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)"> |
|||
</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">The meeting contents are emailed in the following format.</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/4.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"> |
|||
Send transcription and summary files manually or automatically.</p> |
|||
</div> |
|||
|
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div aria-labelledby="faq-tab" |
|||
class="tab-pane fade show" id="faq" |
|||
role="tabpanel"> |
|||
<div class="row" |
|||
style=" background-color:#fff !important"> |
|||
<div class="col-md-12" aria-labelledby="faq-tab" |
|||
id="faq" role="tabpanel"> |
|||
<div class="accordion mb-4" id="accordion6"> |
|||
<div style="background-color:#fff"> |
|||
<!-- accordian --> |
|||
<div class="accordion" |
|||
id="accordion_faq"> |
|||
<!-- Question 1 --> |
|||
<div class="" |
|||
style="margin:1rem 0rem"> |
|||
<div aria-expanded="false" |
|||
class=" card-header collapsed" |
|||
data-bs-toggle="collapse" |
|||
data-bs-target="#collapseFAQOne" |
|||
href="#collapseFAQOne" |
|||
aria-controls="collapseFAQOne" |
|||
style="cursor: pointer; background-color:#f8f8f8; border:none; border-top-right-radius:10px; border-top-left-radius:10px; padding: 12px 24px;"> |
|||
<a class="card-title text-decoration-none" |
|||
style=" font-size:18px; line-height:30px; font-weight:500; color:#040f3a"> |
|||
Which meetings are supported for transcription? |
|||
<img alt="" |
|||
class="float-end" |
|||
src="//apps.odoocdn.com/apps/assets/16.0/index_test_odoo/assets/icons/down.svg?6ef7fd7" |
|||
width="25px"> |
|||
</a> |
|||
</div> |
|||
<div class="accordion-collapse collapse" |
|||
aria-labelledby="collapseFAQOne" |
|||
data-bs-parent="#accordion_faq" |
|||
id="collapseFAQOne" |
|||
style=" box-shadow: rgba(0, 0, 0, 0.05) 0px 6px 24px 0px; border: 1px solid #f8f8f8; border-bottom-right-radius:10px; border-bottom-left-radius:10px"> |
|||
<p style=" padding:0.75rem 1.25rem; font-size:16px; line-height:27px; color:#888; font-weight:normal; border-bottom-right-radius:10px; border-bottom-left-radius:10px"> |
|||
This feature allows you |
|||
to add custom questions |
|||
related to the product during |
|||
the ordering process in a POS session. |
|||
</p> |
|||
</div> |
|||
</div> |
|||
<!-- Question 2 --> |
|||
</div> |
|||
<!-- --> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div aria-labelledby="releases-tab" |
|||
class="tab-pane fade show" id="releases" |
|||
role="tabpanel"> |
|||
<!-- Release Notes --> |
|||
<div class="row pt-5 m-0"> |
|||
<div class="col-md-3"> |
|||
<h4 style="font-size:16px; font-weight:600; color:#514F4F; margin:0; line-height:26px;"> |
|||
Latest Release 18.0.1.0.0 |
|||
</h4> |
|||
<span style="font-size:14px; color:#7A7979; display:block; margin-bottom:20px;"> |
|||
14th May, 2025 |
|||
</span> |
|||
</div> |
|||
<div class="col-md-8"> |
|||
<div style="padding:0 0 40px"> |
|||
<div style="margin:0 0 10px"> |
|||
<div style="display:inline-block; padding:0px 8px; color:#514F4F; background-color:#FFD8D8; border-radius:20px"> |
|||
Add |
|||
</div> |
|||
</div> |
|||
<div class="d-flex m-0" |
|||
style="color:#7A7979;"> |
|||
<ul class="pl-3 mb-0"> |
|||
<li> |
|||
Initial Commit |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
<div style="padding:0 0 0; border-bottom:1px solid #E3E3E3"> |
|||
</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/whatsapp_redirect" 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"> |
|||
Send Whatsapp Message Odoo18</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/rest_api_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/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 rest API</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/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/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"> |
|||
Odoo Whatsapp Connector</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/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"> |
|||
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/crm_dashboard" 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"> |
|||
CRM 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,17 @@ |
|||
/* @odoo-module */ |
|||
import { AttachmentList } from "@mail/core/common/attachment_list"; |
|||
import { registry } from "@web/core/registry"; |
|||
import { patch } from "@web/core/utils/patch"; |
|||
import { user } from "@web/core/user"; |
|||
import { useState } from "@odoo/owl"; |
|||
const ImageActions = AttachmentList.components.ImageActions; |
|||
|
|||
const customImage = { |
|||
setup(){ |
|||
super.setup(); |
|||
this.state = useState({ |
|||
isPublicUser : user.userId === null |
|||
}); |
|||
} |
|||
}; |
|||
patch(AttachmentList.prototype, customImage); |
|||
@ -0,0 +1,580 @@ |
|||
/* @odoo-module */ |
|||
import { patch } from "@web/core/utils/patch"; |
|||
import { CallActionList } from "@mail/discuss/call/common/call_action_list"; |
|||
import { useService } from "@web/core/utils/hooks"; |
|||
import { useState, useEffect } from "@odoo/owl"; |
|||
import { rpc } from "@web/core/network/rpc"; |
|||
import { user } from "@web/core/user"; |
|||
|
|||
class SpeechRecognitionQueue { |
|||
constructor(transcriptionHandler, user) { |
|||
this.transcriptionHandler = transcriptionHandler; |
|||
this.shouldRestart = true; |
|||
this.user = user; |
|||
this.isInitialized = false; |
|||
this.active = false; |
|||
this.silenceTimer = null; |
|||
this.bufferText = ""; |
|||
this.isFinal = false; |
|||
this.stopCall = false; |
|||
this.recognitionActive = false; |
|||
} |
|||
|
|||
async initSpeechRecognition() { |
|||
if (this.isInitialized) return; |
|||
|
|||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; |
|||
if (!SpeechRecognition) { |
|||
console.warn("Speech Recognition not supported in this browser"); |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
// Request audio permissions explicitly
|
|||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); |
|||
// Keep track of the stream to ensure it stays active
|
|||
this.audioStream = stream; |
|||
this.recognition = new SpeechRecognition(); |
|||
this.recognition.continuous = true; |
|||
this.recognition.interimResults = true; |
|||
this.recognition.lang = 'en-US'; |
|||
this.setupEventListeners(); |
|||
this.isInitialized = true; |
|||
} catch (error) { |
|||
console.error("Error initializing speech recognition:", error); |
|||
this.shouldRestart = false; |
|||
// Provide more specific error feedback
|
|||
if (error.name === 'NotAllowedError') { |
|||
console.error("Microphone permission denied. Please enable microphone access."); |
|||
} |
|||
} |
|||
} |
|||
|
|||
setupEventListeners() { |
|||
this.recognition.onerror = (event) => { |
|||
console.error("Speech Recognition Error:", event.error, event); |
|||
if (event.error === 'not-allowed') { |
|||
this.shouldRestart = false; |
|||
console.error("Microphone permission denied"); |
|||
} else if (event.error === 'audio-capture') { |
|||
this.shouldRestart = false; |
|||
console.error("No microphone detected"); |
|||
} else if (this.shouldRestart && this.active) { |
|||
// Use exponential backoff for retries
|
|||
const delay = Math.min(2000, 1000 * Math.pow(1.5, this.errorCount || 0)); |
|||
this.errorCount = (this.errorCount || 0) + 1; |
|||
setTimeout(() => this.restartRecognition(), delay); |
|||
} |
|||
}; |
|||
|
|||
this.recognition.onend = () => { |
|||
this.recognitionActive = false; |
|||
// Process any remaining buffered text
|
|||
if (this.bufferText.trim()) { |
|||
this.transcriptionHandler({ |
|||
text: this.bufferText.trim(), |
|||
userId: this.user?.id, |
|||
timestamp: new Date().toISOString() |
|||
}); |
|||
this.bufferText = ""; |
|||
} |
|||
|
|||
if (this.shouldRestart && this.active) { |
|||
// Add a small delay before restarting
|
|||
setTimeout(() => this.restartRecognition(), 300); |
|||
} |
|||
}; |
|||
|
|||
this.recognition.onstart = () => { |
|||
this.recognitionActive = true; |
|||
this.errorCount = 0; // Reset error count on successful start
|
|||
}; |
|||
|
|||
this.recognition.onresult = (event) => { |
|||
let finalTranscript = ""; |
|||
let interimTranscript = ""; |
|||
let hasFinal = false; |
|||
|
|||
// Process all results since last event
|
|||
for (let i = event.resultIndex; i < event.results.length; i++) { |
|||
const transcript = event.results[i][0].transcript; |
|||
|
|||
if (event.results[i].isFinal) { |
|||
finalTranscript += transcript; |
|||
hasFinal = true; |
|||
} else { |
|||
interimTranscript += transcript; |
|||
} |
|||
} |
|||
|
|||
// Handle final results
|
|||
if (hasFinal && finalTranscript.trim()) { |
|||
this.bufferText += finalTranscript; |
|||
|
|||
// Send complete utterances
|
|||
const completeUtterances = this.extractCompleteUtterances(this.bufferText); |
|||
if (completeUtterances) { |
|||
this.transcriptionHandler({ |
|||
text: completeUtterances, |
|||
userId: this.user?.id, |
|||
timestamp: new Date().toISOString() |
|||
}); |
|||
|
|||
// Remove sent utterances from buffer
|
|||
this.bufferText = this.bufferText.slice(completeUtterances.length); |
|||
} |
|||
} |
|||
|
|||
// Reset silence timer on any speech
|
|||
if (this.silenceTimer) { |
|||
clearTimeout(this.silenceTimer); |
|||
} |
|||
|
|||
// Set silence timer to send remaining buffer after pause
|
|||
this.silenceTimer = setTimeout(() => { |
|||
if (this.bufferText.trim()) { |
|||
this.transcriptionHandler({ |
|||
text: this.bufferText.trim(), |
|||
userId: this.user?.id, |
|||
timestamp: new Date().toISOString() |
|||
}); |
|||
this.bufferText = ""; |
|||
} |
|||
}, 2000); // 2 second silence sends buffer
|
|||
}; |
|||
} |
|||
|
|||
// Helper to extract complete utterances (ending with punctuation)
|
|||
extractCompleteUtterances(text) { |
|||
if (!text) return ""; |
|||
|
|||
// Find the last occurrence of sentence-ending punctuation
|
|||
const match = text.match(/[.!?]\s*(?=[A-Z]|$)/); |
|||
if (!match) return ""; |
|||
|
|||
const endPos = match.index + 1; |
|||
return text.substring(0, endPos + 1).trim(); |
|||
} |
|||
|
|||
async restartRecognition() { |
|||
if (!this.active || !this.shouldRestart) return; |
|||
try { |
|||
// Only stop if actually running
|
|||
if (this.recognitionActive) { |
|||
try { |
|||
await this.recognition.stop(); |
|||
// Wait for onend event to complete
|
|||
await new Promise(resolve => { |
|||
const checkActive = () => { |
|||
if (!this.recognitionActive) { |
|||
resolve(); |
|||
} else { |
|||
setTimeout(checkActive, 100); |
|||
} |
|||
}; |
|||
checkActive(); |
|||
}); |
|||
} catch (error) { |
|||
console.log("Error stopping recognition:", error); |
|||
} |
|||
} |
|||
|
|||
// Small delay to ensure complete shutdown
|
|||
await new Promise(resolve => setTimeout(resolve, 500)); |
|||
|
|||
// Only try to start if we're still supposed to be active
|
|||
if (this.active && this.shouldRestart) { |
|||
await this.recognition.start(); |
|||
} |
|||
} catch (error) { |
|||
console.error("Error in restartRecognition:", error); |
|||
|
|||
// Try again after a longer delay if we're still supposed to be active
|
|||
if (this.active && this.shouldRestart) { |
|||
setTimeout(() => this.restartRecognition(), 2000); |
|||
} |
|||
} |
|||
} |
|||
|
|||
async start() { |
|||
if (!this.isInitialized) { |
|||
await this.initSpeechRecognition(); |
|||
} |
|||
|
|||
if (this.recognition) { |
|||
this.shouldRestart = true; // Reset the flag when starting
|
|||
this.active = true; |
|||
this.bufferText = ""; // Clear any previous buffer
|
|||
this.errorCount = 0; |
|||
|
|||
try { |
|||
// Only start if not already running
|
|||
if (!this.recognitionActive) { |
|||
await this.recognition.start(); |
|||
} else { |
|||
console.log("Recognition already active, not starting again"); |
|||
} |
|||
} catch (error) { |
|||
if (error.name === 'NotAllowedError') { |
|||
this.shouldRestart = false; |
|||
// Request permissions again
|
|||
try { |
|||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); |
|||
this.audioStream = stream; |
|||
// Try starting again after permission granted
|
|||
setTimeout(() => this.start(), 1000); |
|||
} catch (permError) { |
|||
console.error("Permission request failed:", permError); |
|||
} |
|||
} else if (this.active) { |
|||
// Try again after a delay
|
|||
setTimeout(() => this.start(), 1000); |
|||
} |
|||
} |
|||
} else { |
|||
console.error("Recognition not initialized properly"); |
|||
} |
|||
} |
|||
|
|||
stop() { |
|||
this.active = false; |
|||
this.stopCall = true; |
|||
this.shouldRestart = false; // This prevents restarting
|
|||
|
|||
if (this.silenceTimer) { |
|||
clearTimeout(this.silenceTimer); |
|||
this.silenceTimer = null; |
|||
} |
|||
|
|||
// Process any remaining buffer
|
|||
if (this.bufferText.trim()) { |
|||
this.transcriptionHandler({ |
|||
text: this.bufferText.trim(), |
|||
userId: this.user?.id, |
|||
timestamp: new Date().toISOString() |
|||
}); |
|||
this.bufferText = ""; |
|||
} |
|||
|
|||
if (this.recognition && this.recognitionActive) { |
|||
try { |
|||
this.recognition.stop(); |
|||
} catch (error) { |
|||
console.error("Error stopping recognition:", error); |
|||
} |
|||
} |
|||
|
|||
// Release audio stream if it exists
|
|||
if (this.audioStream) { |
|||
this.audioStream.getTracks().forEach(track => track.stop()); |
|||
this.audioStream = null; |
|||
} |
|||
} |
|||
|
|||
reset() { |
|||
this.shouldRestart = true; |
|||
this.active = false; |
|||
this.bufferText = ""; |
|||
this.errorCount = 0; |
|||
|
|||
if (this.silenceTimer) { |
|||
clearTimeout(this.silenceTimer); |
|||
this.silenceTimer = null; |
|||
} |
|||
|
|||
// Stop any active recognition
|
|||
if (this.recognition && this.recognitionActive) { |
|||
try { |
|||
this.recognition.stop(); |
|||
} catch (error) { |
|||
console.error("Error stopping recognition during reset:", error); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
patch(CallActionList.prototype, { |
|||
async setup() { |
|||
super.setup(...arguments); |
|||
try { |
|||
this.action = useService("action"); |
|||
this.user = user; |
|||
} catch (error) { |
|||
console.warn("Service 'action' is not available for guest users."); |
|||
this.action = null; // Fallback for guests
|
|||
this.user = null; // Fallback for guests
|
|||
} |
|||
|
|||
this.state = useState({ |
|||
isRecording: false, |
|||
transcriptions: {}, |
|||
currentSpeaker: null, |
|||
userName: null, |
|||
}); |
|||
|
|||
useEffect(() => { |
|||
this.speechQueue = new SpeechRecognitionQueue( |
|||
this.insertTranscription.bind(this), |
|||
this.user |
|||
); |
|||
this.initializeUserInfo(); |
|||
|
|||
// Cleanup function for when component is destroyed
|
|||
return () => { |
|||
if (this.speechQueue) { |
|||
this.speechQueue.stop(); |
|||
} |
|||
}; |
|||
}, () => []); |
|||
|
|||
useEffect(() => { |
|||
try { |
|||
const hasMultipleMembers = this.props.thread && |
|||
this.props.thread.channelMembers && |
|||
this.props.thread.channelMembers.length > 0; |
|||
|
|||
|
|||
if (hasMultipleMembers && !this.state.isRecording) { |
|||
this.startRecording(); |
|||
} else if (!hasMultipleMembers && this.state.isRecording) { |
|||
this.stopRecording(); |
|||
} |
|||
} catch (error) { |
|||
console.error("Error in channel members effect:", error); |
|||
} |
|||
}, () => [this.props.thread?.channelMembers?.length]); |
|||
}, |
|||
|
|||
async onClickMicrophone(ev) { |
|||
await super.onClickMicrophone(ev); |
|||
|
|||
try { |
|||
if (this.rtc?.state?.selfSession?.isMute) { |
|||
this.stopRecording(); |
|||
} else { |
|||
this.startRecording(); |
|||
} |
|||
} catch (error) { |
|||
console.error("Error handling microphone click:", error); |
|||
} |
|||
}, |
|||
|
|||
async initializeUserInfo() { |
|||
try { |
|||
let userName = "Unknown User"; // Default fallback
|
|||
|
|||
// Try multiple methods to get user name
|
|||
if (this?.user?.name) { |
|||
userName = this.user.name; |
|||
} else if (this?.__owl__?.parent?.parent?.children?.__11?.component?.state?.value) { |
|||
userName = this.__owl__.parent.parent.children.__11.component.state.value; |
|||
} |
|||
|
|||
this.state.userName = userName; |
|||
} catch (error) { |
|||
console.error("Error initializing user info:", error); |
|||
this.state.userName = "Unknown User"; // Fallback
|
|||
} |
|||
}, |
|||
|
|||
async CreateTranscriptionFile() { |
|||
try { |
|||
if (!this.props.thread || !this.props.thread.id) { |
|||
console.error("Cannot create transcription file: thread or thread ID is missing"); |
|||
return; |
|||
} |
|||
|
|||
const fileName = `transcription_${this.props.thread.id}.txt`; |
|||
const Id = this.props.thread.id; |
|||
|
|||
const response = await rpc('/create/transcription_file_summary', { |
|||
kwargs: { id: Id } |
|||
}); |
|||
} catch (error) { |
|||
console.error("Error creating transcription file:", error); |
|||
} |
|||
}, |
|||
|
|||
async onClickToggleAudioCall(ev) { |
|||
try { |
|||
const buttonDis = document.querySelector('[aria-label="Disconnect"]'); |
|||
const res = await super.onClickToggleAudioCall(ev); |
|||
if (!this.props.thread) { |
|||
console.error("Thread is undefined in onClickToggleAudioCall"); |
|||
return res; |
|||
} |
|||
|
|||
const subject = this.props.thread.description || " "; |
|||
await this.CreateTranscriptionFile(); |
|||
|
|||
setTimeout(async () => { |
|||
try { |
|||
const result = await rpc('/get/transcription_data/summary', { |
|||
kwargs: { |
|||
channelId: this.props.thread.id, |
|||
}, |
|||
}); |
|||
|
|||
const MeetingAdmin = await rpc('/get/Meeting/creator', { |
|||
kwargs: { |
|||
channelId: this.props.thread.id, |
|||
}, |
|||
}); |
|||
if ((this.user?.userId == MeetingAdmin) && buttonDis && result?.transcriptionId && result?.summaryId) { |
|||
try { |
|||
this.stopRecording(); |
|||
|
|||
const partner_details = await rpc('/check/auto_mail_send', { |
|||
kwargs: { |
|||
channelId: this.props.thread.id, |
|||
}, |
|||
}); |
|||
|
|||
const partnerIds = partner_details.map(p => p.partner_id); |
|||
|
|||
const transcriptionId = await rpc('/create/send_transcription/record', { |
|||
kwargs: { |
|||
partnerIds: partnerIds, |
|||
subject: subject, |
|||
email_body: "<p>Meeting content here...</p>", |
|||
transcriptionId: result.transcriptionId, |
|||
summaryId: result.summaryId, |
|||
}, |
|||
}); |
|||
|
|||
if (partner_details.length != 0) { |
|||
const partners_email = partner_details.map(p => p.email); |
|||
|
|||
const emailResponse = await rpc('/send/auto_email', { |
|||
kwargs: { |
|||
partners_email: partners_email, |
|||
subject: subject, |
|||
email_body: "<p>Meeting content here...</p>", |
|||
transcriptionId: result.transcriptionId, |
|||
summaryId: result.summaryId, |
|||
}, |
|||
}); |
|||
|
|||
} else { |
|||
await this.tryOpeningTranscription(transcriptionId); |
|||
} |
|||
} catch (error) { |
|||
console.error("Error in admin meeting end processing:", error); |
|||
} |
|||
} |
|||
} catch (error) { |
|||
console.error("Error in transcription processing timeout:", error); |
|||
} |
|||
}, 500); |
|||
|
|||
return res; |
|||
} catch (error) { |
|||
console.error("Error in onClickToggleAudioCall:", error); |
|||
return false; |
|||
} |
|||
}, |
|||
|
|||
async tryOpeningTranscription(transcriptionId) { |
|||
if (!this.action) { |
|||
console.error("Error: this.action is undefined."); |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
const actionData = { |
|||
name: "Send mail Transcription", |
|||
type: "ir.actions.act_window", |
|||
res_model: "send.mail.transcription", |
|||
res_id: transcriptionId, |
|||
view_mode: "form", |
|||
views: [[false, "form"]], // Ensures proper view format
|
|||
target: "new", |
|||
}; |
|||
|
|||
await this.action.doAction(actionData); |
|||
} catch (error) { |
|||
console.error("Error in doAction:", error); |
|||
} |
|||
}, |
|||
|
|||
async startRecording() { |
|||
try { |
|||
if (!this.speechQueue) { |
|||
this.speechQueue = new SpeechRecognitionQueue( |
|||
this.insertTranscription.bind(this), |
|||
this.user |
|||
); |
|||
} |
|||
|
|||
await this.speechQueue.start(); |
|||
this.state.isRecording = true; |
|||
this.state.currentSpeaker = this.state.userName || (this.user?.name || "Unknown User"); |
|||
|
|||
if (!this.state.transcriptions[this.state.currentSpeaker]) { |
|||
this.state.transcriptions[this.state.currentSpeaker] = []; |
|||
} |
|||
|
|||
} catch (error) { |
|||
console.error("Error starting recording:", error); |
|||
this.state.isRecording = false; |
|||
} |
|||
}, |
|||
|
|||
async insertTranscription(transcriptionData) { |
|||
const { text, userId, timestamp } = transcriptionData; |
|||
|
|||
let speakerName = "Unknown User"; |
|||
|
|||
// Try multiple methods to get speaker name
|
|||
if (this.state.userName) { |
|||
speakerName = this.state.userName; |
|||
} else if (this.user && this.user.name) { |
|||
speakerName = this.user.name; |
|||
} |
|||
|
|||
const formattedText = `(${new Date(timestamp).toLocaleString()})\n\t${speakerName} : ${text}\n`; |
|||
|
|||
try { |
|||
if (!this.props.thread || !this.props.thread.id) { |
|||
console.error("Cannot send transcription: thread or thread ID is missing"); |
|||
return; |
|||
} |
|||
|
|||
const response = await rpc('/get/transcription_data', { |
|||
data: formattedText, |
|||
id: this.props.thread.id, |
|||
userId: userId, |
|||
timestamp: timestamp |
|||
}); |
|||
|
|||
} catch (error) { |
|||
console.error("Error sending transcription:", error); |
|||
} |
|||
}, |
|||
|
|||
async stopRecording() { |
|||
|
|||
if (this.state.isRecording) { |
|||
if (this.speechQueue) { |
|||
this.speechQueue.stop(); |
|||
} |
|||
this.state.isRecording = false; |
|||
} |
|||
|
|||
try { |
|||
if (!this.props.thread || !this.props.thread.id) { |
|||
console.error("Cannot attach transcription: thread or thread ID is missing"); |
|||
return; |
|||
} |
|||
|
|||
await rpc('/attach/transcription_data/summary', { |
|||
kwargs: { |
|||
channelId: this.props.thread.id, |
|||
}, |
|||
}); |
|||
|
|||
} catch (error) { |
|||
console.error("Error attaching transcription data:", error); |
|||
} |
|||
}, |
|||
}); |
|||
@ -0,0 +1,15 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<templates xml:space="preserve"> |
|||
<t t-inherit="mail.AttachmentList" t-inherit-mode="extension"> |
|||
<!--Inherit mail.AttachmentList to set invisible download attachment button to public users--> |
|||
<xpath expr="//button[@t-elif='canDownload(attachment)']" position="replace"> |
|||
<button t-if="canDownload(attachment) and !state.isPublicUser" |
|||
class="btn d-flex align-items-center justify-content-center w-100 h-100 rounded-0" |
|||
t-attf-class="bg-300" |
|||
t-att-data-download-url="attachment.downloadUrl" |
|||
t-on-click.stop="() => this.onClickDownload(attachment)" title="Download"> |
|||
<i class="fa fa-download" role="img" aria-label="Download"/> |
|||
</button> |
|||
</xpath> |
|||
</t> |
|||
</templates> |
|||
@ -0,0 +1,29 @@ |
|||
<odoo> |
|||
<record id="view_res_config_settings_inherit" model="ir.ui.view"> |
|||
<!--Inherit model res.config.settings to add a block--> |
|||
<field name="name">res.config.settings.inherit.integrations</field> |
|||
<field name="model">res.config.settings</field> |
|||
<field name="inherit_id" |
|||
ref="base_setup.res_config_settings_view_form"/> |
|||
<field name="arch" type="xml"> |
|||
<xpath expr="//block[@name='integration']" position="after"> |
|||
<block title="Meeting Summarizer" name="meeting_summarizer"> |
|||
<setting help="Add API Key to summarize the transcription" |
|||
id="open_api"> |
|||
<field name="open_api_value"/> |
|||
<div class="col-6" invisible="not open_api_value"> |
|||
<field name="open_api_key" required="open_api_value == True" style="width:80%"/> |
|||
</div> |
|||
</setting> |
|||
<setting help="Enable this feature to send transcription details automatically" |
|||
id="auto_send_mail_id"> |
|||
<field name="auto_mail_send"/> |
|||
<div class="col-6" invisible="not auto_mail_send"> |
|||
<field name="select_user" class="o_light_label" widget="radio" options="{'horizontal': true}" required="auto_mail_send == True"/> |
|||
</div> |
|||
</setting> |
|||
</block> |
|||
</xpath> |
|||
</field> |
|||
</record> |
|||
</odoo> |
|||
@ -0,0 +1,25 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<!--Send mail wizard view --> |
|||
<record id="send_mail_transcription_view_form" model="ir.ui.view"> |
|||
<field name="name">send.mail.transcription.view.form</field> |
|||
<field name="model">send.mail.transcription</field> |
|||
<field name="arch" type="xml"> |
|||
<form> |
|||
<group> |
|||
<group> |
|||
<field name="partner_ids" widget="many2many_tags"/> |
|||
<field name="subject"/> |
|||
<field name="email_body"/> |
|||
<field name="transcription_attachment_ids" widget="many2many_tags"/> |
|||
<field name="summary_attachment_ids" widget="many2many_tags"/> |
|||
</group> |
|||
</group> |
|||
<footer> |
|||
<button name="action_send_transcription" string="Send Mail" type="object" class="oe_highlight"/> |
|||
<button string="Cancel" class="oe_link" special="cancel"/> |
|||
</footer> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
</odoo> |
|||