diff --git a/.aiignore b/.aiignore new file mode 100644 index 0000000..2f31257 --- /dev/null +++ b/.aiignore @@ -0,0 +1,19 @@ +# An .aiignore file follows the same syntax as a .gitignore file. +# .gitignore documentation: https://git-scm.com/docs/gitignore + +# you can ignore files +.DS_Store +*.log +*.tmp + +# or folders +dist/ +build/ +out/ +nginx/node_modules/ +nginx/static/ +db_backups/ +docker/eveai_logs/ +docker/logs/ +docker/minio/ + diff --git a/.gitignore b/.gitignore index ddb859f..ebd0c98 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,7 @@ scripts/__pycache__/run_eveai_app.cpython-312.pyc /docker/grafana/data/ /temp_requirements/ /nginx/node_modules/ +/nginx/static/assets/css/chat.css +/nginx/static/assets/css/chat-components.css +/nginx/static/assets/js/components/ +/nginx/static/assets/js/chat-app.js diff --git a/app/eveai_chat_client/templates/chat.html b/app/eveai_chat_client/templates/chat.html new file mode 100644 index 0000000..07f3f52 --- /dev/null +++ b/app/eveai_chat_client/templates/chat.html @@ -0,0 +1,6 @@ + +{% extends "base.html" %} + +{% block title %}{{ tenant_make.name|default('EveAI') }} - AI Chat{% endblock %} + +{% block head %} diff --git a/common/utils/specialist_utils.py b/common/utils/specialist_utils.py deleted file mode 100644 index f0b3200..0000000 --- a/common/utils/specialist_utils.py +++ /dev/null @@ -1,196 +0,0 @@ -from datetime import datetime as dt, timezone as tz -from typing import Optional, Dict, Any -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.model_logging_utils import set_logging_information, update_logging_information - - -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': - initialize_crewai_specialist(specialist, config) - - -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']: - _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']: - _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']: - _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 - - -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 - - -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 - - -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 diff --git a/docker/docker_env_switch.sh b/docker/docker_env_switch.sh index 00bc8e9..1cfa407 100755 --- a/docker/docker_env_switch.sh +++ b/docker/docker_env_switch.sh @@ -115,15 +115,41 @@ echo "Set COMPOSE_FILE to $COMPOSE_FILE" echo "Set EVEAI_VERSION to $VERSION" echo "Set DOCKER_ACCOUNT to $DOCKER_ACCOUNT" -# Define aliases for common Docker commands -alias docker-compose="docker compose -f $COMPOSE_FILE" -alias dc="docker compose -f $COMPOSE_FILE" -alias dcup="docker compose -f $COMPOSE_FILE up -d --remove-orphans" -alias dcdown="docker compose -f $COMPOSE_FILE down" -alias dcps="docker compose -f $COMPOSE_FILE ps" -alias dclogs="docker compose -f $COMPOSE_FILE logs" -alias dcpull="docker compose -f $COMPOSE_FILE pull" -alias dcrefresh="docker compose -f $COMPOSE_FILE pull && docker compose -f $COMPOSE_FILE up -d --remove-orphans" +docker-compose() { + docker compose -f $COMPOSE_FILE "$@" +} + +dc() { + docker compose -f $COMPOSE_FILE "$@" +} + +dcup() { + docker compose -f $COMPOSE_FILE up -d --remove-orphans "$@" +} + +dcdown() { + docker compose -f $COMPOSE_FILE down "$@" +} + +dcps() { + docker compose -f $COMPOSE_FILE ps "$@" +} + +dclogs() { + docker compose -f $COMPOSE_FILE logs "$@" +} + +dcpull() { + docker compose -f $COMPOSE_FILE pull "$@" +} + +dcrefresh() { + docker compose -f $COMPOSE_FILE pull && docker compose -f $COMPOSE_FILE up -d --remove-orphans "$@" +} + +# Exporteer de functies zodat ze beschikbaar zijn in andere scripts +export -f docker-compose dc dcup dcdown dcps dclogs dcpull dcrefresh + echo "Docker environment switched to $ENVIRONMENT with version $VERSION" echo "You can now use 'docker-compose', 'dc', 'dcup', 'dcdown', 'dcps', 'dclogs', 'dcpull' or 'dcrefresh' commands" \ No newline at end of file diff --git a/docker/rebuild_chat_client.sh b/docker/rebuild_chat_client.sh new file mode 100755 index 0000000..3c05bac --- /dev/null +++ b/docker/rebuild_chat_client.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +source ./docker_env_switch.sh dev +source .env + +dcdown eveai_chat_client nginx +./update_chat_client_statics.sh +./build_and_push_eveai.sh -b nginx +dcup eveai_chat_client nginx diff --git a/docker/update_chat_client_statics.sh b/docker/update_chat_client_statics.sh new file mode 100755 index 0000000..ffa0c9f --- /dev/null +++ b/docker/update_chat_client_statics.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Script to copy eveai_chat_client/static files to nginx/static +# without overwriting existing files + +SRC_DIR="../eveai_chat_client/static" +DEST_DIR="../nginx/static/assets" + +# Check if source directory exists +if [ ! -d "$SRC_DIR" ]; then + echo "Error: Source directory $SRC_DIR does not exist!" + exit 1 +fi + +# Create destination directory if it doesn't exist +if [ ! -d "$DEST_DIR" ]; then + echo "Destination directory $DEST_DIR does not exist. Creating it..." + mkdir -p "$DEST_DIR" +fi + +# Function to recursively copy files without overwriting +copy_without_overwrite() { + local src=$1 + local dest=$2 + + # Loop through all items in source directory + for item in "$src"/*; do + # Get the filename from the path + base_name=$(basename "$item") + + # If it's a directory, create it in the destination and recurse + if [ -d "$item" ]; then + if [ ! -d "$dest/$base_name" ]; then + echo "Creating directory: $dest/$base_name" + mkdir -p "$dest/$base_name" + fi + copy_without_overwrite "$item" "$dest/$base_name" + else + # If it's a file and doesn't exist in the destination, copy it + cp "$item" "$dest/$base_name" + fi + done +} + +# Start the copy process +echo "Starting to copy files from $SRC_DIR to $DEST_DIR..." +copy_without_overwrite "$SRC_DIR" "$DEST_DIR" + +echo "Copy completed!" diff --git a/eveai_chat_client/static/css/chat-components.css b/eveai_chat_client/static/css/chat-components.css new file mode 100644 index 0000000..cbe8c82 --- /dev/null +++ b/eveai_chat_client/static/css/chat-components.css @@ -0,0 +1,806 @@ + +/* Chat App Container Layout */ +.chat-app-container { + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + min-height: 0; /* Belangrijk voor flexbox overflow */ + padding: 20px; /* Algemene padding voor alle kanten */ + box-sizing: border-box; +} + +/* Message Area - neemt alle beschikbare ruimte */ +.chat-messages-area { + flex: 1; /* Neemt alle beschikbare ruimte */ + overflow: hidden; /* Voorkomt dat het groter wordt dan container */ + display: flex; + flex-direction: column; + min-height: 0; /* Belangrijk voor nested flexbox */ + margin-bottom: 20px; /* Ruimte tussen messages en input */ + border-radius: 15px; + background: rgba(255,255,255,0.1); + backdrop-filter: blur(10px); + border: 1px solid rgba(255,255,255,0.2); + box-shadow: 0 4px 20px rgba(0,0,0,0.1); +} + +/* Chat Input - altijd onderaan */ +.chat-input-area { + flex: none; /* Neemt alleen benodigde ruimte */ + border-radius: 15px; + background: rgba(255,255,255,0.15); + backdrop-filter: blur(10px); + border: 1px solid rgba(255,255,255,0.2); + box-shadow: 0 4px 20px rgba(0,0,0,0.1); + z-index: 10; +} + +/* Zorg dat de MessageHistory container ook flexbox gebruikt */ +.message-history-container { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + padding: 20px; /* Interne padding voor MessageHistory */ + box-sizing: border-box; +} + +.chat-messages { + flex: 1; + overflow-y: auto; + padding-right: 10px; /* Ruimte voor scrollbar */ + margin-right: -10px; /* Compenseer voor scrollbar */ + scroll-behavior: smooth; +} + +/* Chat Input styling */ +.chat-input-container { + width: 100%; + position: relative; + padding: 20px; /* Interne padding voor ChatInput */ + box-sizing: border-box; +} + +.chat-input { + display: flex; + align-items: flex-end; + gap: 12px; + padding: 20px; + background: white; + border-radius: 15px; + box-shadow: 0 2px 15px rgba(0,0,0,0.1); + border: 1px solid rgba(0,0,0,0.05); +} + +.input-main { + flex: 1; + position: relative; +} + +.message-input { + width: 100%; + min-height: 45px; + max-height: 120px; + padding: 12px 18px; + border: 1px solid #ddd; + border-radius: 25px; + resize: none; + font-family: inherit; + font-size: 14px; + line-height: 1.4; + outline: none; + transition: all 0.2s ease; + box-sizing: border-box; +} + +.message-input:focus { + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1); +} + +.message-input.over-limit { + border-color: #dc3545; + background-color: rgba(220, 53, 69, 0.05); +} + + +.input-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.send-btn { + width: 45px; + height: 45px; + border: none; + border-radius: 50%; + background: var(--primary-color); + color: white; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + transition: all 0.2s ease; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); +} + +.send-btn:hover:not(:disabled) { + background: var(--secondary-color); + transform: scale(1.05); + box-shadow: 0 4px 15px rgba(0,0,0,0.2); +} + +.send-btn:disabled { + background: #ccc; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + + +/* Character counter */ +.character-counter { + position: absolute; + bottom: -25px; + right: 15px; + font-size: 12px; + color: #666; + padding: 2px 6px; + background: rgba(255,255,255,0.9); + border-radius: 10px; + backdrop-filter: blur(5px); +} + +.character-counter.over-limit { + color: #dc3545; + font-weight: bold; + background: rgba(220, 53, 69, 0.1); +} + +/* Loading spinner */ +.loading-spinner { + font-size: 16px; + animation: spin 1s linear infinite; +} + + +/* Mobile responsiveness */ +@media (max-width: 768px) { + .chat-app-container { + padding: 10px; /* Kleinere padding op mobiel */ + } + + .chat-messages-area { + margin-bottom: 15px; + } + + .message-history-container { + padding: 15px; + } + + .chat-input-container { + padding: 15px; + } + + .chat-input { + padding: 15px; + gap: 10px; + } + + .action-btn { + width: 40px; + height: 40px; + font-size: 16px; + } + + .message-input { + font-size: 16px; /* Voorkomt zoom op iOS */ + padding: 10px 15px; + min-height: 40px; + } +} + +/* Extra small screens */ +@media (max-width: 480px) { + .chat-app-container { + padding: 8px; + } + + .chat-messages-area { + margin-bottom: 12px; + } + + .message-history-container { + padding: 12px; + } + + .chat-input-container { + padding: 12px; + } +} + +/* Loading states */ +.chat-input.loading .message-input { + opacity: 0.7; +} + +.chat-input.loading .action-btn { + animation: pulse 1.5s infinite; +} + +@keyframes pulse { + 0% { opacity: 1; } + 50% { opacity: 0.5; } + 100% { opacity: 1; } +} + +/* Scrollbar styling voor webkit browsers */ +.chat-messages::-webkit-scrollbar { + width: 6px; +} + +.chat-messages::-webkit-scrollbar-track { + background: rgba(0,0,0,0.1); + border-radius: 3px; +} + +.chat-messages::-webkit-scrollbar-thumb { + background: rgba(0,0,0,0.3); + border-radius: 3px; +} + +.chat-messages::-webkit-scrollbar-thumb:hover { + background: rgba(0,0,0,0.5); +} + +/* Verberg lege message bubbles tot er inhoud is */ +.message-text:empty { + display: none; +} + +.progress-tracker .status-icon.error { + color: #f44336; +} + +.progress-tracker.error .progress-header { + background-color: rgba(244, 67, 54, 0.1); + border-color: #f44336; +} + +/* Zorg dat de progress tracker goed wordt weergegeven in een lege message bubble */ +.message-content:has(.message-text:empty) .message-progress { + margin-bottom: 0; +} + +/* Verberg de message content container als er geen inhoud is en de verwerking bezig is */ +.message-content:has(.message-text:empty):not(:has(.message-progress.completed)):not(:has(.message-progress.error)) { + background: transparent; + box-shadow: none; + border: none; + padding: 0; + margin: 0; +} + +/* Focus binnen ChatInput voor toegankelijkheid */ +.chat-input:focus-within { + box-shadow: 0 2px 20px rgba(0, 123, 255, 0.2); + border-color: rgba(0, 123, 255, 0.3); +} + +/* Smooth transitions */ +.chat-messages-area, +.chat-input-area { + transition: all 0.3s ease; +} + +.chat-messages-area:hover, +.chat-input-area:hover { + box-shadow: 0 6px 25px rgba(0,0,0,0.15); +} + + +/* Message Bubbles Styling - Aangepast voor werkelijke template structuur */ + +/* Basis message container */ +.message { + display: flex; + margin-bottom: 16px; + padding: 0 20px; + animation: messageSlideIn 0.3s ease-out; + clear: both; +} + +/* User message alignment - rechts uitgelijnd */ +.message.user { + justify-content: flex-end; +} + +/* AI/Bot message alignment - links uitgelijnd */ +.message.ai, +.message.bot { + justify-content: flex-start; +} + +/* Message content wrapper - dit wordt de bubble */ +.message-content { + max-width: 70%; + padding: 12px 16px; + border-radius: 18px; + word-wrap: break-word; + position: relative; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + transition: all 0.2s ease; + display: inline-block; +} + +/* User message bubble styling */ +.message.user .message-content { + background: linear-gradient(135deg, #007bff, #0056b3); + color: white; + border-bottom-right-radius: 4px; +} + +/* AI/Bot message bubble styling */ +.message.ai .message-content, +.message.bot .message-content { + background: #f8f9fa; + color: #212529; + border: 1px solid #e9ecef; + border-bottom-left-radius: 4px; + margin-right: 60px; +} + +/* Message text content */ +.message-text { + line-height: 1.4; + font-size: 14px; + margin-bottom: 6px; +} + +.message-text p { + margin: 0; +} + +.message-text p + p { + margin-top: 8px; +} + + +/* Edit mode styling */ +.edit-mode .edit-textarea { + width: 100%; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + resize: vertical; + min-height: 60px; + font-family: inherit; + margin-bottom: 8px; +} + +.edit-actions { + display: flex; + gap: 8px; +} + +.btn-small { + padding: 4px 12px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + transition: all 0.2s ease; +} + +.btn-primary { + background: #007bff; + color: white; +} + +.btn-primary:hover { + background: #0056b3; +} + +.btn-secondary { + background: #6c757d; + color: white; +} + +.btn-secondary:hover { + background: #545b62; +} + +/* Special message types */ + +/* Form messages */ +.form-message { + justify-content: center; + margin: 20px 0; +} + +.form-message .message-content { + max-width: 90%; + background: white; + border: 1px solid #e9ecef; + border-radius: 12px; + padding: 20px; + box-shadow: 0 4px 12px rgba(0,0,0,0.1); +} + +/* System messages */ +.system-message { + text-align: center; + background: rgba(108, 117, 125, 0.1); + color: #6c757d; + padding: 8px 16px; + border-radius: 20px; + font-size: 13px; + margin: 10px auto; + max-width: 80%; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.system-icon { + font-size: 14px; +} + +/* Error messages */ +.error-message { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; + border-radius: 8px; + padding: 12px 16px; + margin: 10px auto; + max-width: 80%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.error-icon { + font-size: 16px; + color: #dc3545; +} + +.retry-btn { + background: #dc3545; + color: white; + border: none; + padding: 4px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + transition: background-color 0.2s ease; +} + +.retry-btn:hover { + background: #c82333; +} + +/* Message reactions */ +.message-reactions { + display: flex; + gap: 4px; + margin-top: 8px; + flex-wrap: wrap; +} + +.reaction { + background: rgba(0,0,0,0.05); + border: 1px solid rgba(0,0,0,0.1); + border-radius: 12px; + padding: 2px 8px; + font-size: 12px; + cursor: pointer; + transition: all 0.2s ease; +} + +.reaction:hover { + background: rgba(0,0,0,0.1); + transform: scale(1.05); +} + +/* Image and file messages */ +.message-image { + max-width: 100%; + max-height: 300px; + border-radius: 8px; + margin-bottom: 8px; + cursor: pointer; + transition: transform 0.2s ease; +} + +.message-image:hover { + transform: scale(1.02); +} + +.image-caption { + font-size: 13px; + margin-bottom: 6px; + opacity: 0.9; +} + +.file-attachment { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + background: rgba(0,0,0,0.03); + border-radius: 8px; + margin-bottom: 8px; +} + +.file-icon { + font-size: 24px; +} + +.file-info { + flex: 1; +} + +.file-name { + font-weight: 500; + margin-bottom: 2px; +} + +.file-size { + font-size: 12px; + opacity: 0.7; +} + +.file-download { + font-size: 20px; + text-decoration: none; + cursor: pointer; + transition: transform 0.2s ease; +} + +.file-download:hover { + transform: scale(1.1); +} + +/* Hover effects voor message bubbles */ +.message-content:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0,0,0,0.15); +} + +/* Bestaande animation en date-separator blijven hetzelfde */ +@keyframes messageSlideIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + + +/* Empty state styling - blijft hetzelfde */ +.empty-state { + text-align: center; + padding: 40px 20px; + color: #6c757d; +} + +.empty-icon { + font-size: 48px; + margin-bottom: 16px; + opacity: 0.5; +} + +.empty-text { + font-size: 18px; + font-weight: 500; + margin-bottom: 8px; +} + +.empty-subtext { + font-size: 14px; + opacity: 0.8; +} + +/* Mobile responsiveness */ +@media (max-width: 768px) { + .message { + padding: 0 15px; + } + + .message-content { + max-width: 85%; + padding: 10px 14px; + font-size: 14px; + } + + .message.user .message-content { + margin-left: 40px; + } + + .message.ai .message-content, + .message.bot .message-content { + margin-right: 40px; + } +} + +@media (max-width: 480px) { + .message { + padding: 0 10px; + } + + .message-content { + max-width: 90%; + margin-left: 20px !important; + margin-right: 20px !important; + } +} + +/* Progress Tracker Styling */ +.progress-tracker { + margin: 8px 0; + border: 1px solid #e9ecef; + border-radius: 8px; + background: #f8f9fa; + overflow: hidden; + transition: all 0.3s ease; + font-size: 13px; +} + +.progress-tracker.expanded { + max-height: 200px; +} + +.progress-tracker.completed { + border-color: #28a745; + background: #d4edda; +} + +.progress-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + cursor: pointer; + background: rgba(0,0,0,0.02); + border-bottom: 1px solid transparent; + transition: all 0.2s ease; +} + +.progress-header:hover { + background: rgba(0,0,0,0.05); +} + +.progress-tracker.expanded .progress-header { + border-bottom-color: #e9ecef; +} + +.progress-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; + color: #495057; +} + +.status-icon { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 50%; + position: relative; +} + +.status-icon.completed { + background: #28a745; + color: white; + font-size: 8px; + line-height: 12px; + text-align: center; +} + +.status-icon.in-progress { + background: #007bff; + animation: pulse 1.5s infinite; +} + +.spinner { + display: inline-block; + width: 12px; + height: 12px; + border: 2px solid #f3f3f3; + border-top: 2px solid #007bff; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +.progress-toggle { + color: #6c757d; + font-size: 14px; + transition: transform 0.2s ease; +} + +.progress-tracker.expanded .progress-toggle { + transform: rotate(180deg); +} + +.progress-error { + padding: 8px 12px; + color: #721c24; + background: #f8d7da; + border-top: 1px solid #f5c6cb; + font-size: 12px; +} + +.progress-content { + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease; +} + +.progress-tracker.expanded .progress-content { + max-height: 150px; + overflow-y: auto; +} + +.progress-content.single-line { + max-height: 30px; + overflow: hidden; + padding: 8px 12px; +} + +.progress-line { + padding: 4px 12px; + border-bottom: 1px solid rgba(0,0,0,0.05); + color: #6c757d; + line-height: 1.3; +} + +.progress-line:last-child { + border-bottom: none; +} + +/* Animaties */ +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +@keyframes pulse { + 0% { opacity: 1; } + 50% { opacity: 0.5; } + 100% { opacity: 1; } +} + +/* Integratie met message bubbles */ +.message.ai .progress-tracker, +.message.bot .progress-tracker { + margin-bottom: 8px; +} + +/* Mobile responsiveness */ +@media (max-width: 768px) { + .progress-tracker { + font-size: 12px; + } + + .progress-header { + padding: 6px 10px; + } + + .progress-line { + padding: 3px 10px; + } + + .progress-content.single-line { + padding: 6px 10px; + } +} \ No newline at end of file diff --git a/eveai_chat_client/static/css/chat.css b/eveai_chat_client/static/css/chat.css index 45ba6ff..b97d2fc 100644 --- a/eveai_chat_client/static/css/chat.css +++ b/eveai_chat_client/static/css/chat.css @@ -98,17 +98,9 @@ body { border-bottom: 1px solid rgba(0,0,0,0.1); } -.chat-messages { - flex: 1; - overflow-y: auto; - padding: var(--spacing); -} +/* .chat-messages wordt nu gedefinieerd in chat-components.css */ -.message { - margin-bottom: var(--spacing); - max-width: 80%; - clear: both; -} +/* .message wordt nu gedefinieerd in chat-components.css */ .user-message { float: right; @@ -118,11 +110,7 @@ body { float: left; } -.message-content { - padding: 12px 16px; - border-radius: var(--border-radius); - display: inline-block; -} +/* .message-content wordt nu gedefinieerd in chat-components.css */ .user-message .message-content { background-color: var(--message-user-bg); @@ -134,11 +122,7 @@ body { color: var(--text-color); } -.chat-input-container { - padding: var(--spacing); - border-top: 1px solid rgba(0,0,0,0.1); - display: flex; -} +/* .chat-input-container wordt nu gedefinieerd in chat-components.css */ #chat-input { flex: 1; @@ -150,44 +134,7 @@ body { 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); } -} +/* .typing-indicator en bijbehorende animaties worden nu gedefinieerd in chat-components.css */ /* Error page styles */ .error-container { @@ -215,30 +162,6 @@ body { 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; -} +/* .btn-primary wordt nu gedefinieerd in chat-components.css */ -/* 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%; - } -} \ No newline at end of file +/* Responsieve design regels worden nu gedefinieerd in chat-components.css */ \ No newline at end of file diff --git a/eveai_chat_client/static/js/chat-app.js b/eveai_chat_client/static/js/chat-app.js new file mode 100644 index 0000000..cbbf87d --- /dev/null +++ b/eveai_chat_client/static/js/chat-app.js @@ -0,0 +1,523 @@ +// Import all components +import { TypingIndicator } from '/static/assets/js/components/TypingIndicator.js'; +import { FormField } from '/static/assets/js/components/FormField.js'; +import { DynamicForm } from '/static/assets/js/components/DynamicForm.js'; +import { ChatMessage } from '/static/assets/js/components/ChatMessage.js'; +import { MessageHistory } from '/static/assets/js/components/MessageHistory.js'; +import { ChatInput } from '/static/assets/js/components/ChatInput.js'; +import { ProgressTracker } from '/static/assets/js/components/ProgressTracker.js'; + +// Main Chat Application +export const ChatApp = { + name: 'ChatApp', + components: { + TypingIndicator, + FormField, + DynamicForm, + ChatMessage, + MessageHistory, + ChatInput + }, + + data() { + // Maak een lokale kopie van de chatConfig om undefined errors te voorkomen + const chatConfig = window.chatConfig || {}; + const settings = chatConfig.settings || {}; + + return { + // Base template data (keeping existing functionality) + explanation: chatConfig.explanation || '', + + // Chat-specific data + currentMessage: '', + allMessages: [], + isTyping: false, + isLoading: false, + isSubmittingForm: false, + messageIdCounter: 1, + formValues: {}, + + // API prefix voor endpoints + apiPrefix: chatConfig.apiPrefix || '', + + // Configuration from Flask/server + conversationId: chatConfig.conversationId || 'default', + userId: chatConfig.userId || null, + userName: chatConfig.userName || '', + + // Settings met standaard waarden en overschreven door server config + settings: { + maxMessageLength: settings.maxMessageLength || 2000, + allowFileUpload: settings.allowFileUpload === true, + allowVoiceMessage: settings.allowVoiceMessage === true, + autoScroll: settings.autoScroll === true + }, + + // UI state + isMobile: window.innerWidth <= 768, + showSidebar: window.innerWidth > 768, + + // Advanced features + messageSearch: '', + filteredMessages: [], + isSearching: false + }; + }, + + computed: { + // Keep existing computed from base.html + compiledExplanation() { + if (typeof marked === 'function') { + return marked(this.explanation); + } else if (marked && typeof marked.parse === 'function') { + return marked.parse(this.explanation); + } else { + console.error('Marked library not properly loaded'); + return this.explanation; + } + }, + + displayMessages() { + return this.isSearching ? this.filteredMessages : this.allMessages; + }, + + hasMessages() { + return this.allMessages.length > 0; + } + }, + + mounted() { + this.initializeChat(); + this.setupEventListeners(); + }, + + beforeUnmount() { + this.cleanup(); + }, + + methods: { + // Initialization + initializeChat() { + console.log('Initializing chat application...'); + + // Load historical messages from server + this.loadHistoricalMessages(); + + // Add welcome message if no history + if (this.allMessages.length === 0) { + this.addWelcomeMessage(); + } + + // Focus input after initialization + this.$nextTick(() => { + this.focusChatInput(); + }); + }, + + loadHistoricalMessages() { + // Veilige toegang tot messages met fallback + const chatConfig = window.chatConfig || {}; + const historicalMessages = chatConfig.messages || []; + + if (historicalMessages.length > 0) { + this.allMessages = historicalMessages.map(msg => { + // Zorg voor een correct geformatteerde bericht-object + return { + id: this.messageIdCounter++, + content: typeof msg === 'string' ? msg : msg.content || '', + sender: msg.sender || 'ai', + type: msg.type || 'text', + timestamp: msg.timestamp || new Date().toISOString(), + formData: msg.formData || null, + status: msg.status || 'delivered' + }; + }); + + console.log(`Loaded ${this.allMessages.length} historical messages`); + } + }, + + addWelcomeMessage() { + this.addMessage( + 'Hallo! Ik ben je AI assistant. Vraag gerust om een formulier zoals "contactformulier" of "bestelformulier"!', + 'ai', + 'text' + ); + }, + + setupEventListeners() { + // Window resize listener + window.addEventListener('resize', this.handleResize); + + // Keyboard shortcuts + document.addEventListener('keydown', this.handleGlobalKeydown); + }, + + cleanup() { + window.removeEventListener('resize', this.handleResize); + document.removeEventListener('keydown', this.handleGlobalKeydown); + }, + + // Message management + addMessage(content, sender, type = 'text', formData = null) { + const message = { + id: this.messageIdCounter++, + content, + sender, + type, + formData, + timestamp: new Date().toISOString(), + status: sender === 'user' ? 'sent' : 'delivered' + }; + + this.allMessages.push(message); + + // Initialize form values if it's a form + if (type === 'form' && formData) { + this.$set(this.formValues, message.id, {}); + formData.fields.forEach(field => { + this.$set(this.formValues[message.id], field.name, field.defaultValue || ''); + }); + } + + // Update search results if searching + if (this.isSearching) { + this.performSearch(); + } + + return message; + }, + + updateCurrentMessage(value) { + this.currentMessage = value; + }, + + // Message sending + async sendMessage() { + const text = this.currentMessage.trim(); + if (!text || this.isLoading) return; + + console.log('Sending message:', text); + + // Add user message + const userMessage = this.addMessage(text, 'user', 'text'); + this.currentMessage = ''; + + // Show typing and loading state + this.isTyping = true; + this.isLoading = true; + + try { + const response = await this.callAPI('/api/send_message', { + message: text, + conversation_id: this.conversationId, + user_id: this.userId + }); + + // Hide typing indicator + this.isTyping = false; + + // Mark user message as delivered + userMessage.status = 'delivered'; + + // Add AI response + if (response.type === 'form') { + this.addMessage('', 'ai', 'form', response.formData); + } else { + // Voeg het bericht toe met task_id voor tracking - initieel leeg + const aiMessage = this.addMessage( + '', + 'ai', + 'text' + ); + + // Voeg task_id toe als beschikbaar + if (response.task_id) { + console.log('Monitoring Task ID: ', response.task_id); + aiMessage.taskId = response.task_id; + } + } + + } catch (error) { + console.error('Error sending message:', error); + this.isTyping = false; + + // Mark user message as failed + userMessage.status = 'failed'; + + this.addMessage( + 'Sorry, er ging iets mis bij het verzenden van je bericht. Probeer het opnieuw.', + 'ai', + 'error' + ); + } finally { + this.isLoading = false; + } + }, + + // Form handling + async submitForm(formData, messageId) { + this.isSubmittingForm = true; + + console.log('Submitting form:', formData.title, this.formValues[messageId]); + + try { + const response = await this.callAPI('/api/submit_form', { + formData: this.formValues[messageId], + formType: formData.title, + conversation_id: this.conversationId, + user_id: this.userId + }); + + if (response.success) { + this.addMessage( + `✅ ${response.message || 'Formulier succesvol verzonden!'}`, + 'ai', + 'text' + ); + + // Remove the form message + this.removeMessage(messageId); + } else { + this.addMessage( + `❌ Er ging iets mis: ${response.error || 'Onbekende fout'}`, + 'ai', + 'text' + ); + } + + } catch (error) { + console.error('Error submitting form:', error); + this.addMessage( + 'Sorry, er ging iets mis bij het verzenden van het formulier. Probeer het opnieuw.', + 'ai', + 'text' + ); + } finally { + this.isSubmittingForm = false; + } + }, + + // Message actions + + retryMessage(messageId) { + const message = this.allMessages.find(m => m.id === messageId); + if (message && message.status === 'failed') { + // Retry sending the message + this.currentMessage = message.content; + this.removeMessage(messageId); + this.sendMessage(); + } + }, + + removeMessage(messageId) { + const index = this.allMessages.findIndex(m => m.id === messageId); + if (index !== -1) { + this.allMessages.splice(index, 1); + // Verwijder ook eventuele formuliergegevens + if (this.formValues[messageId]) { + delete this.formValues[messageId]; + } + } + }, + + // File handling + async handleFileUpload(file) { + console.log('Uploading file:', file.name); + + // Add file message + const fileMessage = this.addMessage('', 'user', 'file', { + fileName: file.name, + fileSize: this.formatFileSize(file.size), + fileType: file.type + }); + + try { + // TODO: Implement actual file upload + // const response = await this.uploadFile(file); + // fileMessage.fileUrl = response.url; + + // Simulate file upload + setTimeout(() => { + fileMessage.fileUrl = URL.createObjectURL(file); + fileMessage.status = 'delivered'; + }, 1000); + + } catch (error) { + console.error('Error uploading file:', error); + fileMessage.status = 'failed'; + } + }, + + async handleVoiceRecord(audioBlob) { + console.log('Processing voice recording'); + + // Add voice message + const voiceMessage = this.addMessage('', 'user', 'voice', { + audioBlob, + duration: '00:05' // TODO: Calculate actual duration + }); + + // TODO: Send to speech-to-text service + // const transcription = await this.transcribeAudio(audioBlob); + // this.currentMessage = transcription; + // this.sendMessage(); + }, + + // Search functionality + performSearch() { + if (!this.messageSearch.trim()) { + this.isSearching = false; + this.filteredMessages = []; + return; + } + + this.isSearching = true; + const query = this.messageSearch.toLowerCase(); + + this.filteredMessages = this.allMessages.filter(message => + message.content && + message.content.toLowerCase().includes(query) + ); + }, + + clearSearch() { + this.messageSearch = ''; + this.isSearching = false; + this.filteredMessages = []; + }, + + // Event handlers + handleResize() { + this.isMobile = window.innerWidth <= 768; + this.showSidebar = window.innerWidth > 768; + }, + + handleGlobalKeydown(event) { + // Ctrl/Cmd + K for search + if ((event.ctrlKey || event.metaKey) && event.key === 'k') { + event.preventDefault(); + this.focusSearch(); + } + + // Escape to clear search + if (event.key === 'Escape' && this.isSearching) { + this.clearSearch(); + } + }, + + + // Utility methods + async callAPI(endpoint, data) { + // Gebruik de API prefix uit de lokale variabele + const fullEndpoint = this.apiPrefix + '/chat' + endpoint; + + console.log('Calling API with prefix:', { + prefix: this.apiPrefix, + endpoint: endpoint, + fullEndpoint: fullEndpoint + }); + + const response = await fetch(fullEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return response.json(); + }, + + + formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }, + + focusChatInput() { + this.$refs.chatInput?.focusInput(); + }, + + focusSearch() { + this.$refs.searchInput?.focus(); + }, + + handleSpecialistError(errorData) { + console.error('Specialist error:', errorData); + // Als we willen kunnen we hier nog extra logica toevoegen, zoals statistieken bijhouden of centraal loggen + }, + + }, + + template: ` +
$1')
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
+ .replace(/\n/g, '