3 Commits

Author SHA1 Message Date
Josako
67ceb57b79 - Changelog to 2.3.5-alfa 2025-06-10 20:57:07 +02:00
Josako
23b49516cb - Create framework for chat-client, including logo, explanatory text, color settings, ...
- remove allowed_langages from tenant
- Correct bugs in Tenant, TenantMake, SpecialistMagicLink
- Change chat client customisation elements
2025-06-10 20:52:01 +02:00
Josako
9cc266b97f - Corrections to tenant, catalog, and tenant_make
- Clean-up of tenant elements
- ensure the chat_client get's it's initial call rifht.
2025-06-10 16:10:08 +02:00
17 changed files with 394 additions and 82 deletions

View File

@@ -45,6 +45,21 @@ class Specialist(db.Model):
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now()) updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now())
updated_by = db.Column(db.Integer, db.ForeignKey(User.id)) updated_by = db.Column(db.Integer, db.ForeignKey(User.id))
def __repr__(self):
return f"<Specialist {self.id}: {self.name}>"
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'description': self.description,
'type': self.type,
'type_version': self.type_version,
'configuration': self.configuration,
'arguments': self.arguments,
'active': self.active,
}
class EveAIAsset(db.Model): class EveAIAsset(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@@ -238,3 +253,14 @@ class SpecialistMagicLink(db.Model):
def __repr__(self): def __repr__(self):
return f"<SpecialistMagicLink {self.specialist_id} {self.magic_link_code}>" return f"<SpecialistMagicLink {self.specialist_id} {self.magic_link_code}>"
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'description': self.description,
'magic_link_code': self.magic_link_code,
'valid_from': self.valid_from,
'valid_to': self.valid_to,
'specialist_args': self.specialist_args,
}

View File

@@ -28,7 +28,6 @@ class Tenant(db.Model):
# language information # language information
default_language = db.Column(db.String(2), nullable=True) default_language = db.Column(db.String(2), nullable=True)
allowed_languages = db.Column(ARRAY(sa.String(2)), nullable=True)
# Entitlements # Entitlements
currency = db.Column(db.String(20), nullable=True) currency = db.Column(db.String(20), nullable=True)
@@ -63,7 +62,6 @@ class Tenant(db.Model):
'timezone': self.timezone, 'timezone': self.timezone,
'type': self.type, 'type': self.type,
'default_language': self.default_language, 'default_language': self.default_language,
'allowed_languages': self.allowed_languages,
'currency': self.currency, 'currency': self.currency,
'default_tenant_make_id': self.default_tenant_make_id, 'default_tenant_make_id': self.default_tenant_make_id,
} }
@@ -198,6 +196,20 @@ class TenantMake(db.Model):
updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now()) updated_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now(), onupdate=db.func.now())
updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id')) updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'))
def __repr__(self):
return f"<TenantMake {self.id} for tenant {self.tenant_id}: {self.name}>"
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'description': self.description,
'active': self.active,
'website': self.website,
'logo_url': self.logo_url,
'chat_customisation_options': self.chat_customisation_options,
}
class Partner(db.Model): class Partner(db.Model):
__bind_key__ = 'public' __bind_key__ = 'public'

View File

@@ -220,3 +220,18 @@ class SpecialistServices:
db.session.add(tool) db.session.add(tool)
current_app.logger.info(f"Created tool {tool.id} of type {tool_type}") current_app.logger.info(f"Created tool {tool.id} of type {tool_type}")
return tool return tool
@staticmethod
def get_specialist_system_field(specialist_id, config_name, system_name):
"""Get the value of a system field in a specialist's configuration. Returns the actual value, or None."""
specialist = Specialist.query.get(specialist_id)
if not specialist:
raise ValueError(f"Specialist with ID {specialist_id} not found")
config = cache_manager.specialists_config_cache.get_config(specialist.type, specialist.type_version)
if not config:
raise ValueError(f"No configuration found for {specialist.type} version {specialist.version}")
potential_field = config.get(config_name, None)
if potential_field:
if potential_field.type == 'system' and potential_field.system_name == system_name:
return specialist.configuration.get(config_name, None)
return None

View File

