- 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
This commit is contained in:
Josako
2025-06-10 20:52:01 +02:00
parent 9cc266b97f
commit 23b49516cb
12 changed files with 201 additions and 47 deletions

View File

@@ -223,10 +223,11 @@ class SpecialistServices:
@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.version)
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)

View File

@@ -21,10 +21,13 @@ def get_default_chat_customisation(tenant_customisation=None):
'background_color': '#ffffff',
'text_color': '#212529',
'sidebar_color': '#f8f9fa',
'logo_url': None,
'sidebar_text': None,
'sidebar_background': '#2c3e50',
'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?',
'team_info': []
}
# If no tenant customization is provided, return the defaults

View File

@@ -26,9 +26,34 @@ configuration:
description: "Sidebar Color"
type: "color"
required: false
"sidebar_text":
name: "Sidebar Text"
description: "Text to be shown in the sidebar"
"sidebar_background":
name: "Sidebar Background"
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"
required: false
"welcome_message":

View File

@@ -1,4 +1,4 @@
from flask import session
from flask import session, current_app
from flask_security import current_user
from flask_wtf import FlaskForm
from wtforms import StringField, SelectField
@@ -36,7 +36,7 @@ class SessionDefaultsForm(FlaskForm):
else:
self.partner_name.data = ""
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')
# Get a new session for catalog queries

View File

@@ -190,7 +190,7 @@ class AddDocumentForm(DynamicFormBase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.language.choices = [(language, language) for language in
session.get('tenant').get('allowed_languages')]
current_app.config['SUPPORTED_LANGUAGES']]
if not self.language.data:
self.language.data = session.get('tenant').get('default_language')
@@ -210,7 +210,7 @@ class AddURLForm(DynamicFormBase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.language.choices = [(language, language) for language in
session.get('tenant').get('allowed_languages')]
current_app.config['SUPPORTED_LANGUAGES']]
if not self.language.data:
self.language.data = session.get('tenant').get('default_language')

View File

@@ -182,11 +182,6 @@ class EditSpecialistMagicLinkForm(DynamicFormBase):
tenant_makes = TenantMake.query.all()
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
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))
# Create 'public' SpecialistMagicLinkTenant
@@ -706,7 +702,7 @@ def specialist_magic_link():
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.id,
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

View File

@@ -177,13 +177,17 @@ class EditTenantProjectForm(FlaskForm):
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()
# Als er een bestaande make is gevonden en we zijn niet in edit mode,
# of als we wel in edit mode zijn maar het is een ander record (andere id)
if existing_make and (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.')
if existing_make:
current_app.logger.debug(f'Existing make: {existing_make.id}')
current_app.logger.debug(f'Form has id: {hasattr(form, 'id')}')
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):

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 config.type_defs.service_types import SERVICE_TYPES
from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm, TenantSelectionForm, \
TenantProjectForm, EditTenantProjectForm, TenantMakeForm, EditTenantForm
TenantProjectForm, EditTenantProjectForm, TenantMakeForm, EditTenantForm, EditTenantMakeForm
from common.utils.database import Database
from common.utils.view_assistants import prepare_table_for_macro, form_validation_failed
from common.utils.simple_encryption import generate_api_key
@@ -701,7 +701,7 @@ def edit_tenant_make(tenant_make_id):
tenant_make = TenantMake.query.get_or_404(tenant_make_id)
# 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")
form.add_dynamic_fields("configuration", customisation_config, tenant_make.chat_customisation_options)
@@ -755,6 +755,8 @@ def handle_tenant_make_selection():
# Update session data if necessary
if 'tenant' in session:
session['tenant'] = tenant.to_dict()
return None
return None
except SQLAlchemyError as e:
db.session.rollback()
flash(f'Failed to update default tenant make. Error: {str(e)}', 'danger')
@@ -762,6 +764,9 @@ def handle_tenant_make_selection():
return redirect(prefixed_url_for('user_bp.tenant_makes'))
return None
def reset_uniquifier(user):
security.datastore.set_uniquifier(user)
db.session.add(user)

View File

@@ -8,6 +8,12 @@
<!-- 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 -->
<style>
:root {
@@ -16,16 +22,135 @@
--background-color: {{ customisation.background_color|default('#ffffff') }};
--text-color: {{ customisation.text_color|default('#212529') }};
--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>
{% block head %}{% endblock %}
</head>
<body>
<div class="container">
{% block content %}{% endblock %}
<div id="app" class="app-container">
<!-- 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>
<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 %}
</body>
</html>

View File

@@ -90,9 +90,10 @@ def chat(magic_link_code):
session['chat_session_id'] = SpecialistServices.start_session()
return render_template('chat.html',
tenant=tenant,
specialist=specialist,
customisation=customisation)
tenant=tenant,
tenant_make=tenant_make,
specialist=specialist,
customisation=customisation)
except Exception as e:
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',
'timezone': 'UTC',
'default_language': 'en',
'allowed_languages': ['en', 'fr', 'nl', 'de', 'es'],
'type': 'Active',
'currency': '',
'created_at': dt.now(tz.utc),