Compare commits
15 Commits
v2.3.0-alf
...
v2.3.3-alf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43ee9139d6 | ||
|
|
8f45005713 | ||
|
|
bc1626c4ff | ||
|
|
57c0e7a1ba | ||
|
|
0d05499d2b | ||
|
|
b4e58659a8 | ||
|
|
67078ce925 | ||
|
|
ebdb836448 | ||
|
|
81e754317a | ||
|
|
578981c745 | ||
|
|
8fb2ad43c5 | ||
|
|
49f9077a7b | ||
|
|
d290b46a0c | ||
|
|
73647e4795 | ||
|
|
25e169dbea |
@@ -11,6 +11,7 @@ from flask_restx import Api
|
|||||||
from prometheus_flask_exporter import PrometheusMetrics
|
from prometheus_flask_exporter import PrometheusMetrics
|
||||||
|
|
||||||
from .utils.cache.eveai_cache_manager import EveAICacheManager
|
from .utils.cache.eveai_cache_manager import EveAICacheManager
|
||||||
|
from .utils.content_utils import ContentManager
|
||||||
from .utils.simple_encryption import SimpleEncryption
|
from .utils.simple_encryption import SimpleEncryption
|
||||||
from .utils.minio_utils import MinioClient
|
from .utils.minio_utils import MinioClient
|
||||||
|
|
||||||
@@ -30,4 +31,5 @@ simple_encryption = SimpleEncryption()
|
|||||||
minio_client = MinioClient()
|
minio_client = MinioClient()
|
||||||
metrics = PrometheusMetrics.for_app_factory()
|
metrics = PrometheusMetrics.for_app_factory()
|
||||||
cache_manager = EveAICacheManager()
|
cache_manager = EveAICacheManager()
|
||||||
|
content_manager = ContentManager()
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import sqlalchemy as sa
|
|||||||
|
|
||||||
class Catalog(db.Model):
|
class Catalog(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
name = db.Column(db.String(50), nullable=False)
|
name = db.Column(db.String(50), nullable=False, unique=True)
|
||||||
description = db.Column(db.Text, nullable=True)
|
description = db.Column(db.Text, nullable=True)
|
||||||
type = db.Column(db.String(50), nullable=False, default="STANDARD_CATALOG")
|
type = db.Column(db.String(50), nullable=False, default="STANDARD_CATALOG")
|
||||||
|
|
||||||
|
|||||||
@@ -215,3 +215,24 @@ class SpecialistDispatcher(db.Model):
|
|||||||
dispatcher_id = db.Column(db.Integer, db.ForeignKey(Dispatcher.id, ondelete='CASCADE'), primary_key=True)
|
dispatcher_id = db.Column(db.Integer, db.ForeignKey(Dispatcher.id, ondelete='CASCADE'), primary_key=True)
|
||||||
|
|
||||||
dispatcher = db.relationship("Dispatcher", backref="specialist_dispatchers")
|
dispatcher = db.relationship("Dispatcher", backref="specialist_dispatchers")
|
||||||
|
|
||||||
|
|
||||||
|
class SpecialistMagicLink(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
name = db.Column(db.String(50), nullable=False)
|
||||||
|
description = db.Column(db.Text, nullable=True)
|
||||||
|
specialist_id = db.Column(db.Integer, db.ForeignKey(Specialist.id, ondelete='CASCADE'), nullable=False)
|
||||||
|
magic_link_code = db.Column(db.String(55), nullable=False, unique=True)
|
||||||
|
|
||||||
|
valid_from = db.Column(db.DateTime, nullable=True)
|
||||||
|
valid_to = db.Column(db.DateTime, nullable=True)
|
||||||
|
|
||||||
|
specialist_args = db.Column(JSONB, nullable=True)
|
||||||
|
|
||||||
|
created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now())
|
||||||
|
created_by = db.Column(db.Integer, db.ForeignKey(User.id), nullable=True)
|
||||||
|
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))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<SpecialistMagicLink {self.specialist_id} {self.magic_link_code}>"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from datetime import date
|
|||||||
|
|
||||||
from common.extensions import db
|
from common.extensions import db
|
||||||
from flask_security import UserMixin, RoleMixin
|
from flask_security import UserMixin, RoleMixin
|
||||||
from sqlalchemy.dialects.postgresql import ARRAY
|
from sqlalchemy.dialects.postgresql import ARRAY, JSONB
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from common.models.entitlements import License
|
from common.models.entitlements import License
|
||||||
@@ -173,6 +173,28 @@ class TenantProject(db.Model):
|
|||||||
return f"<TenantProject {self.id}: {self.name}>"
|
return f"<TenantProject {self.id}: {self.name}>"
|
||||||
|
|
||||||
|
|
||||||
|
class TenantMake(db.Model):
|
||||||
|
__bind_key__ = 'public'
|
||||||
|
__table_args__ = {'schema': 'public'}
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False)
|
||||||
|
name = db.Column(db.String(50), nullable=False)
|
||||||
|
description = db.Column(db.Text, nullable=True)
|
||||||
|
active = db.Column(db.Boolean, nullable=False, default=True)
|
||||||
|
website = db.Column(db.String(255), nullable=True)
|
||||||
|
logo_url = db.Column(db.String(255), nullable=True)
|
||||||
|
|
||||||
|
# Chat customisation options
|
||||||
|
chat_customisation_options = db.Column(JSONB, nullable=True)
|
||||||
|
|
||||||
|
# Versioning Information
|
||||||
|
created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now())
|
||||||
|
created_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
|
||||||
|
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'))
|
||||||
|
|
||||||
|
|
||||||
class Partner(db.Model):
|
class Partner(db.Model):
|
||||||
__bind_key__ = 'public'
|
__bind_key__ = 'public'
|
||||||
__table_args__ = {'schema': 'public'}
|
__table_args__ = {'schema': 'public'}
|
||||||
@@ -271,3 +293,11 @@ class PartnerTenant(db.Model):
|
|||||||
created_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
|
created_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
|
||||||
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'), nullable=True)
|
updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
class SpecialistMagicLinkTenant(db.Model):
|
||||||
|
__bind_key__ = 'public'
|
||||||
|
__table_args__ = {'schema': 'public'}
|
||||||
|
|
||||||
|
magic_link_code = db.Column(db.String(55), primary_key=True)
|
||||||
|
tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from sqlalchemy.exc import SQLAlchemyError
|
|||||||
from common.extensions import db
|
from common.extensions import db
|
||||||
from common.models.entitlements import PartnerServiceLicenseTier
|
from common.models.entitlements import PartnerServiceLicenseTier
|
||||||
from common.models.user import Partner
|
from common.models.user import Partner
|
||||||
from common.utils.eveai_exceptions import EveAINoManagementPartnerService
|
from common.utils.eveai_exceptions import EveAINoManagementPartnerService, EveAINoSessionPartner
|
||||||
from common.utils.model_logging_utils import set_logging_information
|
from common.utils.model_logging_utils import set_logging_information
|
||||||
|
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ class LicenseTierServices:
|
|||||||
# Get partner service (MANAGEMENT_SERVICE type)
|
# Get partner service (MANAGEMENT_SERVICE type)
|
||||||
partner = Partner.query.get(partner_id)
|
partner = Partner.query.get(partner_id)
|
||||||
if not partner:
|
if not partner:
|
||||||
return
|
raise EveAINoSessionPartner()
|
||||||
|
|
||||||
# Find a management service for this partner
|
# Find a management service for this partner
|
||||||
management_service = next((service for service in session['partner']['services']
|
management_service = next((service for service in session['partner']['services']
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from typing import Dict, Any, Tuple
|
from datetime import datetime as dt, timezone as tz
|
||||||
|
from typing import Dict, Any, Tuple, Optional
|
||||||
|
from flask import current_app
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
from common.extensions import db, cache_manager
|
||||||
|
from common.models.interaction import (
|
||||||
|
Specialist, EveAIAgent, EveAITask, EveAITool
|
||||||
|
)
|
||||||
from common.utils.celery_utils import current_celery
|
from common.utils.celery_utils import current_celery
|
||||||
|
from common.utils.model_logging_utils import set_logging_information, update_logging_information
|
||||||
|
|
||||||
|
|
||||||
class SpecialistServices:
|
class SpecialistServices:
|
||||||
@@ -27,4 +35,188 @@ class SpecialistServices:
|
|||||||
'status': 'queued',
|
'status': 'queued',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def initialize_specialist(specialist_id: int, specialist_type: str, specialist_version: str):
|
||||||
|
"""
|
||||||
|
Initialize an agentic specialist by creating all its components based on configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
specialist_id: ID of the specialist to initialize
|
||||||
|
specialist_type: Type of the specialist
|
||||||
|
specialist_version: Version of the specialist type to use
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If specialist not found or invalid configuration
|
||||||
|
SQLAlchemyError: If database operations fail
|
||||||
|
"""
|
||||||
|
config = cache_manager.specialists_config_cache.get_config(specialist_type, specialist_version)
|
||||||
|
if not config:
|
||||||
|
raise ValueError(f"No configuration found for {specialist_type} version {specialist_version}")
|
||||||
|
if config['framework'] == 'langchain':
|
||||||
|
pass # Langchain does not require additional items to be initialized. All configuration is in the specialist.
|
||||||
|
|
||||||
|
specialist = Specialist.query.get(specialist_id)
|
||||||
|
if not specialist:
|
||||||
|
raise ValueError(f"Specialist with ID {specialist_id} not found")
|
||||||
|
|
||||||
|
if config['framework'] == 'crewai':
|
||||||
|
SpecialistServices.initialize_crewai_specialist(specialist, config)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def initialize_crewai_specialist(specialist: Specialist, config: Dict[str, Any]):
|
||||||
|
timestamp = dt.now(tz=tz.utc)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Initialize agents
|
||||||
|
if 'agents' in config:
|
||||||
|
for agent_config in config['agents']:
|
||||||
|
SpecialistServices._create_agent(
|
||||||
|
specialist_id=specialist.id,
|
||||||
|
agent_type=agent_config['type'],
|
||||||
|
agent_version=agent_config['version'],
|
||||||
|
name=agent_config.get('name'),
|
||||||
|
description=agent_config.get('description'),
|
||||||
|
timestamp=timestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize tasks
|
||||||
|
if 'tasks' in config:
|
||||||
|
for task_config in config['tasks']:
|
||||||
|
SpecialistServices._create_task(
|
||||||
|
specialist_id=specialist.id,
|
||||||
|
task_type=task_config['type'],
|
||||||
|
task_version=task_config['version'],
|
||||||
|
name=task_config.get('name'),
|
||||||
|
description=task_config.get('description'),
|
||||||
|
timestamp=timestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
# Initialize tools
|
||||||
|
if 'tools' in config:
|
||||||
|
for tool_config in config['tools']:
|
||||||
|
SpecialistServices._create_tool(
|
||||||
|
specialist_id=specialist.id,
|
||||||
|
tool_type=tool_config['type'],
|
||||||
|
tool_version=tool_config['version'],
|
||||||
|
name=tool_config.get('name'),
|
||||||
|
description=tool_config.get('description'),
|
||||||
|
timestamp=timestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
current_app.logger.info(f"Successfully initialized crewai specialist {specialist.id}")
|
||||||
|
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error(f"Database error initializing crewai specialist {specialist.id}: {str(e)}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error(f"Error initializing crewai specialist {specialist.id}: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_agent(
|
||||||
|
specialist_id: int,
|
||||||
|
agent_type: str,
|
||||||
|
agent_version: str,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
description: Optional[str] = None,
|
||||||
|
timestamp: Optional[dt] = None
|
||||||
|
) -> EveAIAgent:
|
||||||
|
"""Create an agent with the given configuration."""
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = dt.now(tz=tz.utc)
|
||||||
|
|
||||||
|
# Get agent configuration from cache
|
||||||
|
agent_config = cache_manager.agents_config_cache.get_config(agent_type, agent_version)
|
||||||
|
|
||||||
|
agent = EveAIAgent(
|
||||||
|
specialist_id=specialist_id,
|
||||||
|
name=name or agent_config.get('name', agent_type),
|
||||||
|
description=description or agent_config.get('metadata').get('description', ''),
|
||||||
|
type=agent_type,
|
||||||
|
type_version=agent_version,
|
||||||
|
role=None,
|
||||||
|
goal=None,
|
||||||
|
backstory=None,
|
||||||
|
tuning=False,
|
||||||
|
configuration=None,
|
||||||
|
arguments=None
|
||||||
|
)
|
||||||
|
|
||||||
|
set_logging_information(agent, timestamp)
|
||||||
|
|
||||||
|
db.session.add(agent)
|
||||||
|
current_app.logger.info(f"Created agent {agent.id} of type {agent_type}")
|
||||||
|
return agent
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_task(
|
||||||
|
specialist_id: int,
|
||||||
|
task_type: str,
|
||||||
|
task_version: str,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
description: Optional[str] = None,
|
||||||
|
timestamp: Optional[dt] = None
|
||||||
|
) -> EveAITask:
|
||||||
|
"""Create a task with the given configuration."""
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = dt.now(tz=tz.utc)
|
||||||
|
|
||||||
|
# Get task configuration from cache
|
||||||
|
task_config = cache_manager.tasks_config_cache.get_config(task_type, task_version)
|
||||||
|
|
||||||
|
task = EveAITask(
|
||||||
|
specialist_id=specialist_id,
|
||||||
|
name=name or task_config.get('name', task_type),
|
||||||
|
description=description or task_config.get('metadata').get('description', ''),
|
||||||
|
type=task_type,
|
||||||
|
type_version=task_version,
|
||||||
|
task_description=None,
|
||||||
|
expected_output=None,
|
||||||
|
tuning=False,
|
||||||
|
configuration=None,
|
||||||
|
arguments=None,
|
||||||
|
context=None,
|
||||||
|
asynchronous=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
set_logging_information(task, timestamp)
|
||||||
|
|
||||||
|
db.session.add(task)
|
||||||
|
current_app.logger.info(f"Created task {task.id} of type {task_type}")
|
||||||
|
return task
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _create_tool(
|
||||||
|
specialist_id: int,
|
||||||
|
tool_type: str,
|
||||||
|
tool_version: str,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
description: Optional[str] = None,
|
||||||
|
timestamp: Optional[dt] = None
|
||||||
|
) -> EveAITool:
|
||||||
|
"""Create a tool with the given configuration."""
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = dt.now(tz=tz.utc)
|
||||||
|
|
||||||
|
# Get tool configuration from cache
|
||||||
|
tool_config = cache_manager.tools_config_cache.get_config(tool_type, tool_version)
|
||||||
|
|
||||||
|
tool = EveAITool(
|
||||||
|
specialist_id=specialist_id,
|
||||||
|
name=name or tool_config.get('name', tool_type),
|
||||||
|
description=description or tool_config.get('metadata').get('description', ''),
|
||||||
|
type=tool_type,
|
||||||
|
type_version=tool_version,
|
||||||
|
tuning=False,
|
||||||
|
configuration=None,
|
||||||
|
arguments=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
set_logging_information(tool, timestamp)
|
||||||
|
|
||||||
|
db.session.add(tool)
|
||||||
|
current_app.logger.info(f"Created tool {tool.id} of type {tool_type}")
|
||||||
|
return tool
|
||||||
|
|||||||
29
common/utils/cache/config_cache.py
vendored
29
common/utils/cache/config_cache.py
vendored
@@ -7,7 +7,7 @@ from flask import current_app
|
|||||||
|
|
||||||
from common.utils.cache.base import CacheHandler, CacheKey
|
from common.utils.cache.base import CacheHandler, CacheKey
|
||||||
from config.type_defs import agent_types, task_types, tool_types, specialist_types, retriever_types, prompt_types, \
|
from config.type_defs import agent_types, task_types, tool_types, specialist_types, retriever_types, prompt_types, \
|
||||||
catalog_types, partner_service_types
|
catalog_types, partner_service_types, processor_types, customisation_types
|
||||||
|
|
||||||
|
|
||||||
def is_major_minor(version: str) -> bool:
|
def is_major_minor(version: str) -> bool:
|
||||||
@@ -59,7 +59,7 @@ class BaseConfigCacheHandler(CacheHandler[Dict[str, Any]]):
|
|||||||
"""Set the version tree cache dependency."""
|
"""Set the version tree cache dependency."""
|
||||||
self.version_tree_cache = cache
|
self.version_tree_cache = cache
|
||||||
|
|
||||||
def _load_specific_config(self, type_name: str, version_str: str) -> Dict[str, Any]:
|
def _load_specific_config(self, type_name: str, version_str: str = 'latest') -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Load a specific configuration version
|
Load a specific configuration version
|
||||||
Automatically handles global vs partner-specific configs
|
Automatically handles global vs partner-specific configs
|
||||||
@@ -456,7 +456,13 @@ CatalogConfigCacheHandler, CatalogConfigVersionTreeCacheHandler, CatalogConfigTy
|
|||||||
types_module=catalog_types.CATALOG_TYPES
|
types_module=catalog_types.CATALOG_TYPES
|
||||||
))
|
))
|
||||||
|
|
||||||
# Add to common/utils/cache/config_cache.py
|
ProcessorConfigCacheHandler, ProcessorConfigVersionTreeCacheHandler, ProcessorConfigTypesCacheHandler = (
|
||||||
|
create_config_cache_handlers(
|
||||||
|
config_type='processors',
|
||||||
|
config_dir='config/processors',
|
||||||
|
types_module=processor_types.PROCESSOR_TYPES
|
||||||
|
))
|
||||||
|
|
||||||
PartnerServiceConfigCacheHandler, PartnerServiceConfigVersionTreeCacheHandler, PartnerServiceConfigTypesCacheHandler = (
|
PartnerServiceConfigCacheHandler, PartnerServiceConfigVersionTreeCacheHandler, PartnerServiceConfigTypesCacheHandler = (
|
||||||
create_config_cache_handlers(
|
create_config_cache_handlers(
|
||||||
config_type='partner_services',
|
config_type='partner_services',
|
||||||
@@ -464,6 +470,14 @@ PartnerServiceConfigCacheHandler, PartnerServiceConfigVersionTreeCacheHandler, P
|
|||||||
types_module=partner_service_types.PARTNER_SERVICE_TYPES
|
types_module=partner_service_types.PARTNER_SERVICE_TYPES
|
||||||
))
|
))
|
||||||
|
|
||||||
|
CustomisationConfigCacheHandler, CustomisationConfigVersionTreeCacheHandler, CustomisationConfigTypesCacheHandler = (
|
||||||
|
create_config_cache_handlers(
|
||||||
|
config_type='customisations',
|
||||||
|
config_dir='config/customisations',
|
||||||
|
types_module=customisation_types.CUSTOMISATION_TYPES
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def register_config_cache_handlers(cache_manager) -> None:
|
def register_config_cache_handlers(cache_manager) -> None:
|
||||||
cache_manager.register_handler(AgentConfigCacheHandler, 'eveai_config')
|
cache_manager.register_handler(AgentConfigCacheHandler, 'eveai_config')
|
||||||
@@ -487,12 +501,18 @@ def register_config_cache_handlers(cache_manager) -> None:
|
|||||||
cache_manager.register_handler(CatalogConfigCacheHandler, 'eveai_config')
|
cache_manager.register_handler(CatalogConfigCacheHandler, 'eveai_config')
|
||||||
cache_manager.register_handler(CatalogConfigTypesCacheHandler, 'eveai_config')
|
cache_manager.register_handler(CatalogConfigTypesCacheHandler, 'eveai_config')
|
||||||
cache_manager.register_handler(CatalogConfigVersionTreeCacheHandler, 'eveai_config')
|
cache_manager.register_handler(CatalogConfigVersionTreeCacheHandler, 'eveai_config')
|
||||||
|
cache_manager.register_handler(ProcessorConfigCacheHandler, 'eveai_config')
|
||||||
|
cache_manager.register_handler(ProcessorConfigTypesCacheHandler, 'eveai_config')
|
||||||
|
cache_manager.register_handler(ProcessorConfigVersionTreeCacheHandler, 'eveai_config')
|
||||||
cache_manager.register_handler(AgentConfigCacheHandler, 'eveai_config')
|
cache_manager.register_handler(AgentConfigCacheHandler, 'eveai_config')
|
||||||
cache_manager.register_handler(AgentConfigTypesCacheHandler, 'eveai_config')
|
cache_manager.register_handler(AgentConfigTypesCacheHandler, 'eveai_config')
|
||||||
cache_manager.register_handler(AgentConfigVersionTreeCacheHandler, 'eveai_config')
|
cache_manager.register_handler(AgentConfigVersionTreeCacheHandler, 'eveai_config')
|
||||||
cache_manager.register_handler(PartnerServiceConfigCacheHandler, 'eveai_config')
|
cache_manager.register_handler(PartnerServiceConfigCacheHandler, 'eveai_config')
|
||||||
cache_manager.register_handler(PartnerServiceConfigTypesCacheHandler, 'eveai_config')
|
cache_manager.register_handler(PartnerServiceConfigTypesCacheHandler, 'eveai_config')
|
||||||
cache_manager.register_handler(PartnerServiceConfigVersionTreeCacheHandler, 'eveai_config')
|
cache_manager.register_handler(PartnerServiceConfigVersionTreeCacheHandler, 'eveai_config')
|
||||||
|
cache_manager.register_handler(CustomisationConfigCacheHandler, 'eveai_config')
|
||||||
|
cache_manager.register_handler(CustomisationConfigTypesCacheHandler, 'eveai_config')
|
||||||
|
cache_manager.register_handler(CustomisationConfigVersionTreeCacheHandler, 'eveai_config')
|
||||||
|
|
||||||
cache_manager.agents_config_cache.set_version_tree_cache(cache_manager.agents_version_tree_cache)
|
cache_manager.agents_config_cache.set_version_tree_cache(cache_manager.agents_version_tree_cache)
|
||||||
cache_manager.tasks_config_cache.set_version_tree_cache(cache_manager.tasks_version_tree_cache)
|
cache_manager.tasks_config_cache.set_version_tree_cache(cache_manager.tasks_version_tree_cache)
|
||||||
@@ -500,4 +520,7 @@ def register_config_cache_handlers(cache_manager) -> None:
|
|||||||
cache_manager.specialists_config_cache.set_version_tree_cache(cache_manager.specialists_version_tree_cache)
|
cache_manager.specialists_config_cache.set_version_tree_cache(cache_manager.specialists_version_tree_cache)
|
||||||
cache_manager.retrievers_config_cache.set_version_tree_cache(cache_manager.retrievers_version_tree_cache)
|
cache_manager.retrievers_config_cache.set_version_tree_cache(cache_manager.retrievers_version_tree_cache)
|
||||||
cache_manager.prompts_config_cache.set_version_tree_cache(cache_manager.prompts_version_tree_cache)
|
cache_manager.prompts_config_cache.set_version_tree_cache(cache_manager.prompts_version_tree_cache)
|
||||||
|
cache_manager.catalogs_config_cache.set_version_tree_cache(cache_manager.catalogs_version_tree_cache)
|
||||||
|
cache_manager.processors_config_cache.set_version_tree_cache(cache_manager.processors_version_tree_cache)
|
||||||
cache_manager.partner_services_config_cache.set_version_tree_cache(cache_manager.partner_services_version_tree_cache)
|
cache_manager.partner_services_config_cache.set_version_tree_cache(cache_manager.partner_services_version_tree_cache)
|
||||||
|
cache_manager.customisations_config_cache.set_version_tree_cache(cache_manager.customisations_version_tree_cache)
|
||||||
|
|||||||
42
common/utils/chat_utils.py
Normal file
42
common/utils/chat_utils.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"""
|
||||||
|
Utility functions for chat customization.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_default_chat_customisation(tenant_customisation=None):
|
||||||
|
"""
|
||||||
|
Get chat customization options with default values for missing options.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tenant_customization (dict, optional): The tenant's customization options.
|
||||||
|
Defaults to None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: A dictionary containing all customization options with default values
|
||||||
|
for any missing options.
|
||||||
|
"""
|
||||||
|
# Default customization options
|
||||||
|
default_customisation = {
|
||||||
|
'primary_color': '#007bff',
|
||||||
|
'secondary_color': '#6c757d',
|
||||||
|
'background_color': '#ffffff',
|
||||||
|
'text_color': '#212529',
|
||||||
|
'sidebar_color': '#f8f9fa',
|
||||||
|
'logo_url': None,
|
||||||
|
'sidebar_text': None,
|
||||||
|
'welcome_message': 'Hello! How can I help you today?',
|
||||||
|
'team_info': []
|
||||||
|
}
|
||||||
|
|
||||||
|
# If no tenant customization is provided, return the defaults
|
||||||
|
if tenant_customisation is None:
|
||||||
|
return default_customisation
|
||||||
|
|
||||||
|
# Start with the default customization
|
||||||
|
customisation = default_customisation.copy()
|
||||||
|
|
||||||
|
# Update with tenant customization
|
||||||
|
for key, value in tenant_customisation.items():
|
||||||
|
if key in customisation:
|
||||||
|
customisation[key] = value
|
||||||
|
|
||||||
|
return customisation
|
||||||
@@ -21,7 +21,7 @@ class TaggingField(BaseModel):
|
|||||||
@field_validator('type', mode='before')
|
@field_validator('type', mode='before')
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_type(cls, v: str) -> str:
|
def validate_type(cls, v: str) -> str:
|
||||||
valid_types = ['string', 'integer', 'float', 'date', 'enum']
|
valid_types = ['string', 'integer', 'float', 'date', 'enum', 'color']
|
||||||
if v not in valid_types:
|
if v not in valid_types:
|
||||||
raise ValueError(f'type must be one of {valid_types}')
|
raise ValueError(f'type must be one of {valid_types}')
|
||||||
return v
|
return v
|
||||||
@@ -243,7 +243,7 @@ class ArgumentDefinition(BaseModel):
|
|||||||
@field_validator('type')
|
@field_validator('type')
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_type(cls, v: str) -> str:
|
def validate_type(cls, v: str) -> str:
|
||||||
valid_types = ['string', 'integer', 'float', 'date', 'enum']
|
valid_types = ['string', 'integer', 'float', 'date', 'enum', 'color']
|
||||||
if v not in valid_types:
|
if v not in valid_types:
|
||||||
raise ValueError(f'type must be one of {valid_types}')
|
raise ValueError(f'type must be one of {valid_types}')
|
||||||
return v
|
return v
|
||||||
@@ -256,7 +256,8 @@ class ArgumentDefinition(BaseModel):
|
|||||||
'integer': NumericConstraint,
|
'integer': NumericConstraint,
|
||||||
'float': NumericConstraint,
|
'float': NumericConstraint,
|
||||||
'date': DateConstraint,
|
'date': DateConstraint,
|
||||||
'enum': EnumConstraint
|
'enum': EnumConstraint,
|
||||||
|
'color': StringConstraint
|
||||||
}
|
}
|
||||||
|
|
||||||
expected_type = expected_constraint_types.get(self.type)
|
expected_type = expected_constraint_types.get(self.type)
|
||||||
|
|||||||
215
common/utils/content_utils.py
Normal file
215
common/utils/content_utils.py
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
from packaging import version
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class ContentManager:
|
||||||
|
def __init__(self, app=None):
|
||||||
|
self.app = app
|
||||||
|
if app:
|
||||||
|
self.init_app(app)
|
||||||
|
|
||||||
|
def init_app(self, app):
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
# Controleer of het pad bestaat
|
||||||
|
if not os.path.exists(app.config['CONTENT_DIR']):
|
||||||
|
logger.warning(f"Content directory not found at: {app.config['CONTENT_DIR']}")
|
||||||
|
else:
|
||||||
|
logger.info(f"Content directory configured at: {app.config['CONTENT_DIR']}")
|
||||||
|
|
||||||
|
def get_content_path(self, content_type, major_minor=None, patch=None):
|
||||||
|
"""
|
||||||
|
Geef het volledige pad naar een contentbestand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content_type (str): Type content (bv. 'changelog', 'terms')
|
||||||
|
major_minor (str, optional): Major.Minor versie (bv. '1.0')
|
||||||
|
patch (str, optional): Patchnummer (bv. '5')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Volledige pad naar de content map of bestand
|
||||||
|
"""
|
||||||
|
content_path = os.path.join(self.app.config['CONTENT_DIR'], content_type)
|
||||||
|
|
||||||
|
if major_minor:
|
||||||
|
content_path = os.path.join(content_path, major_minor)
|
||||||
|
|
||||||
|
if patch:
|
||||||
|
content_path = os.path.join(content_path, f"{major_minor}.{patch}.md")
|
||||||
|
|
||||||
|
return content_path
|
||||||
|
|
||||||
|
def _parse_version(self, filename):
|
||||||
|
"""Parse een versienummer uit een bestandsnaam"""
|
||||||
|
match = re.match(r'(\d+\.\d+)\.(\d+)\.md', filename)
|
||||||
|
if match:
|
||||||
|
return match.group(1), match.group(2)
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def get_latest_version(self, content_type, major_minor=None):
|
||||||
|
"""
|
||||||
|
Verkrijg de laatste versie van een bepaald contenttype
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content_type (str): Type content (bv. 'changelog', 'terms')
|
||||||
|
major_minor (str, optional): Specifieke major.minor versie, anders de hoogste
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (major_minor, patch, full_version) of None als niet gevonden
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Basispad voor dit contenttype
|
||||||
|
content_path = os.path.join(self.app.config['CONTENT_DIR'], content_type)
|
||||||
|
|
||||||
|
if not os.path.exists(content_path):
|
||||||
|
logger.error(f"Content path does not exist: {content_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Als geen major_minor opgegeven, vind de hoogste
|
||||||
|
if not major_minor:
|
||||||
|
available_versions = os.listdir(content_path)
|
||||||
|
if not available_versions:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Sorteer op versienummer (major.minor)
|
||||||
|
available_versions.sort(key=lambda v: version.parse(v))
|
||||||
|
major_minor = available_versions[-1]
|
||||||
|
|
||||||
|
# Nu we major_minor hebben, zoek de hoogste patch
|
||||||
|
major_minor_path = os.path.join(content_path, major_minor)
|
||||||
|
|
||||||
|
if not os.path.exists(major_minor_path):
|
||||||
|
logger.error(f"Version path does not exist: {major_minor_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
files = os.listdir(major_minor_path)
|
||||||
|
version_files = []
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
mm, p = self._parse_version(file)
|
||||||
|
if mm == major_minor and p:
|
||||||
|
version_files.append((mm, p, f"{mm}.{p}"))
|
||||||
|
|
||||||
|
if not version_files:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Sorteer op patch nummer
|
||||||
|
version_files.sort(key=lambda v: int(v[1]))
|
||||||
|
return version_files[-1]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error finding latest version for {content_type}: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def read_content(self, content_type, major_minor=None, patch=None):
|
||||||
|
"""
|
||||||
|
Lees content met versieondersteuning
|
||||||
|
|
||||||
|
Als major_minor en patch niet zijn opgegeven, wordt de laatste versie gebruikt.
|
||||||
|
Als alleen major_minor is opgegeven, wordt de laatste patch van die versie gebruikt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content_type (str): Type content (bv. 'changelog', 'terms')
|
||||||
|
major_minor (str, optional): Major.Minor versie (bv. '1.0')
|
||||||
|
patch (str, optional): Patchnummer (bv. '5')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: {
|
||||||
|
'content': str,
|
||||||
|
'version': str,
|
||||||
|
'content_type': str
|
||||||
|
} of None bij fout
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Als geen versie opgegeven, vind de laatste
|
||||||
|
if not major_minor:
|
||||||
|
version_info = self.get_latest_version(content_type)
|
||||||
|
if not version_info:
|
||||||
|
logger.error(f"No versions found for {content_type}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
major_minor, patch, full_version = version_info
|
||||||
|
|
||||||
|
# Als geen patch opgegeven, vind de laatste patch voor deze major_minor
|
||||||
|
elif not patch:
|
||||||
|
version_info = self.get_latest_version(content_type, major_minor)
|
||||||
|
if not version_info:
|
||||||
|
logger.error(f"No versions found for {content_type} {major_minor}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
major_minor, patch, full_version = version_info
|
||||||
|
else:
|
||||||
|
full_version = f"{major_minor}.{patch}"
|
||||||
|
|
||||||
|
# Nu hebben we major_minor en patch, lees het bestand
|
||||||
|
file_path = self.get_content_path(content_type, major_minor, patch)
|
||||||
|
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
logger.error(f"Content file does not exist: {file_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as file:
|
||||||
|
content = file.read()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'content': content,
|
||||||
|
'version': full_version,
|
||||||
|
'content_type': content_type
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error reading content {content_type} {major_minor}.{patch}: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def list_content_types(self):
|
||||||
|
"""Lijst alle beschikbare contenttypes op"""
|
||||||
|
try:
|
||||||
|
return [d for d in os.listdir(self.app.config['CONTENT_DIR'])
|
||||||
|
if os.path.isdir(os.path.join(self.app.config['CONTENT_DIR'], d))]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error listing content types: {str(e)}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def list_versions(self, content_type):
|
||||||
|
"""
|
||||||
|
Lijst alle beschikbare versies voor een contenttype
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: Lijst van dicts met versie-informatie
|
||||||
|
[{'version': '1.0.0', 'path': '/path/to/file', 'date_modified': datetime}]
|
||||||
|
"""
|
||||||
|
versions = []
|
||||||
|
try:
|
||||||
|
content_path = os.path.join(self.app.config['CONTENT_DIR'], content_type)
|
||||||
|
|
||||||
|
if not os.path.exists(content_path):
|
||||||
|
return []
|
||||||
|
|
||||||
|
for major_minor in os.listdir(content_path):
|
||||||
|
major_minor_path = os.path.join(content_path, major_minor)
|
||||||
|
|
||||||
|
if not os.path.isdir(major_minor_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for file in os.listdir(major_minor_path):
|
||||||
|
mm, p = self._parse_version(file)
|
||||||
|
if mm and p:
|
||||||
|
file_path = os.path.join(major_minor_path, file)
|
||||||
|
mod_time = os.path.getmtime(file_path)
|
||||||
|
versions.append({
|
||||||
|
'version': f"{mm}.{p}",
|
||||||
|
'path': file_path,
|
||||||
|
'date_modified': mod_time
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sorteer op versienummer
|
||||||
|
versions.sort(key=lambda v: version.parse(v['version']))
|
||||||
|
return versions
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error listing versions for {content_type}: {str(e)}")
|
||||||
|
return []
|
||||||
@@ -38,6 +38,8 @@ def create_default_config_from_type_config(type_config):
|
|||||||
default_config[field_name] = 0
|
default_config[field_name] = 0
|
||||||
elif field_type == "boolean":
|
elif field_type == "boolean":
|
||||||
default_config[field_name] = False
|
default_config[field_name] = False
|
||||||
|
elif field_type == "color":
|
||||||
|
default_config[field_name] = "#000000"
|
||||||
else:
|
else:
|
||||||
default_config[field_name] = ""
|
default_config[field_name] = ""
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
import jinja2
|
import jinja2
|
||||||
@@ -12,6 +13,7 @@ def not_found_error(error):
|
|||||||
if not current_user.is_authenticated:
|
if not current_user.is_authenticated:
|
||||||
return redirect(prefixed_url_for('security.login'))
|
return redirect(prefixed_url_for('security.login'))
|
||||||
current_app.logger.error(f"Not Found Error: {error}")
|
current_app.logger.error(f"Not Found Error: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
return render_template('error/404.html'), 404
|
return render_template('error/404.html'), 404
|
||||||
|
|
||||||
|
|
||||||
@@ -19,6 +21,7 @@ def internal_server_error(error):
|
|||||||
if not current_user.is_authenticated:
|
if not current_user.is_authenticated:
|
||||||
return redirect(prefixed_url_for('security.login'))
|
return redirect(prefixed_url_for('security.login'))
|
||||||
current_app.logger.error(f"Internal Server Error: {error}")
|
current_app.logger.error(f"Internal Server Error: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
return render_template('error/500.html'), 500
|
return render_template('error/500.html'), 500
|
||||||
|
|
||||||
|
|
||||||
@@ -26,6 +29,7 @@ def not_authorised_error(error):
|
|||||||
if not current_user.is_authenticated:
|
if not current_user.is_authenticated:
|
||||||
return redirect(prefixed_url_for('security.login'))
|
return redirect(prefixed_url_for('security.login'))
|
||||||
current_app.logger.error(f"Not Authorised Error: {error}")
|
current_app.logger.error(f"Not Authorised Error: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
return render_template('error/401.html')
|
return render_template('error/401.html')
|
||||||
|
|
||||||
|
|
||||||
@@ -33,6 +37,7 @@ def access_forbidden(error):
|
|||||||
if not current_user.is_authenticated:
|
if not current_user.is_authenticated:
|
||||||
return redirect(prefixed_url_for('security.login'))
|
return redirect(prefixed_url_for('security.login'))
|
||||||
current_app.logger.error(f"Access Forbidden: {error}")
|
current_app.logger.error(f"Access Forbidden: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
return render_template('error/403.html')
|
return render_template('error/403.html')
|
||||||
|
|
||||||
|
|
||||||
@@ -42,6 +47,7 @@ def key_error_handler(error):
|
|||||||
return redirect(prefixed_url_for('security.login'))
|
return redirect(prefixed_url_for('security.login'))
|
||||||
# For other KeyErrors, you might want to log the error and return a generic error page
|
# For other KeyErrors, you might want to log the error and return a generic error page
|
||||||
current_app.logger.error(f"Key Error: {error}")
|
current_app.logger.error(f"Key Error: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
return render_template('error/generic.html', error_message="An unexpected error occurred"), 500
|
return render_template('error/generic.html', error_message="An unexpected error occurred"), 500
|
||||||
|
|
||||||
|
|
||||||
@@ -76,6 +82,7 @@ def no_tenant_selected_error(error):
|
|||||||
a long period of inactivity. The user will be redirected to the login page.
|
a long period of inactivity. The user will be redirected to the login page.
|
||||||
"""
|
"""
|
||||||
current_app.logger.error(f"No Session Tenant Error: {error}")
|
current_app.logger.error(f"No Session Tenant Error: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
flash('Your session expired. You will have to re-enter your credentials', 'warning')
|
flash('Your session expired. You will have to re-enter your credentials', 'warning')
|
||||||
|
|
||||||
# Perform logout if user is authenticated
|
# Perform logout if user is authenticated
|
||||||
@@ -95,6 +102,26 @@ def general_exception(e):
|
|||||||
error_details=str(e)), 500
|
error_details=str(e)), 500
|
||||||
|
|
||||||
|
|
||||||
|
def template_not_found_error(error):
|
||||||
|
"""Handle Jinja2 TemplateNotFound exceptions."""
|
||||||
|
current_app.logger.error(f'Template not found: {error.name}')
|
||||||
|
current_app.logger.error(f'Search Paths: {current_app.jinja_loader.list_templates()}')
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
|
return render_template('error/500.html',
|
||||||
|
error_type="Template Not Found",
|
||||||
|
error_details=f"Template '{error.name}' could not be found."), 404
|
||||||
|
|
||||||
|
|
||||||
|
def template_syntax_error(error):
|
||||||
|
"""Handle Jinja2 TemplateSyntaxError exceptions."""
|
||||||
|
current_app.logger.error(f'Template syntax error: {error.message}')
|
||||||
|
current_app.logger.error(f'In template {error.filename}, line {error.lineno}')
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
|
return render_template('error/500.html',
|
||||||
|
error_type="Template Syntax Error",
|
||||||
|
error_details=f"Error in template '{error.filename}' at line {error.lineno}: {error.message}"), 500
|
||||||
|
|
||||||
|
|
||||||
def register_error_handlers(app):
|
def register_error_handlers(app):
|
||||||
app.register_error_handler(404, not_found_error)
|
app.register_error_handler(404, not_found_error)
|
||||||
app.register_error_handler(500, internal_server_error)
|
app.register_error_handler(500, internal_server_error)
|
||||||
@@ -103,17 +130,6 @@ def register_error_handlers(app):
|
|||||||
app.register_error_handler(EveAINoSessionTenant, no_tenant_selected_error)
|
app.register_error_handler(EveAINoSessionTenant, no_tenant_selected_error)
|
||||||
app.register_error_handler(KeyError, key_error_handler)
|
app.register_error_handler(KeyError, key_error_handler)
|
||||||
app.register_error_handler(AttributeError, attribute_error_handler)
|
app.register_error_handler(AttributeError, attribute_error_handler)
|
||||||
|
app.register_error_handler(jinja2.TemplateNotFound, template_not_found_error)
|
||||||
|
app.register_error_handler(jinja2.TemplateSyntaxError, template_syntax_error)
|
||||||
app.register_error_handler(Exception, general_exception)
|
app.register_error_handler(Exception, general_exception)
|
||||||
|
|
||||||
@app.errorhandler(jinja2.TemplateNotFound)
|
|
||||||
def template_not_found(error):
|
|
||||||
app.logger.error(f'Template not found: {error.name}')
|
|
||||||
app.logger.error(f'Search Paths: {app.jinja_loader.list_templates()}')
|
|
||||||
return f'Template not found: {error.name}. Check logs for details.', 404
|
|
||||||
|
|
||||||
@app.errorhandler(jinja2.TemplateSyntaxError)
|
|
||||||
def template_syntax_error(error):
|
|
||||||
app.logger.error(f'Template syntax error: {error.message}')
|
|
||||||
app.logger.error(f'In template {error.filename}, line {error.lineno}')
|
|
||||||
return f'Template syntax error: {error.message}', 500
|
|
||||||
|
|
||||||
25
config/agents/traicie/TRAICIE_RECRUITER/1.0.0.yaml
Normal file
25
config/agents/traicie/TRAICIE_RECRUITER/1.0.0.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "Traicie HR BP "
|
||||||
|
role: >
|
||||||
|
You are an Expert Recruiter working for {tenant_name}
|
||||||
|
{custom_role}
|
||||||
|
goal: >
|
||||||
|
As an expert recruiter, you identify, attract, and secure top talent by building genuine relationships, deeply
|
||||||
|
understanding business needs, and ensuring optimal alignment between candidate potential and organizational goals
|
||||||
|
, while championing diversity, culture fit, and long-term retention.
|
||||||
|
{custom_goal}
|
||||||
|
backstory: >
|
||||||
|
You started your career in a high-pressure agency setting, where you quickly learned the art of fast-paced hiring and
|
||||||
|
relationship building. Over the years, you moved in-house, partnering closely with business leaders to shape
|
||||||
|
recruitment strategies that go beyond filling roles—you focus on finding the right people to drive growth and culture.
|
||||||
|
With a strong grasp of both tech and non-tech profiles, you’ve adapted to changing trends, from remote work to
|
||||||
|
AI-driven sourcing. You’re more than a recruiter—you’re a trusted advisor, a brand ambassador, and a connector of
|
||||||
|
people and purpose.
|
||||||
|
{custom_backstory}
|
||||||
|
full_model_name: "mistral.mistral-medium-latest"
|
||||||
|
temperature: 0.3
|
||||||
|
metadata:
|
||||||
|
author: "Josako"
|
||||||
|
date_added: "2025-05-21"
|
||||||
|
description: "HR BP Agent."
|
||||||
|
changes: "Initial version"
|
||||||
21
config/catalogs/globals/DOSSIER_CATALOG/1.0.0.yaml
Normal file
21
config/catalogs/globals/DOSSIER_CATALOG/1.0.0.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "Dossier Catalog"
|
||||||
|
description: "A Catalog with information in Evie's Library in which several Dossiers can be stored"
|
||||||
|
configuration:
|
||||||
|
tagging_fields:
|
||||||
|
name: "Tagging Fields"
|
||||||
|
type: "tagging_fields"
|
||||||
|
description: "Define the metadata fields that will be used for tagging documents.
|
||||||
|
Each field must have:
|
||||||
|
- type: one of 'string', 'integer', 'float', 'date', 'enum'
|
||||||
|
- required: boolean indicating if the field is mandatory
|
||||||
|
- description: field description
|
||||||
|
- allowed_values: list of values (for enum type only)
|
||||||
|
- min_value/max_value: range limits (for numeric types only)"
|
||||||
|
required: true
|
||||||
|
default: {}
|
||||||
|
document_version_configurations: ["tagging_fields"]
|
||||||
|
metadata:
|
||||||
|
author: "System"
|
||||||
|
date_added: "2023-01-01"
|
||||||
|
description: "A Catalog with information in Evie's Library in which several Dossiers can be stored"
|
||||||
9
config/catalogs/globals/STANDARD_CATALOG/1.0.0.yaml
Normal file
9
config/catalogs/globals/STANDARD_CATALOG/1.0.0.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "Standard Catalog"
|
||||||
|
description: "A Catalog with information in Evie's Library, to be considered as a whole"
|
||||||
|
configuration: {}
|
||||||
|
document_version_configurations: []
|
||||||
|
metadata:
|
||||||
|
author: "System"
|
||||||
|
date_added: "2023-01-01"
|
||||||
|
description: "A Catalog with information in Evie's Library, to be considered as a whole"
|
||||||
@@ -172,6 +172,9 @@ class Config(object):
|
|||||||
# Entitlement Constants
|
# Entitlement Constants
|
||||||
ENTITLEMENTS_MAX_PENDING_DAYS = 5 # Defines the maximum number of days a pending entitlement can be active
|
ENTITLEMENTS_MAX_PENDING_DAYS = 5 # Defines the maximum number of days a pending entitlement can be active
|
||||||
|
|
||||||
|
# Content Directory for static content like the changelog, terms & conditions, privacy statement, ...
|
||||||
|
CONTENT_DIR = '/app/content'
|
||||||
|
|
||||||
|
|
||||||
class DevConfig(Config):
|
class DevConfig(Config):
|
||||||
DEVELOPMENT = True
|
DEVELOPMENT = True
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "Chat Client Customisation"
|
||||||
|
configuration:
|
||||||
|
"primary_color":
|
||||||
|
name: "Primary Color"
|
||||||
|
description: "Primary Color"
|
||||||
|
type: "color"
|
||||||
|
required: false
|
||||||
|
"secondary_color":
|
||||||
|
name: "Secondary Color"
|
||||||
|
description: "Secondary Color"
|
||||||
|
type: "color"
|
||||||
|
required: false
|
||||||
|
"background_color":
|
||||||
|
name: "Background Color"
|
||||||
|
description: "Background Color"
|
||||||
|
type: "color"
|
||||||
|
required: false
|
||||||
|
"text_color":
|
||||||
|
name: "Text Color"
|
||||||
|
description: "Text Color"
|
||||||
|
type: "color"
|
||||||
|
required: false
|
||||||
|
"sidebar_color":
|
||||||
|
name: "Sidebar Color"
|
||||||
|
description: "Sidebar Color"
|
||||||
|
type: "color"
|
||||||
|
required: false
|
||||||
|
"sidebar_text":
|
||||||
|
name: "Sidebar Text"
|
||||||
|
description: "Text to be shown in the sidebar"
|
||||||
|
type: "text"
|
||||||
|
required: false
|
||||||
|
"welcome_message":
|
||||||
|
name: "Welcome Message"
|
||||||
|
description: "Text to be shown as Welcome"
|
||||||
|
type: "text"
|
||||||
|
required: false
|
||||||
|
metadata:
|
||||||
|
author: "Josako"
|
||||||
|
date_added: "2024-06-06"
|
||||||
|
changes: "Initial version"
|
||||||
|
description: "Parameters allowing to customise the chat client"
|
||||||
@@ -303,10 +303,10 @@ LOGGING = {
|
|||||||
'backupCount': 2,
|
'backupCount': 2,
|
||||||
'formatter': 'standard',
|
'formatter': 'standard',
|
||||||
},
|
},
|
||||||
'file_chat': {
|
'file_chat_client': {
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'class': 'logging.handlers.RotatingFileHandler',
|
'class': 'logging.handlers.RotatingFileHandler',
|
||||||
'filename': 'logs/eveai_chat.log',
|
'filename': 'logs/eveai_chat_client.log',
|
||||||
'maxBytes': 1024 * 1024 * 1, # 1MB
|
'maxBytes': 1024 * 1024 * 1, # 1MB
|
||||||
'backupCount': 2,
|
'backupCount': 2,
|
||||||
'formatter': 'standard',
|
'formatter': 'standard',
|
||||||
@@ -432,8 +432,8 @@ LOGGING = {
|
|||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'propagate': False
|
'propagate': False
|
||||||
},
|
},
|
||||||
'eveai_chat': { # logger for the eveai_chat
|
'eveai_chat_client': { # logger for the eveai_chat
|
||||||
'handlers': ['file_chat', 'graylog', ] if env == 'production' else ['file_chat', ],
|
'handlers': ['file_chat_client', 'graylog', ] if env == 'production' else ['file_chat_client', ],
|
||||||
'level': 'DEBUG',
|
'level': 'DEBUG',
|
||||||
'propagate': False
|
'propagate': False
|
||||||
},
|
},
|
||||||
|
|||||||
9
config/processors/globals/AUDIO_PROCESSOR/1.0.0.yaml
Normal file
9
config/processors/globals/AUDIO_PROCESSOR/1.0.0.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "AUDIO Processor"
|
||||||
|
file_types: "mp3, mp4, ogg"
|
||||||
|
description: "A Processor for audio files"
|
||||||
|
configuration: {}
|
||||||
|
metadata:
|
||||||
|
author: "System"
|
||||||
|
date_added: "2023-01-01"
|
||||||
|
description: "A Processor for audio files"
|
||||||
59
config/processors/globals/DOCX_PROCESSOR/1.0.0.yaml
Normal file
59
config/processors/globals/DOCX_PROCESSOR/1.0.0.yaml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "DOCX Processor"
|
||||||
|
file_types: "docx"
|
||||||
|
description: "A processor for DOCX files"
|
||||||
|
configuration:
|
||||||
|
chunking_patterns:
|
||||||
|
name: "Chunking Patterns"
|
||||||
|
description: "A list of Patterns used to chunk files into logical pieces"
|
||||||
|
type: "chunking_patterns"
|
||||||
|
required: false
|
||||||
|
chunking_heading_level:
|
||||||
|
name: "Chunking Heading Level"
|
||||||
|
type: "integer"
|
||||||
|
description: "Maximum heading level to consider for chunking (1-6)"
|
||||||
|
required: false
|
||||||
|
default: 2
|
||||||
|
extract_comments:
|
||||||
|
name: "Extract Comments"
|
||||||
|
type: "boolean"
|
||||||
|
description: "Whether to include document comments in the markdown"
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
extract_headers_footers:
|
||||||
|
name: "Extract Headers/Footers"
|
||||||
|
type: "boolean"
|
||||||
|
description: "Whether to include headers and footers in the markdown"
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
preserve_formatting:
|
||||||
|
name: "Preserve Formatting"
|
||||||
|
type: "boolean"
|
||||||
|
description: "Whether to preserve bold, italic, and other text formatting"
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
list_style:
|
||||||
|
name: "List Style"
|
||||||
|
type: "enum"
|
||||||
|
description: "How to format lists in markdown"
|
||||||
|
required: false
|
||||||
|
default: "dash"
|
||||||
|
allowed_values: ["dash", "asterisk", "plus"]
|
||||||
|
image_handling:
|
||||||
|
name: "Image Handling"
|
||||||
|
type: "enum"
|
||||||
|
description: "How to handle embedded images"
|
||||||
|
required: false
|
||||||
|
default: "skip"
|
||||||
|
allowed_values: ["skip", "extract", "placeholder"]
|
||||||
|
table_alignment:
|
||||||
|
name: "Table Alignment"
|
||||||
|
type: "enum"
|
||||||
|
description: "How to align table contents"
|
||||||
|
required: false
|
||||||
|
default: "left"
|
||||||
|
allowed_values: ["left", "center", "preserve"]
|
||||||
|
metadata:
|
||||||
|
author: "System"
|
||||||
|
date_added: "2023-01-01"
|
||||||
|
description: "A processor for DOCX files"
|
||||||
49
config/processors/globals/HTML_PROCESSOR/1.0.0.yaml
Normal file
49
config/processors/globals/HTML_PROCESSOR/1.0.0.yaml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "HTML Processor"
|
||||||
|
file_types: "html"
|
||||||
|
description: "A processor for HTML files"
|
||||||
|
configuration:
|
||||||
|
chunking_patterns:
|
||||||
|
name: "Chunking Patterns"
|
||||||
|
description: "A list of Patterns used to chunk files into logical pieces"
|
||||||
|
type: "chunking_patterns"
|
||||||
|
required: false
|
||||||
|
chunking_heading_level:
|
||||||
|
name: "Chunking Heading Level"
|
||||||
|
type: "integer"
|
||||||
|
description: "Maximum heading level to consider for chunking (1-6)"
|
||||||
|
required: false
|
||||||
|
default: 2
|
||||||
|
html_tags:
|
||||||
|
name: "HTML Tags"
|
||||||
|
type: "string"
|
||||||
|
description: "A comma-separated list of HTML tags"
|
||||||
|
required: true
|
||||||
|
default: "p, h1, h2, h3, h4, h5, h6, li, table, thead, tbody, tr, td"
|
||||||
|
html_end_tags:
|
||||||
|
name: "HTML End Tags"
|
||||||
|
type: "string"
|
||||||
|
description: "A comma-separated list of HTML end tags (where can the chunk end)"
|
||||||
|
required: true
|
||||||
|
default: "p, li, table"
|
||||||
|
html_included_elements:
|
||||||
|
name: "HTML Included Elements"
|
||||||
|
type: "string"
|
||||||
|
description: "A comma-separated list of elements to be included"
|
||||||
|
required: true
|
||||||
|
default: "article, main"
|
||||||
|
html_excluded_elements:
|
||||||
|
name: "HTML Excluded Elements"
|
||||||
|
type: "string"
|
||||||
|
description: "A comma-separated list of elements to be excluded"
|
||||||
|
required: false
|
||||||
|
default: "header, footer, nav, script"
|
||||||
|
html_excluded_classes:
|
||||||
|
name: "HTML Excluded Classes"
|
||||||
|
type: "string"
|
||||||
|
description: "A comma-separated list of classes to be excluded"
|
||||||
|
required: false
|
||||||
|
metadata:
|
||||||
|
author: "System"
|
||||||
|
date_added: "2023-01-01"
|
||||||
|
description: "A processor for HTML files"
|
||||||
20
config/processors/globals/MARKDOWN_PROCESSOR/1.0.0.yaml
Normal file
20
config/processors/globals/MARKDOWN_PROCESSOR/1.0.0.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "Markdown Processor"
|
||||||
|
file_types: "md"
|
||||||
|
description: "A Processor for markdown files"
|
||||||
|
configuration:
|
||||||
|
chunking_patterns:
|
||||||
|
name: "Chunking Patterns"
|
||||||
|
description: "A list of Patterns used to chunk files into logical pieces"
|
||||||
|
type: "chunking_patterns"
|
||||||
|
required: false
|
||||||
|
chunking_heading_level:
|
||||||
|
name: "Chunking Heading Level"
|
||||||
|
type: "integer"
|
||||||
|
description: "Maximum heading level to consider for chunking (1-6)"
|
||||||
|
required: false
|
||||||
|
default: 2
|
||||||
|
metadata:
|
||||||
|
author: "System"
|
||||||
|
date_added: "2023-01-01"
|
||||||
|
description: "A Processor for markdown files"
|
||||||
20
config/processors/globals/PDF_PROCESSOR/1.0.0.yaml
Normal file
20
config/processors/globals/PDF_PROCESSOR/1.0.0.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
version: "1.0.0"
|
||||||
|
name: "PDF Processor"
|
||||||
|
file_types: "pdf"
|
||||||
|
description: "A Processor for PDF files"
|
||||||
|
configuration:
|
||||||
|
chunking_patterns:
|
||||||
|
name: "Chunking Patterns"
|
||||||
|
description: "A list of Patterns used to chunk files into logical pieces"
|
||||||
|
type: "chunking_patterns"
|
||||||
|
required: false
|
||||||
|
chunking_heading_level:
|
||||||
|
name: "Chunking Heading Level"
|
||||||
|
type: "integer"
|
||||||
|
description: "Maximum heading level to consider for chunking (1-6)"
|
||||||
|
required: false
|
||||||
|
default: 2
|
||||||
|
metadata:
|
||||||
|
author: "System"
|
||||||
|
date_added: "2023-01-01"
|
||||||
|
description: "A Processor for PDF files"
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
version: "1.1.0"
|
||||||
|
name: "Traicie Role Definition Specialist"
|
||||||
|
framework: "crewai"
|
||||||
|
partner: "traicie"
|
||||||
|
chat: false
|
||||||
|
configuration: {}
|
||||||
|
arguments:
|
||||||
|
role_name:
|
||||||
|
name: "Role Name"
|
||||||
|
description: "The name of the role that is being processed. Will be used to create a selection specialist"
|
||||||
|
type: "str"
|
||||||
|
required: true
|
||||||
|
specialist_name:
|
||||||
|
name: "Specialist Name"
|
||||||
|
description: "The name the specialist will be called upon"
|
||||||
|
type: str
|
||||||
|
required: true
|
||||||
|
role_reference:
|
||||||
|
name: "Role Reference"
|
||||||
|
description: "A customer reference to the role"
|
||||||
|
type: "str"
|
||||||
|
required: false
|
||||||
|
vacancy_text:
|
||||||
|
name: "vacancy_text"
|
||||||
|
type: "text"
|
||||||
|
description: "The Vacancy Text"
|
||||||
|
required: true
|
||||||
|
results:
|
||||||
|
competencies:
|
||||||
|
name: "competencies"
|
||||||
|
type: "List[str, str]"
|
||||||
|
description: "List of vacancy competencies and their descriptions"
|
||||||
|
required: false
|
||||||
|
agents:
|
||||||
|
- type: "TRAICIE_HR_BP_AGENT"
|
||||||
|
version: "1.0"
|
||||||
|
tasks:
|
||||||
|
- type: "TRAICIE_GET_COMPETENCIES_TASK"
|
||||||
|
version: "1.1"
|
||||||
|
metadata:
|
||||||
|
author: "Josako"
|
||||||
|
date_added: "2025-05-27"
|
||||||
|
changes: "Updated for unified competencies and ko criteria"
|
||||||
|
description: "Assistant to create a new Vacancy based on Vacancy Text"
|
||||||
@@ -5,12 +5,17 @@ partner: "traicie"
|
|||||||
chat: false
|
chat: false
|
||||||
configuration:
|
configuration:
|
||||||
name:
|
name:
|
||||||
name: "name"
|
name: "Name"
|
||||||
description: "The name the specialist is called upon."
|
description: "The name the specialist is called upon."
|
||||||
type: "str"
|
type: "str"
|
||||||
required: true
|
required: true
|
||||||
|
role_reference:
|
||||||
|
name: "Role Reference"
|
||||||
|
description: "A customer reference to the role"
|
||||||
|
type: "str"
|
||||||
|
required: false
|
||||||
competencies:
|
competencies:
|
||||||
name: "competencies"
|
name: "Competencies"
|
||||||
description: "An ordered list of competencies."
|
description: "An ordered list of competencies."
|
||||||
type: "ordered_list"
|
type: "ordered_list"
|
||||||
list_type: "competency_details"
|
list_type: "competency_details"
|
||||||
@@ -41,17 +46,17 @@ configuration:
|
|||||||
required: false
|
required: false
|
||||||
competency_details:
|
competency_details:
|
||||||
title:
|
title:
|
||||||
name: "title"
|
name: "Title"
|
||||||
description: "Competency Title"
|
description: "Competency Title"
|
||||||
type: "str"
|
type: "str"
|
||||||
required: true
|
required: true
|
||||||
description:
|
description:
|
||||||
name: "description"
|
name: "Description"
|
||||||
description: "Description (in context of the role) of the competency"
|
description: "Description (in context of the role) of the competency"
|
||||||
type: "text"
|
type: "text"
|
||||||
required: true
|
required: true
|
||||||
is_knockout:
|
is_knockout:
|
||||||
name: "Is Knockout"
|
name: "KO"
|
||||||
description: "Defines if the competency is a knock-out criterium"
|
description: "Defines if the competency is a knock-out criterium"
|
||||||
type: "boolean"
|
type: "boolean"
|
||||||
required: true
|
required: true
|
||||||
@@ -63,10 +68,32 @@ competency_details:
|
|||||||
required: true
|
required: true
|
||||||
default: true
|
default: true
|
||||||
arguments:
|
arguments:
|
||||||
vacancy_text:
|
region:
|
||||||
name: "vacancy_text"
|
name: "Region"
|
||||||
type: "text"
|
type: "str"
|
||||||
description: "The Vacancy Text"
|
description: "The region of the specific vacancy"
|
||||||
|
required: false
|
||||||
|
working_schedule:
|
||||||
|
name: "Work Schedule"
|
||||||
|
type: "str"
|
||||||
|
description: "The work schedule or employment type of the specific vacancy"
|
||||||
|
required: false
|
||||||
|
start_date:
|
||||||
|
name: "Start Date"
|
||||||
|
type: "date"
|
||||||
|
description: "The start date of the specific vacancy"
|
||||||
|
required: false
|
||||||
|
language:
|
||||||
|
name: "Language"
|
||||||
|
type: "str"
|
||||||
|
description: "The language (2-letter code) used to start the conversation"
|
||||||
|
required: true
|
||||||
|
interaction_mode:
|
||||||
|
name: "Interaction Mode"
|
||||||
|
type: "enum"
|
||||||
|
description: "The interaction mode the specialist will start working in."
|
||||||
|
allowed_values: ["Job Application", "Seduction"]
|
||||||
|
default: "Job Application"
|
||||||
required: true
|
required: true
|
||||||
results:
|
results:
|
||||||
competencies:
|
competencies:
|
||||||
|
|||||||
@@ -2,28 +2,10 @@
|
|||||||
CATALOG_TYPES = {
|
CATALOG_TYPES = {
|
||||||
"STANDARD_CATALOG": {
|
"STANDARD_CATALOG": {
|
||||||
"name": "Standard Catalog",
|
"name": "Standard Catalog",
|
||||||
"Description": "A Catalog with information in Evie's Library, to be considered as a whole",
|
"description": "A Catalog with information in Evie's Library, to be considered as a whole",
|
||||||
"configuration": {},
|
|
||||||
"document_version_configurations": []
|
|
||||||
},
|
},
|
||||||
"DOSSIER_CATALOG": {
|
"DOSSIER_CATALOG": {
|
||||||
"name": "Dossier Catalog",
|
"name": "Dossier Catalog",
|
||||||
"Description": "A Catalog with information in Evie's Library in which several Dossiers can be stored",
|
"description": "A Catalog with information in Evie's Library in which several Dossiers can be stored",
|
||||||
"configuration": {
|
|
||||||
"tagging_fields": {
|
|
||||||
"name": "Tagging Fields",
|
|
||||||
"type": "tagging_fields",
|
|
||||||
"description": """Define the metadata fields that will be used for tagging documents.
|
|
||||||
Each field must have:
|
|
||||||
- type: one of 'string', 'integer', 'float', 'date', 'enum'
|
|
||||||
- required: boolean indicating if the field is mandatory
|
|
||||||
- description: field description
|
|
||||||
- allowed_values: list of values (for enum type only)
|
|
||||||
- min_value/max_value: range limits (for numeric types only)""",
|
|
||||||
"required": True,
|
|
||||||
"default": {},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"document_version_configurations": ["tagging_fields"]
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
7
config/type_defs/customisation_types.py
Normal file
7
config/type_defs/customisation_types.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Catalog Types
|
||||||
|
CUSTOMISATION_TYPES = {
|
||||||
|
"CHAT_CLIENT_CUSTOMISATION": {
|
||||||
|
"name": "Chat Client Customisation",
|
||||||
|
"description": "Parameters allowing to customise the chat client",
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,168 +1,28 @@
|
|||||||
# Catalog Types
|
# Processor Types
|
||||||
PROCESSOR_TYPES = {
|
PROCESSOR_TYPES = {
|
||||||
"HTML_PROCESSOR": {
|
"HTML_PROCESSOR": {
|
||||||
"name": "HTML Processor",
|
"name": "HTML Processor",
|
||||||
|
"description": "A processor for HTML files",
|
||||||
"file_types": "html",
|
"file_types": "html",
|
||||||
"Description": "A processor for HTML files",
|
|
||||||
"configuration": {
|
|
||||||
"chunking_patterns": {
|
|
||||||
"name": "Chunking Patterns",
|
|
||||||
"description": "A list of Patterns used to chunk files into logical pieces",
|
|
||||||
"type": "chunking_patterns",
|
|
||||||
"required": False
|
|
||||||
},
|
|
||||||
"chunking_heading_level": {
|
|
||||||
"name": "Chunking Heading Level",
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Maximum heading level to consider for chunking (1-6)",
|
|
||||||
"required": False,
|
|
||||||
"default": 2
|
|
||||||
},
|
|
||||||
"html_tags": {
|
|
||||||
"name": "HTML Tags",
|
|
||||||
"type": "string",
|
|
||||||
"description": "A comma-separated list of HTML tags",
|
|
||||||
"required": True,
|
|
||||||
"default": "p, h1, h2, h3, h4, h5, h6, li, table, thead, tbody, tr, td"
|
|
||||||
},
|
|
||||||
"html_end_tags": {
|
|
||||||
"name": "HTML End Tags",
|
|
||||||
"type": "string",
|
|
||||||
"description": "A comma-separated list of HTML end tags (where can the chunk end)",
|
|
||||||
"required": True,
|
|
||||||
"default": "p, li, table"
|
|
||||||
},
|
|
||||||
"html_included_elements": {
|
|
||||||
"name": "HTML Included Elements",
|
|
||||||
"type": "string",
|
|
||||||
"description": "A comma-separated list of elements to be included",
|
|
||||||
"required": True,
|
|
||||||
"default": "article, main"
|
|
||||||
},
|
|
||||||
"html_excluded_elements": {
|
|
||||||
"name": "HTML Excluded Elements",
|
|
||||||
"type": "string",
|
|
||||||
"description": "A comma-separated list of elements to be excluded",
|
|
||||||
"required": False,
|
|
||||||
"default": "header, footer, nav, script"
|
|
||||||
},
|
|
||||||
"html_excluded_classes": {
|
|
||||||
"name": "HTML Excluded Classes",
|
|
||||||
"type": "string",
|
|
||||||
"description": "A comma-separated list of classes to be excluded",
|
|
||||||
"required": False,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"PDF_PROCESSOR": {
|
"PDF_PROCESSOR": {
|
||||||
"name": "PDF Processor",
|
"name": "PDF Processor",
|
||||||
|
"description": "A Processor for PDF files",
|
||||||
"file_types": "pdf",
|
"file_types": "pdf",
|
||||||
"Description": "A Processor for PDF files",
|
|
||||||
"configuration": {
|
|
||||||
"chunking_patterns": {
|
|
||||||
"name": "Chunking Patterns",
|
|
||||||
"description": "A list of Patterns used to chunk files into logical pieces",
|
|
||||||
"type": "chunking_patterns",
|
|
||||||
"required": False
|
|
||||||
},
|
|
||||||
"chunking_heading_level": {
|
|
||||||
"name": "Chunking Heading Level",
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Maximum heading level to consider for chunking (1-6)",
|
|
||||||
"required": False,
|
|
||||||
"default": 2
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"AUDIO_PROCESSOR": {
|
"AUDIO_PROCESSOR": {
|
||||||
"name": "AUDIO Processor",
|
"name": "AUDIO Processor",
|
||||||
|
"description": "A Processor for audio files",
|
||||||
"file_types": "mp3, mp4, ogg",
|
"file_types": "mp3, mp4, ogg",
|
||||||
"Description": "A Processor for audio files",
|
|
||||||
"configuration": {}
|
|
||||||
},
|
},
|
||||||
"MARKDOWN_PROCESSOR": {
|
"MARKDOWN_PROCESSOR": {
|
||||||
"name": "Markdown Processor",
|
"name": "Markdown Processor",
|
||||||
|
"description": "A Processor for markdown files",
|
||||||
"file_types": "md",
|
"file_types": "md",
|
||||||
"Description": "A Processor for markdown files",
|
|
||||||
"configuration": {
|
|
||||||
"chunking_patterns": {
|
|
||||||
"name": "Chunking Patterns",
|
|
||||||
"description": "A list of Patterns used to chunk files into logical pieces",
|
|
||||||
"type": "chunking_patterns",
|
|
||||||
"required": False
|
|
||||||
},
|
|
||||||
"chunking_heading_level": {
|
|
||||||
"name": "Chunking Heading Level",
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Maximum heading level to consider for chunking (1-6)",
|
|
||||||
"required": False,
|
|
||||||
"default": 2
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"DOCX_PROCESSOR": {
|
"DOCX_PROCESSOR": {
|
||||||
"name": "DOCX Processor",
|
"name": "DOCX Processor",
|
||||||
|
"description": "A processor for DOCX files",
|
||||||
"file_types": "docx",
|
"file_types": "docx",
|
||||||
"Description": "A processor for DOCX files",
|
|
||||||
"configuration": {
|
|
||||||
"chunking_patterns": {
|
|
||||||
"name": "Chunking Patterns",
|
|
||||||
"description": "A list of Patterns used to chunk files into logical pieces",
|
|
||||||
"type": "chunking_patterns",
|
|
||||||
"required": False
|
|
||||||
},
|
|
||||||
"chunking_heading_level": {
|
|
||||||
"name": "Chunking Heading Level",
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Maximum heading level to consider for chunking (1-6)",
|
|
||||||
"required": False,
|
|
||||||
"default": 2
|
|
||||||
},
|
|
||||||
"extract_comments": {
|
|
||||||
"name": "Extract Comments",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Whether to include document comments in the markdown",
|
|
||||||
"required": False,
|
|
||||||
"default": False
|
|
||||||
},
|
|
||||||
"extract_headers_footers": {
|
|
||||||
"name": "Extract Headers/Footers",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Whether to include headers and footers in the markdown",
|
|
||||||
"required": False,
|
|
||||||
"default": False
|
|
||||||
},
|
|
||||||
"preserve_formatting": {
|
|
||||||
"name": "Preserve Formatting",
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Whether to preserve bold, italic, and other text formatting",
|
|
||||||
"required": False,
|
|
||||||
"default": True
|
|
||||||
},
|
|
||||||
"list_style": {
|
|
||||||
"name": "List Style",
|
|
||||||
"type": "enum",
|
|
||||||
"description": "How to format lists in markdown",
|
|
||||||
"required": False,
|
|
||||||
"default": "dash",
|
|
||||||
"allowed_values": ["dash", "asterisk", "plus"]
|
|
||||||
},
|
|
||||||
"image_handling": {
|
|
||||||
"name": "Image Handling",
|
|
||||||
"type": "enum",
|
|
||||||
"description": "How to handle embedded images",
|
|
||||||
"required": False,
|
|
||||||
"default": "skip",
|
|
||||||
"allowed_values": ["skip", "extract", "placeholder"]
|
|
||||||
},
|
|
||||||
"table_alignment": {
|
|
||||||
"name": "Table Alignment",
|
|
||||||
"type": "enum",
|
|
||||||
"description": "How to align table contents",
|
|
||||||
"required": False,
|
|
||||||
"default": "left",
|
|
||||||
"allowed_values": ["left", "center", "preserve"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,5 +16,9 @@ SPECIALIST_TYPES = {
|
|||||||
"name": "Traicie Role Definition Specialist",
|
"name": "Traicie Role Definition Specialist",
|
||||||
"description": "Assistant Defining Competencies and KO Criteria",
|
"description": "Assistant Defining Competencies and KO Criteria",
|
||||||
"partner": "traicie"
|
"partner": "traicie"
|
||||||
|
},
|
||||||
|
"TRAICIE_SELECTION_SPECIALIST": {
|
||||||
|
"name": "Traicie Selection Specialist",
|
||||||
|
"description": "Recruitment Selection Assistant",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
375
content/changelog/1.0/1.0.0.md
Normal file
375
content/changelog/1.0/1.0.0.md
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
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/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [2.3.3-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add Tenant Make
|
||||||
|
- Add Chat Client customisation options to Tenant Make
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Catalog name must be unique to avoid mistakes
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Ensure document version is selected in UI before trying to view it.
|
||||||
|
- Remove obsolete tab from tenant overview
|
||||||
|
|
||||||
|
## [2.3.2-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Changelog display
|
||||||
|
- Introduction of Specialist Magic Links
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- dynamic fields for adding documents / urls to dossier catalog
|
||||||
|
- tabs in latest bootstrap version no longer functional
|
||||||
|
- partner association of license tier not working when no partner selected
|
||||||
|
- data-type dynamic field needs conversion to isoformat
|
||||||
|
- Add public tables to env.py of tenant schema
|
||||||
|
|
||||||
|
## [2.3.1-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Introduction of ordered_list dynamic field type (using tabulator)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Bring configuration of PROCESSOR_TYPES & CATALOG_TYPES to new config standard
|
||||||
|
- Specialist Editor: move general information in tab
|
||||||
|
- Role Definition Specialist creates Selection Specialist from generated competencies
|
||||||
|
- Improvements to Selection Specialist (Agent definition to be started)
|
||||||
|
|
||||||
|
## [2.3.0-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Introduction of Push Gateway for Prometheus
|
||||||
|
- Introduction of Partner Models
|
||||||
|
- Introduction of Tenant and Partner codes for more security
|
||||||
|
- Introduction of 'Management Partner' type and additional 'Partner Admin'-role
|
||||||
|
- Introduction of a technical services layer
|
||||||
|
- Introduction of partner-specific configurations
|
||||||
|
- Introduction of additional test environment
|
||||||
|
- Introduction of strict no-overage usage
|
||||||
|
- Introduction of LicensePeriod, Payments & Invoices
|
||||||
|
- Introduction of Processed File Viewer
|
||||||
|
- Introduction of Traicie Role Definition Specialist
|
||||||
|
- Allow invocation of non-interactive specialists in administrative interface (eveai_app)
|
||||||
|
- Introduction of advanced JSON editor
|
||||||
|
- Introduction of ChatSession (Specialist Execution) follow-up in administrative interface
|
||||||
|
- Introduce npm for javascript libraries usage and optimisations
|
||||||
|
- Introduction of new top bar in administrative interface to show session defaults (removing old navbar buttons)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Add 'Register'-button to list views, replacing register menu-items
|
||||||
|
- Add additional environment capabilities in docker
|
||||||
|
- PDF Processor now uses Mistral OCR
|
||||||
|
- Allow additional chunking mechanisms for very long chunks (in case of very large documents)
|
||||||
|
- Allow for TrackedMistralAIEmbedding batching to allow for processing long documents
|
||||||
|
- RAG & SPIN Specialist improvements
|
||||||
|
- Move mail messaging from standard SMTP to Scaleway TEM mails
|
||||||
|
- Improve mail layouts
|
||||||
|
- Add functionality to add a default dictionary for dynamic forms
|
||||||
|
- AI model choices defined by Ask Eve AI iso Tenant (replaces ModelVariables completely)
|
||||||
|
- Improve HTML Processing
|
||||||
|
- Pagination improvements
|
||||||
|
- Update Material Kit Pro to latest version
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Repopack implementation ==> Using PyCharm's new AI capabilities instead
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Synchronous vs Asynchronous behaviour in crewAI type specialists
|
||||||
|
- Nasty dynamic boolean fields bug corrected
|
||||||
|
- Several smaller bugfixes
|
||||||
|
- Tasks & Tools editors finished
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- In case of vulnerabilities.
|
||||||
|
|
||||||
|
## [2.2.0-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Mistral AI as main provider for embeddings, chains and specialists
|
||||||
|
- Usage measuring for specialists
|
||||||
|
- RAG from chain to specialist technology
|
||||||
|
- Dossier catalog management possibilities added to eveai_app
|
||||||
|
- Asset definition (Paused - other priorities)
|
||||||
|
- Prometheus and Grafana
|
||||||
|
- Add prometheus monitoring to business events
|
||||||
|
- Asynchronous execution of specialists
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Moved choice for AI providers / models to specialists and prompts
|
||||||
|
- Improve RAG to not repeat historic answers
|
||||||
|
- Fixed embedding model, no more choices allowed
|
||||||
|
- clean url (of tracking parameters) before adding it to a catalog
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- For soon-to-be removed features.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Add Multiple URLs removed from menu
|
||||||
|
- Old Specialist items removed from interaction menu
|
||||||
|
-
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Set default language when registering Documents or URLs.
|
||||||
|
|
||||||
|
## [2.1.0-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Zapier Refresh Document
|
||||||
|
- SPIN Specialist definition - from start to finish
|
||||||
|
- Introduction of startup scripts in eveai_app
|
||||||
|
- Caching for all configurations added
|
||||||
|
- Caching for processed specialist configurations
|
||||||
|
- Caching for specialist history
|
||||||
|
- Augmented Specialist Editor, including Specialist graphic presentation
|
||||||
|
- Introduction of specialist_execution_api, introducting SSE
|
||||||
|
- Introduction of crewai framework for specialist implementation
|
||||||
|
- Test app for testing specialists - also serves as a sample client application for SSE
|
||||||
|
-
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Improvement of startup of applications using gevent, and better handling and scaling of multiple connections
|
||||||
|
- STANDARD_RAG Specialist improvement
|
||||||
|
-
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- eveai_chat - using sockets - will be replaced with new specialist_execution_api and SSE
|
||||||
|
|
||||||
|
## [2.0.1-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Zapîer Integration (partial - only adding files).
|
||||||
|
- Addition of general chunking parameters (chunking_heading_level and chunking_patterns)
|
||||||
|
- Addition of DocX and markdown Processor Types
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- For changes in existing functionality.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- For soon-to-be removed features.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- For now removed features.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Ensure the RAG Specialist is using the detailed_question
|
||||||
|
- Wordpress Chat Plugin: languages dropdown filled again
|
||||||
|
- OpenAI update - proxies no longer supported
|
||||||
|
- Build & Release script for Wordpress Plugins (including end user download folder)
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- In case of vulnerabilities.
|
||||||
|
|
||||||
|
## [2.0.0-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Introduction of dynamic Retrievers & Specialists
|
||||||
|
- Introduction of dynamic Processors
|
||||||
|
- Introduction of caching system
|
||||||
|
- Introduction of a better template manager
|
||||||
|
- Modernisation of external API/Socket authentication using projects
|
||||||
|
- Creation of new eveai_chat WordPress plugin to support specialists
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Update of eveai_sync WordPress plugin
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Set default language when registering Documents or URLs.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Security improvements to Docker images
|
||||||
|
|
||||||
|
## [1.0.14-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- New release script added to tag images with release number
|
||||||
|
- Allow the addition of multiple types of Catalogs
|
||||||
|
- Generic functionality to enable dynamic fields
|
||||||
|
- Addition of Retrievers to allow for smart collection of information in Catalogs
|
||||||
|
- Add dynamic fields to Catalog / Retriever / DocumentVersion
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Processing parameters defined at Catalog level iso Tenant level
|
||||||
|
- Reroute 'blank' paths to 'admin'
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- For soon-to-be removed features.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- For now removed features.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Set default language when registering Documents or URLs.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- In case of vulnerabilities.
|
||||||
|
|
||||||
|
## [1.0.13-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Finished Catalog introduction
|
||||||
|
- Reinitialization of WordPress site for syncing
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Modification of WordPress Sync Component
|
||||||
|
- Cleanup of attributes in Tenant
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Overall bugfixes as result from the Catalog introduction
|
||||||
|
|
||||||
|
## [1.0.12-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Added Catalog functionality
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- For changes in existing functionality.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- For soon-to-be removed features.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- For now removed features.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Set default language when registering Documents or URLs.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- In case of vulnerabilities.
|
||||||
|
|
||||||
|
## [1.0.11-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- License Usage Calculation realised
|
||||||
|
- View License Usages
|
||||||
|
- Celery Beat container added
|
||||||
|
- First schedule in Celery Beat for calculating usage (hourly)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- repopack can now split for different components
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Various fixes as consequence of changing file_location / file_name ==> bucket_name / object_name
|
||||||
|
- Celery Routing / Queuing updated
|
||||||
|
|
||||||
|
## [1.0.10-alfa]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- BusinessEventLog monitoring using Langchain native code
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Allow longer audio files (or video) to be uploaded and processed
|
||||||
|
- Storage and Embedding usage now expressed in MiB iso tokens (more logical)
|
||||||
|
- Views for License / LicenseTier
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Portkey removed for monitoring usage
|
||||||
|
|
||||||
|
## [1.0.9-alfa] - 2024/10/01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Business Event tracing (eveai_workers & eveai_chat_workers)
|
||||||
|
- Flower Container added for monitoring
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Healthcheck improvements
|
||||||
|
- model_utils turned into a class with lazy loading
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
- For soon-to-be removed features.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- For now removed features.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Set default language when registering Documents or URLs.
|
||||||
|
|
||||||
|
## [1.0.8-alfa] - 2024-09-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Tenant type defined to allow for active, inactive, demo ... tenants
|
||||||
|
- Search and filtering functionality on Tenants
|
||||||
|
- Implementation of health checks (1st version)
|
||||||
|
- Provision for Prometheus monitoring (no implementation yet)
|
||||||
|
- Refine audio_processor and srt_processor to reduce duplicate code and support larger files
|
||||||
|
- Introduction of repopack to reason in LLMs about the code
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Refine audio_processor and srt_processor to reduce duplicate code and support larger files
|
||||||
|
|
||||||
|
## [1.0.7-alfa] - 2024-09-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Full Document API allowing for creation, updating and invalidation of documents.
|
||||||
|
- Metadata fields (JSON) added to DocumentVersion, allowing end-users to add structured information
|
||||||
|
- Wordpress plugin eveai_sync to synchronize Wordpress content with EveAI
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Maximal deduplication of code between views and api in document_utils.py
|
||||||
|
|
||||||
|
## [1.0.6-alfa] - 2024-09-03
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Problems with tenant scheme migrations - may have to be revisited
|
||||||
|
- Correction of default language settings when uploading docs or URLs
|
||||||
|
- Addition of a CHANGELOG.md file
|
||||||
|
|
||||||
|
## [1.0.5-alfa] - 2024-09-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Allow chatwidget to connect to multiple servers (e.g. development and production)
|
||||||
|
- Start implementation of API
|
||||||
|
- Add API-key functionality to tenants
|
||||||
|
- Deduplication of API and Document view code
|
||||||
|
- Allow URL addition to accept all types of files, not just HTML
|
||||||
|
- Allow new file types upload: srt, mp3, ogg, mp4
|
||||||
|
- Improve processing of different file types using Processor classes
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- Removed direct upload of Youtube URLs, due to continuous changes in Youtube website
|
||||||
|
|
||||||
|
## [1.0.4-alfa] - 2024-08-27
|
||||||
|
Skipped
|
||||||
|
|
||||||
|
## [1.0.3-alfa] - 2024-08-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Refinement of HTML processing - allow for excluded classes and elements.
|
||||||
|
- Allow for multiple instances of Evie on 1 website (pure + Wordpress plugin)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- PDF Processing extracted in new PDF Processor class.
|
||||||
|
- Allow for longer and more complex PDFs to be uploaded.
|
||||||
|
|
||||||
|
## [1.0.2-alfa] - 2024-08-22
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Bugfix for ResetPasswordForm in config.py
|
||||||
|
|
||||||
|
## [1.0.1-alfa] - 2024-08-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Full Document Version Overview
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Improvements to user creation and registration, renewal of passwords, ...
|
||||||
|
|
||||||
|
## [1.0.0-alfa] - 2024-08-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial release of the project.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- None
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- None
|
||||||
|
|
||||||
|
[Unreleased]: https://github.com/username/repo/compare/v1.0.0...HEAD
|
||||||
|
[1.0.0]: https://github.com/username/repo/releases/tag/v1.0.0
|
||||||
37
content/privacy/1.0/1.0.0.md
Normal file
37
content/privacy/1.0/1.0.0.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Privacy Policy
|
||||||
|
|
||||||
|
## Version 1.0.0
|
||||||
|
|
||||||
|
*Effective Date: 2025-06-03*
|
||||||
|
|
||||||
|
### 1. Introduction
|
||||||
|
|
||||||
|
This Privacy Policy describes how EveAI collects, uses, and discloses your information when you use our services.
|
||||||
|
|
||||||
|
### 2. Information We Collect
|
||||||
|
|
||||||
|
We collect information you provide directly to us, such as account information, content you process through our services, and communication data.
|
||||||
|
|
||||||
|
### 3. How We Use Your Information
|
||||||
|
|
||||||
|
We use your information to provide, maintain, and improve our services, process transactions, send communications, and comply with legal obligations.
|
||||||
|
|
||||||
|
### 4. Data Security
|
||||||
|
|
||||||
|
We implement appropriate security measures to protect your personal information against unauthorized access, alteration, disclosure, or destruction.
|
||||||
|
|
||||||
|
### 5. International Data Transfers
|
||||||
|
|
||||||
|
Your information may be transferred to and processed in countries other than the country you reside in, where data protection laws may differ.
|
||||||
|
|
||||||
|
### 6. Your Rights
|
||||||
|
|
||||||
|
Depending on your location, you may have certain rights regarding your personal information, such as access, correction, deletion, or restriction of processing.
|
||||||
|
|
||||||
|
### 7. Changes to This Policy
|
||||||
|
|
||||||
|
We may update this Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page.
|
||||||
|
|
||||||
|
### 8. Contact Us
|
||||||
|
|
||||||
|
If you have any questions about this Privacy Policy, please contact us at privacy@askeveai.be.
|
||||||
37
content/terms/1.0/1.0.0.md
Normal file
37
content/terms/1.0/1.0.0.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Terms of Service
|
||||||
|
|
||||||
|
## Version 1.0.0
|
||||||
|
|
||||||
|
*Effective Date: 2025-06-03*
|
||||||
|
|
||||||
|
### 1. Introduction
|
||||||
|
|
||||||
|
Welcome to EveAI. By accessing or using our services, you agree to be bound by these Terms of Service.
|
||||||
|
|
||||||
|
### 2. Service Description
|
||||||
|
|
||||||
|
EveAI provides AI-powered solutions for businesses to optimize their operations through intelligent document processing and specialist execution.
|
||||||
|
|
||||||
|
### 3. User Accounts
|
||||||
|
|
||||||
|
To access certain features of the Service, you must register for an account. You are responsible for maintaining the confidentiality of your account information.
|
||||||
|
|
||||||
|
### 4. Privacy
|
||||||
|
|
||||||
|
Your use of the Service is also governed by our Privacy Policy, which can be found [here](/content/privacy).
|
||||||
|
|
||||||
|
### 5. Intellectual Property
|
||||||
|
|
||||||
|
All content, features, and functionality of the Service are owned by EveAI and are protected by international copyright, trademark, and other intellectual property laws.
|
||||||
|
|
||||||
|
### 6. Limitation of Liability
|
||||||
|
|
||||||
|
In no event shall EveAI be liable for any indirect, incidental, special, consequential or punitive damages.
|
||||||
|
|
||||||
|
### 7. Changes to Terms
|
||||||
|
|
||||||
|
We reserve the right to modify these Terms at any time. Your continued use of the Service after such modifications will constitute your acceptance of the new Terms.
|
||||||
|
|
||||||
|
### 8. Governing Law
|
||||||
|
|
||||||
|
These Terms shall be governed by the laws of Belgium.
|
||||||
@@ -70,6 +70,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- eveai_app
|
- eveai_app
|
||||||
- eveai_api
|
- eveai_api
|
||||||
|
- eveai_chat_client
|
||||||
networks:
|
networks:
|
||||||
- eveai-network
|
- eveai-network
|
||||||
|
|
||||||
@@ -91,6 +92,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ../eveai_app:/app/eveai_app
|
- ../eveai_app:/app/eveai_app
|
||||||
- ../common:/app/common
|
- ../common:/app/common
|
||||||
|
- ../content:/app/content
|
||||||
- ../config:/app/config
|
- ../config:/app/config
|
||||||
- ../migrations:/app/migrations
|
- ../migrations:/app/migrations
|
||||||
- ../scripts:/app/scripts
|
- ../scripts:/app/scripts
|
||||||
@@ -176,6 +178,44 @@ services:
|
|||||||
# networks:
|
# networks:
|
||||||
# - eveai-network
|
# - eveai-network
|
||||||
|
|
||||||
|
eveai_chat_client:
|
||||||
|
image: josakola/eveai_chat_client:latest
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: ./docker/eveai_chat_client/Dockerfile
|
||||||
|
platforms:
|
||||||
|
- linux/amd64
|
||||||
|
- linux/arm64
|
||||||
|
ports:
|
||||||
|
- 5004:5004
|
||||||
|
expose:
|
||||||
|
- 8000
|
||||||
|
environment:
|
||||||
|
<<: *common-variables
|
||||||
|
COMPONENT_NAME: eveai_chat_client
|
||||||
|
volumes:
|
||||||
|
- ../eveai_chat_client:/app/eveai_chat_client
|
||||||
|
- ../common:/app/common
|
||||||
|
- ../config:/app/config
|
||||||
|
- ../scripts:/app/scripts
|
||||||
|
- ../patched_packages:/app/patched_packages
|
||||||
|
- ./eveai_logs:/app/logs
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
minio:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:5004/healthz/ready"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 1s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
networks:
|
||||||
|
- eveai-network
|
||||||
|
|
||||||
eveai_chat_workers:
|
eveai_chat_workers:
|
||||||
image: josakola/eveai_chat_workers:latest
|
image: josakola/eveai_chat_workers:latest
|
||||||
build:
|
build:
|
||||||
@@ -440,4 +480,3 @@ volumes:
|
|||||||
#secrets:
|
#secrets:
|
||||||
# db-password:
|
# db-password:
|
||||||
# file: ./db/password.txt
|
# file: ./db/password.txt
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- eveai_app
|
- eveai_app
|
||||||
- eveai_api
|
- eveai_api
|
||||||
|
- eveai_chat_client
|
||||||
networks:
|
networks:
|
||||||
- eveai-network
|
- eveai-network
|
||||||
restart: "no"
|
restart: "no"
|
||||||
@@ -106,6 +107,33 @@ services:
|
|||||||
- eveai-network
|
- eveai-network
|
||||||
restart: "no"
|
restart: "no"
|
||||||
|
|
||||||
|
eveai_chat_client:
|
||||||
|
image: josakola/eveai_chat_client:${EVEAI_VERSION:-latest}
|
||||||
|
ports:
|
||||||
|
- 5004:5004
|
||||||
|
expose:
|
||||||
|
- 8000
|
||||||
|
environment:
|
||||||
|
<<: *common-variables
|
||||||
|
COMPONENT_NAME: eveai_chat_client
|
||||||
|
volumes:
|
||||||
|
- eveai_logs:/app/logs
|
||||||
|
- crewai_storage:/app/crewai_storage
|
||||||
|
depends_on:
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
minio:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:5004/healthz/ready"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 30s
|
||||||
|
networks:
|
||||||
|
- eveai-network
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
eveai_chat_workers:
|
eveai_chat_workers:
|
||||||
image: josakola/eveai_chat_workers:${EVEAI_VERSION:-latest}
|
image: josakola/eveai_chat_workers:${EVEAI_VERSION:-latest}
|
||||||
expose:
|
expose:
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ COPY config /app/config
|
|||||||
COPY migrations /app/migrations
|
COPY migrations /app/migrations
|
||||||
COPY scripts /app/scripts
|
COPY scripts /app/scripts
|
||||||
COPY patched_packages /app/patched_packages
|
COPY patched_packages /app/patched_packages
|
||||||
|
COPY content /app/content
|
||||||
|
|
||||||
# Set permissions for entrypoint script
|
# Set permissions for entrypoint script
|
||||||
RUN chmod 777 /app/scripts/entrypoint.sh
|
RUN chmod 777 /app/scripts/entrypoint.sh
|
||||||
|
|||||||
72
docker/eveai_chat_client/Dockerfile
Normal file
72
docker/eveai_chat_client/Dockerfile
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
ARG PYTHON_VERSION=3.12.7
|
||||||
|
FROM python:${PYTHON_VERSION}-slim as base
|
||||||
|
|
||||||
|
# Prevents Python from writing pyc files.
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
||||||
|
# Keeps Python from buffering stdout and stderr to avoid situations where
|
||||||
|
# the application crashes without emitting any logs due to buffering.
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# Create directory for patched packages and set permissions
|
||||||
|
RUN mkdir -p /app/patched_packages && \
|
||||||
|
chmod 777 /app/patched_packages
|
||||||
|
|
||||||
|
# Ensure patches are applied to the application.
|
||||||
|
ENV PYTHONPATH=/app/patched_packages:$PYTHONPATH
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Create a non-privileged user that the app will run under.
|
||||||
|
# See https://docs.docker.com/go/dockerfile-user-best-practices/
|
||||||
|
ARG UID=10001
|
||||||
|
RUN adduser \
|
||||||
|
--disabled-password \
|
||||||
|
--gecos "" \
|
||||||
|
--home "/nonexistent" \
|
||||||
|
--shell "/bin/bash" \
|
||||||
|
--no-create-home \
|
||||||
|
--uid "${UID}" \
|
||||||
|
appuser
|
||||||
|
|
||||||
|
# Install necessary packages and build tools
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
gcc \
|
||||||
|
postgresql-client \
|
||||||
|
curl \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Create logs directory and set permissions
|
||||||
|
RUN mkdir -p /app/logs && chown -R appuser:appuser /app/logs
|
||||||
|
|
||||||
|
# Download dependencies as a separate step to take advantage of Docker's caching.
|
||||||
|
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
|
||||||
|
# Leverage a bind mount to requirements.txt to avoid having to copy them into
|
||||||
|
# into this layer.
|
||||||
|
|
||||||
|
COPY requirements.txt /app/
|
||||||
|
RUN python -m pip install -r /app/requirements.txt
|
||||||
|
|
||||||
|
# Copy the source code into the container.
|
||||||
|
COPY eveai_chat_client /app/eveai_chat_client
|
||||||
|
COPY common /app/common
|
||||||
|
COPY config /app/config
|
||||||
|
COPY scripts /app/scripts
|
||||||
|
COPY patched_packages /app/patched_packages
|
||||||
|
COPY content /app/content
|
||||||
|
|
||||||
|
# Set permissions for scripts
|
||||||
|
RUN chmod 777 /app/scripts/entrypoint.sh && \
|
||||||
|
chmod 777 /app/scripts/start_eveai_chat_client.sh
|
||||||
|
|
||||||
|
# Set ownership of the application directory to the non-privileged user
|
||||||
|
RUN chown -R appuser:appuser /app
|
||||||
|
|
||||||
|
# Expose the port that the application listens on.
|
||||||
|
EXPOSE 5004
|
||||||
|
|
||||||
|
# Set entrypoint and command
|
||||||
|
ENTRYPOINT ["/app/scripts/entrypoint.sh"]
|
||||||
|
CMD ["/app/scripts/start_eveai_chat_client.sh"]
|
||||||
516
documentation/Eveai Chat Client Developer Documentation.md
Normal file
516
documentation/Eveai Chat Client Developer Documentation.md
Normal file
@@ -0,0 +1,516 @@
|
|||||||
|
# Evie Chat Client - Developer Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Evie Chat Client is a modern, customizable chat interface for interacting with eveai specialists. It supports both anonymous and authenticated modes, with initial focus on anonymous mode. The client provides real-time interaction with AI specialists, customizable tenant branding, European-compliant analytics tracking, and secure QR code access.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
- **Anonymous Mode**: Public access with tenant UUID and API key authentication
|
||||||
|
- **QR Code Access**: Secure pre-authenticated landing pages for QR code integration
|
||||||
|
- **Real-time Communication**: Server-Sent Events (SSE) for live updates and intermediate states
|
||||||
|
- **Tenant Customization**: Simple CSS variable-based theming with visual editor
|
||||||
|
- **Multiple Choice Options**: Dynamic button/dropdown responses from specialists
|
||||||
|
- **Chat History**: Persistent ChatSession and Interaction storage
|
||||||
|
- **File Upload Support**: Planned for future implementation
|
||||||
|
- **European Analytics**: Umami integration for GDPR-compliant tracking
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
evie-project/
|
||||||
|
├── common/ # Shared code across components
|
||||||
|
│ ├── services/ # Reusable business logic
|
||||||
|
│ │ ├── chat_service.py # Chat session management
|
||||||
|
│ │ ├── specialist_service.py # Specialist interaction wrapper
|
||||||
|
│ │ ├── tenant_service.py # Tenant config & theming
|
||||||
|
│ │ └── qr_service.py # QR code session management
|
||||||
|
│ └── utils/ # Utility functions
|
||||||
|
│ ├── auth.py # API key validation
|
||||||
|
│ ├── tracking.py # Umami analytics integration
|
||||||
|
│ └── qr_utils.py # QR code generation utilities
|
||||||
|
├── eveai_chat_client/ # Chat client component
|
||||||
|
│ ├── app.py # Flask app entry point
|
||||||
|
│ ├── routes/
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── chat_routes.py # Main chat interface routes
|
||||||
|
│ │ ├── api_routes.py # SSE/API endpoints
|
||||||
|
│ │ └── qr_routes.py # QR code landing pages
|
||||||
|
│ └── templates/
|
||||||
|
│ ├── base.html # Base template
|
||||||
|
│ ├── chat.html # Main chat interface
|
||||||
|
│ ├── qr_expired.html # QR code error page
|
||||||
|
│ └── components/
|
||||||
|
│ ├── message.html # Individual message component
|
||||||
|
│ ├── options.html # Multiple choice options
|
||||||
|
│ └── thinking.html # Intermediate states display
|
||||||
|
└── eveai_app/ # Admin interface (existing)
|
||||||
|
└── qr_management/ # QR code creation interface
|
||||||
|
├── create_qr.py
|
||||||
|
└── qr_templates.html
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Approach
|
||||||
|
|
||||||
|
- **Services Layer**: Direct integration with common/services for better performance
|
||||||
|
- **Database**: Utilizes existing ChatSession and Interaction models
|
||||||
|
- **Caching**: Leverages existing Redis setup
|
||||||
|
- **Static Files**: Uses existing nginx/static structure
|
||||||
|
|
||||||
|
## QR Code Access Flow
|
||||||
|
|
||||||
|
### QR Code System Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Admin as Admin (eveai_app)
|
||||||
|
participant QRService as QR Service (common)
|
||||||
|
participant PublicDB as Public Schema
|
||||||
|
participant TenantDB as Tenant Schema
|
||||||
|
participant User as End User
|
||||||
|
participant ChatClient as Chat Client
|
||||||
|
participant ChatSession as Chat Session
|
||||||
|
|
||||||
|
%% QR Code Creation Flow
|
||||||
|
Admin->>QRService: Create QR code with specialist config
|
||||||
|
QRService->>PublicDB: Store qr_lookup (qr_id → tenant_code)
|
||||||
|
QRService->>TenantDB: Store qr_sessions (full config + args)
|
||||||
|
QRService->>Admin: Return QR code image with /qr/{qr_id}
|
||||||
|
|
||||||
|
%% QR Code Usage Flow
|
||||||
|
User->>ChatClient: Scan QR → GET /qr/{qr_id}
|
||||||
|
ChatClient->>PublicDB: Lookup tenant_code by qr_id
|
||||||
|
ChatClient->>TenantDB: Get full QR session data
|
||||||
|
ChatClient->>ChatSession: Create ChatSession with pre-filled args
|
||||||
|
ChatClient->>User: Set temp auth + redirect to chat interface
|
||||||
|
User->>ChatClient: Access chat with pre-authenticated session
|
||||||
|
```
|
||||||
|
|
||||||
|
### QR Code Data Flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[Admin Creates QR Code] --> B[Generate UUID for QR Session]
|
||||||
|
B --> C[Store Lookup in Public Schema]
|
||||||
|
C --> D[Store Full Data in Tenant Schema]
|
||||||
|
D --> E[Generate QR Code Image]
|
||||||
|
|
||||||
|
F[User Scans QR Code] --> G[Extract QR Session ID from URL]
|
||||||
|
G --> H[Lookup Tenant Code in Public Schema]
|
||||||
|
H --> I[Retrieve Full QR Data from Tenant Schema]
|
||||||
|
I --> J{QR Valid & Not Expired?}
|
||||||
|
J -->|No| K[Show Error Page]
|
||||||
|
J -->|Yes| L[Create ChatSession with Pre-filled Args]
|
||||||
|
L --> M[Set Temporary Browser Authentication]
|
||||||
|
M --> N[Redirect to Chat Interface]
|
||||||
|
N --> O[Start Chat with Specialist]
|
||||||
|
```
|
||||||
|
|
||||||
|
## URL Structure & Parameters
|
||||||
|
|
||||||
|
### Main Chat Interface
|
||||||
|
```
|
||||||
|
GET /chat/{tenant_code}/{specialist_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `api_key` (required for direct access): Tenant API key for authentication
|
||||||
|
- `session` (optional): Existing chat session ID
|
||||||
|
- `utm_source`, `utm_campaign`, `utm_medium` (optional): Analytics tracking
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
# Direct access
|
||||||
|
/chat/550e8400-e29b-41d4-a716-446655440000/document-analyzer?api_key=xxx&utm_source=email
|
||||||
|
|
||||||
|
# QR code access (after redirect)
|
||||||
|
/chat/550e8400-e29b-41d4-a716-446655440000/document-analyzer?session=abc123-def456
|
||||||
|
```
|
||||||
|
|
||||||
|
### QR Code Landing Pages
|
||||||
|
```
|
||||||
|
GET /qr/{qr_session_id} # QR code entry point (redirects, no HTML page)
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
```
|
||||||
|
POST /api/chat/{tenant_code}/interact # Send message to specialist
|
||||||
|
GET /api/chat/{tenant_code}/status/{session_id} # SSE endpoint for updates
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication & Security
|
||||||
|
|
||||||
|
### Anonymous Mode Access Methods
|
||||||
|
|
||||||
|
1. **Direct Access**: URL with API key parameter
|
||||||
|
2. **QR Code Access**: Pre-authenticated via secure landing page
|
||||||
|
|
||||||
|
### QR Code Security Model
|
||||||
|
- **QR Code Contains**: Only a UUID session identifier
|
||||||
|
- **Sensitive Data**: Stored securely in tenant database schema
|
||||||
|
- **Usage Control**: Configurable expiration and usage limits
|
||||||
|
- **Audit Trail**: Track QR code creation and usage
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
- Use tenant UUIDs to prevent enumeration attacks
|
||||||
|
- Validate API keys against tenant database
|
||||||
|
- Implement CORS policies for cross-origin requests
|
||||||
|
- Sanitize all user messages and file uploads
|
||||||
|
- QR sessions have configurable expiration and usage limits
|
||||||
|
|
||||||
|
## QR Code Management
|
||||||
|
|
||||||
|
### Database Schema
|
||||||
|
|
||||||
|
#### Public Schema (Routing Only)
|
||||||
|
```sql
|
||||||
|
CREATE TABLE qr_lookup (
|
||||||
|
qr_session_id UUID PRIMARY KEY,
|
||||||
|
tenant_code UUID NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
INDEX idx_tenant_code (tenant_code)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Tenant Schema (Full QR Data)
|
||||||
|
```sql
|
||||||
|
CREATE TABLE qr_sessions (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
specialist_id UUID NOT NULL,
|
||||||
|
api_key VARCHAR(255) NOT NULL,
|
||||||
|
specialist_args JSONB,
|
||||||
|
metadata JSONB,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
expires_at TIMESTAMP,
|
||||||
|
usage_count INTEGER DEFAULT 0,
|
||||||
|
usage_limit INTEGER,
|
||||||
|
created_by_user_id UUID
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### QR Code Creation (eveai_app)
|
||||||
|
```python
|
||||||
|
# In eveai_app admin interface
|
||||||
|
from common.services.qr_service import QRService
|
||||||
|
|
||||||
|
def create_specialist_qr_code():
|
||||||
|
qr_data = {
|
||||||
|
'tenant_code': current_tenant.code,
|
||||||
|
'specialist_id': selected_specialist.id,
|
||||||
|
'api_key': current_tenant.api_key,
|
||||||
|
'specialist_args': {
|
||||||
|
'department': 'sales',
|
||||||
|
'language': 'en',
|
||||||
|
'context': 'product_inquiry'
|
||||||
|
},
|
||||||
|
'metadata': {
|
||||||
|
'name': 'Sales Support QR - Product Brochure',
|
||||||
|
'usage_limit': 500,
|
||||||
|
'expires_days': 90
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qr_service = QRService()
|
||||||
|
qr_session_id, qr_image = qr_service.create_qr_session(qr_data)
|
||||||
|
return qr_image
|
||||||
|
```
|
||||||
|
|
||||||
|
### QR Code Processing (eveai_chat_client)
|
||||||
|
```python
|
||||||
|
# In eveai_chat_client routes
|
||||||
|
from common.services.qr_service import QRService
|
||||||
|
from common.services.chat_service import ChatService
|
||||||
|
|
||||||
|
@app.route('/qr/<qr_session_id>')
|
||||||
|
def handle_qr_code(qr_session_id):
|
||||||
|
qr_service = QRService()
|
||||||
|
qr_data = qr_service.get_and_validate_qr_session(qr_session_id)
|
||||||
|
|
||||||
|
if not qr_data:
|
||||||
|
return render_template('qr_expired.html'), 410
|
||||||
|
|
||||||
|
# Create ChatSession with pre-filled arguments
|
||||||
|
chat_service = ChatService()
|
||||||
|
chat_session = chat_service.create_session(
|
||||||
|
tenant_code=qr_data['tenant_code'],
|
||||||
|
specialist_id=qr_data['specialist_id'],
|
||||||
|
initial_args=qr_data['specialist_args'],
|
||||||
|
source='qr_code'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set temporary authentication
|
||||||
|
flask_session['qr_auth'] = {
|
||||||
|
'tenant_code': qr_data['tenant_code'],
|
||||||
|
'api_key': qr_data['api_key'],
|
||||||
|
'chat_session_id': chat_session.id,
|
||||||
|
'expires_at': datetime.utcnow() + timedelta(hours=24)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Redirect to chat interface
|
||||||
|
return redirect(f"/chat/{qr_data['tenant_code']}/{qr_data['specialist_id']}?session={chat_session.id}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Real-time Communication
|
||||||
|
|
||||||
|
### Server-Sent Events (SSE)
|
||||||
|
- **Connection**: Long-lived SSE connection per chat session
|
||||||
|
- **Message Types**:
|
||||||
|
- `message`: Complete specialist response
|
||||||
|
- `thinking`: Intermediate processing states
|
||||||
|
- `options`: Multiple choice response options
|
||||||
|
- `error`: Error messages
|
||||||
|
- `complete`: Interaction completion
|
||||||
|
|
||||||
|
### SSE Message Format
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "thinking",
|
||||||
|
"data": {
|
||||||
|
"message": "Analyzing your request...",
|
||||||
|
"step": 1,
|
||||||
|
"total_steps": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tenant Customization
|
||||||
|
|
||||||
|
### Theme Configuration
|
||||||
|
Stored in tenant table as JSONB column:
|
||||||
|
```sql
|
||||||
|
ALTER TABLE tenants ADD COLUMN theme_config JSONB;
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS Variables Approach
|
||||||
|
Inline CSS variables in chat template:
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
/* Brand Colors */
|
||||||
|
--primary-color: {{ tenant.theme_config.primary_color or '#007bff' }};
|
||||||
|
--secondary-color: {{ tenant.theme_config.secondary_color or '#6c757d' }};
|
||||||
|
--accent-color: {{ tenant.theme_config.accent_color or '#28a745' }};
|
||||||
|
|
||||||
|
/* Chat Interface */
|
||||||
|
--user-message-bg: {{ tenant.theme_config.user_message_bg or 'var(--primary-color)' }};
|
||||||
|
--bot-message-bg: {{ tenant.theme_config.bot_message_bg or '#f8f9fa' }};
|
||||||
|
--chat-bg: {{ tenant.theme_config.chat_bg or '#ffffff' }};
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
--font-family: {{ tenant.theme_config.font_family or 'system-ui, -apple-system, sans-serif' }};
|
||||||
|
--font-size-base: {{ tenant.theme_config.font_size or '16px' }};
|
||||||
|
|
||||||
|
/* Branding */
|
||||||
|
--logo-url: url('/api/tenant/{{ tenant.code }}/logo');
|
||||||
|
--header-bg: {{ tenant.theme_config.header_bg or 'var(--primary-color)' }};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Theme Editor (eveai_app)
|
||||||
|
Simple form interface with:
|
||||||
|
- Color pickers for brand colors
|
||||||
|
- Font selection dropdown
|
||||||
|
- Logo upload functionality
|
||||||
|
- Live preview of chat interface
|
||||||
|
- Reset to defaults option
|
||||||
|
|
||||||
|
## Multiple Choice Options
|
||||||
|
|
||||||
|
### Dynamic Rendering Logic
|
||||||
|
```python
|
||||||
|
def render_options(options_list):
|
||||||
|
if len(options_list) <= 3:
|
||||||
|
return render_template('components/options.html',
|
||||||
|
display_type='buttons',
|
||||||
|
options=options_list)
|
||||||
|
else:
|
||||||
|
return render_template('components/options.html',
|
||||||
|
display_type='dropdown',
|
||||||
|
options=options_list)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option Data Structure
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "options",
|
||||||
|
"data": {
|
||||||
|
"question": "How would you like to proceed?",
|
||||||
|
"options": [
|
||||||
|
{"id": "option1", "text": "Continue analysis", "value": "continue"},
|
||||||
|
{"id": "option2", "text": "Generate report", "value": "report"},
|
||||||
|
{"id": "option3", "text": "Start over", "value": "restart"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Analytics Integration
|
||||||
|
|
||||||
|
### Umami Setup
|
||||||
|
- **European Hosting**: Self-hosted Umami instance
|
||||||
|
- **Privacy Compliant**: No cookies, GDPR compliant by design
|
||||||
|
- **Tracking Events**:
|
||||||
|
- Chat session start (including QR code source)
|
||||||
|
- Message sent
|
||||||
|
- Option selected
|
||||||
|
- Session duration
|
||||||
|
- Specialist interaction completion
|
||||||
|
- QR code usage
|
||||||
|
|
||||||
|
### Tracking Implementation
|
||||||
|
```javascript
|
||||||
|
// Track chat events
|
||||||
|
function trackEvent(eventName, eventData) {
|
||||||
|
if (window.umami) {
|
||||||
|
umami.track(eventName, eventData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track QR code usage
|
||||||
|
function trackQRUsage(qrSessionId, tenantCode) {
|
||||||
|
trackEvent('qr_code_used', {
|
||||||
|
qr_session_id: qrSessionId,
|
||||||
|
tenant_code: tenantCode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Upload Support (Future)
|
||||||
|
|
||||||
|
### Planned Implementation
|
||||||
|
- **Multipart Upload**: Standard HTML5 file upload
|
||||||
|
- **File Types**: Documents, images, spreadsheets
|
||||||
|
- **Storage**: Tenant-specific S3 buckets
|
||||||
|
- **Processing**: Integration with existing document processing pipeline
|
||||||
|
- **UI**: Drag-and-drop interface with progress indicators
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
- File type validation
|
||||||
|
- Size limits per tenant
|
||||||
|
- Virus scanning integration
|
||||||
|
- Temporary file cleanup
|
||||||
|
|
||||||
|
## Development Guidelines
|
||||||
|
|
||||||
|
### Code Organization
|
||||||
|
- **Services**: Place reusable business logic in `common/services/`
|
||||||
|
- **Utils**: Place utility functions in `common/utils/`
|
||||||
|
- **Multi-tenant**: Maintain data isolation using existing patterns
|
||||||
|
- **Error Handling**: Implement proper error handling and logging
|
||||||
|
|
||||||
|
### Service Layer Examples
|
||||||
|
```python
|
||||||
|
# common/services/qr_service.py
|
||||||
|
class QRService:
|
||||||
|
def create_qr_session(self, qr_data):
|
||||||
|
# Create QR session with hybrid storage approach
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_and_validate_qr_session(self, qr_session_id):
|
||||||
|
# Validate and retrieve QR session data
|
||||||
|
pass
|
||||||
|
|
||||||
|
# common/services/chat_service.py
|
||||||
|
class ChatService:
|
||||||
|
def create_session(self, tenant_code, specialist_id, initial_args=None, source='direct'):
|
||||||
|
# Create chat session with optional pre-filled arguments
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Strategy
|
||||||
|
- Unit tests for services and utilities in `common/`
|
||||||
|
- Integration tests for chat flow including QR code access
|
||||||
|
- UI tests for theme customization
|
||||||
|
- Load testing for SSE connections
|
||||||
|
- Cross-browser compatibility testing
|
||||||
|
|
||||||
|
### Performance Considerations
|
||||||
|
- Cache tenant configurations in Redis
|
||||||
|
- Cache QR session lookups in Redis
|
||||||
|
- Optimize SSE connection management
|
||||||
|
- Implement connection pooling for database
|
||||||
|
- Use CDN for static assets
|
||||||
|
- Monitor real-time connection limits
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Container Configuration
|
||||||
|
- New `eveai_chat_client` container
|
||||||
|
- Integration with existing docker setup
|
||||||
|
- Environment configuration for tenant isolation
|
||||||
|
- Load balancer configuration for SSE connections
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- Flask and Flask-restx (existing)
|
||||||
|
- Celery integration (existing)
|
||||||
|
- PostgreSQL and Redis (existing)
|
||||||
|
- Umami analytics client library
|
||||||
|
- QR code generation library (qrcode)
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### Authenticated Mode
|
||||||
|
- User login integration
|
||||||
|
- Session persistence across devices
|
||||||
|
- Advanced specialist access controls
|
||||||
|
- User-specific chat history
|
||||||
|
|
||||||
|
### Advanced Features
|
||||||
|
- Voice message support
|
||||||
|
- Screen sharing capabilities
|
||||||
|
- Collaborative chat sessions
|
||||||
|
- Advanced analytics dashboard
|
||||||
|
- Mobile app integration
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
```bash
|
||||||
|
CHAT_CLIENT_PORT=5000
|
||||||
|
TENANT_API_VALIDATION_CACHE_TTL=3600
|
||||||
|
SSE_CONNECTION_TIMEOUT=300
|
||||||
|
QR_SESSION_DEFAULT_EXPIRY_DAYS=30
|
||||||
|
QR_SESSION_MAX_USAGE_LIMIT=1000
|
||||||
|
UMAMI_WEBSITE_ID=your-website-id
|
||||||
|
UMAMI_SCRIPT_URL=https://your-umami.domain/script.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Theme Configuration
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"primary_color": "#2563eb",
|
||||||
|
"secondary_color": "#64748b",
|
||||||
|
"accent_color": "#059669",
|
||||||
|
"user_message_bg": "#2563eb",
|
||||||
|
"bot_message_bg": "#f1f5f9",
|
||||||
|
"chat_bg": "#ffffff",
|
||||||
|
"font_family": "Inter, system-ui, sans-serif",
|
||||||
|
"font_size": "16px",
|
||||||
|
"header_bg": "#1e40af"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample QR Session Data
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"tenant_code": "123e4567-e89b-12d3-a456-426614174000",
|
||||||
|
"specialist_id": "789e0123-e45f-67g8-h901-234567890123",
|
||||||
|
"api_key": "tenant_api_key_here",
|
||||||
|
"specialist_args": {
|
||||||
|
"department": "technical_support",
|
||||||
|
"product_category": "software",
|
||||||
|
"priority": "high",
|
||||||
|
"language": "en"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"name": "Technical Support QR - Software Issues",
|
||||||
|
"created_by": "admin_user_id",
|
||||||
|
"usage_limit": 100,
|
||||||
|
"expires_at": "2025-09-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This documentation provides a comprehensive foundation for developing the Evie Chat Client with secure QR code integration while maintaining consistency with the existing eveai multi-tenant architecture.
|
||||||
@@ -7,7 +7,7 @@ from werkzeug.middleware.proxy_fix import ProxyFix
|
|||||||
import logging.config
|
import logging.config
|
||||||
|
|
||||||
from common.extensions import (db, migrate, bootstrap, security, login_manager, cors, csrf, session,
|
from common.extensions import (db, migrate, bootstrap, security, login_manager, cors, csrf, session,
|
||||||
minio_client, simple_encryption, metrics, cache_manager)
|
minio_client, simple_encryption, metrics, cache_manager, content_manager)
|
||||||
from common.models.user import User, Role, Tenant, TenantDomain
|
from common.models.user import User, Role, Tenant, TenantDomain
|
||||||
import common.models.interaction
|
import common.models.interaction
|
||||||
import common.models.entitlements
|
import common.models.entitlements
|
||||||
@@ -15,7 +15,7 @@ import common.models.document
|
|||||||
from common.utils.startup_eveai import perform_startup_actions
|
from common.utils.startup_eveai import perform_startup_actions
|
||||||
from config.logging_config import LOGGING
|
from config.logging_config import LOGGING
|
||||||
from common.utils.security import set_tenant_session_data
|
from common.utils.security import set_tenant_session_data
|
||||||
from .errors import register_error_handlers
|
from common.utils.errors import register_error_handlers
|
||||||
from common.utils.celery_utils import make_celery, init_celery
|
from common.utils.celery_utils import make_celery, init_celery
|
||||||
from common.utils.template_filters import register_filters
|
from common.utils.template_filters import register_filters
|
||||||
from config.config import get_config
|
from config.config import get_config
|
||||||
@@ -124,6 +124,7 @@ def register_extensions(app):
|
|||||||
minio_client.init_app(app)
|
minio_client.init_app(app)
|
||||||
cache_manager.init_app(app)
|
cache_manager.init_app(app)
|
||||||
metrics.init_app(app)
|
metrics.init_app(app)
|
||||||
|
content_manager.init_app(app)
|
||||||
|
|
||||||
|
|
||||||
def register_blueprints(app):
|
def register_blueprints(app):
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
<hr>
|
<hr>
|
||||||
{% include 'footer.html' %}
|
{% include 'footer.html' %}
|
||||||
{% include 'scripts.html' %}
|
{% include 'scripts.html' %}
|
||||||
|
{% include 'ordered_list_configs.html' %}
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
102
eveai_app/templates/basic/view_markdown.html
Normal file
102
eveai_app/templates/basic/view_markdown.html
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content_title %}{{ title }}{% endblock %}
|
||||||
|
{% block content_description %}{{ description }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-5">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" id="showRaw">Show Raw</button>
|
||||||
|
<button class="btn btn-sm btn-outline-primary active" id="showRendered">Show Rendered</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Raw markdown view (hidden by default) -->
|
||||||
|
<div id="rawMarkdown" class="code-wrapper" style="display: none;">
|
||||||
|
<pre><code class="language-markdown">{{ markdown_content }}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rendered markdown view -->
|
||||||
|
<div id="renderedMarkdown" class="markdown-body">
|
||||||
|
{{ markdown_content | markdown }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
{{ super() }}
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@4.0.0/github-markdown.min.css">
|
||||||
|
<style>
|
||||||
|
pre, code {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
white-space: pre-wrap !important;
|
||||||
|
word-wrap: break-word !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code {
|
||||||
|
padding: 1rem !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body {
|
||||||
|
padding: 1rem;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode styling (optional) */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.markdown-body {
|
||||||
|
color: #c9d1d9;
|
||||||
|
background-color: #0d1117;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{{ super() }}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize syntax highlighting
|
||||||
|
document.querySelectorAll('pre code').forEach((block) => {
|
||||||
|
hljs.highlightElement(block);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle buttons for display
|
||||||
|
const showRawBtn = document.getElementById('showRaw');
|
||||||
|
const showRenderedBtn = document.getElementById('showRendered');
|
||||||
|
const rawMarkdown = document.getElementById('rawMarkdown');
|
||||||
|
const renderedMarkdown = document.getElementById('renderedMarkdown');
|
||||||
|
|
||||||
|
showRawBtn.addEventListener('click', function() {
|
||||||
|
rawMarkdown.style.display = 'block';
|
||||||
|
renderedMarkdown.style.display = 'none';
|
||||||
|
showRawBtn.classList.add('active');
|
||||||
|
showRenderedBtn.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
showRenderedBtn.addEventListener('click', function() {
|
||||||
|
rawMarkdown.style.display = 'none';
|
||||||
|
renderedMarkdown.style.display = 'block';
|
||||||
|
showRawBtn.classList.remove('active');
|
||||||
|
showRenderedBtn.classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -5,16 +5,18 @@
|
|||||||
|
|
||||||
{% block content_title %}Document Versions{% endblock %}
|
{% block content_title %}Document Versions{% endblock %}
|
||||||
{% block content_description %}View Versions for {{ document }}{% endblock %}
|
{% block content_description %}View Versions for {{ document }}{% endblock %}
|
||||||
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto">{% endblock %}
|
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<form method="POST" action="{{ url_for('document_bp.handle_document_version_selection') }}">
|
<form method="POST" action="{{ url_for('document_bp.handle_document_version_selection') }}" id="documentVersionsForm">
|
||||||
{{ render_selectable_table(headers=["ID", "URL", "Object Name", "File Type", "Process.", "Proces. Start", "Proces. Finish", "Proces. Error"], rows=rows, selectable=True, id="versionsTable") }}
|
{{ render_selectable_table(headers=["ID", "URL", "Object Name", "File Type", "Process.", "Proces. Start", "Proces. Finish", "Proces. Error"], rows=rows, selectable=True, id="versionsTable") }}
|
||||||
<div class="form-group mt-3">
|
<div class="form-group mt-3 d-flex justify-content-between">
|
||||||
<button type="submit" name="action" value="edit_document_version" class="btn btn-primary">Edit Document Version</button>
|
<div>
|
||||||
<button type="submit" name="action" value="view_document_version_markdown" class="btn btn-danger">View Processed Document</button>
|
<button type="submit" name="action" value="edit_document_version" class="btn btn-primary" onclick="return validateTableSelection('documentVersionsForm')">Edit Document Version</button>
|
||||||
<button type="submit" name="action" value="process_document_version" class="btn btn-danger">Process Document Version</button>
|
<button type="submit" name="action" value="view_document_version_markdown" class="btn btn-danger" onclick="return validateTableSelection('documentVersionsForm')">View Processed Document</button>
|
||||||
|
<button type="submit" name="action" value="process_document_version" class="btn btn-danger" onclick="return validateTableSelection('documentVersionsForm')">Process Document Version</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
<div class="form-group mt-3 d-flex justify-content-between">
|
<div class="form-group mt-3 d-flex justify-content-between">
|
||||||
<div>
|
<div>
|
||||||
<button type="submit" name="action" value="edit_document_version" class="btn btn-primary" onclick="return validateTableSelection('documentVersionsForm')">Edit Document Version</button>
|
<button type="submit" name="action" value="edit_document_version" class="btn btn-primary" onclick="return validateTableSelection('documentVersionsForm')">Edit Document Version</button>
|
||||||
<button type="submit" name="action" value="view_document_version_markdown" class="btn btn-danger">View Processed Document</button>
|
<button type="submit" name="action" value="view_document_version_markdown" class="btn btn-danger" onclick="return validateTableSelection('documentVersionsForm')">View Processed Document</button>
|
||||||
<button type="submit" name="action" value="process_document_version" class="btn btn-danger" onclick="return validateTableSelection('documentVersionsForm')">Process Document Version</button>
|
<button type="submit" name="action" value="process_document_version" class="btn btn-danger" onclick="return validateTableSelection('documentVersionsForm')">Process Document Version</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,17 +19,17 @@
|
|||||||
<div class="nav-wrapper position-relative end-0">
|
<div class="nav-wrapper position-relative end-0">
|
||||||
<ul class="nav nav-pills nav-fill p-1" role="tablist">
|
<ul class="nav nav-pills nav-fill p-1" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link mb-0 px-0 py-1 active" data-toggle="tab" href="#storage-tab" role="tab" aria-controls="model-info" aria-selected="true">
|
<a class="nav-link mb-0 px-0 py-1 active" data-bs-toggle="tab" href="#storage-tab" role="tab" aria-controls="model-info" aria-selected="true">
|
||||||
Storage
|
Storage
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#embedding-tab" role="tab" aria-controls="license-info" aria-selected="false">
|
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#embedding-tab" role="tab" aria-controls="license-info" aria-selected="false">
|
||||||
Embedding
|
Embedding
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#interaction-tab" role="tab" aria-controls="chunking" aria-selected="false">
|
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#interaction-tab" role="tab" aria-controls="chunking" aria-selected="false">
|
||||||
Interaction
|
Interaction
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -19,17 +19,17 @@
|
|||||||
<div class="nav-wrapper position-relative end-0">
|
<div class="nav-wrapper position-relative end-0">
|
||||||
<ul class="nav nav-pills nav-fill p-1" role="tablist">
|
<ul class="nav nav-pills nav-fill p-1" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link mb-0 px-0 py-1 active" data-toggle="tab" href="#storage-tab" role="tab" aria-controls="model-info" aria-selected="true">
|
<a class="nav-link mb-0 px-0 py-1 active" data-bs-toggle="tab" href="#storage-tab" role="tab" aria-controls="model-info" aria-selected="true">
|
||||||
Storage
|
Storage
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#embedding-tab" role="tab" aria-controls="license-info" aria-selected="false">
|
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#embedding-tab" role="tab" aria-controls="license-info" aria-selected="false">
|
||||||
Embedding
|
Embedding
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#interaction-tab" role="tab" aria-controls="chunking" aria-selected="false">
|
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#interaction-tab" role="tab" aria-controls="chunking" aria-selected="false">
|
||||||
Interaction
|
Interaction
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -107,17 +107,17 @@
|
|||||||
<!-- Nav Tabs -->
|
<!-- Nav Tabs -->
|
||||||
<ul class="nav nav-tabs" id="periodTabs" role="tablist">
|
<ul class="nav nav-tabs" id="periodTabs" role="tablist">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" id="status-tab" data-toggle="tab" href="#status" role="tab">
|
<a class="nav-link active" id="status-tab" data-bs-toggle="tab" href="#status" role="tab">
|
||||||
Status & Timeline
|
Status & Timeline
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" id="usage-tab" data-toggle="tab" href="#usage" role="tab">
|
<a class="nav-link" id="usage-tab" data-bs-toggle="tab" href="#usage" role="tab">
|
||||||
Usage
|
Usage
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" id="financial-tab" data-toggle="tab" href="#financial" role="tab">
|
<a class="nav-link" id="financial-tab" data-bs-toggle="tab" href="#financial" role="tab">
|
||||||
Financial
|
Financial
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% from "macros.html" import render_field, render_included_field %}
|
{% from "macros.html" import render_field, render_included_field %}
|
||||||
|
|
||||||
@@ -19,17 +20,17 @@
|
|||||||
<div class="nav-wrapper position-relative end-0">
|
<div class="nav-wrapper position-relative end-0">
|
||||||
<ul class="nav nav-pills nav-fill p-1" role="tablist">
|
<ul class="nav nav-pills nav-fill p-1" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link mb-0 px-0 py-1 active" data-toggle="tab" href="#storage-tab" role="tab" aria-controls="model-info" aria-selected="true">
|
<a class="nav-link mb-0 px-0 py-1 active" data-bs-toggle="tab" href="#storage-tab" role="tab" aria-controls="storage-tab" aria-selected="true">
|
||||||
Storage
|
Storage
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#embedding-tab" role="tab" aria-controls="license-info" aria-selected="false">
|
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#embedding-tab" role="tab" aria-controls="embedding-tab" aria-selected="false">
|
||||||
Embedding
|
Embedding
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#interaction-tab" role="tab" aria-controls="chunking" aria-selected="false">
|
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#interaction-tab" role="tab" aria-controls="interaction-tab" aria-selected="false">
|
||||||
Interaction
|
Interaction
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
364
eveai_app/templates/eveai_ordered_list_editor.html
Normal file
364
eveai_app/templates/eveai_ordered_list_editor.html
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
<script type="module">
|
||||||
|
window.EveAI = window.EveAI || {};
|
||||||
|
window.EveAI.OrderedListEditors = {
|
||||||
|
instances: {},
|
||||||
|
initialize: function(containerId, data, listType, options = {}) {
|
||||||
|
console.log('Initializing OrderedListEditor for', containerId, 'with data', data, 'and listType', listType);
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
if (!container || typeof container !== 'object' || !('classList' in container)) {
|
||||||
|
console.error(`Container with ID ${containerId} not found or not a valid element:`, container);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.instances[containerId]) return this.instances[containerId];
|
||||||
|
|
||||||
|
if (typeof window.Tabulator !== 'function') {
|
||||||
|
console.error('Tabulator not loaded (window.Tabulator missing).');
|
||||||
|
container.innerHTML = `<div class="alert alert-danger p-3">
|
||||||
|
<strong>Error:</strong> Tabulator not loaded
|
||||||
|
</div>`;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the list type configuration
|
||||||
|
const listTypeConfig = this._getListTypeConfig(listType);
|
||||||
|
if (!listTypeConfig) {
|
||||||
|
console.error(`List type configuration for ${listType} not found.`);
|
||||||
|
container.innerHTML = `<div class="alert alert-danger p-3">
|
||||||
|
<strong>Error:</strong> List type configuration for ${listType} not found
|
||||||
|
</div>`;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create column definitions from list type
|
||||||
|
const columns = this._createColumnsFromListType(listTypeConfig);
|
||||||
|
|
||||||
|
// Debug log for data and columns
|
||||||
|
console.log('Data for Tabulator:', data);
|
||||||
|
console.log('Columns for Tabulator:', columns);
|
||||||
|
|
||||||
|
// Debug log for column titles
|
||||||
|
console.log('Column titles:', columns.map(col => col.title || ''));
|
||||||
|
|
||||||
|
// Initialize Tabulator
|
||||||
|
try {
|
||||||
|
console.log('Creating Tabulator for', containerId);
|
||||||
|
const table = new Tabulator(container, {
|
||||||
|
data: data || [],
|
||||||
|
columns: columns,
|
||||||
|
layout: "fitColumns", // Changed to fitColumns to ensure columns display horizontally
|
||||||
|
movableRows: true,
|
||||||
|
movableRowsPlaceholder: false, // Don't use placeholder, show actual row content
|
||||||
|
movableRowsSender: "table", // Keep a copy of the row in the table while dragging
|
||||||
|
rowHeader: {headerSort:false, resizable: false, minWidth:30, width:30, rowHandle:true, formatter:"handle"},
|
||||||
|
maxHeight: "50%", // Auto height to display all rows
|
||||||
|
placeholder: "No Data Available",
|
||||||
|
autoResize: false,
|
||||||
|
resizableColumnFit: true,
|
||||||
|
responsiveLayout: false,
|
||||||
|
tooltips: true, // Enable tooltips
|
||||||
|
tooltipsHeader: true,
|
||||||
|
selectable: false, // Disable row selection to prevent jumping
|
||||||
|
selectableRangeMode: "click", // Only select on click, not on drag
|
||||||
|
selectableRollingSelection: false, // Disable rolling selection
|
||||||
|
scrollToRowIfVisible: false, // Don't scroll to row even if it's already visible
|
||||||
|
scrollToRowPosition: "nearest",
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Tabulator created for', containerId);
|
||||||
|
container.classList.add('tabulator-initialized');
|
||||||
|
|
||||||
|
// Debug: Log table structure
|
||||||
|
console.log('Table structure:', {
|
||||||
|
tableElement: container,
|
||||||
|
tableData: table.getData(),
|
||||||
|
tableColumns: table.getColumnDefinitions()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add row button
|
||||||
|
const addRowBtn = document.createElement('button');
|
||||||
|
addRowBtn.className = 'btn btn-sm btn-primary mt-2';
|
||||||
|
addRowBtn.innerHTML = 'Add Row';
|
||||||
|
addRowBtn.addEventListener('click', () => {
|
||||||
|
const newRow = {};
|
||||||
|
// Create empty row with default values
|
||||||
|
Object.entries(listTypeConfig).forEach(([key, field]) => {
|
||||||
|
if (field.type === 'boolean') {
|
||||||
|
newRow[key] = field.default === true;
|
||||||
|
} else {
|
||||||
|
newRow[key] = field.default !== undefined ? field.default : '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
table.addRow(newRow);
|
||||||
|
this._updateTextarea(containerId, table);
|
||||||
|
});
|
||||||
|
container.parentNode.insertBefore(addRowBtn, container.nextSibling);
|
||||||
|
|
||||||
|
// Add explode button for fullscreen mode
|
||||||
|
const explodeBtn = document.createElement('button');
|
||||||
|
explodeBtn.className = 'btn btn-sm btn-secondary mt-2 ms-2';
|
||||||
|
explodeBtn.innerHTML = '<i class="material-icons">fullscreen</i> Expand';
|
||||||
|
explodeBtn.addEventListener('click', () => {
|
||||||
|
container.classList.toggle('fullscreen-mode');
|
||||||
|
|
||||||
|
// Update button text based on current state
|
||||||
|
if (container.classList.contains('fullscreen-mode')) {
|
||||||
|
explodeBtn.innerHTML = '<i class="material-icons">fullscreen_exit</i> Collapse';
|
||||||
|
} else {
|
||||||
|
explodeBtn.innerHTML = '<i class="material-icons">fullscreen</i> Expand';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redraw table to adjust to new size
|
||||||
|
table.redraw(true);
|
||||||
|
});
|
||||||
|
container.parentNode.insertBefore(explodeBtn, addRowBtn.nextSibling);
|
||||||
|
|
||||||
|
// Store instance
|
||||||
|
this.instances[containerId] = {
|
||||||
|
table: table,
|
||||||
|
textarea: document.getElementById(containerId.replace('-editor', ''))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prevent scrolling when clicking on cells
|
||||||
|
container.addEventListener('click', function(e) {
|
||||||
|
// Prevent the default behavior which might cause scrolling
|
||||||
|
if (e.target.closest('.tabulator-cell')) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
// Update textarea on various events that change data
|
||||||
|
table.on("dataChanged", () => {
|
||||||
|
console.log("dataChanged event triggered");
|
||||||
|
this._updateTextarea(containerId, table);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for row movement
|
||||||
|
table.on("rowMoved", () => {
|
||||||
|
console.log("rowMoved event triggered");
|
||||||
|
this._updateTextarea(containerId, table);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for cell edits
|
||||||
|
table.on("cellEdited", () => {
|
||||||
|
console.log("cellEdited event triggered");
|
||||||
|
this._updateTextarea(containerId, table);
|
||||||
|
});
|
||||||
|
|
||||||
|
return table;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error initializing Tabulator for ${containerId}:`, e);
|
||||||
|
container.innerHTML = `<div class="alert alert-danger p-3">
|
||||||
|
<strong>Error initializing Tabulator:</strong><br>${e.message}
|
||||||
|
</div>`;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateTextarea: function(containerId, table) {
|
||||||
|
const instance = this.instances[containerId];
|
||||||
|
if (instance && instance.textarea) {
|
||||||
|
const data = table.getData();
|
||||||
|
console.log('Updating textarea with data:', data);
|
||||||
|
instance.textarea.value = JSON.stringify(data);
|
||||||
|
console.log('Textarea value updated:', instance.textarea.value);
|
||||||
|
|
||||||
|
// Trigger change event on textarea to ensure form validation recognizes the change
|
||||||
|
const event = new Event('change', { bubbles: true });
|
||||||
|
instance.textarea.dispatchEvent(event);
|
||||||
|
|
||||||
|
// Also trigger input event for any listeners that might be using that
|
||||||
|
const inputEvent = new Event('input', { bubbles: true });
|
||||||
|
instance.textarea.dispatchEvent(inputEvent);
|
||||||
|
} else {
|
||||||
|
console.error('Cannot update textarea: instance or textarea not found for', containerId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_getListTypeConfig: function(listType) {
|
||||||
|
// Try to get the list type configuration from window.listTypeConfigs
|
||||||
|
if (window.listTypeConfigs && window.listTypeConfigs[listType]) {
|
||||||
|
return window.listTypeConfigs[listType];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found, log a warning and return a default configuration
|
||||||
|
console.warn(`List type configuration for ${listType} not found in window.listTypeConfigs. Using a default configuration.`);
|
||||||
|
return {
|
||||||
|
title: {
|
||||||
|
name: "Title",
|
||||||
|
description: "Title",
|
||||||
|
type: "str",
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
name: "Description",
|
||||||
|
description: "Description",
|
||||||
|
type: "text",
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Custom formatter for text columns to truncate text in normal mode
|
||||||
|
_truncateFormatter: function(cell, formatterParams, onRendered) {
|
||||||
|
const value = cell.getValue();
|
||||||
|
const maxLength = formatterParams.maxLength || 100;
|
||||||
|
|
||||||
|
if (value && value.length > maxLength) {
|
||||||
|
// Create a truncated version with "..." and show more indicator
|
||||||
|
const truncated = value.substring(0, maxLength) + "...";
|
||||||
|
|
||||||
|
// Return HTML with truncated text and a "show more" button
|
||||||
|
return `<div class="truncated-cell">
|
||||||
|
<div class="truncated-content">${truncated}</div>
|
||||||
|
<div class="show-more" title="Click to edit and see full text">
|
||||||
|
<i class="material-icons">more_horiz</i>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
|
||||||
|
_createColumnsFromListType: function(listTypeConfig) {
|
||||||
|
const columns = [];
|
||||||
|
|
||||||
|
// Add columns for each field in the list type
|
||||||
|
Object.entries(listTypeConfig).forEach(([key, field]) => {
|
||||||
|
const column = {
|
||||||
|
title: field.name || key,
|
||||||
|
field: key,
|
||||||
|
headerTooltip: field.description,
|
||||||
|
headerSort: false,
|
||||||
|
visible: true,
|
||||||
|
resizable: "header",
|
||||||
|
};
|
||||||
|
console.log("Column ", field.name, " type: ", field.type)
|
||||||
|
// Set width based on field type
|
||||||
|
if (field.type === 'boolean') {
|
||||||
|
column.minWidth = 50;
|
||||||
|
column.maxWidth = 80; // Limit maximum width
|
||||||
|
column.widthGrow = 0; // Don't allow boolean columns to grow
|
||||||
|
} else if (field.type === 'text') {
|
||||||
|
column.width = 400; // Much larger width for text columns (especially description)
|
||||||
|
column.minWidth = 300; // Ensure text columns have adequate minimum width
|
||||||
|
column.widthGrow = 2; // Allow text columns to grow significantly more
|
||||||
|
} else {
|
||||||
|
column.width = 150; // Default width for other columns
|
||||||
|
column.minWidth = 100;
|
||||||
|
column.widthGrow = 1; // Allow some growth
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure consistent width calculation
|
||||||
|
column.widthShrink = 0; // Don't allow shrinking below minWidth
|
||||||
|
|
||||||
|
// Set editor based on field type
|
||||||
|
if (field.type === 'boolean') {
|
||||||
|
column.formatter = 'tickCross';
|
||||||
|
column.editor = 'tickCross';
|
||||||
|
column.hozAlign = 'center';
|
||||||
|
column.headerHozAlign = 'center';
|
||||||
|
column.formatterParams = {
|
||||||
|
allowEmpty: true,
|
||||||
|
allowTruthy: true,
|
||||||
|
tickElement: "<i class='material-icons'>check_circle</i>",
|
||||||
|
crossElement: "<i class='material-icons'>cancel</i>"
|
||||||
|
};
|
||||||
|
} else if (field.type === 'enum' && field.allowed_values) {
|
||||||
|
column.editor = 'select';
|
||||||
|
column.editorParams = {
|
||||||
|
values: field.allowed_values
|
||||||
|
};
|
||||||
|
column.hozAlign = 'left';
|
||||||
|
column.headerHozAlign = 'left';
|
||||||
|
} else if (field.type === 'text') {
|
||||||
|
column.editor = 'textarea';
|
||||||
|
column.formatter = this._truncateFormatter; // Use custom formatter to truncate text
|
||||||
|
column.variableHeight = true;
|
||||||
|
// Configure formatter parameters
|
||||||
|
column.formatterParams = {
|
||||||
|
maxLength: 50,
|
||||||
|
autoResize: true
|
||||||
|
};
|
||||||
|
// Prevent scrolling when editing text cells
|
||||||
|
column.editorParams = {
|
||||||
|
elementAttributes: {
|
||||||
|
preventScroll: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
column.hozAlign = 'left';
|
||||||
|
column.headerHozAlign = 'left';
|
||||||
|
} else {
|
||||||
|
column.editor = 'input';
|
||||||
|
column.hozAlign = 'left';
|
||||||
|
column.headerHozAlign = 'left';
|
||||||
|
}
|
||||||
|
|
||||||
|
columns.push(column);
|
||||||
|
});
|
||||||
|
|
||||||
|
// We don't add a delete button column as per requirements
|
||||||
|
// to prevent users from deleting rows
|
||||||
|
|
||||||
|
return columns;
|
||||||
|
},
|
||||||
|
|
||||||
|
get: function(containerId) {
|
||||||
|
return this.instances[containerId] || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy: function(containerId) {
|
||||||
|
if (this.instances[containerId]) {
|
||||||
|
if (this.instances[containerId].table && typeof this.instances[containerId].table.destroy === 'function') {
|
||||||
|
this.instances[containerId].table.destroy();
|
||||||
|
}
|
||||||
|
delete this.instances[containerId];
|
||||||
|
}
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
if (container) {
|
||||||
|
container.classList.remove('tabulator-initialized');
|
||||||
|
container.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize list type configurations
|
||||||
|
window.listTypeConfigs = window.listTypeConfigs || {};
|
||||||
|
|
||||||
|
// Initialize ordered list editors
|
||||||
|
document.querySelectorAll('.ordered-list-field').forEach(function(textarea) {
|
||||||
|
const containerId = textarea.id + '-editor';
|
||||||
|
console.log('Initializing ordered list editor for', containerId);
|
||||||
|
|
||||||
|
// Create container if it doesn't exist
|
||||||
|
let container = document.getElementById(containerId);
|
||||||
|
if (!container) {
|
||||||
|
container = document.createElement('div');
|
||||||
|
container.id = containerId;
|
||||||
|
container.className = 'ordered-list-editor';
|
||||||
|
textarea.parentNode.insertBefore(container, textarea.nextSibling);
|
||||||
|
textarea.classList.add('d-none'); // Hide the textarea
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = textarea.value ? JSON.parse(textarea.value) : [];
|
||||||
|
const listType = textarea.getAttribute('data-list-type');
|
||||||
|
|
||||||
|
// Check if we have the list type configuration
|
||||||
|
if (listType && !window.listTypeConfigs[listType]) {
|
||||||
|
console.warn(`List type configuration for ${listType} not found. Using default configuration.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.EveAI.OrderedListEditors.initialize(containerId, data, listType);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error initializing ordered list editor:', e);
|
||||||
|
container.innerHTML = `<div class="alert alert-danger p-3">
|
||||||
|
<strong>Error initializing ordered list editor:</strong><br>${e.message}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
@@ -15,29 +15,17 @@
|
|||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
{% set disabled_fields = ['type', 'type_version'] %}
|
{% set disabled_fields = ['type', 'type_version'] %}
|
||||||
{% set exclude_fields = [] %}
|
{% set exclude_fields = [] %}
|
||||||
<!-- Render Static Fields -->
|
|
||||||
{% for field in form.get_static_fields() %}
|
|
||||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<!-- Overview Section -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="specialist-overview" id="specialist-svg">
|
|
||||||
<img src="{{ svg_path }}" alt="Specialist Overview" class="w-100">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Nav Tabs -->
|
<!-- Nav Tabs -->
|
||||||
<div class="row mt-5">
|
<div class="row mt-5">
|
||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
<div class="nav-wrapper position-relative end-0">
|
<div class="nav-wrapper position-relative end-0">
|
||||||
<ul class="nav nav-pills nav-fill p-1" role="tablist">
|
<ul class="nav nav-pills nav-fill p-1" role="tablist">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link mb-0 px-0 py-1" data-bs-toggle="tab" href="#general-tab" role="tab">
|
||||||
|
General
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link mb-0 px-0 py-1 active" data-bs-toggle="tab" href="#configuration-tab" role="tab">
|
<a class="nav-link mb-0 px-0 py-1 active" data-bs-toggle="tab" href="#configuration-tab" role="tab">
|
||||||
Configuration
|
Configuration
|
||||||
@@ -67,6 +55,27 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-content tab-space">
|
<div class="tab-content tab-space">
|
||||||
|
<!-- General Tab -->
|
||||||
|
<div class="tab-pane fade" id="general-tab" role="tabpanel">
|
||||||
|
<!-- Render Static Fields -->
|
||||||
|
{% for field in form.get_static_fields() %}
|
||||||
|
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- Overview Section -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="specialist-overview" id="specialist-svg">
|
||||||
|
<img src="{{ svg_path }}" alt="Specialist Overview" class="w-100">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Configuration Tab -->
|
<!-- Configuration Tab -->
|
||||||
<div class="tab-pane fade show active" id="configuration-tab" role="tabpanel">
|
<div class="tab-pane fade show active" id="configuration-tab" role="tabpanel">
|
||||||
{% for collection_name, fields in form.get_dynamic_fields().items() %}
|
{% for collection_name, fields in form.get_dynamic_fields().items() %}
|
||||||
@@ -420,6 +429,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
color: #344767 !important; /* Default dark color */
|
color: #344767 !important; /* Default dark color */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Style for active tabs */
|
||||||
|
.nav-link.active {
|
||||||
|
background-color: #5e72e4 !important; /* Primary blue color */
|
||||||
|
color: white !important;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
/* Style for disabled tabs */
|
/* Style for disabled tabs */
|
||||||
.nav-link.disabled {
|
.nav-link.disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
@@ -476,4 +493,3 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% from "macros.html" import render_field %}
|
||||||
|
|
||||||
|
{% block title %}Edit Specialist Magic Link{% endblock %}
|
||||||
|
|
||||||
|
{% block content_title %}Edit Specialist Magic Link{% endblock %}
|
||||||
|
{% block content_description %}Edit a Specialist Magic Link{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{% set disabled_fields = ['magic_link_code'] %}
|
||||||
|
{% set exclude_fields = [] %}
|
||||||
|
<!-- Render Static Fields -->
|
||||||
|
{% for field in form.get_static_fields() %}
|
||||||
|
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||||
|
{% endfor %}
|
||||||
|
<!-- Render Dynamic Fields -->
|
||||||
|
{% for collection_name, fields in form.get_dynamic_fields().items() %}
|
||||||
|
{% if fields|length > 0 %}
|
||||||
|
<h4 class="mt-4">{{ collection_name }}</h4>
|
||||||
|
{% endif %}
|
||||||
|
{% for field in fields %}
|
||||||
|
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
<button type="submit" class="btn btn-primary">Save Specialist Magic Link</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content_footer %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
23
eveai_app/templates/interaction/specialist_magic_link.html
Normal file
23
eveai_app/templates/interaction/specialist_magic_link.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% from "macros.html" import render_field %}
|
||||||
|
|
||||||
|
{% block title %}Specialist Magic Link{% endblock %}
|
||||||
|
|
||||||
|
{% block content_title %}Register Specialist Magic Link{% endblock %}
|
||||||
|
{% block content_description %}Define a new specialist magic link{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{% set disabled_fields = [] %}
|
||||||
|
{% set exclude_fields = [] %}
|
||||||
|
{% for field in form %}
|
||||||
|
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||||
|
{% endfor %}
|
||||||
|
<button type="submit" class="btn btn-primary">Register Specialist Magic Link</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content_footer %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
26
eveai_app/templates/interaction/specialist_magic_links.html
Normal file
26
eveai_app/templates/interaction/specialist_magic_links.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% from 'macros.html' import render_selectable_table, render_pagination %}
|
||||||
|
|
||||||
|
{% block title %}Specialist Magic Links{% endblock %}
|
||||||
|
|
||||||
|
{% block content_title %}Specialist Magic Links{% endblock %}
|
||||||
|
{% block content_description %}View Specialists Magic Links{% endblock %}
|
||||||
|
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<form method="POST" action="{{ url_for('interaction_bp.handle_specialist_magic_link_selection') }}" id="specialistMagicLinksForm">
|
||||||
|
{{ render_selectable_table(headers=["Specialist ML ID", "Name", "Magic Link Code"], rows=rows, selectable=True, id="specialistMagicLinksTable") }}
|
||||||
|
<div class="form-group mt-3 d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<button type="submit" name="action" value="edit_specialist_magic_link" class="btn btn-primary" onclick="return validateTableSelection('specialistMagicLinksForm')">Edit Specialist Magic Link</button>
|
||||||
|
</div>
|
||||||
|
<button type="submit" name="action" value="create_specialist_magic_link" class="btn btn-success">Register Specialist Magic Link</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content_footer %}
|
||||||
|
{{ render_pagination(pagination, 'interaction_bp.specialist_magic_links') }}
|
||||||
|
{% endblock %}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% from 'macros.html' import render_selectable_table, render_pagination %}
|
{% from 'macros.html' import render_selectable_table, render_pagination %}
|
||||||
|
|
||||||
{% block title %}Retrievers{% endblock %}
|
{% block title %}Specialists{% endblock %}
|
||||||
|
|
||||||
{% block content_title %}Specialists{% endblock %}
|
{% block content_title %}Specialists{% endblock %}
|
||||||
{% block content_description %}View Specialists for Tenant{% endblock %}
|
{% block content_description %}View Specialists for Tenant{% endblock %}
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
{# Helper functie om veilig de class van een veld te krijgen #}
|
||||||
|
{% macro get_field_class(field, default='') %}
|
||||||
|
{% if field.render_kw is not none and field.render_kw.get('class') is not none %}
|
||||||
|
{{ field.render_kw.get('class') }}
|
||||||
|
{% else %}
|
||||||
|
{{ default }}
|
||||||
|
{% endif %}
|
||||||
|
{% 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' %}
|
{% if field.type == 'BooleanField' %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -55,9 +64,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% set field_class = get_field_class(field) %}
|
||||||
{% if field.type == 'TextAreaField' and 'json-editor' in class %}
|
{% if field.type == 'TextAreaField' and 'json-editor' in class %}
|
||||||
<div id="{{ field.id }}-editor" class="json-editor-container"></div>
|
<div id="{{ field.id }}-editor" class="json-editor-container"></div>
|
||||||
{{ field(class="form-control d-none " + class, disabled=disabled, readonly=readonly) }}
|
{{ field(class="form-control d-none " + class, disabled=disabled, readonly=readonly) }}
|
||||||
|
{% elif field.type == 'OrderedListField' or 'ordered-list-field' in field_class %}
|
||||||
|
{# Create container for ordered list editor and hide the textarea #}
|
||||||
|
<div id="{{ field.id }}-editor" class="ordered-list-editor"></div>
|
||||||
|
{{ field(class="form-control d-none " + field_class|trim, disabled=disabled, readonly=readonly) }}
|
||||||
{% elif field.type == 'SelectField' %}
|
{% elif field.type == 'SelectField' %}
|
||||||
{{ field(class="form-control form-select " + class, disabled=disabled, readonly=readonly) }}
|
{{ field(class="form-control form-select " + class, disabled=disabled, readonly=readonly) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -76,6 +90,7 @@
|
|||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% macro render_field(field, disabled_fields=[], readonly_fields=[], exclude_fields=[], class='') %}
|
{% macro render_field(field, disabled_fields=[], readonly_fields=[], exclude_fields=[], class='') %}
|
||||||
<!-- Debug info -->
|
<!-- Debug info -->
|
||||||
<!-- Field name: {{ field.name }}, Field type: {{ field.__class__.__name__ }} -->
|
<!-- Field name: {{ field.name }}, Field type: {{ field.__class__.__name__ }} -->
|
||||||
@@ -123,7 +138,7 @@
|
|||||||
{% elif cell.type == 'badge' %}
|
{% elif cell.type == 'badge' %}
|
||||||
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
||||||
{% elif cell.type == 'link' %}
|
{% elif cell.type == 'link' %}
|
||||||
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-bs-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ cell.value }}
|
{{ cell.value }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -177,7 +192,7 @@
|
|||||||
{% elif cell.type == 'badge' %}
|
{% elif cell.type == 'badge' %}
|
||||||
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
||||||
{% elif cell.type == 'link' %}
|
{% elif cell.type == 'link' %}
|
||||||
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-bs-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ cell.value }}
|
{{ cell.value }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -342,7 +357,7 @@
|
|||||||
{% elif cell.type == 'badge' %}
|
{% elif cell.type == 'badge' %}
|
||||||
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
||||||
{% elif cell.type == 'link' %}
|
{% elif cell.type == 'link' %}
|
||||||
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-bs-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ cell.value }}
|
{{ cell.value }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -436,3 +451,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro debug_to_console(var_name, var_value) %}
|
||||||
|
<script>
|
||||||
|
console.log('{{ var_name }}:', {{ var_value|tojson }});
|
||||||
|
</script>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,7 @@
|
|||||||
{'name': 'Tenant Overview', 'url': '/user/tenant_overview', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
{'name': 'Tenant Overview', 'url': '/user/tenant_overview', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
{'name': 'Edit Tenant', 'url': '/user/tenant/' ~ session['tenant'].get('id'), 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
{'name': 'Edit Tenant', 'url': '/user/tenant/' ~ session['tenant'].get('id'), 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
{'name': 'Tenant Domains', 'url': '/user/view_tenant_domains', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
{'name': 'Tenant Domains', 'url': '/user/view_tenant_domains', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
|
{'name': 'Tenant Makes', 'url': '/user/tenant_makes', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
{'name': 'Tenant Projects', 'url': '/user/tenant_projects', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
{'name': 'Tenant Projects', 'url': '/user/tenant_projects', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
{'name': 'Users', 'url': '/user/view_users', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
{'name': 'Users', 'url': '/user/view_users', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
]) }}
|
]) }}
|
||||||
@@ -106,6 +107,7 @@
|
|||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
{{ dropdown('Interactions', 'hub', [
|
{{ dropdown('Interactions', 'hub', [
|
||||||
{'name': 'Specialists', 'url': '/interaction/specialists', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
{'name': 'Specialists', 'url': '/interaction/specialists', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
|
{'name': 'Specialist Magic Links', 'url': '/interaction/specialist_magic_links', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
{'name': 'Chat Sessions', 'url': '/interaction/chat_sessions', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
{'name': 'Chat Sessions', 'url': '/interaction/chat_sessions', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
]) }}
|
]) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -120,6 +122,7 @@
|
|||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
{{ dropdown(current_user.user_name, 'person', [
|
{{ dropdown(current_user.user_name, 'person', [
|
||||||
{'name': 'Session Defaults', 'url': '/session_defaults', 'roles': ['Super User', 'Tenant Admin']},
|
{'name': 'Session Defaults', 'url': '/session_defaults', 'roles': ['Super User', 'Tenant Admin']},
|
||||||
|
{'name': 'Release Notes', 'url': '/release_notes', 'roles': ['Super User', 'Partner Admin', 'Tenant Admin']},
|
||||||
{'name': 'Logout', 'url': '/logout'}
|
{'name': 'Logout', 'url': '/logout'}
|
||||||
]) }}
|
]) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
7
eveai_app/templates/ordered_list_configs.html
Normal file
7
eveai_app/templates/ordered_list_configs.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{# Include this template in any page that uses ordered_list fields #}
|
||||||
|
{# Usage: {% include 'ordered_list_configs.html' %} #}
|
||||||
|
{# The form must be available in the template context as 'form' #}
|
||||||
|
|
||||||
|
{% if form and form.get_list_type_configs_js %}
|
||||||
|
{{ form.get_list_type_configs_js()|safe }}
|
||||||
|
{% endif %}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% from "macros.html" import render_field %}
|
{% from "macros.html" import render_field, debug_to_console %}
|
||||||
{% block title %}Register Partner Service{% endblock %}
|
{% block title %}Edit Partner Service{% endblock %}
|
||||||
|
|
||||||
{% block content_title %}Register Partner Service{% endblock %}
|
{% block content_title %}Edit Partner Service{% endblock %}
|
||||||
{% block content_description %}Register Partner Service{% endblock %}
|
{% block content_description %}Edit Partner Service{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
<!-- Render Dynamic Fields -->
|
<!-- Render Dynamic Fields -->
|
||||||
{% for collection_name, fields in form.get_dynamic_fields().items() %}
|
{% for collection_name, fields in form.get_dynamic_fields().items() %}
|
||||||
|
{{ debug_to_console('collection_name', collection_name) }}
|
||||||
|
{{ debug_to_console('fields', fields) }}
|
||||||
{% if fields|length > 0 %}
|
{% if fields|length > 0 %}
|
||||||
<h4 class="mt-4">{{ collection_name }}</h4>
|
<h4 class="mt-4">{{ collection_name }}</h4>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -23,6 +25,6 @@
|
|||||||
{{ render_field(field, disabled_fields, exclude_fields) }}
|
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<button type="submit" class="btn btn-primary">Register Partner Service</button>
|
<button type="submit" class="btn btn-primary">Save Partner Service</button>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -9,14 +9,19 @@
|
|||||||
<script src="{{url_for('static', filename='assets/js/material-kit-pro.min.js')}}"></script>
|
<script src="{{url_for('static', filename='assets/js/material-kit-pro.min.js')}}"></script>
|
||||||
|
|
||||||
{% include 'eveai_json_editor.html' %}
|
{% include 'eveai_json_editor.html' %}
|
||||||
|
{% include 'eveai_ordered_list_editor.html' %}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Initialize tooltips
|
// Initialize tooltips if bootstrap is available
|
||||||
|
if (typeof bootstrap !== 'undefined') {
|
||||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
||||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||||
return new bootstrap.Tooltip(tooltipTriggerEl)
|
return new bootstrap.Tooltip(tooltipTriggerEl)
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
console.warn('Bootstrap is not defined. Tooltips will not be initialized.');
|
||||||
|
}
|
||||||
|
|
||||||
// De JSON editor initialisatie is hierboven al samengevoegd.
|
// De JSON editor initialisatie is hierboven al samengevoegd.
|
||||||
// Deze dubbele DOMContentLoaded listener en .json-editor initialisatie kan verwijderd worden.
|
// Deze dubbele DOMContentLoaded listener en .json-editor initialisatie kan verwijderd worden.
|
||||||
@@ -46,9 +51,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Find and click the corresponding tab button
|
// Find and click the corresponding tab button
|
||||||
const tabButton = document.querySelector(`[data-bs-toggle="tab"][data-bs-target="#${tabId}"]`);
|
const tabButton = document.querySelector(`[data-bs-toggle="tab"][data-bs-target="#${tabId}"]`);
|
||||||
if (tabButton) {
|
if (tabButton && typeof bootstrap !== 'undefined') {
|
||||||
const tab = new bootstrap.Tab(tabButton);
|
const tab = new bootstrap.Tab(tabButton);
|
||||||
tab.show();
|
tab.show();
|
||||||
|
} else if (tabButton) {
|
||||||
|
// Fallback if bootstrap is not available
|
||||||
|
tabButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scroll the invalid field into view and focus it
|
// Scroll the invalid field into view and focus it
|
||||||
|
|||||||
@@ -19,3 +19,37 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content_footer %} {% endblock %}
|
{% block content_footer %} {% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// JavaScript om de gebruiker's timezone te detecteren
|
||||||
|
document.addEventListener('DOMContentLoaded', (event) => {
|
||||||
|
// Detect timezone
|
||||||
|
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
|
||||||
|
// Send timezone to the server via a POST request
|
||||||
|
fetch('/set_user_timezone', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ timezone: userTimezone })
|
||||||
|
}).then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
console.log('Timezone sent to server successfully');
|
||||||
|
} else {
|
||||||
|
console.error('Failed to send timezone to server');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialiseer Select2 voor timezone selectie
|
||||||
|
$('#timezone').select2({
|
||||||
|
placeholder: 'Selecteer een timezone...',
|
||||||
|
allowClear: true,
|
||||||
|
maximumSelectionLength: 10,
|
||||||
|
theme: 'bootstrap',
|
||||||
|
width: '100%'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
33
eveai_app/templates/user/edit_tenant_make.html
Normal file
33
eveai_app/templates/user/edit_tenant_make.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% from "macros.html" import render_field %}
|
||||||
|
|
||||||
|
{% block title %}Edit Tenant Make{% endblock %}
|
||||||
|
|
||||||
|
{% block content_title %}Edit Tenant Make{% endblock %}
|
||||||
|
{% block content_description %}Edit a Tenant Make.{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{% set disabled_fields = [] %}
|
||||||
|
{% set exclude_fields = [] %}
|
||||||
|
<!-- Render Static Fields -->
|
||||||
|
{% for field in form.get_static_fields() %}
|
||||||
|
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||||
|
{% endfor %}
|
||||||
|
<!-- Render Dynamic Fields -->
|
||||||
|
{% for collection_name, fields in form.get_dynamic_fields().items() %}
|
||||||
|
{% if fields|length > 0 %}
|
||||||
|
<h4 class="mt-4">{{ collection_name }}</h4>
|
||||||
|
{% endif %}
|
||||||
|
{% for field in fields %}
|
||||||
|
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
<button type="submit" class="btn btn-primary">Save Tenant Make</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content_footer %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
// JavaScript to detect user's timezone
|
// JavaScript om de gebruiker's timezone te detecteren
|
||||||
document.addEventListener('DOMContentLoaded', (event) => {
|
document.addEventListener('DOMContentLoaded', (event) => {
|
||||||
// Detect timezone
|
// Detect timezone
|
||||||
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
@@ -45,6 +45,31 @@
|
|||||||
console.error('Failed to send timezone to server');
|
console.error('Failed to send timezone to server');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#timezone').select2({
|
||||||
|
placeholder: 'Selecteer een timezone...',
|
||||||
|
allowClear: true,
|
||||||
|
theme: 'bootstrap',
|
||||||
|
width: '100%',
|
||||||
|
dropdownAutoWidth: true,
|
||||||
|
dropdownCssClass: 'timezone-dropdown', // Een custom class voor specifieke styling
|
||||||
|
scrollAfterSelect: false,
|
||||||
|
// Verbeterd scroll gedrag
|
||||||
|
dropdownParent: $('body')
|
||||||
|
});
|
||||||
|
|
||||||
|
// Stel de huidige waarde in als de dropdown wordt geopend
|
||||||
|
$('#timezone').on('select2:open', function() {
|
||||||
|
if ($(this).val()) {
|
||||||
|
setTimeout(function() {
|
||||||
|
let selectedOption = $('.select2-results__option[aria-selected=true]');
|
||||||
|
if (selectedOption.length) {
|
||||||
|
selectedOption[0].scrollIntoView({ behavior: 'auto', block: 'center' });
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
32
eveai_app/templates/user/tenant_make.html
Normal file
32
eveai_app/templates/user/tenant_make.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% from "macros.html" import render_field %}
|
||||||
|
|
||||||
|
{% block title %}Tenant Make Registration{% endblock %}
|
||||||
|
|
||||||
|
{% block content_title %}Register Tenant Make{% endblock %}
|
||||||
|
{% block content_description %}Define a new tenant make{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post">
|
||||||
|
{{ form.hidden_tag() }}
|
||||||
|
{% set disabled_fields = [] %}
|
||||||
|
{% set exclude_fields = [] %}
|
||||||
|
{% for field in form.get_static_fields() %}
|
||||||
|
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||||
|
{% endfor %}
|
||||||
|
<!-- Render Dynamic Fields -->
|
||||||
|
{% for collection_name, fields in form.get_dynamic_fields().items() %}
|
||||||
|
{% if fields|length > 0 %}
|
||||||
|
<h4 class="mt-4">{{ collection_name }}</h4>
|
||||||
|
{% endif %}
|
||||||
|
{% for field in fields %}
|
||||||
|
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
<button type="submit" class="btn btn-primary">Register Tenant Make</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content_footer %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
26
eveai_app/templates/user/tenant_makes.html
Normal file
26
eveai_app/templates/user/tenant_makes.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% from 'macros.html' import render_selectable_table, render_pagination %}
|
||||||
|
|
||||||
|
{% block title %}Tenant Makes{% endblock %}
|
||||||
|
|
||||||
|
{% block content_title %}Tenant Makes{% endblock %}
|
||||||
|
{% block content_description %}View Tenant Makes for Tenant{% endblock %}
|
||||||
|
{% block content_class %}<div class="col-xl-12 col-lg-5 col-md-7 mx-auto"></div>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<form method="POST" action="{{ url_for('user_bp.handle_tenant_make_selection') }}" id="tenantMakesForm">
|
||||||
|
{{ render_selectable_table(headers=["Tenant Make ID", "Name", "Website", "Active"], rows=rows, selectable=True, id="tenantMakesTable") }}
|
||||||
|
<div class="form-group mt-3 d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<button type="submit" name="action" value="edit_tenant_make" class="btn btn-primary" onclick="return validateTableSelection('tenantMakesForm')">Edit Tenant Make</button>
|
||||||
|
</div>
|
||||||
|
<button type="submit" name="action" value="create_tenant_make" class="btn btn-success">Register Tenant Make</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content_footer %}
|
||||||
|
{{ render_pagination(pagination, "user_bp.tenant_makes") }}
|
||||||
|
{% endblock %}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% from "macros.html" import render_field, render_included_field %}
|
{% from "macros.html" import render_field, render_included_field, debug_to_console %}
|
||||||
|
|
||||||
{% block title %}Tenant Overview{% endblock %}
|
{% block title %}Tenant Overview{% endblock %}
|
||||||
|
|
||||||
@@ -9,162 +9,23 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
<!-- Main Tenant Information -->
|
{% set disabled_fields = [] %}
|
||||||
{% set main_fields = ['name', 'code', 'website', 'default_language', 'allowed_languages', 'type'] %}
|
|
||||||
{% for field in form %}
|
{% for field in form %}
|
||||||
{{ render_included_field(field, disabled_fields=main_fields, include_fields=main_fields) }}
|
{{ debug_to_console('field to disable', field.name) }}
|
||||||
|
{{ debug_to_console('field type to disable', field.type) }}
|
||||||
|
{% if field.name != 'csrf_token' and field.type != 'HiddenField' %}
|
||||||
|
{% set disabled_fields = disabled_fields + [field.name] %}
|
||||||
|
{{ debug_to_console('disable', '!') }}
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{{ debug_to_console('disabled_fields', disabled_fields) }}
|
||||||
<!-- Nav Tabs -->
|
{% set exclude_fields = [] %}
|
||||||
<div class="row mt-5">
|
|
||||||
<div class="col-lg-12">
|
|
||||||
<div class="nav-wrapper position-relative end-0">
|
|
||||||
<ul class="nav nav-pills nav-fill p-1" role="tablist">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link mb-0 px-0 py-1" data-toggle="tab" href="#license-info-tab" role="tab" aria-controls="license-info" aria-selected="false">
|
|
||||||
License Information
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="tab-content tab-space">
|
|
||||||
<!-- License Information Tab -->
|
|
||||||
<div class="tab-pane fade" id="license-info-tab" role="tabpanel">
|
|
||||||
{% set license_fields = ['currency', 'usage_email', ] %}
|
|
||||||
{% for field in form %}
|
{% for field in form %}
|
||||||
{{ render_included_field(field, disabled_fields=license_fields, include_fields=license_fields) }}
|
{{ render_field(field, disabled_fields, exclude_fields) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<!-- Register API Key Button -->
|
|
||||||
<button type="button" class="btn btn-primary" onclick="generateNewChatApiKey()">Register Chat API Key</button>
|
|
||||||
<button type="button" class="btn btn-primary" onclick="generateNewApiKey()">Register API Key</button>
|
|
||||||
<!-- API Key Display Field -->
|
|
||||||
<div id="chat-api-key-field" style="display:none;">
|
|
||||||
<label for="chat-api-key">Chat API Key:</label>
|
|
||||||
<input type="text" id="chat-api-key" class="form-control" readonly>
|
|
||||||
<button type="button" id="copy-chat-button" class="btn btn-primary">Copy to Clipboard</button>
|
|
||||||
<p id="copy-chat-message" style="display:none;color:green;">Chat API key copied to clipboard</p>
|
|
||||||
</div>
|
|
||||||
<div id="api-key-field" style="display:none;">
|
|
||||||
<label for="api-key">API Key:</label>
|
|
||||||
<input type="text" id="api-key" class="form-control" readonly>
|
|
||||||
<button type="button" id="copy-api-button" class="btn btn-primary">Copy to Clipboard</button>
|
|
||||||
<p id="copy-message" style="display:none;color:green;">API key copied to clipboard</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block content_footer %}
|
{% block content_footer %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script>
|
|
||||||
// Function to generate a new Chat API Key
|
|
||||||
function generateNewChatApiKey() {
|
|
||||||
generateApiKey('/admin/user/generate_chat_api_key', '#chat-api-key', '#chat-api-key-field');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to generate a new general API Key
|
|
||||||
function generateNewApiKey() {
|
|
||||||
generateApiKey('/admin/user/generate_api_api_key', '#api-key', '#api-key-field');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reusable function to handle API key generation
|
|
||||||
function generateApiKey(url, inputSelector, fieldSelector) {
|
|
||||||
$.ajax({
|
|
||||||
url: url,
|
|
||||||
type: 'POST',
|
|
||||||
contentType: 'application/json',
|
|
||||||
success: function(response) {
|
|
||||||
$(inputSelector).val(response.api_key);
|
|
||||||
$(fieldSelector).show();
|
|
||||||
},
|
|
||||||
error: function(error) {
|
|
||||||
alert('Error generating new API key: ' + error.responseText);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to copy text to clipboard
|
|
||||||
function copyToClipboard(selector, messageSelector) {
|
|
||||||
const element = document.querySelector(selector);
|
|
||||||
if (element) {
|
|
||||||
const text = element.value;
|
|
||||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
||||||
navigator.clipboard.writeText(text).then(function() {
|
|
||||||
showCopyMessage(messageSelector);
|
|
||||||
}).catch(function(error) {
|
|
||||||
alert('Failed to copy text: ' + error);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
fallbackCopyToClipboard(text, messageSelector);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error('Element not found for selector:', selector);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback method for copying text to clipboard
|
|
||||||
function fallbackCopyToClipboard(text, messageSelector) {
|
|
||||||
const textArea = document.createElement('textarea');
|
|
||||||
textArea.value = text;
|
|
||||||
document.body.appendChild(textArea);
|
|
||||||
textArea.focus();
|
|
||||||
textArea.select();
|
|
||||||
try {
|
|
||||||
document.execCommand('copy');
|
|
||||||
showCopyMessage(messageSelector);
|
|
||||||
} catch (err) {
|
|
||||||
alert('Fallback: Oops, unable to copy', err);
|
|
||||||
}
|
|
||||||
document.body.removeChild(textArea);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to show copy confirmation message
|
|
||||||
function showCopyMessage(messageSelector) {
|
|
||||||
const message = document.querySelector(messageSelector);
|
|
||||||
if (message) {
|
|
||||||
message.style.display = 'block';
|
|
||||||
setTimeout(function() {
|
|
||||||
message.style.display = 'none';
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event listeners for copy buttons
|
|
||||||
document.getElementById('copy-chat-button').addEventListener('click', function() {
|
|
||||||
copyToClipboard('#chat-api-key', '#copy-chat-message');
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('copy-api-button').addEventListener('click', function() {
|
|
||||||
copyToClipboard('#api-key', '#copy-message');
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
// JavaScript to detect user's timezone
|
|
||||||
document.addEventListener('DOMContentLoaded', (event) => {
|
|
||||||
// Detect timezone
|
|
||||||
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
||||||
|
|
||||||
// Send timezone to the server via a POST request
|
|
||||||
fetch('/set_user_timezone', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ timezone: userTimezone })
|
|
||||||
}).then(response => {
|
|
||||||
if (response.ok) {
|
|
||||||
console.log('Timezone sent to server successfully');
|
|
||||||
} else {
|
|
||||||
console.error('Failed to send timezone to server');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,15 +1,25 @@
|
|||||||
from flask import request, render_template, Blueprint, session, current_app, jsonify, flash, redirect
|
from flask import request, render_template, Blueprint, session, current_app, jsonify, flash, redirect, url_for
|
||||||
from flask_security import roles_required, roles_accepted
|
from flask_security import roles_required, roles_accepted
|
||||||
from flask_wtf.csrf import generate_csrf
|
from flask_wtf.csrf import generate_csrf
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
|
||||||
from common.models.document import Catalog
|
from common.models.document import Catalog
|
||||||
from common.models.user import Tenant
|
from common.models.user import Tenant
|
||||||
from common.utils.database import Database
|
from common.utils.database import Database
|
||||||
from common.utils.nginx_utils import prefixed_url_for
|
from common.utils.nginx_utils import prefixed_url_for
|
||||||
from .basic_forms import SessionDefaultsForm
|
from .basic_forms import SessionDefaultsForm
|
||||||
|
from common.extensions import content_manager
|
||||||
|
|
||||||
|
import markdown
|
||||||
|
|
||||||
basic_bp = Blueprint('basic_bp', __name__)
|
basic_bp = Blueprint('basic_bp', __name__)
|
||||||
|
|
||||||
|
# Markdown filter toevoegen aan Jinja2
|
||||||
|
@basic_bp.app_template_filter('markdown')
|
||||||
|
def render_markdown(text):
|
||||||
|
return markdown.markdown(text, extensions=['tables', 'fenced_code'])
|
||||||
|
|
||||||
|
|
||||||
@basic_bp.before_request
|
@basic_bp.before_request
|
||||||
def log_before_request():
|
def log_before_request():
|
||||||
@@ -101,3 +111,58 @@ def check_csrf():
|
|||||||
'session_data': dict(session)
|
'session_data': dict(session)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@basic_bp.route('/content/<content_type>', methods=['GET'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
def view_content(content_type):
|
||||||
|
"""
|
||||||
|
Show content like release notes, terms of use, etc.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content_type (str): Type content (eg. 'changelog', 'terms', 'privacy')
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
current_app.logger.debug(f"Showing content {content_type}")
|
||||||
|
major_minor = request.args.get('version')
|
||||||
|
patch = request.args.get('patch')
|
||||||
|
|
||||||
|
# Gebruik de ContentManager om de content op te halen
|
||||||
|
content_data = content_manager.read_content(content_type, major_minor, patch)
|
||||||
|
|
||||||
|
if not content_data:
|
||||||
|
flash(f'Content van type {content_type} werd niet gevonden.', 'danger')
|
||||||
|
return redirect(prefixed_url_for('basic_bp.index'))
|
||||||
|
|
||||||
|
# Titels en beschrijvingen per contenttype
|
||||||
|
titles = {
|
||||||
|
'changelog': 'Release Notes',
|
||||||
|
'terms': 'Terms & Conditions',
|
||||||
|
'privacy': 'Privacy Statement',
|
||||||
|
# Voeg andere types toe indien nodig
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptions = {
|
||||||
|
'changelog': 'EveAI Release Notes',
|
||||||
|
'terms': "Terms & Conditions for using AskEveAI's Evie",
|
||||||
|
'privacy': "Privacy Statement for AskEveAI's Evie",
|
||||||
|
# Voeg andere types toe indien nodig
|
||||||
|
}
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'basic/view_markdown.html',
|
||||||
|
title=titles.get(content_type, content_type.capitalize()),
|
||||||
|
description=descriptions.get(content_type, ''),
|
||||||
|
markdown_content=content_data['content'],
|
||||||
|
version=content_data['version']
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Error displaying content {content_type}: {str(e)}")
|
||||||
|
flash(f'Error displaying content: {str(e)}', 'danger')
|
||||||
|
return redirect(prefixed_url_for('basic_bp.index'))
|
||||||
|
|
||||||
|
@basic_bp.route('/release_notes', methods=['GET'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
def release_notes():
|
||||||
|
"""Doorverwijzen naar de nieuwe content view voor changelog"""
|
||||||
|
current_app.logger.debug(f"Redirecting to content viewer")
|
||||||
|
return redirect(prefixed_url_for('basic_bp.view_content', content_type='changelog'))
|
||||||
|
|||||||
@@ -17,8 +17,15 @@ from config.type_defs.processor_types import PROCESSOR_TYPES
|
|||||||
from .dynamic_form_base import DynamicFormBase
|
from .dynamic_form_base import DynamicFormBase
|
||||||
|
|
||||||
|
|
||||||
|
def validate_catalog_name(form, field):
|
||||||
|
# Controleer of een catalog met deze naam al bestaat
|
||||||
|
existing_catalog = Catalog.query.filter_by(name=field.data).first()
|
||||||
|
if existing_catalog:
|
||||||
|
raise ValidationError(f'A Catalog with name "{field.data}" already exists. Choose another name.')
|
||||||
|
|
||||||
|
|
||||||
class CatalogForm(FlaskForm):
|
class CatalogForm(FlaskForm):
|
||||||
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_catalog_name])
|
||||||
description = TextAreaField('Description', validators=[Optional()])
|
description = TextAreaField('Description', validators=[Optional()])
|
||||||
|
|
||||||
# Select Field for Catalog Type (Uses the CATALOG_TYPES defined in config)
|
# Select Field for Catalog Type (Uses the CATALOG_TYPES defined in config)
|
||||||
@@ -41,7 +48,7 @@ class CatalogForm(FlaskForm):
|
|||||||
|
|
||||||
|
|
||||||
class EditCatalogForm(DynamicFormBase):
|
class EditCatalogForm(DynamicFormBase):
|
||||||
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
name = StringField('Name', validators=[DataRequired(), Length(max=50), validate_catalog_name])
|
||||||
description = TextAreaField('Description', validators=[Optional()])
|
description = TextAreaField('Description', validators=[Optional()])
|
||||||
|
|
||||||
# Select Field for Catalog Type (Uses the CATALOG_TYPES defined in config)
|
# Select Field for Catalog Type (Uses the CATALOG_TYPES defined in config)
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ from common.utils.document_utils import create_document_stack, start_embedding_t
|
|||||||
from common.utils.dynamic_field_utils import create_default_config_from_type_config
|
from common.utils.dynamic_field_utils import create_default_config_from_type_config
|
||||||
from common.utils.eveai_exceptions import EveAIInvalidLanguageException, EveAIUnsupportedFileType, \
|
from common.utils.eveai_exceptions import EveAIInvalidLanguageException, EveAIUnsupportedFileType, \
|
||||||
EveAIDoubleURLException, EveAIException
|
EveAIDoubleURLException, EveAIException
|
||||||
from config.type_defs.processor_types import PROCESSOR_TYPES
|
|
||||||
from .document_forms import AddDocumentForm, AddURLForm, EditDocumentForm, EditDocumentVersionForm, \
|
from .document_forms import AddDocumentForm, AddURLForm, EditDocumentForm, EditDocumentVersionForm, \
|
||||||
CatalogForm, EditCatalogForm, RetrieverForm, EditRetrieverForm, ProcessorForm, EditProcessorForm
|
CatalogForm, EditCatalogForm, RetrieverForm, EditRetrieverForm, ProcessorForm, EditProcessorForm
|
||||||
from common.utils.middleware import mw_before_request
|
from common.utils.middleware import mw_before_request
|
||||||
@@ -29,7 +28,6 @@ from common.utils.nginx_utils import prefixed_url_for
|
|||||||
from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro
|
from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro
|
||||||
from .document_list_view import DocumentListView
|
from .document_list_view import DocumentListView
|
||||||
from .document_version_list_view import DocumentVersionListView
|
from .document_version_list_view import DocumentVersionListView
|
||||||
from config.type_defs.catalog_types import CATALOG_TYPES
|
|
||||||
|
|
||||||
document_bp = Blueprint('document_bp', __name__, url_prefix='/document')
|
document_bp = Blueprint('document_bp', __name__, url_prefix='/document')
|
||||||
|
|
||||||
@@ -126,8 +124,8 @@ def edit_catalog(catalog_id):
|
|||||||
tenant_id = session.get('tenant').get('id')
|
tenant_id = session.get('tenant').get('id')
|
||||||
|
|
||||||
form = EditCatalogForm(request.form, obj=catalog)
|
form = EditCatalogForm(request.form, obj=catalog)
|
||||||
configuration_config = CATALOG_TYPES[catalog.type]["configuration"]
|
full_config = cache_manager.catalogs_config_cache.get_config(catalog.type)
|
||||||
form.add_dynamic_fields("configuration", configuration_config, catalog.configuration)
|
form.add_dynamic_fields("configuration", full_config, catalog.configuration)
|
||||||
|
|
||||||
if request.method == 'POST' and form.validate_on_submit():
|
if request.method == 'POST' and form.validate_on_submit():
|
||||||
form.populate_obj(catalog)
|
form.populate_obj(catalog)
|
||||||
@@ -160,8 +158,9 @@ def processor():
|
|||||||
new_processor = Processor()
|
new_processor = Processor()
|
||||||
form.populate_obj(new_processor)
|
form.populate_obj(new_processor)
|
||||||
new_processor.catalog_id = form.catalog.data.id
|
new_processor.catalog_id = form.catalog.data.id
|
||||||
|
processor_config = cache_manager.processors_config_cache.get_config(new_processor.type)
|
||||||
new_processor.configuration = create_default_config_from_type_config(
|
new_processor.configuration = create_default_config_from_type_config(
|
||||||
PROCESSOR_TYPES[new_processor.type]["configuration"])
|
processor_config["configuration"])
|
||||||
|
|
||||||
set_logging_information(new_processor, dt.now(tz.utc))
|
set_logging_information(new_processor, dt.now(tz.utc))
|
||||||
|
|
||||||
@@ -197,8 +196,8 @@ def edit_processor(processor_id):
|
|||||||
# Create form instance with the processor
|
# Create form instance with the processor
|
||||||
form = EditProcessorForm(request.form, obj=processor)
|
form = EditProcessorForm(request.form, obj=processor)
|
||||||
|
|
||||||
configuration_config = PROCESSOR_TYPES[processor.type]["configuration"]
|
full_config = cache_manager.processors_config_cache.get_config(processor.type)
|
||||||
form.add_dynamic_fields("configuration", configuration_config, processor.configuration)
|
form.add_dynamic_fields("configuration", full_config, processor.configuration)
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
# Update basic fields
|
# Update basic fields
|
||||||
@@ -390,9 +389,7 @@ def add_document():
|
|||||||
|
|
||||||
catalog = Catalog.query.get_or_404(catalog_id)
|
catalog = Catalog.query.get_or_404(catalog_id)
|
||||||
if catalog.configuration and len(catalog.configuration) > 0:
|
if catalog.configuration and len(catalog.configuration) > 0:
|
||||||
document_version_configurations = CATALOG_TYPES[catalog.type]['document_version_configurations']
|
form.add_dynamic_fields("tagging_fields", catalog.configuration)
|
||||||
for config in document_version_configurations:
|
|
||||||
form.add_dynamic_fields(config, catalog.configuration[config])
|
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
try:
|
try:
|
||||||
@@ -402,10 +399,8 @@ def add_document():
|
|||||||
sub_file_type = form.sub_file_type.data
|
sub_file_type = form.sub_file_type.data
|
||||||
filename = secure_filename(file.filename)
|
filename = secure_filename(file.filename)
|
||||||
extension = filename.rsplit('.', 1)[1].lower()
|
extension = filename.rsplit('.', 1)[1].lower()
|
||||||
catalog_properties = {}
|
|
||||||
document_version_configurations = CATALOG_TYPES[catalog.type]['document_version_configurations']
|
catalog_properties = form.get_dynamic_data("tagging_fields")
|
||||||
for config in document_version_configurations:
|
|
||||||
catalog_properties[config] = form.get_dynamic_data(config)
|
|
||||||
|
|
||||||
api_input = {
|
api_input = {
|
||||||
'catalog_id': catalog_id,
|
'catalog_id': catalog_id,
|
||||||
@@ -445,9 +440,7 @@ def add_url():
|
|||||||
|
|
||||||
catalog = Catalog.query.get_or_404(catalog_id)
|
catalog = Catalog.query.get_or_404(catalog_id)
|
||||||
if catalog.configuration and len(catalog.configuration) > 0:
|
if catalog.configuration and len(catalog.configuration) > 0:
|
||||||
document_version_configurations = CATALOG_TYPES[catalog.type]['document_version_configurations']
|
form.add_dynamic_fields("tagging_fields", catalog.configuration)
|
||||||
for config in document_version_configurations:
|
|
||||||
form.add_dynamic_fields(config, catalog.configuration[config])
|
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
try:
|
try:
|
||||||
@@ -459,7 +452,8 @@ def add_url():
|
|||||||
file_content, filename, extension = process_url(url, tenant_id)
|
file_content, filename, extension = process_url(url, tenant_id)
|
||||||
|
|
||||||
catalog_properties = {}
|
catalog_properties = {}
|
||||||
document_version_configurations = CATALOG_TYPES[catalog.type]['document_version_configurations']
|
full_config = cache_manager.catalogs_config_cache.get_config(catalog.type)
|
||||||
|
document_version_configurations = full_config['document_version_configurations']
|
||||||
for config in document_version_configurations:
|
for config in document_version_configurations:
|
||||||
catalog_properties[config] = form.get_dynamic_data(config)
|
catalog_properties[config] = form.get_dynamic_data(config)
|
||||||
|
|
||||||
@@ -582,13 +576,14 @@ def edit_document_version_view(document_version_id):
|
|||||||
|
|
||||||
catalog = Catalog.query.get_or_404(catalog_id)
|
catalog = Catalog.query.get_or_404(catalog_id)
|
||||||
if catalog.configuration and len(catalog.configuration) > 0:
|
if catalog.configuration and len(catalog.configuration) > 0:
|
||||||
document_version_configurations = CATALOG_TYPES[catalog.type]['document_version_configurations']
|
full_config = cache_manager.catalogs_config_cache.get_config(catalog.type)
|
||||||
|
document_version_configurations = full_config['document_version_configurations']
|
||||||
for config in document_version_configurations:
|
for config in document_version_configurations:
|
||||||
form.add_dynamic_fields(config, catalog.configuration[config], doc_vers.catalog_properties[config])
|
form.add_dynamic_fields(config, full_config, doc_vers.catalog_properties[config])
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
catalog_properties = {}
|
catalog_properties = {}
|
||||||
document_version_configurations = CATALOG_TYPES[catalog.type]['document_version_configurations']
|
# Use the full_config variable we already defined
|
||||||
for config in document_version_configurations:
|
for config in document_version_configurations:
|
||||||
catalog_properties[config] = form.get_dynamic_data(config)
|
catalog_properties[config] = form.get_dynamic_data(config)
|
||||||
|
|
||||||
@@ -897,4 +892,3 @@ def clean_markdown(markdown):
|
|||||||
if markdown.endswith("```"):
|
if markdown.endswith("```"):
|
||||||
markdown = markdown[:-3].strip()
|
markdown = markdown[:-3].strip()
|
||||||
return markdown
|
return markdown
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from datetime import date
|
||||||
|
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import (IntegerField, FloatField, BooleanField, StringField, TextAreaField, FileField,
|
from wtforms import (IntegerField, FloatField, BooleanField, StringField, TextAreaField, FileField,
|
||||||
validators, ValidationError)
|
validators, ValidationError)
|
||||||
@@ -6,6 +8,7 @@ import json
|
|||||||
|
|
||||||
from wtforms.fields.choices import SelectField
|
from wtforms.fields.choices import SelectField
|
||||||
from wtforms.fields.datetime import DateField
|
from wtforms.fields.datetime import DateField
|
||||||
|
from wtforms.fields.simple import ColorField
|
||||||
from common.utils.config_field_types import TaggingFields, json_to_patterns, patterns_to_json
|
from common.utils.config_field_types import TaggingFields, json_to_patterns, patterns_to_json
|
||||||
|
|
||||||
|
|
||||||
@@ -49,6 +52,51 @@ class ChunkingPatternsField(TextAreaField):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderedListField(TextAreaField):
|
||||||
|
"""Field for ordered list data that will be rendered as a Tabulator table"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
list_type = kwargs.pop('list_type', '')
|
||||||
|
|
||||||
|
# Behoud bestaande render_kw attributen als die er zijn
|
||||||
|
if 'render_kw' in kwargs:
|
||||||
|
existing_render_kw = kwargs['render_kw']
|
||||||
|
else:
|
||||||
|
existing_render_kw = {}
|
||||||
|
|
||||||
|
current_app.logger.debug(f"incomming render_kw for ordered list field: {existing_render_kw}")
|
||||||
|
|
||||||
|
# Stel nieuwe render_kw samen
|
||||||
|
new_render_kw = {
|
||||||
|
'data-list-type': list_type,
|
||||||
|
'data-handle-enter': 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Voeg klasse toe en behoud bestaande klassen
|
||||||
|
if 'class' in existing_render_kw:
|
||||||
|
existing_classes = existing_render_kw['class']
|
||||||
|
if isinstance(existing_classes, list):
|
||||||
|
existing_classes += ' ordered-list-field'
|
||||||
|
new_render_kw['class'] = existing_classes
|
||||||
|
else:
|
||||||
|
# String classes samenvoegen
|
||||||
|
new_render_kw['class'] = f"{existing_classes} ordered-list-field"
|
||||||
|
else:
|
||||||
|
new_render_kw['class'] = 'ordered-list-field'
|
||||||
|
|
||||||
|
# Voeg alle bestaande attributen toe aan nieuwe render_kw
|
||||||
|
for key, value in existing_render_kw.items():
|
||||||
|
if key != 'class': # Klassen hebben we al verwerkt
|
||||||
|
new_render_kw[key] = value
|
||||||
|
|
||||||
|
current_app.logger.debug(f"final render_kw for ordered list field: {new_render_kw}")
|
||||||
|
|
||||||
|
# Update kwargs met de nieuwe gecombineerde render_kw
|
||||||
|
kwargs['render_kw'] = new_render_kw
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class DynamicFormBase(FlaskForm):
|
class DynamicFormBase(FlaskForm):
|
||||||
def __init__(self, formdata=None, *args, **kwargs):
|
def __init__(self, formdata=None, *args, **kwargs):
|
||||||
super(DynamicFormBase, self).__init__(*args, **kwargs)
|
super(DynamicFormBase, self).__init__(*args, **kwargs)
|
||||||
@@ -89,6 +137,8 @@ class DynamicFormBase(FlaskForm):
|
|||||||
validators_list.append(self._validate_tagging_fields_filter)
|
validators_list.append(self._validate_tagging_fields_filter)
|
||||||
elif field_type == 'dynamic_arguments':
|
elif field_type == 'dynamic_arguments':
|
||||||
validators_list.append(self._validate_dynamic_arguments)
|
validators_list.append(self._validate_dynamic_arguments)
|
||||||
|
elif field_type == 'ordered_list':
|
||||||
|
validators_list.append(self._validate_ordered_list)
|
||||||
|
|
||||||
return validators_list
|
return validators_list
|
||||||
|
|
||||||
@@ -227,10 +277,52 @@ class DynamicFormBase(FlaskForm):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValidationError(f"Invalid argument definition: {str(e)}")
|
raise ValidationError(f"Invalid argument definition: {str(e)}")
|
||||||
|
|
||||||
|
def _validate_ordered_list(self, form, field):
|
||||||
|
"""Validate the ordered list structure"""
|
||||||
|
if not field.data:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Parse JSON data
|
||||||
|
list_data = json.loads(field.data)
|
||||||
|
|
||||||
|
# Validate it's a list
|
||||||
|
if not isinstance(list_data, list):
|
||||||
|
raise ValidationError("Ordered list must be a list")
|
||||||
|
|
||||||
|
# Validate each item in the list is a dictionary
|
||||||
|
for i, item in enumerate(list_data):
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
raise ValidationError(f"Item {i} in ordered list must be an object")
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
raise ValidationError("Invalid JSON format")
|
||||||
|
except Exception as e:
|
||||||
|
raise ValidationError(f"Invalid ordered list: {str(e)}")
|
||||||
|
|
||||||
def add_dynamic_fields(self, collection_name, config, initial_data=None):
|
def add_dynamic_fields(self, collection_name, config, initial_data=None):
|
||||||
"""Add dynamic fields to the form based on the configuration."""
|
"""Add dynamic fields to the form based on the configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
collection_name: The name of the collection of fields to add
|
||||||
|
config: The full configuration object, which should contain the field definitions
|
||||||
|
for the collection_name and may also contain list_type definitions
|
||||||
|
initial_data: Optional initial data for the fields
|
||||||
|
"""
|
||||||
|
current_app.logger.debug(f"Adding dynamic fields for collection {collection_name} with config: {config}")
|
||||||
|
# Store the full configuration for later use in get_list_type_configs_js
|
||||||
|
if not hasattr(self, '_full_configs'):
|
||||||
|
self._full_configs = {}
|
||||||
|
self._full_configs[collection_name] = config
|
||||||
|
|
||||||
|
# Get the specific field configuration for this collection
|
||||||
|
field_config = config.get(collection_name, {})
|
||||||
|
if not field_config:
|
||||||
|
# Handle the case where config is already the specific field configuration
|
||||||
|
return
|
||||||
|
|
||||||
self.dynamic_fields[collection_name] = []
|
self.dynamic_fields[collection_name] = []
|
||||||
for field_name, field_def in config.items():
|
for field_name, field_def in field_config.items():
|
||||||
# Prefix the field name with the collection name
|
# Prefix the field name with the collection name
|
||||||
full_field_name = f"{collection_name}_{field_name}"
|
full_field_name = f"{collection_name}_{field_name}"
|
||||||
label = field_def.get('name', field_name)
|
label = field_def.get('name', field_name)
|
||||||
@@ -264,6 +356,12 @@ class DynamicFormBase(FlaskForm):
|
|||||||
field_class = ChunkingPatternsField
|
field_class = ChunkingPatternsField
|
||||||
extra_classes = ['monospace-text', 'pattern-input']
|
extra_classes = ['monospace-text', 'pattern-input']
|
||||||
field_kwargs = {}
|
field_kwargs = {}
|
||||||
|
elif field_type == 'ordered_list':
|
||||||
|
current_app.logger.debug(f"Adding ordered list field for {full_field_name}")
|
||||||
|
field_class = OrderedListField
|
||||||
|
extra_classes = ''
|
||||||
|
list_type = field_def.get('list_type', '')
|
||||||
|
field_kwargs = {'list_type': list_type}
|
||||||
else:
|
else:
|
||||||
extra_classes = ''
|
extra_classes = ''
|
||||||
field_class = {
|
field_class = {
|
||||||
@@ -275,6 +373,7 @@ class DynamicFormBase(FlaskForm):
|
|||||||
'text': TextAreaField,
|
'text': TextAreaField,
|
||||||
'date': DateField,
|
'date': DateField,
|
||||||
'file': FileField,
|
'file': FileField,
|
||||||
|
'color': ColorField,
|
||||||
}.get(field_type, StringField)
|
}.get(field_type, StringField)
|
||||||
field_kwargs = {}
|
field_kwargs = {}
|
||||||
|
|
||||||
@@ -289,12 +388,24 @@ class DynamicFormBase(FlaskForm):
|
|||||||
except (TypeError, ValueError) as e:
|
except (TypeError, ValueError) as e:
|
||||||
current_app.logger.error(f"Error converting initial data to JSON: {e}")
|
current_app.logger.error(f"Error converting initial data to JSON: {e}")
|
||||||
field_data = "{}"
|
field_data = "{}"
|
||||||
|
elif field_type == 'ordered_list' and isinstance(field_data, list):
|
||||||
|
try:
|
||||||
|
field_data = json.dumps(field_data)
|
||||||
|
except (TypeError, ValueError) as e:
|
||||||
|
current_app.logger.error(f"Error converting ordered list data to JSON: {e}")
|
||||||
|
field_data = "[]"
|
||||||
elif field_type == 'chunking_patterns':
|
elif field_type == 'chunking_patterns':
|
||||||
try:
|
try:
|
||||||
field_data = json_to_patterns(field_data)
|
field_data = json_to_patterns(field_data)
|
||||||
except (TypeError, ValueError) as e:
|
except (TypeError, ValueError) as e:
|
||||||
current_app.logger.error(f"Error converting initial data to a list of patterns: {e}")
|
current_app.logger.error(f"Error converting initial data to a list of patterns: {e}")
|
||||||
field_data = {}
|
field_data = {}
|
||||||
|
elif field_type == 'date' and isinstance(field_data, str):
|
||||||
|
try:
|
||||||
|
field_data = date.fromisoformat(field_data)
|
||||||
|
except ValueError:
|
||||||
|
current_app.logger.error(f"Error converting ISO date string '{field_data}' to date object")
|
||||||
|
field_data = None
|
||||||
elif default is not None:
|
elif default is not None:
|
||||||
field_data = default
|
field_data = default
|
||||||
|
|
||||||
@@ -305,6 +416,17 @@ class DynamicFormBase(FlaskForm):
|
|||||||
render_kw['data-bs-toggle'] = 'tooltip'
|
render_kw['data-bs-toggle'] = 'tooltip'
|
||||||
render_kw['data-bs-placement'] = 'right'
|
render_kw['data-bs-placement'] = 'right'
|
||||||
|
|
||||||
|
# Add special styling for color fields to make them more compact and visible
|
||||||
|
if field_type == 'color':
|
||||||
|
render_kw['style'] = 'width: 100px; height: 40px;'
|
||||||
|
if 'class' in render_kw:
|
||||||
|
render_kw['class'] = f"{render_kw['class']} color-field"
|
||||||
|
else:
|
||||||
|
render_kw['class'] = 'color-field'
|
||||||
|
|
||||||
|
|
||||||
|
current_app.logger.debug(f"render_kw for {full_field_name}: {render_kw}")
|
||||||
|
|
||||||
# Create the field
|
# Create the field
|
||||||
field_kwargs.update({
|
field_kwargs.update({
|
||||||
'label': label,
|
'label': label,
|
||||||
@@ -340,6 +462,73 @@ class DynamicFormBase(FlaskForm):
|
|||||||
# Return all fields that are not dynamic
|
# Return all fields that are not dynamic
|
||||||
return [field for name, field in self._fields.items() if name not in dynamic_field_names]
|
return [field for name, field in self._fields.items() if name not in dynamic_field_names]
|
||||||
|
|
||||||
|
def get_list_type_configs_js(self):
|
||||||
|
"""Generate JavaScript code for list type configurations used by ordered_list fields."""
|
||||||
|
from common.extensions import cache_manager
|
||||||
|
|
||||||
|
list_types = {}
|
||||||
|
|
||||||
|
# First check if we have any full configurations stored
|
||||||
|
if hasattr(self, '_full_configs'):
|
||||||
|
# Look for list types in the stored full configurations
|
||||||
|
for config_name, config in self._full_configs.items():
|
||||||
|
for key, value in config.items():
|
||||||
|
# Check if this is a list type definition (not a field definition)
|
||||||
|
if isinstance(value, dict) and all(isinstance(v, dict) for v in value.values()):
|
||||||
|
# This looks like a list type definition
|
||||||
|
list_types[key] = value
|
||||||
|
|
||||||
|
# Collect all list types used in ordered_list fields
|
||||||
|
for collection_name, field_names in self.dynamic_fields.items():
|
||||||
|
for full_field_name in field_names:
|
||||||
|
field = getattr(self, full_field_name)
|
||||||
|
if isinstance(field, OrderedListField):
|
||||||
|
list_type = field.render_kw.get('data-list-type')
|
||||||
|
if list_type and list_type not in list_types:
|
||||||
|
# First try to get from current_app.config
|
||||||
|
list_type_config = current_app.config.get('LIST_TYPES', {}).get(list_type)
|
||||||
|
if list_type_config:
|
||||||
|
list_types[list_type] = list_type_config
|
||||||
|
else:
|
||||||
|
# Try to find the list type in specialist configurations using the cache
|
||||||
|
try:
|
||||||
|
# Get all specialist types
|
||||||
|
specialist_types = cache_manager.specialists_types_cache.get_types()
|
||||||
|
|
||||||
|
# For each specialist type, check if it has the list type we're looking for
|
||||||
|
for specialist_type in specialist_types:
|
||||||
|
try:
|
||||||
|
# Get the latest version for this specialist type
|
||||||
|
latest_version = cache_manager.specialists_version_tree_cache.get_latest_version(specialist_type)
|
||||||
|
|
||||||
|
# Get the configuration for this specialist type and version
|
||||||
|
specialist_config = cache_manager.specialists_config_cache.get_config(specialist_type, latest_version)
|
||||||
|
|
||||||
|
# Check if this specialist has the list type we're looking for
|
||||||
|
if list_type in specialist_config:
|
||||||
|
list_types[list_type] = specialist_config[list_type]
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.debug(f"Error checking specialist {specialist_type}: {e}")
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Error retrieving specialist configurations: {e}")
|
||||||
|
|
||||||
|
# If no list types found, return empty script
|
||||||
|
if not list_types:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Generate JavaScript code
|
||||||
|
js_code = "<script>\n"
|
||||||
|
js_code += "window.listTypeConfigs = window.listTypeConfigs || {};\n"
|
||||||
|
|
||||||
|
for list_type, config in list_types.items():
|
||||||
|
js_code += f"window.listTypeConfigs['{list_type}'] = {json.dumps(config, indent=2)};\n"
|
||||||
|
|
||||||
|
js_code += "</script>\n"
|
||||||
|
|
||||||
|
return js_code
|
||||||
|
|
||||||
def get_dynamic_fields(self):
|
def get_dynamic_fields(self):
|
||||||
"""Return a dictionary of dynamic fields per collection."""
|
"""Return a dictionary of dynamic fields per collection."""
|
||||||
result = {}
|
result = {}
|
||||||
@@ -361,7 +550,7 @@ class DynamicFormBase(FlaskForm):
|
|||||||
if field.type == 'BooleanField':
|
if field.type == 'BooleanField':
|
||||||
data[original_field_name] = full_field_name in self.raw_formdata
|
data[original_field_name] = full_field_name in self.raw_formdata
|
||||||
current_app.logger.debug(f"Value for {original_field_name} is {data[original_field_name]}")
|
current_app.logger.debug(f"Value for {original_field_name} is {data[original_field_name]}")
|
||||||
elif isinstance(field, (TaggingFieldsField, TaggingFieldsFilterField, DynamicArgumentsField)) and field.data:
|
elif isinstance(field, (TaggingFieldsField, TaggingFieldsFilterField, DynamicArgumentsField, OrderedListField)) and field.data:
|
||||||
try:
|
try:
|
||||||
data[original_field_name] = json.loads(field.data)
|
data[original_field_name] = json.loads(field.data)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
@@ -372,6 +561,8 @@ class DynamicFormBase(FlaskForm):
|
|||||||
data[original_field_name] = patterns_to_json(field.data)
|
data[original_field_name] = patterns_to_json(field.data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"Error converting initial data to patterns: {e}")
|
current_app.logger.error(f"Error converting initial data to patterns: {e}")
|
||||||
|
elif isinstance(field, DateField):
|
||||||
|
data[original_field_name] = field.data.isoformat()
|
||||||
else:
|
else:
|
||||||
data[original_field_name] = field.data
|
data[original_field_name] = field.data
|
||||||
return data
|
return data
|
||||||
@@ -422,7 +613,7 @@ def validate_tagging_fields(form, field):
|
|||||||
raise ValidationError(f"Field {field_name} missing required 'type' property")
|
raise ValidationError(f"Field {field_name} missing required 'type' property")
|
||||||
|
|
||||||
# Validate type
|
# Validate type
|
||||||
if field_def['type'] not in ['string', 'integer', 'float', 'date', 'enum']:
|
if field_def['type'] not in ['string', 'integer', 'float', 'date', 'enum', 'color']:
|
||||||
raise ValidationError(f"Field {field_name} has invalid type: {field_def['type']}")
|
raise ValidationError(f"Field {field_name} has invalid type: {field_def['type']}")
|
||||||
|
|
||||||
# Validate enum fields have allowed_values
|
# Validate enum fields have allowed_values
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ from wtforms.validators import DataRequired, Length, Optional
|
|||||||
from wtforms_sqlalchemy.fields import QuerySelectMultipleField
|
from wtforms_sqlalchemy.fields import QuerySelectMultipleField
|
||||||
|
|
||||||
from common.models.document import Retriever
|
from common.models.document import Retriever
|
||||||
from common.models.interaction import EveAITool
|
from common.models.interaction import EveAITool, Specialist
|
||||||
from common.extensions import cache_manager
|
from common.extensions import cache_manager
|
||||||
|
from common.utils.form_assistants import validate_json
|
||||||
|
|
||||||
from .dynamic_form_base import DynamicFormBase
|
from .dynamic_form_base import DynamicFormBase
|
||||||
|
|
||||||
@@ -132,4 +133,46 @@ class ExecuteSpecialistForm(DynamicFormBase):
|
|||||||
description = TextAreaField('Specialist Description', validators=[Optional()], render_kw={'readonly': True})
|
description = TextAreaField('Specialist Description', validators=[Optional()], render_kw={'readonly': True})
|
||||||
|
|
||||||
|
|
||||||
|
class SpecialistMagicLinkForm(FlaskForm):
|
||||||
|
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
||||||
|
description = TextAreaField('Description', validators=[Optional()])
|
||||||
|
magic_link_code = StringField('Magic Link Code', validators=[DataRequired(), Length(max=55)], render_kw={'readonly': True})
|
||||||
|
specialist_id = SelectField('Specialist', validators=[DataRequired()])
|
||||||
|
valid_from = DateField('Valid From', id='form-control datepicker', validators=[Optional()])
|
||||||
|
valid_to = DateField('Valid To', id='form-control datepicker', validators=[Optional()])
|
||||||
|
|
||||||
|
# Metadata fields
|
||||||
|
user_metadata = TextAreaField('User Metadata', validators=[Optional(), validate_json])
|
||||||
|
system_metadata = TextAreaField('System Metadata', validators=[Optional(), validate_json])
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
specialists = Specialist.query.all()
|
||||||
|
# Dynamically populate the 'type' field using the constructor
|
||||||
|
self.specialist_id.choices = [(specialist.id, specialist.name) for specialist in specialists]
|
||||||
|
|
||||||
|
|
||||||
|
class EditSpecialistMagicLinkForm(DynamicFormBase):
|
||||||
|
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
||||||
|
description = TextAreaField('Description', validators=[Optional()])
|
||||||
|
magic_link_code = StringField('Magic Link Code', validators=[DataRequired(), Length(max=55)],
|
||||||
|
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})
|
||||||
|
valid_from = DateField('Valid From', id='form-control datepicker', validators=[Optional()])
|
||||||
|
valid_to = DateField('Valid To', id='form-control datepicker', validators=[Optional()])
|
||||||
|
|
||||||
|
# Metadata fields
|
||||||
|
user_metadata = TextAreaField('User Metadata', validators=[Optional(), validate_json])
|
||||||
|
system_metadata = TextAreaField('System Metadata', validators=[Optional(), validate_json])
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
specialist = Specialist.query.get(kwargs['specialist_id'])
|
||||||
|
if specialist:
|
||||||
|
self.specialist_name.data = specialist.name
|
||||||
|
else:
|
||||||
|
self.specialist_name.data = ''
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import ast
|
import ast
|
||||||
import json
|
import json
|
||||||
|
import uuid
|
||||||
from datetime import datetime as dt, timezone as tz
|
from datetime import datetime as dt, timezone as tz
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -13,9 +14,10 @@ from werkzeug.utils import secure_filename
|
|||||||
|
|
||||||
from common.models.document import Embedding, DocumentVersion, Retriever
|
from common.models.document import Embedding, DocumentVersion, Retriever
|
||||||
from common.models.interaction import (ChatSession, Interaction, InteractionEmbedding, Specialist, SpecialistRetriever,
|
from common.models.interaction import (ChatSession, Interaction, InteractionEmbedding, Specialist, SpecialistRetriever,
|
||||||
EveAIAgent, EveAITask, EveAITool, EveAIAssetVersion)
|
EveAIAgent, EveAITask, EveAITool, EveAIAssetVersion, SpecialistMagicLink)
|
||||||
|
|
||||||
from common.extensions import db, cache_manager
|
from common.extensions import db, cache_manager
|
||||||
|
from common.models.user import SpecialistMagicLinkTenant
|
||||||
from common.services.interaction.specialist_services import SpecialistServices
|
from common.services.interaction.specialist_services import SpecialistServices
|
||||||
from common.utils.asset_utils import create_asset_stack, add_asset_version_file
|
from common.utils.asset_utils import create_asset_stack, add_asset_version_file
|
||||||
from common.utils.execution_progress import ExecutionProgressTracker
|
from common.utils.execution_progress import ExecutionProgressTracker
|
||||||
@@ -24,12 +26,10 @@ from common.utils.model_logging_utils import set_logging_information, update_log
|
|||||||
from common.utils.middleware import mw_before_request
|
from common.utils.middleware import mw_before_request
|
||||||
from common.utils.nginx_utils import prefixed_url_for
|
from common.utils.nginx_utils import prefixed_url_for
|
||||||
from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro
|
from common.utils.view_assistants import form_validation_failed, prepare_table_for_macro
|
||||||
from common.utils.specialist_utils import initialize_specialist
|
|
||||||
|
|
||||||
from config.type_defs.specialist_types import SPECIALIST_TYPES
|
|
||||||
|
|
||||||
from .interaction_forms import (SpecialistForm, EditSpecialistForm, EditEveAIAgentForm, EditEveAITaskForm,
|
from .interaction_forms import (SpecialistForm, EditSpecialistForm, EditEveAIAgentForm, EditEveAITaskForm,
|
||||||
EditEveAIToolForm, AddEveAIAssetForm, EditEveAIAssetVersionForm, ExecuteSpecialistForm)
|
EditEveAIToolForm, AddEveAIAssetForm, EditEveAIAssetVersionForm, ExecuteSpecialistForm,
|
||||||
|
SpecialistMagicLinkForm, EditSpecialistMagicLinkForm)
|
||||||
|
|
||||||
interaction_bp = Blueprint('interaction_bp', __name__, url_prefix='/interaction')
|
interaction_bp = Blueprint('interaction_bp', __name__, url_prefix='/interaction')
|
||||||
|
|
||||||
@@ -184,7 +184,7 @@ def specialist():
|
|||||||
current_app.logger.info(f'Specialist {new_specialist.name} successfully added for tenant {tenant_id}!')
|
current_app.logger.info(f'Specialist {new_specialist.name} successfully added for tenant {tenant_id}!')
|
||||||
|
|
||||||
# Initialize the newly create specialist
|
# Initialize the newly create specialist
|
||||||
initialize_specialist(new_specialist.id, new_specialist.type, new_specialist.type_version)
|
SpecialistServices.initialize_specialist(new_specialist.id, new_specialist.type, new_specialist.type_version)
|
||||||
|
|
||||||
return redirect(prefixed_url_for('interaction_bp.edit_specialist', specialist_id=new_specialist.id))
|
return redirect(prefixed_url_for('interaction_bp.edit_specialist', specialist_id=new_specialist.id))
|
||||||
|
|
||||||
@@ -204,8 +204,7 @@ def edit_specialist(specialist_id):
|
|||||||
form = EditSpecialistForm(request.form, obj=specialist)
|
form = EditSpecialistForm(request.form, obj=specialist)
|
||||||
|
|
||||||
specialist_config = cache_manager.specialists_config_cache.get_config(specialist.type, specialist.type_version)
|
specialist_config = cache_manager.specialists_config_cache.get_config(specialist.type, specialist.type_version)
|
||||||
configuration_config = specialist_config.get('configuration')
|
form.add_dynamic_fields("configuration", specialist_config, specialist.configuration)
|
||||||
form.add_dynamic_fields("configuration", configuration_config, specialist.configuration)
|
|
||||||
|
|
||||||
agent_rows = prepare_table_for_macro(specialist.agents,
|
agent_rows = prepare_table_for_macro(specialist.agents,
|
||||||
[('id', ''), ('name', ''), ('type', ''), ('type_version', '')])
|
[('id', ''), ('name', ''), ('type', ''), ('type_version', '')])
|
||||||
@@ -521,8 +520,7 @@ def edit_asset_version(asset_version_id):
|
|||||||
form = EditEveAIAssetVersionForm(asset_version)
|
form = EditEveAIAssetVersionForm(asset_version)
|
||||||
asset_config = cache_manager.assets_config_cache.get_config(asset_version.asset.type,
|
asset_config = cache_manager.assets_config_cache.get_config(asset_version.asset.type,
|
||||||
asset_version.asset.type_version)
|
asset_version.asset.type_version)
|
||||||
configuration_config = asset_config.get('configuration')
|
form.add_dynamic_fields("configuration", asset_config, asset_version.configuration)
|
||||||
form.add_dynamic_fields("configuration", configuration_config, asset_version.configuration)
|
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
# Update the configuration dynamic fields
|
# Update the configuration dynamic fields
|
||||||
@@ -582,9 +580,8 @@ def execute_specialist(specialist_id):
|
|||||||
return redirect(prefixed_url_for('interaction_bp.specialists'))
|
return redirect(prefixed_url_for('interaction_bp.specialists'))
|
||||||
|
|
||||||
form = ExecuteSpecialistForm(request.form, obj=specialist)
|
form = ExecuteSpecialistForm(request.form, obj=specialist)
|
||||||
arguments_config = specialist_config.get('arguments', None)
|
if 'arguments' in specialist_config:
|
||||||
if arguments_config:
|
form.add_dynamic_fields('arguments', specialist_config)
|
||||||
form.add_dynamic_fields('arguments', arguments_config)
|
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
# We're only interested in gathering the dynamic arguments
|
# We're only interested in gathering the dynamic arguments
|
||||||
@@ -675,3 +672,119 @@ def session_interactions(chat_session_id):
|
|||||||
"""
|
"""
|
||||||
chat_session = ChatSession.query.get_or_404(chat_session_id)
|
chat_session = ChatSession.query.get_or_404(chat_session_id)
|
||||||
return session_interactions_by_session_id(chat_session.session_id)
|
return session_interactions_by_session_id(chat_session.session_id)
|
||||||
|
|
||||||
|
|
||||||
|
# Routes for SpecialistMagicLink Management -------------------------------------------------------
|
||||||
|
@interaction_bp.route('/specialist_magic_link', methods=['GET', 'POST'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
def specialist_magic_link():
|
||||||
|
form = SpecialistMagicLinkForm()
|
||||||
|
|
||||||
|
if request.method == 'GET':
|
||||||
|
magic_link_code = f"SPECIALIST_ML-{str(uuid.uuid4())}"
|
||||||
|
form.magic_link_code.data = magic_link_code
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
tenant_id = session.get('tenant').get('id')
|
||||||
|
try:
|
||||||
|
new_specialist_magic_link = SpecialistMagicLink()
|
||||||
|
|
||||||
|
# Populate fields individually instead of using populate_obj (gives problem with QueryMultipleSelectField)
|
||||||
|
form.populate_obj(new_specialist_magic_link)
|
||||||
|
|
||||||
|
set_logging_information(new_specialist_magic_link, dt.now(tz.utc))
|
||||||
|
|
||||||
|
# Create 'public' SpecialistMagicLinkTenant
|
||||||
|
new_spec_ml_tenant = SpecialistMagicLinkTenant()
|
||||||
|
new_spec_ml_tenant.magic_link_code = new_specialist_magic_link.magic_link_code
|
||||||
|
new_spec_ml_tenant.tenant_id = tenant_id
|
||||||
|
|
||||||
|
db.session.add(new_specialist_magic_link)
|
||||||
|
db.session.add(new_spec_ml_tenant)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
flash('Specialist Magic Link successfully added!', 'success')
|
||||||
|
current_app.logger.info(f'Specialist {new_specialist_magic_link.name} successfully added for '
|
||||||
|
f'tenant {tenant_id}!')
|
||||||
|
|
||||||
|
return redirect(prefixed_url_for('interaction_bp.edit_specialist_magic_link',
|
||||||
|
specialist_magic_link_id=new_specialist_magic_link.id))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error(f'Failed to add specialist magic link. Error: {str(e)}', exc_info=True)
|
||||||
|
flash(f'Failed to add specialist magic link. Error: {str(e)}', 'danger')
|
||||||
|
|
||||||
|
return render_template('interaction/specialist_magic_link.html', form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@interaction_bp.route('/specialist_magic_link/<int:specialist_magic_link_id>', methods=['GET', 'POST'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
def edit_specialist_magic_link(specialist_magic_link_id):
|
||||||
|
specialist_ml = SpecialistMagicLink.query.get_or_404(specialist_magic_link_id)
|
||||||
|
# We need to pass along the extra kwarg specialist_id, as this id is required to initialize the form
|
||||||
|
form = EditSpecialistMagicLinkForm(request.form, obj=specialist_ml, specialist_id=specialist_ml.specialist_id)
|
||||||
|
|
||||||
|
# Find the Specialist type and type_version to enable to retrieve the arguments
|
||||||
|
specialist = Specialist.query.get_or_404(specialist_ml.specialist_id)
|
||||||
|
specialist_config = cache_manager.specialists_config_cache.get_config(specialist.type, specialist.type_version)
|
||||||
|
|
||||||
|
form.add_dynamic_fields("arguments", specialist_config, specialist_ml.specialist_args)
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
# Update the basic fields
|
||||||
|
form.populate_obj(specialist_ml)
|
||||||
|
# Update the arguments dynamic fields
|
||||||
|
specialist_ml.specialist_args = form.get_dynamic_data("arguments")
|
||||||
|
|
||||||
|
# Update logging information
|
||||||
|
update_logging_information(specialist_ml, dt.now(tz.utc))
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.commit()
|
||||||
|
flash('Specialist Magic Link updated successfully!', 'success')
|
||||||
|
current_app.logger.info(f'Specialist Magic Link {specialist_ml.id} updated successfully')
|
||||||
|
return redirect(prefixed_url_for('interaction_bp.specialist_magic_links'))
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
db.session.rollback()
|
||||||
|
flash(f'Failed to update specialist Magic Link. Error: {str(e)}', 'danger')
|
||||||
|
current_app.logger.error(f'Failed to update specialist Magic Link {specialist_ml.id}. Error: {str(e)}')
|
||||||
|
else:
|
||||||
|
form_validation_failed(request, form)
|
||||||
|
|
||||||
|
return render_template('interaction/edit_specialist_magic_link.html', form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@interaction_bp.route('/specialist_magic_links', methods=['GET', 'POST'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
def specialist_magic_links():
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
per_page = request.args.get('per_page', 10, type=int)
|
||||||
|
|
||||||
|
query = SpecialistMagicLink.query.order_by(SpecialistMagicLink.id)
|
||||||
|
|
||||||
|
pagination = query.paginate(page=page, per_page=per_page)
|
||||||
|
the_specialist_magic_links = pagination.items
|
||||||
|
|
||||||
|
# prepare table data
|
||||||
|
rows = prepare_table_for_macro(the_specialist_magic_links, [('id', ''), ('name', ''), ('magic_link_code', ''),])
|
||||||
|
|
||||||
|
# Render the catalogs in a template
|
||||||
|
return render_template('interaction/specialist_magic_links.html', rows=rows, pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
|
@interaction_bp.route('/handle_specialist_magic_link_selection', methods=['POST'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
def handle_specialist_magic_link_selection():
|
||||||
|
action = request.form.get('action')
|
||||||
|
if action == 'create_specialist_magic_link':
|
||||||
|
return redirect(prefixed_url_for('interaction_bp.specialist_magic_link'))
|
||||||
|
|
||||||
|
specialist_ml_identification = request.form.get('selected_row')
|
||||||
|
specialist_ml_id = ast.literal_eval(specialist_ml_identification).get('value')
|
||||||
|
|
||||||
|
if action == "edit_specialist_magic_link":
|
||||||
|
return redirect(prefixed_url_for('interaction_bp.edit_specialist_magic_link',
|
||||||
|
specialist_magic_link_id=specialist_ml_id))
|
||||||
|
|
||||||
|
return redirect(prefixed_url_for('interaction_bp.specialists'))
|
||||||
|
|||||||
@@ -161,19 +161,19 @@ def edit_partner_service(partner_service_id):
|
|||||||
partner_service = PartnerService.query.get_or_404(partner_service_id)
|
partner_service = PartnerService.query.get_or_404(partner_service_id)
|
||||||
partner = session.get('partner', None)
|
partner = session.get('partner', None)
|
||||||
partner_id = session['partner']['id']
|
partner_id = session['partner']['id']
|
||||||
|
current_app.logger.debug(f"Request Type: {request.method}")
|
||||||
|
|
||||||
form = EditPartnerServiceForm(obj=partner_service)
|
form = EditPartnerServiceForm(obj=partner_service)
|
||||||
if request.method == 'GET':
|
|
||||||
partner_service_config = cache_manager.partner_services_config_cache.get_config(partner_service.type,
|
partner_service_config = cache_manager.partner_services_config_cache.get_config(partner_service.type,
|
||||||
partner_service.type_version)
|
partner_service.type_version)
|
||||||
configuration_config = partner_service_config.get('configuration')
|
configuration_config = partner_service_config.get('configuration')
|
||||||
current_app.logger.debug(f"Configuration config for {partner_service.type} {partner_service.type_version}: "
|
current_app.logger.debug(f"Configuration config for {partner_service.type} {partner_service.type_version}: "
|
||||||
f"{configuration_config}")
|
f"{configuration_config}")
|
||||||
form.add_dynamic_fields("configuration", configuration_config, partner_service.configuration)
|
form.add_dynamic_fields("configuration", partner_service_config, partner_service.configuration)
|
||||||
permissions_config = partner_service_config.get('permissions')
|
permissions_config = partner_service_config.get('permissions')
|
||||||
current_app.logger.debug(f"Permissions config for {partner_service.type} {partner_service.type_version}: "
|
current_app.logger.debug(f"Permissions config for {partner_service.type} {partner_service.type_version}: "
|
||||||
f"{permissions_config}")
|
f"{permissions_config}")
|
||||||
form.add_dynamic_fields("permissions", permissions_config, partner_service.permissions)
|
form.add_dynamic_fields("permissions", partner_service_config, partner_service.permissions)
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
current_app.logger.debug(f"Form returned: {form.data}")
|
current_app.logger.debug(f"Form returned: {form.data}")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from flask_security import current_user
|
|||||||
|
|
||||||
from common.services.user import UserServices
|
from common.services.user import UserServices
|
||||||
from config.type_defs.service_types import SERVICE_TYPES
|
from config.type_defs.service_types import SERVICE_TYPES
|
||||||
|
from eveai_app.views.dynamic_form_base import DynamicFormBase
|
||||||
|
|
||||||
|
|
||||||
class TenantForm(FlaskForm):
|
class TenantForm(FlaskForm):
|
||||||
@@ -36,7 +37,7 @@ class TenantForm(FlaskForm):
|
|||||||
# 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
|
||||||
self.timezone.choices = [(tz, tz) for tz in pytz.all_timezones]
|
self.timezone.choices = [(tz, tz) for tz in pytz.common_timezones]
|
||||||
# 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']]
|
||||||
# Show field only for Super Users with partner in session
|
# Show field only for Super Users with partner in session
|
||||||
@@ -131,4 +132,13 @@ class EditTenantProjectForm(FlaskForm):
|
|||||||
self.services.choices = [(key, value['description']) for key, value in SERVICE_TYPES.items()]
|
self.services.choices = [(key, value['description']) for key, value in SERVICE_TYPES.items()]
|
||||||
|
|
||||||
|
|
||||||
|
class TenantMakeForm(DynamicFormBase):
|
||||||
|
name = StringField('Name', validators=[DataRequired(), Length(max=50)])
|
||||||
|
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)])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ from flask_security import roles_accepted, current_user
|
|||||||
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
|
from sqlalchemy.exc import SQLAlchemyError, IntegrityError
|
||||||
import ast
|
import ast
|
||||||
|
|
||||||
from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, PartnerTenant
|
from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, PartnerTenant, TenantMake
|
||||||
from common.extensions import db, security, minio_client, simple_encryption
|
from common.extensions import db, security, minio_client, simple_encryption, cache_manager
|
||||||
|
from common.utils.dynamic_field_utils import create_default_config_from_type_config
|
||||||
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
|
TenantProjectForm, EditTenantProjectForm, TenantMakeForm
|
||||||
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
|
||||||
@@ -622,6 +623,108 @@ def delete_tenant_project(tenant_project_id):
|
|||||||
return redirect(prefixed_url_for('user_bp.tenant_projects'))
|
return redirect(prefixed_url_for('user_bp.tenant_projects'))
|
||||||
|
|
||||||
|
|
||||||
|
@user_bp.route('/tenant_make', methods=['GET', 'POST'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
def tenant_make():
|
||||||
|
form = TenantMakeForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
tenant_id = session['tenant']['id']
|
||||||
|
new_tenant_make = TenantMake()
|
||||||
|
form.populate_obj(new_tenant_make)
|
||||||
|
new_tenant_make.tenant_id = tenant_id
|
||||||
|
customisation_config = cache_manager.customisations_config_cache.get_config("CHAT_CLIENT_CUSTOMISATION")
|
||||||
|
new_tenant_make.chat_customisation_options = create_default_config_from_type_config(
|
||||||
|
customisation_config["configuration"])
|
||||||
|
form.add_dynamic_fields("configuration", customisation_config, new_tenant_make.chat_customisation_options)
|
||||||
|
set_logging_information(new_tenant_make, dt.now(tz.utc))
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.session.add(new_tenant_make)
|
||||||
|
db.session.commit()
|
||||||
|
flash('Tenant Make successfully added!', 'success')
|
||||||
|
current_app.logger.info(f'Tenant Make {new_tenant_make.name} successfully added for tenant {tenant_id}!')
|
||||||
|
# Enable step 2 of creation of retriever - add configuration of the retriever (dependent on type)
|
||||||
|
return redirect(prefixed_url_for('user_bp.tenant_makes', tenant_make_id=new_tenant_make.id))
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
db.session.rollback()
|
||||||
|
flash(f'Failed to add Tenant Make. Error: {e}', 'danger')
|
||||||
|
current_app.logger.error(f'Failed to add Tenant Make {new_tenant_make.name}'
|
||||||
|
f'for tenant {tenant_id}. Error: {str(e)}')
|
||||||
|
|
||||||
|
return render_template('user/tenant_make.html', form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@user_bp.route('/tenant_makes', methods=['GET', 'POST'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
def tenant_makes():
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
per_page = request.args.get('per_page', 10, type=int)
|
||||||
|
|
||||||
|
query = TenantMake.query.order_by(TenantMake.id)
|
||||||
|
|
||||||
|
pagination = query.paginate(page=page, per_page=per_page)
|
||||||
|
tenant_makes = pagination.items
|
||||||
|
|
||||||
|
# prepare table data
|
||||||
|
rows = prepare_table_for_macro(tenant_makes,
|
||||||
|
[('id', ''), ('name', ''), ('website', ''), ('active', '')])
|
||||||
|
|
||||||
|
# Render the tenant makes in a template
|
||||||
|
return render_template('user/tenant_makes.html', rows=rows, pagination=pagination)
|
||||||
|
|
||||||
|
|
||||||
|
@user_bp.route('/tenant_make/<int:tenant_make_id>', methods=['GET', 'POST'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
def edit_tenant_make(tenant_make_id):
|
||||||
|
"""Edit an existing tenant make configuration."""
|
||||||
|
# Get the tenant make or return 404
|
||||||
|
tenant_make = TenantMake.query.get_or_404(tenant_make_id)
|
||||||
|
|
||||||
|
# Create form instance with the tenant make
|
||||||
|
form = TenantMakeForm(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)
|
||||||
|
|
||||||
|
if form.validate_on_submit():
|
||||||
|
# Update basic fields
|
||||||
|
form.populate_obj(tenant_make)
|
||||||
|
tenant_make.chat_customisation_options = form.get_dynamic_data("configuration")
|
||||||
|
|
||||||
|
# Update logging information
|
||||||
|
update_logging_information(tenant_make, dt.now(tz.utc))
|
||||||
|
|
||||||
|
# Save changes to database
|
||||||
|
try:
|
||||||
|
db.session.add(tenant_make)
|
||||||
|
db.session.commit()
|
||||||
|
flash('Tenant Make updated successfully!', 'success')
|
||||||
|
current_app.logger.info(f'Tenant Make {tenant_make.id} updated successfully')
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
db.session.rollback()
|
||||||
|
flash(f'Failed to update tenant make. Error: {str(e)}', 'danger')
|
||||||
|
current_app.logger.error(f'Failed to update tenant make {tenant_make_id}. Error: {str(e)}')
|
||||||
|
return render_template('user/edit_tenant_make.html', form=form, tenant_make_id=tenant_make_id)
|
||||||
|
|
||||||
|
return redirect(prefixed_url_for('user_bp.tenant_makes'))
|
||||||
|
else:
|
||||||
|
form_validation_failed(request, form)
|
||||||
|
|
||||||
|
return render_template('user/edit_tenant_make.html', form=form, tenant_make_id=tenant_make_id)
|
||||||
|
|
||||||
|
|
||||||
|
@user_bp.route('/handle_tenant_make_selection', methods=['POST'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
def handle_tenant_make_selection():
|
||||||
|
action = request.form['action']
|
||||||
|
if action == 'create_tenant_make':
|
||||||
|
return redirect(prefixed_url_for('user_bp.tenant_make'))
|
||||||
|
tenant_make_identification = request.form.get('selected_row')
|
||||||
|
tenant_make_id = ast.literal_eval(tenant_make_identification).get('value')
|
||||||
|
|
||||||
|
if action == 'edit_tenant_make':
|
||||||
|
return redirect(prefixed_url_for('user_bp.edit_tenant_make', tenant_make_id=tenant_make_id))
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
106
eveai_chat_client/__init__.py
Normal file
106
eveai_chat_client/__init__.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from flask import Flask, jsonify
|
||||||
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
import logging.config
|
||||||
|
|
||||||
|
from common.extensions import (db, bootstrap, cors, csrf, session,
|
||||||
|
minio_client, simple_encryption, metrics, cache_manager, content_manager)
|
||||||
|
from common.models.user import Tenant, SpecialistMagicLinkTenant
|
||||||
|
from common.utils.startup_eveai import perform_startup_actions
|
||||||
|
from config.logging_config import LOGGING
|
||||||
|
from eveai_chat_client.utils.errors import register_error_handlers
|
||||||
|
from common.utils.celery_utils import make_celery, init_celery
|
||||||
|
from common.utils.template_filters import register_filters
|
||||||
|
from config.config import get_config
|
||||||
|
|
||||||
|
|
||||||
|
def create_app(config_file=None):
|
||||||
|
app = Flask(__name__, static_url_path='/static')
|
||||||
|
|
||||||
|
# Ensure all necessary headers are handled
|
||||||
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
|
||||||
|
|
||||||
|
environment = os.getenv('FLASK_ENV', 'development')
|
||||||
|
|
||||||
|
match environment:
|
||||||
|
case 'development':
|
||||||
|
app.config.from_object(get_config('dev'))
|
||||||
|
case 'production':
|
||||||
|
app.config.from_object(get_config('prod'))
|
||||||
|
case _:
|
||||||
|
app.config.from_object(get_config('dev'))
|
||||||
|
|
||||||
|
app.config['SESSION_KEY_PREFIX'] = 'eveai_chat_client_'
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(app.instance_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logging.config.dictConfig(LOGGING)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
logger.info("eveai_chat_client starting up")
|
||||||
|
|
||||||
|
# Register extensions
|
||||||
|
register_extensions(app)
|
||||||
|
|
||||||
|
# Configure CSRF protection
|
||||||
|
app.config['WTF_CSRF_CHECK_DEFAULT'] = False # Disable global CSRF protection
|
||||||
|
app.config['WTF_CSRF_TIME_LIMIT'] = None # Remove time limit for CSRF tokens
|
||||||
|
|
||||||
|
app.celery = make_celery(app.name, app.config)
|
||||||
|
init_celery(app.celery, app)
|
||||||
|
|
||||||
|
# Register Blueprints
|
||||||
|
register_blueprints(app)
|
||||||
|
|
||||||
|
# Register Error Handlers
|
||||||
|
register_error_handlers(app)
|
||||||
|
|
||||||
|
# Register Cache Handlers
|
||||||
|
register_cache_handlers(app)
|
||||||
|
|
||||||
|
# Debugging settings
|
||||||
|
if app.config['DEBUG'] is True:
|
||||||
|
app.logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
# Register template filters
|
||||||
|
register_filters(app)
|
||||||
|
|
||||||
|
# Perform startup actions such as cache invalidation
|
||||||
|
perform_startup_actions(app)
|
||||||
|
|
||||||
|
app.logger.info(f"EveAI Chat Client Started Successfully (PID: {os.getpid()})")
|
||||||
|
app.logger.info("-------------------------------------------------------------------------------------------------")
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def register_extensions(app):
|
||||||
|
db.init_app(app)
|
||||||
|
bootstrap.init_app(app)
|
||||||
|
csrf.init_app(app)
|
||||||
|
cors.init_app(app)
|
||||||
|
simple_encryption.init_app(app)
|
||||||
|
session.init_app(app)
|
||||||
|
minio_client.init_app(app)
|
||||||
|
cache_manager.init_app(app)
|
||||||
|
metrics.init_app(app)
|
||||||
|
content_manager.init_app(app)
|
||||||
|
|
||||||
|
|
||||||
|
def register_blueprints(app):
|
||||||
|
from .views.chat_views import chat_bp
|
||||||
|
app.register_blueprint(chat_bp)
|
||||||
|
from .views.error_views import error_bp
|
||||||
|
app.register_blueprint(error_bp)
|
||||||
|
from .views.healthz_views import healthz_bp
|
||||||
|
app.register_blueprint(healthz_bp)
|
||||||
|
|
||||||
|
|
||||||
|
def register_cache_handlers(app):
|
||||||
|
from common.utils.cache.config_cache import register_config_cache_handlers
|
||||||
|
register_config_cache_handlers(cache_manager)
|
||||||
|
from common.utils.cache.crewai_processed_config_cache import register_specialist_cache_handlers
|
||||||
|
register_specialist_cache_handlers(cache_manager)
|
||||||
244
eveai_chat_client/static/css/chat.css
Normal file
244
eveai_chat_client/static/css/chat.css
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
/* Base styles */
|
||||||
|
:root {
|
||||||
|
--primary-color: #007bff;
|
||||||
|
--secondary-color: #6c757d;
|
||||||
|
--background-color: #ffffff;
|
||||||
|
--text-color: #212529;
|
||||||
|
--sidebar-color: #f8f9fa;
|
||||||
|
--message-user-bg: #e9f5ff;
|
||||||
|
--message-bot-bg: #f8f9fa;
|
||||||
|
--border-radius: 8px;
|
||||||
|
--spacing: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--background-color);
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chat layout */
|
||||||
|
.chat-container {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 280px;
|
||||||
|
background-color: var(--sidebar-color);
|
||||||
|
border-right: 1px solid rgba(0,0,0,0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: var(--spacing);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
margin-bottom: var(--spacing);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-text {
|
||||||
|
margin-bottom: var(--spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-info {
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: var(--spacing);
|
||||||
|
border-top: 1px solid rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-member {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-member img {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-header {
|
||||||
|
padding: var(--spacing);
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: var(--spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin-bottom: var(--spacing);
|
||||||
|
max-width: 80%;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-message {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-message .message-content {
|
||||||
|
background-color: var(--message-user-bg);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bot-message .message-content {
|
||||||
|
background-color: var(--message-bot-bg);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input-container {
|
||||||
|
padding: var(--spacing);
|
||||||
|
border-top: 1px solid rgba(0,0,0,0.1);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chat-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.2);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
resize: none;
|
||||||
|
height: 60px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#send-button {
|
||||||
|
padding: 0 24px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading indicator */
|
||||||
|
.typing-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span {
|
||||||
|
height: 8px;
|
||||||
|
width: 8px;
|
||||||
|
background-color: rgba(0,0,0,0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 4px;
|
||||||
|
animation: typing 1.5s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes typing {
|
||||||
|
0% { transform: scale(1); }
|
||||||
|
50% { transform: scale(1.5); }
|
||||||
|
100% { transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error page styles */
|
||||||
|
.error-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-box {
|
||||||
|
background-color: white;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
margin: 1rem 0;
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-actions {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.chat-container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 30%;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
eveai_chat_client/templates/base.html
Normal file
31
eveai_chat_client/templates/base.html
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}EveAI Chat{% endblock %}</title>
|
||||||
|
|
||||||
|
<!-- CSS -->
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/chat.css') }}">
|
||||||
|
|
||||||
|
<!-- Custom theme colors from tenant settings -->
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary-color: {{ customization.primary_color|default('#007bff') }};
|
||||||
|
--secondary-color: {{ customization.secondary_color|default('#6c757d') }};
|
||||||
|
--background-color: {{ customization.background_color|default('#ffffff') }};
|
||||||
|
--text-color: {{ customization.text_color|default('#212529') }};
|
||||||
|
--sidebar-color: {{ customization.sidebar_color|default('#f8f9fa') }};
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
214
eveai_chat_client/templates/chat.html
Normal file
214
eveai_chat_client/templates/chat.html
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Chat{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="chat-container">
|
||||||
|
<!-- Left sidebar with customizable content -->
|
||||||
|
<div class="sidebar">
|
||||||
|
{% if customisation.logo_url %}
|
||||||
|
<div class="logo">
|
||||||
|
<img src="{{ customisation.logo_url }}" alt="{{ tenant.name }} Logo">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="sidebar-content">
|
||||||
|
{% if customisation.sidebar_text %}
|
||||||
|
<div class="sidebar-text">
|
||||||
|
{{ customisation.sidebar_text|safe }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if customisation.team_info %}
|
||||||
|
<div class="team-info">
|
||||||
|
<h3>Team</h3>
|
||||||
|
<div class="team-members">
|
||||||
|
{% for member in customisation.team_info %}
|
||||||
|
<div class="team-member">
|
||||||
|
{% if member.avatar %}
|
||||||
|
<img src="{{ member.avatar }}" alt="{{ member.name }}">
|
||||||
|
{% endif %}
|
||||||
|
<div class="member-info">
|
||||||
|
<h4>{{ member.name }}</h4>
|
||||||
|
<p>{{ member.role }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main chat area -->
|
||||||
|
<div class="chat-main">
|
||||||
|
<div class="chat-header">
|
||||||
|
<h1>{{ specialist.name }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-messages" id="chat-messages">
|
||||||
|
<!-- Messages will be added here dynamically -->
|
||||||
|
{% if customisation.welcome_message %}
|
||||||
|
<div class="message bot-message">
|
||||||
|
<div class="message-content">{{ customisation.welcome_message|safe }}</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="message bot-message">
|
||||||
|
<div class="message-content">Hello! How can I help you today?</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chat-input-container">
|
||||||
|
<textarea id="chat-input" placeholder="Type your message here..."></textarea>
|
||||||
|
<button id="send-button">Send</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// Store session information
|
||||||
|
const sessionInfo = {
|
||||||
|
tenantId: {{ tenant.id }},
|
||||||
|
specialistId: {{ specialist.id }},
|
||||||
|
chatSessionId: "{{ session.chat_session_id }}"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Chat functionality
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const chatInput = document.getElementById('chat-input');
|
||||||
|
const sendButton = document.getElementById('send-button');
|
||||||
|
const chatMessages = document.getElementById('chat-messages');
|
||||||
|
|
||||||
|
let currentTaskId = null;
|
||||||
|
let pollingInterval = null;
|
||||||
|
|
||||||
|
// Function to add a message to the chat
|
||||||
|
function addMessage(message, isUser = false) {
|
||||||
|
const messageDiv = document.createElement('div');
|
||||||
|
messageDiv.className = isUser ? 'message user-message' : 'message bot-message';
|
||||||
|
|
||||||
|
const contentDiv = document.createElement('div');
|
||||||
|
contentDiv.className = 'message-content';
|
||||||
|
contentDiv.innerHTML = message;
|
||||||
|
|
||||||
|
messageDiv.appendChild(contentDiv);
|
||||||
|
chatMessages.appendChild(messageDiv);
|
||||||
|
|
||||||
|
// Scroll to bottom
|
||||||
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to send a message
|
||||||
|
function sendMessage() {
|
||||||
|
const message = chatInput.value.trim();
|
||||||
|
if (!message) return;
|
||||||
|
|
||||||
|
// Add user message to chat
|
||||||
|
addMessage(message, true);
|
||||||
|
|
||||||
|
// Clear input
|
||||||
|
chatInput.value = '';
|
||||||
|
|
||||||
|
// Add loading indicator
|
||||||
|
const loadingDiv = document.createElement('div');
|
||||||
|
loadingDiv.className = 'message bot-message loading';
|
||||||
|
loadingDiv.innerHTML = '<div class="message-content"><div class="typing-indicator"><span></span><span></span><span></span></div></div>';
|
||||||
|
chatMessages.appendChild(loadingDiv);
|
||||||
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||||
|
|
||||||
|
// Send message to server
|
||||||
|
fetch('/api/send_message', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: message,
|
||||||
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'processing') {
|
||||||
|
currentTaskId = data.task_id;
|
||||||
|
|
||||||
|
// Start polling for results
|
||||||
|
if (pollingInterval) clearInterval(pollingInterval);
|
||||||
|
pollingInterval = setInterval(checkTaskStatus, 1000);
|
||||||
|
} else {
|
||||||
|
// Remove loading indicator
|
||||||
|
chatMessages.removeChild(loadingDiv);
|
||||||
|
|
||||||
|
// Show error if any
|
||||||
|
if (data.error) {
|
||||||
|
addMessage(`Error: ${data.error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// Remove loading indicator
|
||||||
|
chatMessages.removeChild(loadingDiv);
|
||||||
|
addMessage(`Error: ${error.message}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to check task status
|
||||||
|
function checkTaskStatus() {
|
||||||
|
if (!currentTaskId) return;
|
||||||
|
|
||||||
|
fetch(`/api/check_status?task_id=${currentTaskId}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
// Remove loading indicator
|
||||||
|
const loadingDiv = document.querySelector('.loading');
|
||||||
|
if (loadingDiv) chatMessages.removeChild(loadingDiv);
|
||||||
|
|
||||||
|
// Add bot response
|
||||||
|
addMessage(data.answer);
|
||||||
|
|
||||||
|
// Clear polling
|
||||||
|
clearInterval(pollingInterval);
|
||||||
|
currentTaskId = null;
|
||||||
|
} else if (data.status === 'error') {
|
||||||
|
// Remove loading indicator
|
||||||
|
const loadingDiv = document.querySelector('.loading');
|
||||||
|
if (loadingDiv) chatMessages.removeChild(loadingDiv);
|
||||||
|
|
||||||
|
// Show error
|
||||||
|
addMessage(`Error: ${data.message}`);
|
||||||
|
|
||||||
|
// Clear polling
|
||||||
|
clearInterval(pollingInterval);
|
||||||
|
currentTaskId = null;
|
||||||
|
}
|
||||||
|
// If status is 'pending', continue polling
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// Remove loading indicator
|
||||||
|
const loadingDiv = document.querySelector('.loading');
|
||||||
|
if (loadingDiv) chatMessages.removeChild(loadingDiv);
|
||||||
|
|
||||||
|
addMessage(`Error checking status: ${error.message}`);
|
||||||
|
|
||||||
|
// Clear polling
|
||||||
|
clearInterval(pollingInterval);
|
||||||
|
currentTaskId = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
sendButton.addEventListener('click', sendMessage);
|
||||||
|
|
||||||
|
chatInput.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
15
eveai_chat_client/templates/error.html
Normal file
15
eveai_chat_client/templates/error.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Error{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="error-container">
|
||||||
|
<div class="error-box">
|
||||||
|
<h1>Oops! Something went wrong</h1>
|
||||||
|
<p class="error-message">{{ message }}</p>
|
||||||
|
<div class="error-actions">
|
||||||
|
<a href="/" class="btn-primary">Go to Home</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
1
eveai_chat_client/utils/__init__.py
Normal file
1
eveai_chat_client/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Utils package for eveai_chat_client
|
||||||
85
eveai_chat_client/utils/errors.py
Normal file
85
eveai_chat_client/utils/errors.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import traceback
|
||||||
|
|
||||||
|
import jinja2
|
||||||
|
from flask import render_template, request, jsonify, redirect, current_app, flash
|
||||||
|
|
||||||
|
from common.utils.eveai_exceptions import EveAINoSessionTenant
|
||||||
|
|
||||||
|
|
||||||
|
def not_found_error(error):
|
||||||
|
current_app.logger.error(f"Not Found Error: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
|
return render_template('error.html', message="Page not found."), 404
|
||||||
|
|
||||||
|
|
||||||
|
def internal_server_error(error):
|
||||||
|
current_app.logger.error(f"Internal Server Error: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
|
return render_template('error.html', message="Internal server error."), 500
|
||||||
|
|
||||||
|
|
||||||
|
def not_authorised_error(error):
|
||||||
|
current_app.logger.error(f"Not Authorised Error: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
|
return render_template('error.html', message="Not authorized."), 401
|
||||||
|
|
||||||
|
|
||||||
|
def access_forbidden(error):
|
||||||
|
current_app.logger.error(f"Access Forbidden: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
|
return render_template('error.html', message="Access forbidden."), 403
|
||||||
|
|
||||||
|
|
||||||
|
def key_error_handler(error):
|
||||||
|
current_app.logger.error(f"Key Error: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
|
return render_template('error.html', message="An unexpected error occurred."), 500
|
||||||
|
|
||||||
|
|
||||||
|
def attribute_error_handler(error):
|
||||||
|
"""Handle AttributeError exceptions."""
|
||||||
|
error_msg = str(error)
|
||||||
|
current_app.logger.error(f"AttributeError: {error_msg}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
|
return render_template('error.html', message="An application error occurred."), 500
|
||||||
|
|
||||||
|
|
||||||
|
def no_tenant_selected_error(error):
|
||||||
|
"""Handle errors when no tenant is selected in the current session."""
|
||||||
|
current_app.logger.error(f"No Session Tenant Error: {error}")
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
|
return render_template('error.html', message="Session expired. Please use a valid magic link."), 401
|
||||||
|
|
||||||
|
|
||||||
|
def general_exception(e):
|
||||||
|
current_app.logger.error(f"Unhandled Exception: {e}", exc_info=True)
|
||||||
|
return render_template('error.html', message="An application error occurred."), 500
|
||||||
|
|
||||||
|
|
||||||
|
def template_not_found_error(error):
|
||||||
|
"""Handle Jinja2 TemplateNotFound exceptions."""
|
||||||
|
current_app.logger.error(f'Template not found: {error.name}')
|
||||||
|
current_app.logger.error(f'Search Paths: {current_app.jinja_loader.list_templates()}')
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
|
return render_template('error.html', message="Template not found."), 404
|
||||||
|
|
||||||
|
|
||||||
|
def template_syntax_error(error):
|
||||||
|
"""Handle Jinja2 TemplateSyntaxError exceptions."""
|
||||||
|
current_app.logger.error(f'Template syntax error: {error.message}')
|
||||||
|
current_app.logger.error(f'In template {error.filename}, line {error.lineno}')
|
||||||
|
current_app.logger.error(traceback.format_exc())
|
||||||
|
return render_template('error.html', message="Template syntax error."), 500
|
||||||
|
|
||||||
|
|
||||||
|
def register_error_handlers(app):
|
||||||
|
app.register_error_handler(404, not_found_error)
|
||||||
|
app.register_error_handler(500, internal_server_error)
|
||||||
|
app.register_error_handler(401, not_authorised_error)
|
||||||
|
app.register_error_handler(403, not_authorised_error)
|
||||||
|
app.register_error_handler(EveAINoSessionTenant, no_tenant_selected_error)
|
||||||
|
app.register_error_handler(KeyError, key_error_handler)
|
||||||
|
app.register_error_handler(AttributeError, attribute_error_handler)
|
||||||
|
app.register_error_handler(jinja2.TemplateNotFound, template_not_found_error)
|
||||||
|
app.register_error_handler(jinja2.TemplateSyntaxError, template_syntax_error)
|
||||||
|
app.register_error_handler(Exception, general_exception)
|
||||||
1
eveai_chat_client/views/__init__.py
Normal file
1
eveai_chat_client/views/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Views package for eveai_chat_client
|
||||||
170
eveai_chat_client/views/chat_views.py
Normal file
170
eveai_chat_client/views/chat_views.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import uuid
|
||||||
|
from flask import Blueprint, render_template, request, session, current_app, jsonify, abort
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
from common.extensions import db
|
||||||
|
from common.models.user import Tenant, SpecialistMagicLinkTenant
|
||||||
|
from common.models.interaction import SpecialistMagicLink, Specialist, ChatSession, Interaction
|
||||||
|
from common.services.interaction.specialist_services import SpecialistServices
|
||||||
|
from common.utils.database import Database
|
||||||
|
from common.utils.chat_utils import get_default_chat_customisation
|
||||||
|
|
||||||
|
chat_bp = Blueprint('chat', __name__)
|
||||||
|
|
||||||
|
@chat_bp.route('/')
|
||||||
|
def index():
|
||||||
|
customisation = get_default_chat_customisation()
|
||||||
|
return render_template('error.html', message="Please use a valid magic link to access the chat.",
|
||||||
|
customisation=customisation)
|
||||||
|
|
||||||
|
|
||||||
|
@chat_bp.route('/<magic_link_code>')
|
||||||
|
def chat(magic_link_code):
|
||||||
|
"""
|
||||||
|
Main chat interface accessed via magic link
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Find the tenant using the magic link code
|
||||||
|
magic_link_tenant = SpecialistMagicLinkTenant.query.filter_by(magic_link_code=magic_link_code).first()
|
||||||
|
|
||||||
|
if not magic_link_tenant:
|
||||||
|
current_app.logger.error(f"Invalid magic link code: {magic_link_code}")
|
||||||
|
return render_template('error.html', message="Invalid magic link code.")
|
||||||
|
|
||||||
|
tenant_id = magic_link_tenant.tenant_id
|
||||||
|
|
||||||
|
# Get tenant information
|
||||||
|
tenant = Tenant.query.get(tenant_id)
|
||||||
|
if not tenant:
|
||||||
|
current_app.logger.error(f"Tenant not found for ID: {tenant_id}")
|
||||||
|
return render_template('error.html', message="Tenant not found.")
|
||||||
|
|
||||||
|
# Switch to tenant schema
|
||||||
|
Database(tenant_id).switch_schema()
|
||||||
|
|
||||||
|
# Get specialist magic link details from tenant schema
|
||||||
|
specialist_ml = SpecialistMagicLink.query.filter_by(magic_link_code=magic_link_code).first()
|
||||||
|
if not specialist_ml:
|
||||||
|
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.")
|
||||||
|
|
||||||
|
# Get specialist details
|
||||||
|
specialist = Specialist.query.get(specialist_ml.specialist_id)
|
||||||
|
if not specialist:
|
||||||
|
current_app.logger.error(f"Specialist not found: {specialist_ml.specialist_id}")
|
||||||
|
return render_template('error.html', message="Specialist not found.")
|
||||||
|
|
||||||
|
# Store necessary information in session
|
||||||
|
session['tenant_id'] = tenant_id
|
||||||
|
session['specialist_id'] = specialist_ml.specialist_id
|
||||||
|
session['specialist_args'] = specialist_ml.specialist_args or {}
|
||||||
|
session['magic_link_code'] = magic_link_code
|
||||||
|
|
||||||
|
# Get customisation options with defaults
|
||||||
|
customisation = get_default_chat_customisation(tenant.chat_customisation_options)
|
||||||
|
|
||||||
|
# Start a new chat session
|
||||||
|
session['chat_session_id'] = SpecialistServices.start_session()
|
||||||
|
|
||||||
|
return render_template('chat.html',
|
||||||
|
tenant=tenant,
|
||||||
|
specialist=specialist,
|
||||||
|
customisation=customisation)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Error in chat view: {str(e)}", exc_info=True)
|
||||||
|
return render_template('error.html', message="An error occurred while setting up the chat.")
|
||||||
|
|
||||||
|
@chat_bp.route('/api/send_message', methods=['POST'])
|
||||||
|
def send_message():
|
||||||
|
"""
|
||||||
|
API endpoint to send a message to the specialist
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
message = data.get('message')
|
||||||
|
|
||||||
|
if not message:
|
||||||
|
return jsonify({'error': 'No message provided'}), 400
|
||||||
|
|
||||||
|
tenant_id = session.get('tenant_id')
|
||||||
|
specialist_id = session.get('specialist_id')
|
||||||
|
chat_session_id = session.get('chat_session_id')
|
||||||
|
specialist_args = session.get('specialist_args', {})
|
||||||
|
|
||||||
|
if not all([tenant_id, specialist_id, chat_session_id]):
|
||||||
|
return jsonify({'error': 'Session expired or invalid'}), 400
|
||||||
|
|
||||||
|
# Switch to tenant schema
|
||||||
|
Database(tenant_id).switch_schema()
|
||||||
|
|
||||||
|
# Add user message to specialist arguments
|
||||||
|
specialist_args['user_message'] = message
|
||||||
|
|
||||||
|
# Execute specialist
|
||||||
|
result = SpecialistServices.execute_specialist(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
specialist_id=specialist_id,
|
||||||
|
specialist_arguments=specialist_args,
|
||||||
|
session_id=chat_session_id,
|
||||||
|
user_timezone=data.get('timezone', 'UTC')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store the task ID for polling
|
||||||
|
session['current_task_id'] = result['task_id']
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'processing',
|
||||||
|
'task_id': result['task_id']
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Error sending message: {str(e)}", exc_info=True)
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@chat_bp.route('/api/check_status', methods=['GET'])
|
||||||
|
def check_status():
|
||||||
|
"""
|
||||||
|
API endpoint to check the status of a task
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
task_id = request.args.get('task_id') or session.get('current_task_id')
|
||||||
|
|
||||||
|
if not task_id:
|
||||||
|
return jsonify({'error': 'No task ID provided'}), 400
|
||||||
|
|
||||||
|
tenant_id = session.get('tenant_id')
|
||||||
|
if not tenant_id:
|
||||||
|
return jsonify({'error': 'Session expired or invalid'}), 400
|
||||||
|
|
||||||
|
# Switch to tenant schema
|
||||||
|
Database(tenant_id).switch_schema()
|
||||||
|
|
||||||
|
# Check task status using Celery
|
||||||
|
task_result = current_app.celery.AsyncResult(task_id)
|
||||||
|
|
||||||
|
if task_result.state == 'PENDING':
|
||||||
|
return jsonify({'status': 'pending'})
|
||||||
|
elif task_result.state == 'SUCCESS':
|
||||||
|
result = task_result.result
|
||||||
|
|
||||||
|
# Format the response
|
||||||
|
specialist_result = result.get('result', {})
|
||||||
|
response = {
|
||||||
|
'status': 'success',
|
||||||
|
'answer': specialist_result.get('answer', ''),
|
||||||
|
'citations': specialist_result.get('citations', []),
|
||||||
|
'insufficient_info': specialist_result.get('insufficient_info', False),
|
||||||
|
'interaction_id': result.get('interaction_id')
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify(response)
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
'status': 'error',
|
||||||
|
'message': str(task_result.info)
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Error checking status: {str(e)}", exc_info=True)
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
24
eveai_chat_client/views/error_views.py
Normal file
24
eveai_chat_client/views/error_views.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from flask import Blueprint, render_template
|
||||||
|
|
||||||
|
error_bp = Blueprint('error', __name__)
|
||||||
|
|
||||||
|
@error_bp.route('/error')
|
||||||
|
def error_page():
|
||||||
|
"""
|
||||||
|
Generic error page
|
||||||
|
"""
|
||||||
|
return render_template('error.html', message="An error occurred.")
|
||||||
|
|
||||||
|
@error_bp.app_errorhandler(404)
|
||||||
|
def page_not_found(e):
|
||||||
|
"""
|
||||||
|
Handle 404 errors
|
||||||
|
"""
|
||||||
|
return render_template('error.html', message="Page not found."), 404
|
||||||
|
|
||||||
|
@error_bp.app_errorhandler(500)
|
||||||
|
def internal_server_error(e):
|
||||||
|
"""
|
||||||
|
Handle 500 errors
|
||||||
|
"""
|
||||||
|
return render_template('error.html', message="Internal server error."), 500
|
||||||
17
eveai_chat_client/views/healthz_views.py
Normal file
17
eveai_chat_client/views/healthz_views.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from flask import Blueprint, jsonify
|
||||||
|
|
||||||
|
healthz_bp = Blueprint('healthz', __name__)
|
||||||
|
|
||||||
|
@healthz_bp.route('/healthz/ready')
|
||||||
|
def ready():
|
||||||
|
"""
|
||||||
|
Health check endpoint for readiness probe
|
||||||
|
"""
|
||||||
|
return jsonify({"status": "ok"})
|
||||||
|
|
||||||
|
@healthz_bp.route('/healthz/live')
|
||||||
|
def live():
|
||||||
|
"""
|
||||||
|
Health check endpoint for liveness probe
|
||||||
|
"""
|
||||||
|
return jsonify({"status": "ok"})
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from os import wait
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from crewai.flow.flow import start, listen, and_
|
||||||
|
from flask import current_app
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
from common.extensions import db
|
||||||
|
from common.models.user import Tenant
|
||||||
|
from common.models.interaction import Specialist
|
||||||
|
from eveai_chat_workers.outputs.globals.basic_types.list_item import ListItem
|
||||||
|
from eveai_chat_workers.specialists.crewai_base_specialist import CrewAIBaseSpecialistExecutor
|
||||||
|
from eveai_chat_workers.specialists.specialist_typing import SpecialistResult, SpecialistArguments
|
||||||
|
from eveai_chat_workers.outputs.traicie.competencies.competencies_v1_1 import Competencies
|
||||||
|
from eveai_chat_workers.specialists.crewai_base_classes import EveAICrewAICrew, EveAICrewAIFlow, EveAIFlowState
|
||||||
|
from common.services.interaction.specialist_services import SpecialistServices
|
||||||
|
|
||||||
|
|
||||||
|
class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
|
||||||
|
"""
|
||||||
|
type: TRAICIE_ROLE_DEFINITION_SPECIALIST
|
||||||
|
type_version: 1.0
|
||||||
|
Traicie Role Definition Specialist Executor class
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, tenant_id, specialist_id, session_id, task_id, **kwargs):
|
||||||
|
self.role_definition_crew = None
|
||||||
|
|
||||||
|
super().__init__(tenant_id, specialist_id, session_id, task_id)
|
||||||
|
|
||||||
|
# Load the Tenant & set language
|
||||||
|
self.tenant = Tenant.query.get_or_404(tenant_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self) -> str:
|
||||||
|
return "TRAICIE_ROLE_DEFINITION_SPECIALIST"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type_version(self) -> str:
|
||||||
|
return "1.2"
|
||||||
|
|
||||||
|
def _config_task_agents(self):
|
||||||
|
self._add_task_agent("traicie_get_competencies_task", "traicie_hr_bp_agent")
|
||||||
|
|
||||||
|
def _config_pydantic_outputs(self):
|
||||||
|
self._add_pydantic_output("traicie_get_competencies_task", Competencies, "competencies")
|
||||||
|
|
||||||
|
def _instantiate_specialist(self):
|
||||||
|
verbose = self.tuning
|
||||||
|
|
||||||
|
role_definition_agents = [self.traicie_hr_bp_agent]
|
||||||
|
role_definition_tasks = [self.traicie_get_competencies_task]
|
||||||
|
self.role_definition_crew = EveAICrewAICrew(
|
||||||
|
self,
|
||||||
|
"Role Definition Crew",
|
||||||
|
agents=role_definition_agents,
|
||||||
|
tasks=role_definition_tasks,
|
||||||
|
verbose=verbose,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.flow = RoleDefinitionFlow(
|
||||||
|
self,
|
||||||
|
self.role_definition_crew
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult:
|
||||||
|
self.log_tuning("Traicie Role Definition Specialist execution started", {})
|
||||||
|
|
||||||
|
flow_inputs = {
|
||||||
|
"vacancy_text": arguments.vacancy_text,
|
||||||
|
"role_name": arguments.role_name,
|
||||||
|
'role_reference': arguments.role_reference,
|
||||||
|
}
|
||||||
|
|
||||||
|
flow_results = self.flow.kickoff(inputs=flow_inputs)
|
||||||
|
|
||||||
|
flow_state = self.flow.state
|
||||||
|
|
||||||
|
results = RoleDefinitionSpecialistResult.create_for_type(self.type, self.type_version)
|
||||||
|
if flow_state.competencies:
|
||||||
|
results.competencies = flow_state.competencies
|
||||||
|
|
||||||
|
self.create_selection_specialist(arguments, flow_state.competencies)
|
||||||
|
|
||||||
|
self.log_tuning(f"Traicie Role Definition Specialist execution ended", {"Results": results.model_dump()})
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def create_selection_specialist(self, arguments: SpecialistArguments, competencies: List[ListItem]):
|
||||||
|
"""This method creates a new TRAICIE_SELECTION_SPECIALIST specialist with the given competencies."""
|
||||||
|
current_app.logger.info(f"Creating selection with arguments: {arguments.model_dump()}")
|
||||||
|
selection_comptencies = []
|
||||||
|
for competency in competencies:
|
||||||
|
selection_competency = {
|
||||||
|
"title": competency.title,
|
||||||
|
"description": competency.description,
|
||||||
|
"assess": True,
|
||||||
|
"is_knockout": False,
|
||||||
|
}
|
||||||
|
selection_comptencies.append(selection_competency)
|
||||||
|
|
||||||
|
selection_config = {
|
||||||
|
"name": arguments.specialist_name,
|
||||||
|
"competencies": selection_comptencies,
|
||||||
|
"tone_of_voice": "Professional & Neutral",
|
||||||
|
"language_level": "Standard",
|
||||||
|
"role_reference": arguments.role_reference,
|
||||||
|
}
|
||||||
|
name = arguments.role_name
|
||||||
|
if len(name) > 50:
|
||||||
|
name = name[:47] + "..."
|
||||||
|
|
||||||
|
new_specialist = Specialist(
|
||||||
|
name=name,
|
||||||
|
description=f"Specialist for {arguments.role_name} role",
|
||||||
|
type="TRAICIE_SELECTION_SPECIALIST",
|
||||||
|
type_version="1.0",
|
||||||
|
tuning=False,
|
||||||
|
configuration=selection_config,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
db.session.add(new_specialist)
|
||||||
|
db.session.commit()
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error(f"Error creating selection specialist: {str(e)}")
|
||||||
|
raise e
|
||||||
|
|
||||||
|
SpecialistServices.initialize_specialist(new_specialist.id, "TRAICIE_SELECTION_SPECIALIST", "1.0")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RoleDefinitionSpecialistInput(BaseModel):
|
||||||
|
role_name: str = Field(..., alias="role_name")
|
||||||
|
role_reference: Optional[str] = Field(..., alias="role_reference")
|
||||||
|
vacancy_text: Optional[str] = Field(None, alias="vacancy_text")
|
||||||
|
|
||||||
|
|
||||||
|
class RoleDefinitionSpecialistResult(SpecialistResult):
|
||||||
|
competencies: Optional[List[ListItem]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class RoleDefFlowState(EveAIFlowState):
|
||||||
|
"""Flow state for Traicie Role Definition specialist that automatically updates from task outputs"""
|
||||||
|
input: Optional[RoleDefinitionSpecialistInput] = None
|
||||||
|
competencies: Optional[List[ListItem]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class RoleDefinitionFlow(EveAICrewAIFlow[RoleDefFlowState]):
|
||||||
|
def __init__(self,
|
||||||
|
specialist_executor: CrewAIBaseSpecialistExecutor,
|
||||||
|
role_definitiion_crew: EveAICrewAICrew,
|
||||||
|
**kwargs):
|
||||||
|
super().__init__(specialist_executor, "Traicie Role Definition Specialist Flow", **kwargs)
|
||||||
|
self.specialist_executor = specialist_executor
|
||||||
|
self.role_definition_crew = role_definitiion_crew
|
||||||
|
self.exception_raised = False
|
||||||
|
|
||||||
|
@start()
|
||||||
|
def process_inputs(self):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@listen(process_inputs)
|
||||||
|
async def execute_role_definition (self):
|
||||||
|
inputs = self.state.input.model_dump()
|
||||||
|
try:
|
||||||
|
current_app.logger.debug("In execute_role_definition")
|
||||||
|
crew_output = await self.role_definition_crew.kickoff_async(inputs=inputs)
|
||||||
|
# Unfortunately, crew_output will only contain the output of the latest task.
|
||||||
|
# As we will only take into account the flow state, we need to ensure both competencies and criteria
|
||||||
|
# are copies to the flow state.
|
||||||
|
update = {}
|
||||||
|
for task in self.role_definition_crew.tasks:
|
||||||
|
current_app.logger.debug(f"Task {task.name} output:\n{task.output}")
|
||||||
|
if task.name == "traicie_get_competencies_task":
|
||||||
|
# update["competencies"] = task.output.pydantic.competencies
|
||||||
|
self.state.competencies = task.output.pydantic.competencies
|
||||||
|
# crew_output.pydantic = crew_output.pydantic.model_copy(update=update)
|
||||||
|
current_app.logger.debug(f"State after execute_role_definition: {self.state}")
|
||||||
|
current_app.logger.debug(f"State dump after execute_role_definition: {self.state.model_dump()}")
|
||||||
|
return crew_output
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"CREW execute_role_definition Kickoff Error: {str(e)}")
|
||||||
|
self.exception_raised = True
|
||||||
|
raise e
|
||||||
|
|
||||||
|
async def kickoff_async(self, inputs=None):
|
||||||
|
current_app.logger.debug(f"Async kickoff {self.name}")
|
||||||
|
current_app.logger.debug(f"Inputs: {inputs}")
|
||||||
|
self.state.input = RoleDefinitionSpecialistInput.model_validate(inputs)
|
||||||
|
current_app.logger.debug(f"State: {self.state}")
|
||||||
|
result = await super().kickoff_async(inputs)
|
||||||
|
return self.state
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from os import wait
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from crewai.flow.flow import start, listen, and_
|
||||||
|
from flask import current_app
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
|
from common.extensions import db
|
||||||
|
from common.models.user import Tenant
|
||||||
|
from common.models.interaction import Specialist
|
||||||
|
from eveai_chat_workers.outputs.globals.basic_types.list_item import ListItem
|
||||||
|
from eveai_chat_workers.specialists.crewai_base_specialist import CrewAIBaseSpecialistExecutor
|
||||||
|
from eveai_chat_workers.specialists.specialist_typing import SpecialistResult, SpecialistArguments
|
||||||
|
from eveai_chat_workers.outputs.traicie.competencies.competencies_v1_1 import Competencies
|
||||||
|
from eveai_chat_workers.specialists.crewai_base_classes import EveAICrewAICrew, EveAICrewAIFlow, EveAIFlowState
|
||||||
|
from common.services.interaction.specialist_services import SpecialistServices
|
||||||
|
|
||||||
|
|
||||||
|
class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
|
||||||
|
"""
|
||||||
|
type: TRAICIE_SELECTION_SPECIALIST
|
||||||
|
type_version: 1.0
|
||||||
|
Traicie Selection Specialist Executor class
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, tenant_id, specialist_id, session_id, task_id, **kwargs):
|
||||||
|
self.role_definition_crew = None
|
||||||
|
|
||||||
|
super().__init__(tenant_id, specialist_id, session_id, task_id)
|
||||||
|
|
||||||
|
# Load the Tenant & set language
|
||||||
|
self.tenant = Tenant.query.get_or_404(tenant_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self) -> str:
|
||||||
|
return "TRAICIE_SELECTION_SPECIALIST"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type_version(self) -> str:
|
||||||
|
return "1.0"
|
||||||
|
|
||||||
|
def _config_task_agents(self):
|
||||||
|
self._add_task_agent("traicie_get_competencies_task", "traicie_hr_bp_agent")
|
||||||
|
|
||||||
|
def _config_pydantic_outputs(self):
|
||||||
|
self._add_pydantic_output("traicie_get_competencies_task", Competencies, "competencies")
|
||||||
|
|
||||||
|
def _instantiate_specialist(self):
|
||||||
|
verbose = self.tuning
|
||||||
|
|
||||||
|
role_definition_agents = [self.traicie_hr_bp_agent]
|
||||||
|
role_definition_tasks = [self.traicie_get_competencies_task]
|
||||||
|
self.role_definition_crew = EveAICrewAICrew(
|
||||||
|
self,
|
||||||
|
"Role Definition Crew",
|
||||||
|
agents=role_definition_agents,
|
||||||
|
tasks=role_definition_tasks,
|
||||||
|
verbose=verbose,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.flow = RoleDefinitionFlow(
|
||||||
|
self,
|
||||||
|
self.role_definition_crew
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult:
|
||||||
|
self.log_tuning("Traicie Role Definition Specialist execution started", {})
|
||||||
|
|
||||||
|
flow_inputs = {
|
||||||
|
"vacancy_text": arguments.vacancy_text,
|
||||||
|
"role_name": arguments.role_name,
|
||||||
|
'role_reference': arguments.role_reference,
|
||||||
|
}
|
||||||
|
|
||||||
|
flow_results = self.flow.kickoff(inputs=flow_inputs)
|
||||||
|
|
||||||
|
flow_state = self.flow.state
|
||||||
|
|
||||||
|
results = RoleDefinitionSpecialistResult.create_for_type(self.type, self.type_version)
|
||||||
|
if flow_state.competencies:
|
||||||
|
results.competencies = flow_state.competencies
|
||||||
|
|
||||||
|
self.create_selection_specialist(arguments, flow_state.competencies)
|
||||||
|
|
||||||
|
self.log_tuning(f"Traicie Role Definition Specialist execution ended", {"Results": results.model_dump()})
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def create_selection_specialist(self, arguments: SpecialistArguments, competencies: List[ListItem]):
|
||||||
|
"""This method creates a new TRAICIE_SELECTION_SPECIALIST specialist with the given competencies."""
|
||||||
|
current_app.logger.info(f"Creating selection with arguments: {arguments.model_dump()}")
|
||||||
|
selection_comptencies = []
|
||||||
|
for competency in competencies:
|
||||||
|
selection_competency = {
|
||||||
|
"title": competency.title,
|
||||||
|
"description": competency.description,
|
||||||
|
"assess": True,
|
||||||
|
"is_knockout": False,
|
||||||
|
}
|
||||||
|
selection_comptencies.append(selection_competency)
|
||||||
|
|
||||||
|
selection_config = {
|
||||||
|
"name": arguments.specialist_name,
|
||||||
|
"competencies": selection_comptencies,
|
||||||
|
"tone_of_voice": "Professional & Neutral",
|
||||||
|
"language_level": "Standard",
|
||||||
|
"role_reference": arguments.role_reference,
|
||||||
|
}
|
||||||
|
name = arguments.role_name
|
||||||
|
if len(name) > 50:
|
||||||
|
name = name[:47] + "..."
|
||||||
|
|
||||||
|
new_specialist = Specialist(
|
||||||
|
name=name,
|
||||||
|
description=f"Specialist for {arguments.role_name} role",
|
||||||
|
type="TRAICIE_SELECTION_SPECIALIST",
|
||||||
|
type_version="1.0",
|
||||||
|
tuning=False,
|
||||||
|
configuration=selection_config,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
db.session.add(new_specialist)
|
||||||
|
db.session.commit()
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error(f"Error creating selection specialist: {str(e)}")
|
||||||
|
raise e
|
||||||
|
|
||||||
|
SpecialistServices.initialize_specialist(new_specialist.id, "TRAICIE_SELECTION_SPECIALIST", "1.0")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RoleDefinitionSpecialistInput(BaseModel):
|
||||||
|
role_name: str = Field(..., alias="role_name")
|
||||||
|
role_reference: Optional[str] = Field(..., alias="role_reference")
|
||||||
|
vacancy_text: Optional[str] = Field(None, alias="vacancy_text")
|
||||||
|
|
||||||
|
|
||||||
|
class RoleDefinitionSpecialistResult(SpecialistResult):
|
||||||
|
competencies: Optional[List[ListItem]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class RoleDefFlowState(EveAIFlowState):
|
||||||
|
"""Flow state for Traicie Role Definition specialist that automatically updates from task outputs"""
|
||||||
|
input: Optional[RoleDefinitionSpecialistInput] = None
|
||||||
|
competencies: Optional[List[ListItem]] = None
|
||||||
|
|
||||||
|
|
||||||
|
class RoleDefinitionFlow(EveAICrewAIFlow[RoleDefFlowState]):
|
||||||
|
def __init__(self,
|
||||||
|
specialist_executor: CrewAIBaseSpecialistExecutor,
|
||||||
|
role_definitiion_crew: EveAICrewAICrew,
|
||||||
|
**kwargs):
|
||||||
|
super().__init__(specialist_executor, "Traicie Role Definition Specialist Flow", **kwargs)
|
||||||
|
self.specialist_executor = specialist_executor
|
||||||
|
self.role_definition_crew = role_definitiion_crew
|
||||||
|
self.exception_raised = False
|
||||||
|
|
||||||
|
@start()
|
||||||
|
def process_inputs(self):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@listen(process_inputs)
|
||||||
|
async def execute_role_definition (self):
|
||||||
|
inputs = self.state.input.model_dump()
|
||||||
|
try:
|
||||||
|
current_app.logger.debug("In execute_role_definition")
|
||||||
|
crew_output = await self.role_definition_crew.kickoff_async(inputs=inputs)
|
||||||
|
# Unfortunately, crew_output will only contain the output of the latest task.
|
||||||
|
# As we will only take into account the flow state, we need to ensure both competencies and criteria
|
||||||
|
# are copies to the flow state.
|
||||||
|
update = {}
|
||||||
|
for task in self.role_definition_crew.tasks:
|
||||||
|
current_app.logger.debug(f"Task {task.name} output:\n{task.output}")
|
||||||
|
if task.name == "traicie_get_competencies_task":
|
||||||
|
# update["competencies"] = task.output.pydantic.competencies
|
||||||
|
self.state.competencies = task.output.pydantic.competencies
|
||||||
|
# crew_output.pydantic = crew_output.pydantic.model_copy(update=update)
|
||||||
|
current_app.logger.debug(f"State after execute_role_definition: {self.state}")
|
||||||
|
current_app.logger.debug(f"State dump after execute_role_definition: {self.state.model_dump()}")
|
||||||
|
return crew_output
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"CREW execute_role_definition Kickoff Error: {str(e)}")
|
||||||
|
self.exception_raised = True
|
||||||
|
raise e
|
||||||
|
|
||||||
|
async def kickoff_async(self, inputs=None):
|
||||||
|
current_app.logger.debug(f"Async kickoff {self.name}")
|
||||||
|
current_app.logger.debug(f"Inputs: {inputs}")
|
||||||
|
self.state.input = RoleDefinitionSpecialistInput.model_validate(inputs)
|
||||||
|
current_app.logger.debug(f"State: {self.state}")
|
||||||
|
result = await super().kickoff_async(inputs)
|
||||||
|
return self.state
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
{% elif cell.type == 'badge' %}
|
{% elif cell.type == 'badge' %}
|
||||||
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
||||||
{% elif cell.type == 'link' %}
|
{% elif cell.type == 'link' %}
|
||||||
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-bs-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ cell.value }}
|
{{ cell.value }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -177,7 +177,7 @@
|
|||||||
{% elif cell.type == 'badge' %}
|
{% elif cell.type == 'badge' %}
|
||||||
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
||||||
{% elif cell.type == 'link' %}
|
{% elif cell.type == 'link' %}
|
||||||
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-bs-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ cell.value }}
|
{{ cell.value }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -342,7 +342,7 @@
|
|||||||
{% elif cell.type == 'badge' %}
|
{% elif cell.type == 'badge' %}
|
||||||
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
<span class="badge badge-sm {{ cell.badge_class }}">{{ cell.value }}</span>
|
||||||
{% elif cell.type == 'link' %}
|
{% elif cell.type == 'link' %}
|
||||||
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
<a href="{{ cell.href }}" class="text-secondary font-weight-normal text-xs" data-bs-toggle="tooltip" data-original-title="{{ cell.title }}">{{ cell.value }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ cell.value }}
|
{{ cell.value }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"""Add TenantMake model
|
||||||
|
|
||||||
|
Revision ID: 200bda7f5251
|
||||||
|
Revises: b6146237f298
|
||||||
|
Create Date: 2025-06-06 13:48:40.208711
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '200bda7f5251'
|
||||||
|
down_revision = 'b6146237f298'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('tenant_make',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('tenant_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('active', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('website', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('logo_url', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('chat_customisation_options', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('created_by', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('updated_by', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['created_by'], ['public.user.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['tenant_id'], ['public.tenant.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['updated_by'], ['public.user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
schema='public'
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('tenant', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('chat_customisation_options')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('tenant', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('chat_customisation_options', postgresql.JSONB(astext_type=sa.Text()), autoincrement=False, nullable=True))
|
||||||
|
|
||||||
|
op.drop_table('tenant_make', schema='public')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""Add SpecialistMagicLinkTenant model
|
||||||
|
|
||||||
|
Revision ID: 2b4cb553530e
|
||||||
|
Revises: 7d3c6f48735c
|
||||||
|
Create Date: 2025-06-03 20:26:36.423880
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '2b4cb553530e'
|
||||||
|
down_revision = '7d3c6f48735c'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('specialist_magic_link_tenant',
|
||||||
|
sa.Column('magic_link_code', sa.String(length=55), nullable=False),
|
||||||
|
sa.Column('tenant_id', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['tenant_id'], ['public.tenant.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('magic_link_code'),
|
||||||
|
schema='public'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('specialist_magic_link_tenant', schema='public')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""Add Chat Configuration Options to Tenant model
|
||||||
|
|
||||||
|
Revision ID: b6146237f298
|
||||||
|
Revises: 2b4cb553530e
|
||||||
|
Create Date: 2025-06-06 03:45:24.264045
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'b6146237f298'
|
||||||
|
down_revision = '2b4cb553530e'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('tenant', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('chat_customisation_options', postgresql.JSONB(astext_type=sa.Text()), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('tenant', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('chat_customisation_options')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -71,8 +71,8 @@ target_db = current_app.extensions['migrate'].db
|
|||||||
def get_public_table_names():
|
def get_public_table_names():
|
||||||
# TODO: This function should include the necessary functionality to automatically retrieve table names
|
# TODO: This function should include the necessary functionality to automatically retrieve table names
|
||||||
return ['role', 'roles_users', 'tenant', 'user', 'tenant_domain','license_tier', 'license', 'license_usage',
|
return ['role', 'roles_users', 'tenant', 'user', 'tenant_domain','license_tier', 'license', 'license_usage',
|
||||||
'business_event_log', 'tenant_project']
|
'business_event_log', 'tenant_project', 'partner', 'partner_service', 'invoice', 'license_period',
|
||||||
|
'license_change_log', 'partner_service_license_tier', 'payment', 'partner_tenant']
|
||||||
|
|
||||||
PUBLIC_TABLES = get_public_table_names()
|
PUBLIC_TABLES = get_public_table_names()
|
||||||
logger.info(f"Public tables: {PUBLIC_TABLES}")
|
logger.info(f"Public tables: {PUBLIC_TABLES}")
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""Make Catalog Name Unique
|
||||||
|
|
||||||
|
Revision ID: c71facc0ce7e
|
||||||
|
Revises: d69520ec540d
|
||||||
|
Create Date: 2025-06-07 08:38:23.759681
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import pgvector
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'c71facc0ce7e'
|
||||||
|
down_revision = 'd69520ec540d'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_unique_constraint(None, 'catalog', ['name'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
pass
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"""Add SpecialistMagicLink model
|
||||||
|
|
||||||
|
Revision ID: d69520ec540d
|
||||||
|
Revises: 55c696c4a687
|
||||||
|
Create Date: 2025-06-03 20:25:51.129869
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import pgvector
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'd69520ec540d'
|
||||||
|
down_revision = '55c696c4a687'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('specialist_magic_link',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('specialist_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('magic_link_code', sa.String(length=55), nullable=False),
|
||||||
|
sa.Column('valid_from', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('valid_to', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('specialist_args', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('created_by', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('updated_by', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['created_by'], ['public.user.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['specialist_id'], ['specialist.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['updated_by'], ['public.user.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('magic_link_code')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('specialist_magic_link')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -16,7 +16,9 @@ import * as Popper from '@popperjs/core';
|
|||||||
window.Popper = Popper; // Maak het globaal beschikbaar als Bootstrap het extern verwacht.
|
window.Popper = Popper; // Maak het globaal beschikbaar als Bootstrap het extern verwacht.
|
||||||
|
|
||||||
// Bootstrap JavaScript
|
// Bootstrap JavaScript
|
||||||
import 'bootstrap'; // Importeert alle BS JS componenten.
|
import * as bootstrap from 'bootstrap'; // Importeer Bootstrap als object
|
||||||
|
window.bootstrap = bootstrap; // Maak bootstrap globaal beschikbaar
|
||||||
|
|
||||||
// Bootstrap's JS koppelt zichzelf meestal aan jQuery en gebruikt Popper.
|
// Bootstrap's JS koppelt zichzelf meestal aan jQuery en gebruikt Popper.
|
||||||
// Als je 'bootstrap' als object nodig hebt (bijv. voor new bootstrap.Modal()), importeer het dan als:
|
// Als je 'bootstrap' als object nodig hebt (bijv. voor new bootstrap.Modal()), importeer het dan als:
|
||||||
// import * as bootstrap from 'bootstrap';
|
// import * as bootstrap from 'bootstrap';
|
||||||
@@ -42,5 +44,7 @@ import { createJSONEditor } from 'vanilla-jsoneditor';
|
|||||||
// Maak de factory functie globaal beschikbaar als je dit elders in je code gebruikt.
|
// Maak de factory functie globaal beschikbaar als je dit elders in je code gebruikt.
|
||||||
window.createJSONEditor = createJSONEditor;
|
window.createJSONEditor = createJSONEditor;
|
||||||
|
|
||||||
|
import './tabulator-setup.js';
|
||||||
|
|
||||||
// Eventueel een log om te bevestigen dat de bundel is geladen
|
// Eventueel een log om te bevestigen dat de bundel is geladen
|
||||||
console.log('JavaScript bibliotheken gebundeld en geladen via main.js.');
|
console.log('JavaScript bibliotheken gebundeld en geladen via main.js.');
|
||||||
|
|||||||
8
nginx/frontend_src/js/tabulator-setup.js
Normal file
8
nginx/frontend_src/js/tabulator-setup.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// CSS importeren
|
||||||
|
import 'tabulator-tables/dist/css/tabulator.min.css';
|
||||||
|
|
||||||
|
// JavaScript imports
|
||||||
|
import { TabulatorFull as Tabulator } from 'tabulator-tables';
|
||||||
|
|
||||||
|
// Maak Tabulator globaal beschikbaar
|
||||||
|
window.Tabulator = Tabulator;
|
||||||
@@ -18,6 +18,11 @@ http {
|
|||||||
include mime.types;
|
include mime.types;
|
||||||
default_type application/octet-stream;
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
# Define upstream servers
|
||||||
|
upstream eveai_chat_client {
|
||||||
|
server eveai_chat_client:5004;
|
||||||
|
}
|
||||||
|
|
||||||
log_format custom_log_format '$remote_addr - $remote_user [$time_local] "$request" '
|
log_format custom_log_format '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
'"$http_user_agent" "$http_x_forwarded_for" '
|
'"$http_user_agent" "$http_x_forwarded_for" '
|
||||||
@@ -93,6 +98,26 @@ http {
|
|||||||
# add_header 'Access-Control-Allow-Credentials' 'true' always;
|
# add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||||
# }
|
# }
|
||||||
|
|
||||||
|
location /chat-client/ {
|
||||||
|
proxy_pass http://eveai_chat_client/;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Prefix /chat-client;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_buffering off;
|
||||||
|
|
||||||
|
# Add CORS headers
|
||||||
|
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
|
||||||
|
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||||
|
}
|
||||||
|
|
||||||
location /admin/ {
|
location /admin/ {
|
||||||
# include uwsgi_params;
|
# include uwsgi_params;
|
||||||
# uwsgi_pass 127.0.0.1:5001;
|
# uwsgi_pass 127.0.0.1:5001;
|
||||||
|
|||||||
7
nginx/package-lock.json
generated
7
nginx/package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"datatables.net": "^2.3.1",
|
"datatables.net": "^2.3.1",
|
||||||
"jquery": "^3.7.1",
|
"jquery": "^3.7.1",
|
||||||
"select2": "^4.1.0-rc.0",
|
"select2": "^4.1.0-rc.0",
|
||||||
|
"tabulator-tables": "^6.3.1",
|
||||||
"vanilla-jsoneditor": "^3.5.0"
|
"vanilla-jsoneditor": "^3.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -3648,6 +3649,12 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tabulator-tables": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tabulator-tables/-/tabulator-tables-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-qFW7kfadtcaISQIibKAIy0f3eeIXUVi8242Vly1iJfMD79kfEGzfczNuPBN/80hDxHzQJXYbmJ8VipI40hQtfA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/term-size": {
|
"node_modules/term-size": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"datatables.net": "^2.3.1",
|
"datatables.net": "^2.3.1",
|
||||||
"jquery": "^3.7.1",
|
"jquery": "^3.7.1",
|
||||||
"select2": "^4.1.0-rc.0",
|
"select2": "^4.1.0-rc.0",
|
||||||
|
"tabulator-tables": "^6.3.1",
|
||||||
"vanilla-jsoneditor": "^3.5.0"
|
"vanilla-jsoneditor": "^3.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
--bs-danger: #9c2d66;
|
--bs-danger: #9c2d66;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Overriding the background gradient and text colors */
|
/* Overriding the background gradient and text colors ------------------------------------------ */
|
||||||
.bg-gradient-success {
|
.bg-gradient-success {
|
||||||
background: linear-gradient(90deg, var(--bs-primary) 0%, var(--bs-secondary) 100%);
|
background: linear-gradient(90deg, var(--bs-primary) 0%, var(--bs-secondary) 100%);
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
font-weight: 700; /* Retain bold text */
|
font-weight: 700; /* Retain bold text */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Navbar customization */
|
/* Navbar customization ------------------------------------------------------------------------ */
|
||||||
.navbar-light .navbar-brand {
|
.navbar-light .navbar-brand {
|
||||||
color: var(--bs-primary) !important; /* Primary color for the brand text */
|
color: var(--bs-primary) !important; /* Primary color for the brand text */
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
color: var(--bs-white) !important;
|
color: var(--bs-white) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Page header customization */
|
/* Page header customization ------------------------------------------------------------------- */
|
||||||
.page-header {
|
.page-header {
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
@@ -148,7 +148,7 @@
|
|||||||
margin-top: -5rem; /* Adjust margin to improve vertical alignment */
|
margin-top: -5rem; /* Adjust margin to improve vertical alignment */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Card and table customization */
|
/* Card and table customization ---------------------------------------------------------------- */
|
||||||
.card {
|
.card {
|
||||||
border: 1px solid var(--bs-secondary) !important; /* Secondary color for the card border */
|
border: 1px solid var(--bs-secondary) !important; /* Secondary color for the card border */
|
||||||
border-radius: 0.5rem; /* Keeps the border-radius consistent */
|
border-radius: 0.5rem; /* Keeps the border-radius consistent */
|
||||||
@@ -258,7 +258,6 @@ input[type="radio"] {
|
|||||||
.pagination {
|
.pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-left: 0;
|
|
||||||
list-style: none;
|
list-style: none;
|
||||||
border-radius: 0.375rem;
|
border-radius: 0.375rem;
|
||||||
margin-left: 0 !important;
|
margin-left: 0 !important;
|
||||||
@@ -314,7 +313,7 @@ input[type="radio"] {
|
|||||||
text-align: center !important;
|
text-align: center !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form and Input Fields */
|
/* Form and Input Fields ----------------------------------------------------------------------- */
|
||||||
.form-group label.form-label {
|
.form-group label.form-label {
|
||||||
color: var(--bs-secondary) !important; /* Secondary color for labels */
|
color: var(--bs-secondary) !important; /* Secondary color for labels */
|
||||||
font-weight: 500; /* Slightly bolder labels */
|
font-weight: 500; /* Slightly bolder labels */
|
||||||
@@ -353,7 +352,7 @@ input[type="radio"] {
|
|||||||
color: var(--bs-body-color) !important; /* Consistent text color for check labels */
|
color: var(--bs-body-color) !important; /* Consistent text color for check labels */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tabs Navigation */
|
/* Tabs Navigation ----------------------------------------------------------------------------- */
|
||||||
.nav-pills .nav-link {
|
.nav-pills .nav-link {
|
||||||
color: var(--bs-primary) !important; /* Primary color for inactive tab text */
|
color: var(--bs-primary) !important; /* Primary color for inactive tab text */
|
||||||
border-radius: 0.375rem !important; /* Rounded corners for tabs */
|
border-radius: 0.375rem !important; /* Rounded corners for tabs */
|
||||||
@@ -370,7 +369,7 @@ input[type="radio"] {
|
|||||||
color: var(--bs-white) !important; /* White text on hover */
|
color: var(--bs-white) !important; /* White text on hover */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tabs Content */
|
/* Tabs Content -------------------------------------------------------------------------------- */
|
||||||
.tab-pane {
|
.tab-pane {
|
||||||
padding-top: 1rem; /* Consistent padding inside tabs */
|
padding-top: 1rem; /* Consistent padding inside tabs */
|
||||||
}
|
}
|
||||||
@@ -379,7 +378,7 @@ input[type="radio"] {
|
|||||||
background-color: var(--bs-primary) !important; /* Primary color for the moving tab indicator */
|
background-color: var(--bs-primary) !important; /* Primary color for the moving tab indicator */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Buttons */
|
/* Buttons ------------------------------------------------------------------------------------- */
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
background-color: var(--bs-secondary) !important;
|
background-color: var(--bs-secondary) !important;
|
||||||
@@ -399,8 +398,8 @@ input[type="radio"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger:hover {
|
.btn-danger:hover {
|
||||||
background-color: darken(var(--bs-danger), 10%) !important; /* Darken the background on hover */
|
background-color: var(--bs-secondary) !important;
|
||||||
border-color: darken(var(--bs-danger), 10%) !important; /* Darken the border on hover */
|
border-color: var(--bs-secondary) !important;
|
||||||
color: var(--bs-white) !important; /* Ensure the text remains white and readable */
|
color: var(--bs-white) !important; /* Ensure the text remains white and readable */
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,7 +435,7 @@ input[type="radio"] {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom styles for chat session view */
|
/* Custom styles for chat session view --------------------------------------------------------- */
|
||||||
.accordion-button:not(.collapsed) {
|
.accordion-button:not(.collapsed) {
|
||||||
background-color: var(--bs-primary);
|
background-color: var(--bs-primary);
|
||||||
color: var(--bs-white);
|
color: var(--bs-white);
|
||||||
@@ -489,7 +488,7 @@ input[type="radio"] {
|
|||||||
background-color: var(--bs-light);
|
background-color: var(--bs-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Markdown content styles */
|
/* Markdown content styles --------------------------------------------------------------------- */
|
||||||
.markdown-content {
|
.markdown-content {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
@@ -533,7 +532,7 @@ input[type="radio"] {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure the original select is visible and styled */
|
/* Ensure the original select is visible and styled -------------------------------------------- */
|
||||||
select.select2 {
|
select.select2 {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
@@ -554,7 +553,7 @@ select.select2[multiple] {
|
|||||||
height: auto !important;
|
height: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* REQUIRED FIELD SETTINGS ---------------------------------------------------- */
|
/* REQUIRED FIELD SETTINGS --------------------------------------------------------------------- */
|
||||||
/* Required field indicator styling */
|
/* Required field indicator styling */
|
||||||
.field-label-wrapper {
|
.field-label-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -614,7 +613,7 @@ select.select2[multiple] {
|
|||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TAB ERROR STYLES ----------------------------------------------------------- */
|
/* TAB ERROR STYLES ---------------------------------------------------------------------------- */
|
||||||
/* Style for tabs with errors */
|
/* Style for tabs with errors */
|
||||||
.nav-link.has-error {
|
.nav-link.has-error {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -704,7 +703,7 @@ select.select2[multiple] {
|
|||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* JSON Editor Styling - EveAI Aanpassingen */
|
/* JSON Editor Styling - EveAI Aanpassingen ---------------------------------------------------- */
|
||||||
:root {
|
:root {
|
||||||
/* Hoofdkleuren gebaseerd op EveAI kleurenschema */
|
/* Hoofdkleuren gebaseerd op EveAI kleurenschema */
|
||||||
--jse-theme-color: var(--bs-primary); /* Paars als hoofdkleur */
|
--jse-theme-color: var(--bs-primary); /* Paars als hoofdkleur */
|
||||||
@@ -813,4 +812,407 @@ select.select2[multiple] {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tabulator styling / ordered_list ------------------------------------------------------------ */
|
||||||
|
.ordered-list-editor {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
min-height: 200px; /* Minimum height, will expand as needed */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make sure the Tabulator container has a proper height */
|
||||||
|
.ordered-list-editor .tabulator {
|
||||||
|
height: auto; /* Auto height to display all rows */
|
||||||
|
min-height: 200px; /* Minimum height */
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--bs-primary); /* Primary color for border */
|
||||||
|
border-radius: 0.375rem; /* Match application's border-radius */
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
box-shadow: 0 4px 8px rgba(118, 89, 154, 0.2); /* Match application's shadow style */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure the table holder has a scrollbar */
|
||||||
|
.ordered-list-editor .tabulator-tableholder {
|
||||||
|
/* overflow-y: auto !important; - Removed to allow Tabulator to handle overflow */
|
||||||
|
/* max-height: calc(100% - 42px) !important; - Removed to allow Tabulator to handle height */
|
||||||
|
/* Consider using non-!important values if specific scrolling behavior is needed */
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: calc(100% - 42px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the table element */
|
||||||
|
.ordered-list-editor .tabulator-table {
|
||||||
|
display: table !important; /* Force display as table */
|
||||||
|
width: 100% !important;
|
||||||
|
table-layout: fixed !important; /* Use fixed table layout for consistent column widths */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the handle column */
|
||||||
|
.ordered-list-editor .tabulator-row-handle {
|
||||||
|
cursor: move;
|
||||||
|
background-color: var(--bs-light, #f8f9fa);
|
||||||
|
border-right: 1px solid var(--bs-gray-300, #dee2e6);
|
||||||
|
transition: background-color 0.3s ease; /* Smooth transition for hover effect */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover effect for handle column */
|
||||||
|
.ordered-list-editor .tabulator-row:hover .tabulator-row-handle {
|
||||||
|
background-color: var(--bs-secondary); /* Secondary color on hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the handle bars to make them more visible */
|
||||||
|
.ordered-list-editor .tabulator-row-handle-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ordered-list-editor .tabulator-row-handle-bar {
|
||||||
|
background: var(--bs-primary); /* Primary color for handle bars */
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 2px;
|
||||||
|
margin: 1px 0;
|
||||||
|
transition: background-color 0.3s ease; /* Smooth transition for hover effect */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Change handle bar color on hover */
|
||||||
|
.ordered-list-editor .tabulator-row:hover .tabulator-row-handle-bar {
|
||||||
|
background: #ffffff; /* White handle bars on hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the delete button */
|
||||||
|
.ordered-list-editor .tabulator-cell button.btn-danger {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for boolean columns */
|
||||||
|
.ordered-list-editor .tabulator-cell[data-type="boolean"] {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for boolean cell icons */
|
||||||
|
.ordered-list-editor .tabulator-cell .material-icons {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for true/checked icons */
|
||||||
|
.ordered-list-editor .tabulator-cell[aria-checked="true"] .material-icons {
|
||||||
|
color: var(--bs-primary); /* Primary color for checked state */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for false/unchecked icons */
|
||||||
|
.ordered-list-editor .tabulator-cell[aria-checked="false"] .material-icons {
|
||||||
|
color: var(--bs-danger); /* Danger color for unchecked state */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the table header */
|
||||||
|
.ordered-list-editor .tabulator-header {
|
||||||
|
background: linear-gradient(90deg, var(--bs-primary) 0%, var(--bs-secondary) 100%); /* Match JSE gradient */
|
||||||
|
border-bottom: 2px solid var(--bs-secondary); /* Secondary color for border */
|
||||||
|
color: #ffffff; /* White text for better contrast on gradient */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the headers container */
|
||||||
|
.ordered-list-editor .tabulator-headers {
|
||||||
|
display: table-row !important; /* Force display as table row */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the header cells */
|
||||||
|
.ordered-list-editor .tabulator-col {
|
||||||
|
background: transparent; /* Let the header gradient show through */
|
||||||
|
padding: 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
display: table-cell !important; /* Force display as table cell */
|
||||||
|
box-sizing: border-box !important; /* Include padding in width calculation */
|
||||||
|
position: relative !important; /* Ensure proper positioning */
|
||||||
|
color: #ffffff; /* White text for better contrast on gradient */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override any inline styles that might hide column headers */
|
||||||
|
.ordered-list-editor .tabulator-col[style*="display: none"] {
|
||||||
|
display: table-cell !important; /* Force display as table cell */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure header cells have the same width as their corresponding data cells */
|
||||||
|
.ordered-list-editor .tabulator-col,
|
||||||
|
.ordered-list-editor .tabulator-cell
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the header cell content */
|
||||||
|
.ordered-list-editor .tabulator-col-title {
|
||||||
|
white-space: normal; /* Allow header text to wrap */
|
||||||
|
word-break: break-word; /* Break words to prevent horizontal overflow */
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffffff; /* White text for better contrast on gradient */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the table rows */
|
||||||
|
.ordered-list-editor .tabulator-row {
|
||||||
|
border-bottom: 1px solid var(--bs-gray-300, #dee2e6); /* Match application's row border color */
|
||||||
|
display: table-row !important; /* Force display as table row */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover effect for rows */
|
||||||
|
.ordered-list-editor .tabulator-row:hover {
|
||||||
|
background-color: var(--bs-secondary) !important; /* Secondary color on hover */
|
||||||
|
color: #ffffff !important; /* White text on hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure all text in hovered rows changes to white */
|
||||||
|
.ordered-list-editor .tabulator-row:hover .tabulator-cell,
|
||||||
|
.ordered-list-editor .tabulator-row:hover .tabulator-cell * {
|
||||||
|
color: #ffffff !important; /* White text for all elements in hovered rows */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for even rows */
|
||||||
|
.ordered-list-editor .tabulator-row-even {
|
||||||
|
background-color: #f8f9fa; /* Light gray for even rows */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for odd rows */
|
||||||
|
.ordered-list-editor .tabulator-row-odd {
|
||||||
|
background-color: #ffffff; /* White for odd rows */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for selected rows */
|
||||||
|
.ordered-list-editor .tabulator-row.tabulator-selected {
|
||||||
|
background-color: var(--bs-primary) !important; /* Primary color for selected rows */
|
||||||
|
color: #ffffff !important; /* White text for contrast */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for row being moved */
|
||||||
|
.ordered-list-editor .tabulator-row.tabulator-moving {
|
||||||
|
background-color: var(--bs-primary) !important; /* Primary color for moving rows */
|
||||||
|
color: #ffffff !important; /* White text for contrast */
|
||||||
|
border: 2px dashed var(--bs-secondary) !important; /* Dashed border to indicate movement */
|
||||||
|
opacity: 0.9 !important; /* Slightly transparent to distinguish from other rows */
|
||||||
|
box-shadow: 0 0 10px rgba(118, 89, 154, 0.5) !important; /* Shadow for depth */
|
||||||
|
z-index: 100 !important; /* Ensure it appears above other rows */
|
||||||
|
pointer-events: none !important; /* Allow events to pass through to elements below */
|
||||||
|
transform: scale(1.02) !important; /* Slightly larger to stand out */
|
||||||
|
transition: transform 0.2s ease !important; /* Smooth transition */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for cells in the row being moved */
|
||||||
|
.ordered-list-editor .tabulator-row.tabulator-moving .tabulator-cell {
|
||||||
|
color: #ffffff !important; /* Ensure text is white for contrast */
|
||||||
|
background-color: transparent !important; /* Use the row's background color */
|
||||||
|
border-color: transparent !important; /* Hide cell borders */
|
||||||
|
display: table-cell !important; /* Ensure cells are visible */
|
||||||
|
overflow: visible !important; /* Show all content */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the moving element (the ghost row that follows the cursor) */
|
||||||
|
.tabulator-moving-element {
|
||||||
|
background-color: var(--bs-primary) !important; /* Primary color for moving element */
|
||||||
|
color: #ffffff !important; /* White text for contrast */
|
||||||
|
border: 2px dashed var(--bs-secondary) !important; /* Dashed border to indicate movement */
|
||||||
|
opacity: 0.9 !important; /* Slightly transparent */
|
||||||
|
box-shadow: 0 0 15px rgba(118, 89, 154, 0.7) !important; /* Stronger shadow for better visibility */
|
||||||
|
border-radius: 0.375rem !important; /* Rounded corners */
|
||||||
|
overflow: visible !important; /* Show all content */
|
||||||
|
width: auto !important; /* Allow width to adjust to content */
|
||||||
|
max-width: none !important; /* Don't limit width */
|
||||||
|
pointer-events: none !important; /* Allow events to pass through */
|
||||||
|
display: table !important; /* Ensure it's displayed as a table */
|
||||||
|
table-layout: fixed !important; /* Fixed table layout for consistent cell widths */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for cells in the moving element */
|
||||||
|
.tabulator-moving-element .tabulator-cell,
|
||||||
|
.tabulator-moving-element .tabulator-row .tabulator-cell {
|
||||||
|
color: #ffffff !important; /* White text for contrast */
|
||||||
|
background-color: transparent !important; /* Use the row's background color */
|
||||||
|
border-color: transparent !important; /* Hide cell borders */
|
||||||
|
display: table-cell !important; /* Ensure cells are visible */
|
||||||
|
overflow: visible !important; /* Show all content */
|
||||||
|
padding: 8px !important; /* Consistent padding */
|
||||||
|
white-space: normal !important; /* Allow text to wrap */
|
||||||
|
word-break: break-word !important; /* Break words to prevent overflow */
|
||||||
|
font-size: 0.85rem !important; /* Consistent font size */
|
||||||
|
vertical-align: middle !important; /* Center content vertically */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the active moving element */
|
||||||
|
.tabulator-moving-element-active {
|
||||||
|
opacity: 1 !important; /* Fully opaque when active */
|
||||||
|
transform: scale(1.05) !important; /* Slightly larger when active */
|
||||||
|
box-shadow: 0 0 20px rgba(118, 89, 154, 0.8) !important; /* Stronger shadow when active */
|
||||||
|
z-index: 1000 !important; /* Higher z-index to ensure it's on top */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the table cells */
|
||||||
|
.ordered-list-editor .tabulator-cell {
|
||||||
|
padding: 8px;
|
||||||
|
white-space: normal; /* Allow text to wrap */
|
||||||
|
overflow: visible; /* Show overflowing content */
|
||||||
|
height: auto !important; /* Allow cell to grow as needed */
|
||||||
|
word-break: break-word; /* Break words to prevent horizontal overflow */
|
||||||
|
display: table-cell !important; /* Force display as table cell */
|
||||||
|
scroll-margin-top: 100px; /* Prevent unwanted scrolling when focusing */
|
||||||
|
scroll-behavior: auto; /* Disable smooth scrolling which might cause jumping */
|
||||||
|
font-size: 0.85rem; /* Smaller font size */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for truncated cells */
|
||||||
|
.ordered-list-editor .truncated-cell {
|
||||||
|
position: relative;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ordered-list-editor .truncated-content {
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ordered-list-editor .show-more {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: var(--bs-primary); /* Use primary color for the show more indicator */
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s ease; /* Smooth transition for hover effect */
|
||||||
|
}
|
||||||
|
|
||||||
|
.ordered-list-editor .show-more:hover {
|
||||||
|
color: var(--bs-secondary); /* Use secondary color on hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the visible cells */
|
||||||
|
.ordered-list-editor .tabulator-cell-visible {
|
||||||
|
display: table-cell !important; /* Force display as table cell */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override any inline styles that might hide cells */
|
||||||
|
.ordered-list-editor .tabulator-cell[style*="display: none"] {
|
||||||
|
display: table-cell !important; /* Force display as table cell */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the textarea editor */
|
||||||
|
.ordered-list-editor .tabulator-cell textarea {
|
||||||
|
min-height: 60px;
|
||||||
|
resize: vertical;
|
||||||
|
width: 100%; /* Ensure textarea fills the cell */
|
||||||
|
border: 1px solid var(--bs-gray-300, #dee2e6); /* Match application's input border */
|
||||||
|
border-radius: 0.375rem; /* Match application's border-radius */
|
||||||
|
padding: 0.625rem 0.75rem; /* Match application's input padding */
|
||||||
|
transition: border-color 0.3s ease, box-shadow 0.3s ease; /* Smooth transition for focus effect */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus state for textarea */
|
||||||
|
.ordered-list-editor .tabulator-cell textarea:focus {
|
||||||
|
border-color: var(--bs-primary); /* Primary color for focus state */
|
||||||
|
outline: 0;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(118, 89, 154, 0.25); /* Subtle glow with primary color */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the placeholder */
|
||||||
|
.ordered-list-editor .tabulator-placeholder {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--bs-secondary); /* Secondary color for placeholder text */
|
||||||
|
font-style: italic;
|
||||||
|
background-color: var(--bs-light, #f8f9fa); /* Light background for placeholder */
|
||||||
|
border-radius: 0.375rem; /* Match application's border-radius */
|
||||||
|
margin: 10px;
|
||||||
|
border: 1px dashed var(--bs-gray-300, #dee2e6); /* Dashed border for empty state */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the Add Row button */
|
||||||
|
.ordered-list-editor + .btn-primary {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
background-color: var(--bs-primary) !important;
|
||||||
|
border-color: var(--bs-primary) !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover effect for primary button */
|
||||||
|
.ordered-list-editor + .btn-primary:hover {
|
||||||
|
background-color: var(--bs-secondary) !important;
|
||||||
|
border-color: var(--bs-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style for the Expand button */
|
||||||
|
.ordered-list-editor + .btn-primary + .btn-secondary {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
background-color: var(--bs-secondary) !important;
|
||||||
|
border-color: var(--bs-secondary) !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover effect for secondary button */
|
||||||
|
.ordered-list-editor + .btn-primary + .btn-secondary:hover {
|
||||||
|
background-color: var(--bs-primary) !important;
|
||||||
|
border-color: var(--bs-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fullscreen mode styles */
|
||||||
|
.ordered-list-editor.fullscreen-mode {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
z-index: 9999;
|
||||||
|
background: var(--bs-light, #f8f9fa); /* Use light background color */
|
||||||
|
padding: 20px;
|
||||||
|
margin: 0;
|
||||||
|
overflow: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 2px solid var(--bs-primary); /* Primary color border */
|
||||||
|
box-shadow: 0 0 20px rgba(118, 89, 154, 0.3); /* Larger shadow for modal effect */
|
||||||
|
}
|
||||||
|
|
||||||
|
.ordered-list-editor.fullscreen-mode .tabulator {
|
||||||
|
height: calc(100vh - 100px) !important;
|
||||||
|
width: 100% !important;
|
||||||
|
border: 1px solid var(--bs-primary); /* Consistent border */
|
||||||
|
box-shadow: 0 4px 8px rgba(118, 89, 154, 0.2); /* Consistent shadow */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tekst in invoervelden zwart maken voor betere leesbaarheid */
|
||||||
|
.ordered-list-editor .tabulator-row:hover .tabulator-cell input,
|
||||||
|
.ordered-list-editor .tabulator-row:hover .tabulator-cell select,
|
||||||
|
.ordered-list-editor .tabulator-row:hover .tabulator-cell textarea,
|
||||||
|
.ordered-list-editor .tabulator-row:hover .tabulator-cell .tabulator-editor,
|
||||||
|
.ordered-list-editor .tabulator-row.tabulator-selected .tabulator-cell input,
|
||||||
|
.ordered-list-editor .tabulator-row.tabulator-selected .tabulator-cell select,
|
||||||
|
.ordered-list-editor .tabulator-row.tabulator-selected .tabulator-cell textarea,
|
||||||
|
.ordered-list-editor .tabulator-row.tabulator-selected .tabulator-cell .tabulator-editor {
|
||||||
|
color: #000000 !important; /* Zwarte tekst op witte achtergrond */
|
||||||
|
background-color: #ffffff !important; /* Witte achtergrond verzekeren */
|
||||||
|
border: 1px solid var(--bs-primary) !important; /* Duidelijke rand toevoegen */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select2 settings ---------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
.select2-container--default .select2-results > .select2-results__options {
|
||||||
|
max-height: 200px !important; /* Pas deze waarde aan naar wens */
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zorg voor een consistente breedte */
|
||||||
|
.select2-container {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Voorkom dat de dropdown de pagina uitbreidt */
|
||||||
|
.select2-dropdown {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timezone-dropdown {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
0
nginx/static/css/eveai-chat-style.css
Normal file
0
nginx/static/css/eveai-chat-style.css
Normal file
2
nginx/static/dist/main.css
vendored
Normal file
2
nginx/static/dist/main.css
vendored
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user