@@ -21,10 +21,13 @@ def get_default_chat_customisation(tenant_customisation=None):
'background_color': '#ffffff', 'background_color': '#ffffff',
'text_color': '#212529', 'text_color': '#212529',
'sidebar_color': '#f8f9fa', 'sidebar_color': '#f8f9fa',
'logo_url': None, 'sidebar_background': '#2c3e50',
'sidebar_text': None, 'gradient_start_color': '#f5f7fa',
'gradient_end_color': '#c3cfe2',
'markdown_background_color': 'transparent',
'markdown_text_color': '#ffffff',
'sidebar_markdown': '',
'welcome_message': 'Hello! How can I help you today?', 'welcome_message': 'Hello! How can I help you today?',
'team_info': []
} }
# If no tenant customization is provided, return the defaults # If no tenant customization is provided, return the defaults

View File

@@ -26,9 +26,34 @@ configuration:
description: "Sidebar Color" description: "Sidebar Color"
type: "color" type: "color"
required: false required: false
"sidebar_text": "sidebar_background":
name: "Sidebar Text" name: "Sidebar Background"
description: "Text to be shown in the sidebar" description: "Sidebar Background Color"
type: "color"
required: false
"markdown_background_color":
name: "Markdown Background"
description: "Markdown Background Color"
type: "color"
required: false
"markdown_text_color":
name: "Markdown Text"
description: "Markdown Text Color"
type: "color"
required: false
"gradient_start_color":
name: "Gradient Start Color"
description: "Start Color for the gradient in the Chat Area"
type: "color"
required: false
"gradient_end_color":
name: "Gradient End Color"
description: "End Color for the gradient in the Chat Area"
type: "color"
required: false
"sidebar_markdown":
name: "Sidebar Markdown"
description: "Sidebar Markdown-formatted Text"
type: "text" type: "text"
required: false required: false
"welcome_message": "welcome_message":

View File

@@ -5,6 +5,31 @@ All notable changes to EveAI will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.3.5-alfa]
### Added
- Chat Client Initialisation (based on SpecialistMagicLink code)
- Definition of framework for the chat_client (using vue.js)
### Changed
- Remove AllowedLanguages from Tenant
- Remove Tenant URL (now in Make)
- Adapt chat client customisation options
### Fixed
- Several Bugfixes to administrative app
## [2.3.4-alfa]
### Added
- Introduction of Tenant Make
- Introduction of 'system' type for dynamic attributes
- Introduce Tenant Make to Traicie Specialists
### Changed
- Enable Specialist 'activation' / 'deactivation'
- Unique constraints introduced for Catalog Name (tenant level) and make name (public level)
## [2.3.3-alfa] ## [2.3.3-alfa]
### Added ### Added

View File

@@ -8,7 +8,19 @@
{% endmacro %} {% endmacro %}
{% macro render_field_content(field, disabled=False, readonly=False, class='') %} {% macro render_field_content(field, disabled=False, readonly=False, class='') %}
{% if field.type == 'BooleanField' %} {# Check if this is a hidden input field, if so, render only the field without label #}
{{ debug_to_console("Field Class: ", field.widget.__class__.__name__) }}
{% if field.widget.__class__.__name__ == 'HiddenInput' %}
{{ debug_to_console("Hidden Field: ", "Detected") }}
{{ field(class="form-control " + class, disabled=disabled, readonly=readonly) }}
{% if field.errors %}
<div class="invalid-feedback d-block">
{% for error in field.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
{% elif field.type == 'BooleanField' %}
<div class="form-group"> <div class="form-group">
<div class="form-check form-switch"> <div class="form-check form-switch">
{{ field(class="form-check-input " + class, disabled=disabled, readonly=readonly, required=False) }} {{ field(class="form-check-input " + class, disabled=disabled, readonly=readonly, required=False) }}

View File

@@ -1,4 +1,4 @@
from flask import session from flask import session, current_app
from flask_security import current_user from flask_security import current_user
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, SelectField from wtforms import StringField, SelectField
@@ -36,7 +36,7 @@ class SessionDefaultsForm(FlaskForm):
else: else:
self.partner_name.data = "" self.partner_name.data = ""
self.default_language.choices = [(lang, lang.lower()) for lang in self.default_language.choices = [(lang, lang.lower()) for lang in
session.get('tenant').get('allowed_languages')] current_app.config['SUPPORTED_LANGUAGES']]
self.default_language.data = session.get('default_language') self.default_language.data = session.get('default_language')
# Get a new session for catalog queries # Get a new session for catalog queries

View File

@@ -26,7 +26,6 @@ def validate_catalog_name(form, field):
class CatalogForm(FlaskForm): class CatalogForm(FlaskForm):
id = IntegerField('ID', widget=HiddenInput())
name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_catalog_name]) name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_catalog_name])
description = TextAreaField('Description', validators=[Optional()]) description = TextAreaField('Description', validators=[Optional()])
@@ -191,7 +190,7 @@ class AddDocumentForm(DynamicFormBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.language.choices = [(language, language) for language in self.language.choices = [(language, language) for language in
session.get('tenant').get('allowed_languages')] current_app.config['SUPPORTED_LANGUAGES']]
if not self.language.data: if not self.language.data:
self.language.data = session.get('tenant').get('default_language') self.language.data = session.get('tenant').get('default_language')
@@ -211,7 +210,7 @@ class AddURLForm(DynamicFormBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.language.choices = [(language, language) for language in self.language.choices = [(language, language) for language in
session.get('tenant').get('allowed_languages')] current_app.config['SUPPORTED_LANGUAGES']]
if not self.language.data: if not self.language.data:
self.language.data = session.get('tenant').get('default_language') self.language.data = session.get('tenant').get('default_language')

View File

@@ -141,7 +141,6 @@ class SpecialistMagicLinkForm(FlaskForm):
description = TextAreaField('Description', validators=[Optional()]) description = TextAreaField('Description', validators=[Optional()])
magic_link_code = StringField('Magic Link Code', validators=[DataRequired(), Length(max=55)], render_kw={'readonly': True}) magic_link_code = StringField('Magic Link Code', validators=[DataRequired(), Length(max=55)], render_kw={'readonly': True})
specialist_id = SelectField('Specialist', validators=[DataRequired()]) specialist_id = SelectField('Specialist', validators=[DataRequired()])
tenant_make_id = SelectField('Tenant Make', validators=[Optional()], coerce=int)
valid_from = DateField('Valid From', id='form-control datepicker', validators=[Optional()]) valid_from = DateField('Valid From', id='form-control datepicker', validators=[Optional()])
valid_to = DateField('Valid To', id='form-control datepicker', validators=[Optional()]) valid_to = DateField('Valid To', id='form-control datepicker', validators=[Optional()])
@@ -155,10 +154,6 @@ class SpecialistMagicLinkForm(FlaskForm):
# Dynamically populate the specialist field # Dynamically populate the specialist field
self.specialist_id.choices = [(specialist.id, specialist.name) for specialist in specialists] self.specialist_id.choices = [(specialist.id, specialist.name) for specialist in specialists]
# Dynamically populate the tenant_make field with None as first option
tenant_makes = TenantMake.query.all()
self.tenant_make_id.choices = [(0, 'None')] + [(make.id, make.name) for make in tenant_makes]
class EditSpecialistMagicLinkForm(DynamicFormBase): class EditSpecialistMagicLinkForm(DynamicFormBase):
name = StringField('Name', validators=[DataRequired(), Length(max=50)]) name = StringField('Name', validators=[DataRequired(), Length(max=50)])
@@ -168,7 +163,6 @@ class EditSpecialistMagicLinkForm(DynamicFormBase):
specialist_id = IntegerField('Specialist', validators=[DataRequired()], render_kw={'readonly': True}) specialist_id = IntegerField('Specialist', validators=[DataRequired()], render_kw={'readonly': True})
specialist_name = StringField('Specialist Name', validators=[DataRequired()], render_kw={'readonly': True}) specialist_name = StringField('Specialist Name', validators=[DataRequired()], render_kw={'readonly': True})
tenant_make_id = SelectField('Tenant Make', validators=[Optional()], coerce=int) tenant_make_id = SelectField('Tenant Make', validators=[Optional()], coerce=int)
tenant_make_name = StringField('Tenant Make Name', validators=[Optional()], render_kw={'readonly': True})
valid_from = DateField('Valid From', id='form-control datepicker', validators=[Optional()]) valid_from = DateField('Valid From', id='form-control datepicker', validators=[Optional()])
valid_to = DateField('Valid To', id='form-control datepicker', validators=[Optional()]) valid_to = DateField('Valid To', id='form-control datepicker', validators=[Optional()])
@@ -188,11 +182,6 @@ class EditSpecialistMagicLinkForm(DynamicFormBase):
tenant_makes = TenantMake.query.all() tenant_makes = TenantMake.query.all()
self.tenant_make_id.choices = [(0, 'None')] + [(make.id, make.name) for make in tenant_makes] self.tenant_make_id.choices = [(0, 'None')] + [(make.id, make.name) for make in tenant_makes]
# If the form has a tenant_make_id that's not zero, set the tenant_make_name
if hasattr(self, 'tenant_make_id') and self.tenant_make_id.data and self.tenant_make_id.data > 0:
tenant_make = TenantMake.query.get(self.tenant_make_id.data)
if tenant_make:
self.tenant_make_name.data = tenant_make.name

View File

@@ -694,10 +694,6 @@ def specialist_magic_link():
# Populate fields individually instead of using populate_obj # Populate fields individually instead of using populate_obj
form.populate_obj(new_specialist_magic_link) form.populate_obj(new_specialist_magic_link)
# Handle the tenant_make_id special case (0 = None)
if form.tenant_make_id.data == 0:
new_specialist_magic_link.tenant_make_id = None
set_logging_information(new_specialist_magic_link, dt.now(tz.utc)) set_logging_information(new_specialist_magic_link, dt.now(tz.utc))
# Create 'public' SpecialistMagicLinkTenant # Create 'public' SpecialistMagicLinkTenant
@@ -705,6 +701,14 @@ def specialist_magic_link():
new_spec_ml_tenant.magic_link_code = new_specialist_magic_link.magic_link_code new_spec_ml_tenant.magic_link_code = new_specialist_magic_link.magic_link_code
new_spec_ml_tenant.tenant_id = tenant_id new_spec_ml_tenant.tenant_id = tenant_id
# Define the make valid for this magic link
make_id = SpecialistServices.get_specialist_system_field(new_specialist_magic_link.specialist_id,
"make", "tenant_make")
if make_id:
new_spec_ml_tenant.tenant_make_id = make_id
elif session.get('tenant').get('default_tenant_make_id'):
new_spec_ml_tenant.tenant_make_id = session.get('tenant').get('default_tenant_make_id')
db.session.add(new_specialist_magic_link) db.session.add(new_specialist_magic_link)
db.session.add(new_spec_ml_tenant) db.session.add(new_spec_ml_tenant)

View File

@@ -20,7 +20,41 @@ class TenantForm(FlaskForm):
website = StringField('Website', validators=[DataRequired(), Length(max=255)]) website = StringField('Website', validators=[DataRequired(), Length(max=255)])
# language fields # language fields
default_language = SelectField('Default Language', choices=[], validators=[DataRequired()]) default_language = SelectField('Default Language', choices=[], validators=[DataRequired()])
allowed_languages = SelectMultipleField('Allowed Languages', choices=[], validators=[DataRequired()]) # invoicing fields
currency = SelectField('Currency', choices=[], validators=[DataRequired()])
# Timezone
timezone = SelectField('Timezone', choices=[], validators=[DataRequired()])
# For Super Users only - Allow to assign the tenant to the partner
assign_to_partner = BooleanField('Assign to Partner', default=False)
# Embedding variables
submit = SubmitField('Submit')
def __init__(self, *args, **kwargs):
super(TenantForm, self).__init__(*args, **kwargs)
# initialise language fields
self.default_language.choices = [(lang, lang.lower()) for lang in current_app.config['SUPPORTED_LANGUAGES']]
# initialise currency field
self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']]
# initialise timezone
self.timezone.choices = [(tz, tz) for tz in pytz.common_timezones]
# Initialize fallback algorithms
self.type.choices = [(t, t) for t in current_app.config['TENANT_TYPES']]
# Initialize default tenant make choices
tenant_id = session.get('tenant', {}).get('id') if 'tenant' in session else None
# Show field only for Super Users with partner in session
if not current_user.has_roles('Super User') or 'partner' not in session:
self._fields.pop('assign_to_partner', None)
class EditTenantForm(FlaskForm):
id = IntegerField('ID', widget=HiddenInput())
name = StringField('Name', validators=[DataRequired(), Length(max=80)])
code = StringField('Code', validators=[DataRequired()], render_kw={'readonly': True})
type = SelectField('Tenant Type', validators=[Optional()], default='Active')
website = StringField('Website', validators=[DataRequired(), Length(max=255)])
# language fields
default_language = SelectField('Default Language', choices=[], validators=[DataRequired()])
# invoicing fields # invoicing fields
currency = SelectField('Currency', choices=[], validators=[DataRequired()]) currency = SelectField('Currency', choices=[], validators=[DataRequired()])
# Timezone # Timezone
@@ -34,10 +68,9 @@ class TenantForm(FlaskForm):
submit = SubmitField('Submit') submit = SubmitField('Submit')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(TenantForm, self).__init__(*args, **kwargs) super(EditTenantForm, self).__init__(*args, **kwargs)
# initialise language fields # initialise language fields
self.default_language.choices = [(lang, lang.lower()) for lang in current_app.config['SUPPORTED_LANGUAGES']] self.default_language.choices = [(lang, lang.lower()) for lang in current_app.config['SUPPORTED_LANGUAGES']]
self.allowed_languages.choices = [(lang, lang.lower()) for lang in current_app.config['SUPPORTED_LANGUAGES']]
# initialise currency field # initialise currency field
self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']] self.currency.choices = [(curr, curr) for curr in current_app.config['SUPPORTED_CURRENCIES']]
# initialise timezone # initialise timezone
@@ -45,7 +78,7 @@ class TenantForm(FlaskForm):
# Initialize fallback algorithms # Initialize fallback algorithms
self.type.choices = [(t, t) for t in current_app.config['TENANT_TYPES']] self.type.choices = [(t, t) for t in current_app.config['TENANT_TYPES']]
# Initialize default tenant make choices # Initialize default tenant make choices
tenant_id = session.get('tenant', {}).get('id') if 'tenant' in session else None tenant_id = self.id.data
if tenant_id: if tenant_id:
tenant_makes = TenantMake.query.filter_by(tenant_id=tenant_id, active=True).all() tenant_makes = TenantMake.query.filter_by(tenant_id=tenant_id, active=True).all()
self.default_tenant_make_id.choices = [(str(make.id), make.name) for make in tenant_makes] self.default_tenant_make_id.choices = [(str(make.id), make.name) for make in tenant_makes]
@@ -144,16 +177,27 @@ class EditTenantProjectForm(FlaskForm):
def validate_make_name(form, field): def validate_make_name(form, field):
# Controleer of een TenantMake met deze naam al bestaat # Check if tenant_make already exists in the database
existing_make = TenantMake.query.filter_by(name=field.data).first() existing_make = TenantMake.query.filter_by(name=field.data).first()
# Als er een bestaande make is gevonden en we zijn niet in edit mode, if existing_make:
# of als we wel in edit mode zijn maar het is een ander record (andere id) current_app.logger.debug(f'Existing make: {existing_make.id}')
if existing_make and (not hasattr(form, 'id') or form.id.data != existing_make.id): current_app.logger.debug(f'Form has id: {hasattr(form, 'id')}')
raise ValidationError(f'A Make with name "{field.data}" already exists. Choose another name.') if hasattr(form, 'id'):
current_app.logger.debug(f'Form has id: {form.id.data}')
if existing_make:
if not hasattr(form, 'id') or form.id.data != existing_make.id:
raise ValidationError(f'A Make with name "{field.data}" already exists. Choose another name.')
class TenantMakeForm(DynamicFormBase): class TenantMakeForm(DynamicFormBase):
name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_make_name])
description = TextAreaField('Description', validators=[Optional()])
active = BooleanField('Active', validators=[Optional()], default=True)
website = StringField('Website', validators=[DataRequired(), Length(max=255)])
logo_url = StringField('Logo URL', validators=[Optional(), Length(max=255)])
class EditTenantMakeForm(DynamicFormBase):
id = IntegerField('ID', widget=HiddenInput()) id = IntegerField('ID', widget=HiddenInput())
name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_make_name]) name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_make_name])
description = TextAreaField('Description', validators=[Optional()]) description = TextAreaField('Description', validators=[Optional()])

View File

@@ -12,7 +12,7 @@ from common.utils.dynamic_field_utils import create_default_config_from_type_con
from common.utils.security_utils import send_confirmation_email, send_reset_email from common.utils.security_utils import send_confirmation_email, send_reset_email
from config.type_defs.service_types import SERVICE_TYPES from config.type_defs.service_types import SERVICE_TYPES
from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm, TenantSelectionForm, \ from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm, TenantSelectionForm, \
TenantProjectForm, EditTenantProjectForm, TenantMakeForm TenantProjectForm, EditTenantProjectForm, TenantMakeForm, EditTenantForm, EditTenantMakeForm
from common.utils.database import Database from common.utils.database import Database
from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed
from common.utils.simple_encryption import generate_api_key from common.utils.simple_encryption import generate_api_key
@@ -53,10 +53,6 @@ def tenant():
new_tenant = Tenant() new_tenant = Tenant()
form.populate_obj(new_tenant) form.populate_obj(new_tenant)
# Convert default_tenant_make_id to integer if not empty
if form.default_tenant_make_id.data:
new_tenant.default_tenant_make_id = int(form.default_tenant_make_id.data)
timestamp = dt.now(tz.utc) timestamp = dt.now(tz.utc)
new_tenant.created_at = timestamp new_tenant.created_at = timestamp
new_tenant.updated_at = timestamp new_tenant.updated_at = timestamp
@@ -116,7 +112,7 @@ def tenant():
@roles_accepted('Super User', 'Partner Admin') @roles_accepted('Super User', 'Partner Admin')
def edit_tenant(tenant_id): def edit_tenant(tenant_id):
tenant = Tenant.query.get_or_404(tenant_id) # This will return a 404 if no tenant is found tenant = Tenant.query.get_or_404(tenant_id) # This will return a 404 if no tenant is found
form = TenantForm(obj=tenant) form = EditTenantForm(obj=tenant)
if form.validate_on_submit(): if form.validate_on_submit():
# Populate the tenant with form data # Populate the tenant with form data
@@ -471,7 +467,7 @@ def edit_tenant_domain(tenant_domain_id):
def tenant_overview(): def tenant_overview():
tenant_id = session['tenant']['id'] tenant_id = session['tenant']['id']
tenant = Tenant.query.get_or_404(tenant_id) tenant = Tenant.query.get_or_404(tenant_id)
form = TenantForm(obj=tenant) form = EditTenantForm(obj=tenant)
# Zet de waarde van default_tenant_make_id # Zet de waarde van default_tenant_make_id
if tenant.default_tenant_make_id: if tenant.default_tenant_make_id:
@@ -683,7 +679,8 @@ def tenant_makes():
page = request.args.get('page', 1, type=int) page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int) per_page = request.args.get('per_page', 10, type=int)
query = TenantMake.query.order_by(TenantMake.id) tenant_id = session['tenant']['id']
query = TenantMake.query.filter_by(tenant_id=tenant_id).order_by(TenantMake.id)
pagination = query.paginate(page=page, per_page=per_page) pagination = query.paginate(page=page, per_page=per_page)
tenant_makes = pagination.items tenant_makes = pagination.items
@@ -704,7 +701,7 @@ def edit_tenant_make(tenant_make_id):
tenant_make = TenantMake.query.get_or_404(tenant_make_id) tenant_make = TenantMake.query.get_or_404(tenant_make_id)
# Create form instance with the tenant make # Create form instance with the tenant make
form = TenantMakeForm(request.form, obj=tenant_make) form = EditTenantMakeForm(request.form, obj=tenant_make)
customisation_config = cache_manager.customisations_config_cache.get_config("CHAT_CLIENT_CUSTOMISATION") customisation_config = cache_manager.customisations_config_cache.get_config("CHAT_CLIENT_CUSTOMISATION")
form.add_dynamic_fields("configuration", customisation_config, tenant_make.chat_customisation_options) form.add_dynamic_fields("configuration", customisation_config, tenant_make.chat_customisation_options)
@@ -758,6 +755,8 @@ def handle_tenant_make_selection():
# Update session data if necessary # Update session data if necessary
if 'tenant' in session: if 'tenant' in session:
session['tenant'] = tenant.to_dict() session['tenant'] = tenant.to_dict()
return None
return None
except SQLAlchemyError as e: except SQLAlchemyError as e:
db.session.rollback() db.session.rollback()
flash(f'Failed to update default tenant make. Error: {str(e)}', 'danger') flash(f'Failed to update default tenant make. Error: {str(e)}', 'danger')
@@ -765,6 +764,9 @@ def handle_tenant_make_selection():
return redirect(prefixed_url_for('user_bp.tenant_makes')) return redirect(prefixed_url_for('user_bp.tenant_makes'))
return None
def reset_uniquifier(user): def reset_uniquifier(user):
security.datastore.set_uniquifier(user) security.datastore.set_uniquifier(user)
db.session.add(user) db.session.add(user)

View File

@@ -1,6 +1,7 @@
import logging import logging
import os import os
from flask import Flask, jsonify
from flask import Flask, jsonify, request
from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.middleware.proxy_fix import ProxyFix
import logging.config import logging.config
@@ -74,6 +75,13 @@ def create_app(config_file=None):
app.logger.info(f"EveAI Chat Client Started Successfully (PID: {os.getpid()})") app.logger.info(f"EveAI Chat Client Started Successfully (PID: {os.getpid()})")
app.logger.info("-------------------------------------------------------------------------------------------------") app.logger.info("-------------------------------------------------------------------------------------------------")
# @app.before_request
# def app_before_request():
# app.logger.debug(f'App before request: {request.path} ===== Method: {request.method} =====')
# app.logger.debug(f'Full URL: {request.url}')
# app.logger.debug(f'Endpoint: {request.endpoint}')
return app return app

View File

@@ -8,24 +8,149 @@
<!-- CSS --> <!-- CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/chat.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/chat.css') }}">
<!-- Vue.js -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- Markdown parser for explanation text -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- Custom theme colors from tenant settings --> <!-- Custom theme colors from tenant settings -->
<style> <style>
:root { :root {
--primary-color: {{ customization.primary_color|default('#007bff') }}; --primary-color: {{ customisation.primary_color|default('#007bff') }};
--secondary-color: {{ customization.secondary_color|default('#6c757d') }}; --secondary-color: {{ customisation.secondary_color|default('#6c757d') }};
--background-color: {{ customization.background_color|default('#ffffff') }}; --background-color: {{ customisation.background_color|default('#ffffff') }};
--text-color: {{ customization.text_color|default('#212529') }}; --text-color: {{ customisation.text_color|default('#212529') }};
--sidebar-color: {{ customization.sidebar_color|default('#f8f9fa') }}; --sidebar-color: {{ customisation.sidebar_color|default('#f8f9fa') }};
--sidebar-background: {{ customisation.sidebar_background|default('#2c3e50') }};
--gradient-start-color: {{ customisation.gradient_start_color|default('#f5f7fa') }};
--gradient-end-color: {{ customisation.gradient_end_color|default('#c3cfe2') }};
--markdown-background-color: {{ customisation.markdown_background_color|default('transparent') }};
--markdown-text-color: {{ customisation.markdown_text_color|default('#ffffff') }};
}
body, html {
margin: 0;
padding: 0;
height: 100%;
font-family: Arial, sans-serif;
}
.app-container {
display: flex;
height: 100vh;
width: 100%;
}
.sidebar {
width: 300px;
background-color: var(--sidebar-background);
color: white;
padding: 20px;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.sidebar-logo {
text-align: center;
margin-bottom: 20px;
}
.sidebar-logo img {
max-width: 100%;
max-height: 100px;
}
.sidebar-make-name {
font-size: 24px;
font-weight: bold;
margin-bottom: 20px;
text-align: center;
}
.sidebar-explanation {
margin-top: 20px;
overflow-y: auto;
background-color: var(--markdown-background-color);
color: var(--markdown-text-color);
padding: 10px;
border-radius: 5px;
}
/* Ensure all elements in the markdown content inherit the text color */
.sidebar-explanation * {
color: inherit;
}
/* Style links in the markdown content */
.sidebar-explanation a {
color: var(--primary-color);
text-decoration: underline;
}
.content-area {
flex: 1;
background: linear-gradient(135deg, var(--gradient-start-color), var(--gradient-end-color));
overflow-y: auto;
display: flex;
flex-direction: column;
}
.chat-container {
flex: 1;
padding: 20px;
display: flex;
flex-direction: column;
} }
</style> </style>
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
<div class="container"> <div id="app" class="app-container">
{% block content %}{% endblock %} <!-- Left sidebar - never changes -->
<div class="sidebar">
<div class="sidebar-logo">
<img src="{{ tenant_make.logo_url|default('') }}" alt="{{ tenant_make.name|default('Logo') }}">
</div>
<div class="sidebar-make-name">
{{ tenant_make.name|default('') }}
</div>
<div class="sidebar-explanation" v-html="compiledExplanation"></div>
</div>
<!-- Right content area - contains the chat client -->
<div class="content-area">
<div class="chat-container">
{% block content %}{% endblock %}
</div>
</div>
</div> </div>
<script>
Vue.createApp({
data() {
return {
explanation: `{{ customisation.sidebar_markdown|default('') }}`
}
},
computed: {
compiledExplanation: function() {
// Handle different versions of the marked library
if (typeof marked === 'function') {
return marked(this.explanation);
} else if (marked && typeof marked.parse === 'function') {
return marked.parse(this.explanation);
} else {
console.error('Marked library not properly loaded');
return this.explanation; // Fallback to raw text
}
}
}
}).mount('#app');
</script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
</body> </body>
</html> </html>

View File

@@ -3,13 +3,32 @@ from flask import Blueprint, render_template, request, session, current_app, jso
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from common.extensions import db from common.extensions import db
from common.models.user import Tenant, SpecialistMagicLinkTenant from common.models.user import Tenant, SpecialistMagicLinkTenant, TenantMake
from common.models.interaction import SpecialistMagicLink, Specialist, ChatSession, Interaction from common.models.interaction import SpecialistMagicLink, Specialist, ChatSession, Interaction
from common.services.interaction.specialist_services import SpecialistServices from common.services.interaction.specialist_services import SpecialistServices
from common.utils.database import Database from common.utils.database import Database
from common.utils.chat_utils import get_default_chat_customisation from common.utils.chat_utils import get_default_chat_customisation
chat_bp = Blueprint('chat', __name__) chat_bp = Blueprint('chat_bp', __name__, url_prefix='/chat')
@chat_bp.before_request
def log_before_request():
current_app.logger.debug(f'Before request: {request.path} =====================================')
@chat_bp.after_request
def log_after_request(response):
return response
# @chat_bp.before_request
# def before_request():
# try:
# mw_before_request()
# except Exception as e:
# current_app.logger.error(f'Error switching schema in Document Blueprint: {e}')
# raise
@chat_bp.route('/') @chat_bp.route('/')
def index(): def index():
@@ -31,14 +50,12 @@ def chat(magic_link_code):
current_app.logger.error(f"Invalid magic link code: {magic_link_code}") current_app.logger.error(f"Invalid magic link code: {magic_link_code}")
return render_template('error.html', message="Invalid magic link code.") return render_template('error.html', message="Invalid magic link code.")
tenant_id = magic_link_tenant.tenant_id
# Get tenant information # Get tenant information
tenant_id = magic_link_tenant.tenant_id
tenant = Tenant.query.get(tenant_id) tenant = Tenant.query.get(tenant_id)
if not tenant: if not tenant:
current_app.logger.error(f"Tenant not found for ID: {tenant_id}") current_app.logger.error(f"Tenant not found for ID: {tenant_id}")
return render_template('error.html', message="Tenant not found.") return render_template('error.html', message="Tenant not found.")
# Switch to tenant schema # Switch to tenant schema
Database(tenant_id).switch_schema() Database(tenant_id).switch_schema()
@@ -48,6 +65,12 @@ def chat(magic_link_code):
current_app.logger.error(f"Specialist magic link not found in tenant schema: {tenant_id}") current_app.logger.error(f"Specialist magic link not found in tenant schema: {tenant_id}")
return render_template('error.html', message="Specialist configuration not found.") return render_template('error.html', message="Specialist configuration not found.")
# Get relevant TenantMake
tenant_make = TenantMake.query.get(specialist_ml.tenant_make_id)
if not tenant_make:
current_app.logger.error(f"Tenant make not found: {specialist_ml.tenant_make_id}")
return render_template('error.html', message="Tenant make not found.")
# Get specialist details # Get specialist details
specialist = Specialist.query.get(specialist_ml.specialist_id) specialist = Specialist.query.get(specialist_ml.specialist_id)
if not specialist: if not specialist:
@@ -55,21 +78,22 @@ def chat(magic_link_code):
return render_template('error.html', message="Specialist not found.") return render_template('error.html', message="Specialist not found.")
# Store necessary information in session # Store necessary information in session
session['tenant_id'] = tenant_id session['tenant'] = tenant.to_dict()
session['specialist_id'] = specialist_ml.specialist_id session['specialist'] = specialist.to_dict()
session['specialist_args'] = specialist_ml.specialist_args or {} session['magic_link'] = specialist_ml.to_dict()
session['magic_link_code'] = magic_link_code session['tenant_make'] = tenant_make.to_dict()
# Get customisation options with defaults # Get customisation options with defaults
customisation = get_default_chat_customisation(tenant.chat_customisation_options) customisation = get_default_chat_customisation(tenant_make.chat_customisation_options)
# Start a new chat session # Start a new chat session
session['chat_session_id'] = SpecialistServices.start_session() session['chat_session_id'] = SpecialistServices.start_session()
return render_template('chat.html', return render_template('chat.html',
tenant=tenant, tenant=tenant,
specialist=specialist, tenant_make=tenant_make,
customisation=customisation) specialist=specialist,
customisation=customisation)
except Exception as e: except Exception as e:
current_app.logger.error(f"Error in chat view: {str(e)}", exc_info=True) current_app.logger.error(f"Error in chat view: {str(e)}", exc_info=True)

View File

@@ -82,7 +82,6 @@ def initialize_default_tenant():
'website': 'https://www.askeveai.com', 'website': 'https://www.askeveai.com',
'timezone': 'UTC', 'timezone': 'UTC',
'default_language': 'en', 'default_language': 'en',
'allowed_languages': ['en', 'fr', 'nl', 'de', 'es'],
'type': 'Active', 'type': 'Active',
'currency': '', 'currency': '',
'created_at': dt.now(tz.utc), 'created_at': dt.now(tz.utc),