- Build of the Chat Client using Vue.js
- Accompanying css - Views to serve the Chat Client - first test version of the TRACIE_SELECTION_SPECIALIST - ESS Implemented.
This commit is contained in:
19
.aiignore
Normal file
19
.aiignore
Normal file
@@ -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/
|
||||||
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -53,3 +53,7 @@ scripts/__pycache__/run_eveai_app.cpython-312.pyc
|
|||||||
/docker/grafana/data/
|
/docker/grafana/data/
|
||||||
/temp_requirements/
|
/temp_requirements/
|
||||||
/nginx/node_modules/
|
/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
|
||||||
|
|||||||
6
app/eveai_chat_client/templates/chat.html
Normal file
6
app/eveai_chat_client/templates/chat.html
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<!-- chat.html -->
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ tenant_make.name|default('EveAI') }} - AI Chat{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
@@ -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
|
|
||||||
@@ -115,15 +115,41 @@ echo "Set COMPOSE_FILE to $COMPOSE_FILE"
|
|||||||
echo "Set EVEAI_VERSION to $VERSION"
|
echo "Set EVEAI_VERSION to $VERSION"
|
||||||
echo "Set DOCKER_ACCOUNT to $DOCKER_ACCOUNT"
|
echo "Set DOCKER_ACCOUNT to $DOCKER_ACCOUNT"
|
||||||
|
|
||||||
# Define aliases for common Docker commands
|
docker-compose() {
|
||||||
alias docker-compose="docker compose -f $COMPOSE_FILE"
|
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"
|
dc() {
|
||||||
alias dcps="docker compose -f $COMPOSE_FILE ps"
|
docker compose -f $COMPOSE_FILE "$@"
|
||||||
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"
|
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 "Docker environment switched to $ENVIRONMENT with version $VERSION"
|
||||||
echo "You can now use 'docker-compose', 'dc', 'dcup', 'dcdown', 'dcps', 'dclogs', 'dcpull' or 'dcrefresh' commands"
|
echo "You can now use 'docker-compose', 'dc', 'dcup', 'dcdown', 'dcps', 'dclogs', 'dcpull' or 'dcrefresh' commands"
|
||||||
9
docker/rebuild_chat_client.sh
Executable file
9
docker/rebuild_chat_client.sh
Executable file
@@ -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
|
||||||
49
docker/update_chat_client_statics.sh
Executable file
49
docker/update_chat_client_statics.sh
Executable file
@@ -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!"
|
||||||
806
eveai_chat_client/static/css/chat-components.css
Normal file
806
eveai_chat_client/static/css/chat-components.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,17 +98,9 @@ body {
|
|||||||
border-bottom: 1px solid rgba(0,0,0,0.1);
|
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-messages {
|
/* .chat-messages wordt nu gedefinieerd in chat-components.css */
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: var(--spacing);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
/* .message wordt nu gedefinieerd in chat-components.css */
|
||||||
margin-bottom: var(--spacing);
|
|
||||||
max-width: 80%;
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-message {
|
.user-message {
|
||||||
float: right;
|
float: right;
|
||||||
@@ -118,11 +110,7 @@ body {
|
|||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-content {
|
/* .message-content wordt nu gedefinieerd in chat-components.css */
|
||||||
padding: 12px 16px;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-message .message-content {
|
.user-message .message-content {
|
||||||
background-color: var(--message-user-bg);
|
background-color: var(--message-user-bg);
|
||||||
@@ -134,11 +122,7 @@ body {
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input-container {
|
/* .chat-input-container wordt nu gedefinieerd in chat-components.css */
|
||||||
padding: var(--spacing);
|
|
||||||
border-top: 1px solid rgba(0,0,0,0.1);
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
#chat-input {
|
#chat-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -150,44 +134,7 @@ body {
|
|||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#send-button {
|
/* .typing-indicator en bijbehorende animaties worden nu gedefinieerd in chat-components.css */
|
||||||
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 page styles */
|
||||||
.error-container {
|
.error-container {
|
||||||
@@ -215,30 +162,6 @@ body {
|
|||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
/* .btn-primary wordt nu gedefinieerd in chat-components.css */
|
||||||
display: inline-block;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive design */
|
/* Responsieve design regels worden nu gedefinieerd in chat-components.css */
|
||||||
@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%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
523
eveai_chat_client/static/js/chat-app.js
Normal file
523
eveai_chat_client/static/js/chat-app.js
Normal file
@@ -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: `
|
||||||
|
<div class="chat-app-container">
|
||||||
|
<!-- Message History - takes available space -->
|
||||||
|
<message-history
|
||||||
|
:messages="displayMessages"
|
||||||
|
:is-typing="isTyping"
|
||||||
|
:form-values="formValues"
|
||||||
|
:is-submitting-form="isSubmittingForm"
|
||||||
|
:api-prefix="apiPrefix"
|
||||||
|
:auto-scroll="true"
|
||||||
|
@submit-form="submitForm"
|
||||||
|
@specialist-error="handleSpecialistError"
|
||||||
|
ref="messageHistory"
|
||||||
|
class="chat-messages-area"
|
||||||
|
></message-history>
|
||||||
|
|
||||||
|
<!-- Chat Input - to the bottom -->
|
||||||
|
<chat-input
|
||||||
|
:current-message="currentMessage"
|
||||||
|
:is-loading="isLoading"
|
||||||
|
:max-length="2000"
|
||||||
|
:allow-file-upload="true"
|
||||||
|
:allow-voice-message="false"
|
||||||
|
@send-message="sendMessage"
|
||||||
|
@update-message="updateCurrentMessage"
|
||||||
|
@upload-file="handleFileUpload"
|
||||||
|
@record-voice="handleVoiceRecord"
|
||||||
|
ref="chatInput"
|
||||||
|
class="chat-input-area"
|
||||||
|
></chat-input>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize app when DOM is ready
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
console.log('Initializing Chat Application');
|
||||||
|
|
||||||
|
// Get access to the existing Vue app instance
|
||||||
|
if (window.__vueApp) {
|
||||||
|
// Register ALL components globally
|
||||||
|
window.__vueApp.component('TypingIndicator', TypingIndicator);
|
||||||
|
window.__vueApp.component('FormField', FormField);
|
||||||
|
window.__vueApp.component('DynamicForm', DynamicForm);
|
||||||
|
window.__vueApp.component('ChatMessage', ChatMessage);
|
||||||
|
window.__vueApp.component('MessageHistory', MessageHistory);
|
||||||
|
window.__vueApp.component('ChatInput', ChatInput);
|
||||||
|
window.__vueApp.component('ProgressTracker', ProgressTracker);
|
||||||
|
console.log('All chat components registered with existing Vue instance');
|
||||||
|
|
||||||
|
// Register the ChatApp component
|
||||||
|
window.__vueApp.component('ChatApp', ChatApp);
|
||||||
|
console.log('ChatApp component registered with existing Vue instance');
|
||||||
|
|
||||||
|
// Mount the Vue app
|
||||||
|
window.__vueApp.mount('#app');
|
||||||
|
console.log('Vue app mounted with chat components');
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.error('No existing Vue instance found on window.__vueApp');
|
||||||
|
}
|
||||||
|
});
|
||||||
132
eveai_chat_client/static/js/components/ChatInput.js
Normal file
132
eveai_chat_client/static/js/components/ChatInput.js
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
// static/js/components/ChatInput.js
|
||||||
|
|
||||||
|
export const ChatInput = {
|
||||||
|
name: 'ChatInput',
|
||||||
|
props: {
|
||||||
|
currentMessage: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: 'Typ je bericht hier... (Enter om te verzenden, Shift+Enter voor nieuwe regel)'
|
||||||
|
},
|
||||||
|
maxLength: {
|
||||||
|
type: Number,
|
||||||
|
default: 2000
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ['send-message', 'update-message'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
localMessage: this.currentMessage,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
characterCount() {
|
||||||
|
return this.localMessage.length;
|
||||||
|
},
|
||||||
|
|
||||||
|
isOverLimit() {
|
||||||
|
return this.characterCount > this.maxLength;
|
||||||
|
},
|
||||||
|
|
||||||
|
canSend() {
|
||||||
|
return this.localMessage.trim() &&
|
||||||
|
!this.isLoading &&
|
||||||
|
!this.isOverLimit;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
currentMessage(newVal) {
|
||||||
|
this.localMessage = newVal;
|
||||||
|
},
|
||||||
|
localMessage(newVal) {
|
||||||
|
this.$emit('update-message', newVal);
|
||||||
|
this.autoResize();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.autoResize();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleKeydown(event) {
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.sendMessage();
|
||||||
|
} else if (event.key === 'Escape') {
|
||||||
|
this.localMessage = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
sendMessage() {
|
||||||
|
if (this.canSend) {
|
||||||
|
this.$emit('send-message');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
autoResize() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const textarea = this.$refs.messageInput;
|
||||||
|
if (textarea) {
|
||||||
|
textarea.style.height = 'auto';
|
||||||
|
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
focusInput() {
|
||||||
|
this.$refs.messageInput?.focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
clearInput() {
|
||||||
|
this.localMessage = '';
|
||||||
|
this.focusInput();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div class="chat-input-container">
|
||||||
|
<div class="chat-input">
|
||||||
|
<!-- Main input area -->
|
||||||
|
<div class="input-main">
|
||||||
|
<textarea
|
||||||
|
ref="messageInput"
|
||||||
|
v-model="localMessage"
|
||||||
|
@keydown="handleKeydown"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
rows="1"
|
||||||
|
:disabled="isLoading"
|
||||||
|
:maxlength="maxLength"
|
||||||
|
class="message-input"
|
||||||
|
:class="{ 'over-limit': isOverLimit }"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<!-- Character counter -->
|
||||||
|
<div v-if="maxLength" class="character-counter" :class="{ 'over-limit': isOverLimit }">
|
||||||
|
{{ characterCount }}/{{ maxLength }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input actions -->
|
||||||
|
<div class="input-actions">
|
||||||
|
<!-- Send button -->
|
||||||
|
<button
|
||||||
|
@click="sendMessage"
|
||||||
|
class="send-btn"
|
||||||
|
:disabled="!canSend"
|
||||||
|
:title="isOverLimit ? 'Bericht te lang' : 'Bericht verzenden'"
|
||||||
|
>
|
||||||
|
<span v-if="isLoading" class="loading-spinner">⏳</span>
|
||||||
|
<svg v-else width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
240
eveai_chat_client/static/js/components/ChatMessage.js
Normal file
240
eveai_chat_client/static/js/components/ChatMessage.js
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
export const ChatMessage = {
|
||||||
|
name: 'ChatMessage',
|
||||||
|
props: {
|
||||||
|
message: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
validator: (message) => {
|
||||||
|
return message.id && message.content !== undefined && message.sender && message.type;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formValues: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
isSubmittingForm: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
apiPrefix: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['submit-form', 'image-loaded', 'retry-message', 'specialist-complete', 'specialist-error'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isEditing: false,
|
||||||
|
editedContent: ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleSpecialistError(eventData) {
|
||||||
|
console.log('ChatMessage received specialist-error event:', eventData);
|
||||||
|
|
||||||
|
// Creëer een error message met correcte styling
|
||||||
|
this.message.type = 'error';
|
||||||
|
this.message.content = eventData.message || 'Er is een fout opgetreden bij het verwerken van uw verzoek.';
|
||||||
|
this.message.retryable = true;
|
||||||
|
this.message.error = true; // Voeg error flag toe voor styling
|
||||||
|
|
||||||
|
// Bubble up naar parent component voor verdere afhandeling
|
||||||
|
this.$emit('specialist-error', {
|
||||||
|
messageId: this.message.id,
|
||||||
|
...eventData
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSpecialistComplete(eventData) {
|
||||||
|
console.log('ChatMessage received specialist-complete event:', eventData);
|
||||||
|
|
||||||
|
// Update de inhoud van het bericht met het antwoord
|
||||||
|
if (eventData.answer) {
|
||||||
|
console.log('Updating message content with answer:', eventData.answer);
|
||||||
|
this.message.content = eventData.answer;
|
||||||
|
} else {
|
||||||
|
console.error('No answer in specialist-complete event data');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bubble up naar parent component voor eventuele verdere afhandeling
|
||||||
|
this.$emit('specialist-complete', {
|
||||||
|
messageId: this.message.id,
|
||||||
|
...eventData
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
formatMessage(content) {
|
||||||
|
if (!content) return '';
|
||||||
|
|
||||||
|
// Enhanced markdown-like formatting
|
||||||
|
return content
|
||||||
|
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||||
|
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||||||
|
.replace(/`(.*?)`/g, '<code>$1</code>')
|
||||||
|
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>')
|
||||||
|
.replace(/\n/g, '<br>');
|
||||||
|
},
|
||||||
|
|
||||||
|
startEdit() {
|
||||||
|
this.editedContent = this.message.content;
|
||||||
|
this.isEditing = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
saveEdit() {
|
||||||
|
// Implementatie van bewerkingen zou hier komen
|
||||||
|
this.message.content = this.editedContent;
|
||||||
|
this.isEditing = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelEdit() {
|
||||||
|
this.isEditing = false;
|
||||||
|
this.editedContent = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
submitForm() {
|
||||||
|
this.$emit('submit-form', this.message.formData, this.message.id);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeMessage() {
|
||||||
|
// Dit zou een event moeten triggeren naar de parent component
|
||||||
|
},
|
||||||
|
|
||||||
|
reactToMessage(emoji) {
|
||||||
|
// Implementatie van reacties zou hier komen
|
||||||
|
},
|
||||||
|
|
||||||
|
getMessageClass() {
|
||||||
|
if (this.message.type === 'form') {
|
||||||
|
return 'form-message';
|
||||||
|
}
|
||||||
|
return `message ${this.message.sender}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div :class="getMessageClass()" :data-message-id="message.id">
|
||||||
|
<!-- Normal text messages -->
|
||||||
|
<template v-if="message.type === 'text'">
|
||||||
|
<div class="message-content">
|
||||||
|
<!-- Voortgangstracker voor AI berichten met task_id - NU BINNEN DE BUBBLE -->
|
||||||
|
<progress-tracker
|
||||||
|
v-if="message.sender === 'ai' && message.taskId"
|
||||||
|
:task-id="message.taskId"
|
||||||
|
:api-prefix="apiPrefix"
|
||||||
|
class="message-progress"
|
||||||
|
@specialist-complete="handleSpecialistComplete"
|
||||||
|
@specialist-error="handleSpecialistError"
|
||||||
|
></progress-tracker>
|
||||||
|
|
||||||
|
<!-- Edit mode -->
|
||||||
|
<div v-if="isEditing" class="edit-mode">
|
||||||
|
<textarea
|
||||||
|
v-model="editedContent"
|
||||||
|
class="edit-textarea"
|
||||||
|
rows="3"
|
||||||
|
@keydown.enter.ctrl="saveEdit"
|
||||||
|
@keydown.escape="cancelEdit"
|
||||||
|
></textarea>
|
||||||
|
<div class="edit-actions">
|
||||||
|
<button @click="saveEdit" class="btn-small btn-primary">Opslaan</button>
|
||||||
|
<button @click="cancelEdit" class="btn-small btn-secondary">Annuleren</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- View mode -->
|
||||||
|
<div v-else>
|
||||||
|
<div
|
||||||
|
v-html="formatMessage(message.content)"
|
||||||
|
class="message-text"
|
||||||
|
></div>
|
||||||
|
<!-- Debug info -->
|
||||||
|
<div v-if="false" class="debug-info">
|
||||||
|
Content: {{ message.content ? message.content.length + ' chars' : 'empty' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Image messages -->
|
||||||
|
<template v-if="message.type === 'image'">
|
||||||
|
<div class="message-content">
|
||||||
|
<img
|
||||||
|
:src="message.imageUrl"
|
||||||
|
:alt="message.alt || 'Afbeelding'"
|
||||||
|
class="message-image"
|
||||||
|
@load="$emit('image-loaded')"
|
||||||
|
>
|
||||||
|
<div v-if="message.caption" class="image-caption">
|
||||||
|
{{ message.caption }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- File messages -->
|
||||||
|
<template v-if="message.type === 'file'">
|
||||||
|
<div class="message-content">
|
||||||
|
<div class="file-attachment">
|
||||||
|
<div class="file-icon">📎</div>
|
||||||
|
<div class="file-info">
|
||||||
|
<div class="file-name">{{ message.fileName }}</div>
|
||||||
|
<div class="file-size">{{ message.fileSize }}</div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
:href="message.fileUrl"
|
||||||
|
download
|
||||||
|
class="file-download"
|
||||||
|
title="Download"
|
||||||
|
>
|
||||||
|
⬇️
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Dynamic forms -->
|
||||||
|
<template v-if="message.type === 'form'">
|
||||||
|
<dynamic-form
|
||||||
|
:form-data="message.formData"
|
||||||
|
:form-values="formValues[message.id] || {}"
|
||||||
|
:is-submitting="isSubmittingForm"
|
||||||
|
@submit="submitForm"
|
||||||
|
@cancel="removeMessage"
|
||||||
|
></dynamic-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- System messages -->
|
||||||
|
<template v-if="message.type === 'system'">
|
||||||
|
<div class="system-message">
|
||||||
|
<span class="system-icon">ℹ️</span>
|
||||||
|
{{ message.content }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Error messages -->
|
||||||
|
<template v-if="message.type === 'error'">
|
||||||
|
<div class="error-message">
|
||||||
|
<span class="error-icon">⚠️</span>
|
||||||
|
{{ message.content }}
|
||||||
|
<button
|
||||||
|
v-if="message.retryable"
|
||||||
|
@click="$emit('retry-message', message.id)"
|
||||||
|
class="retry-btn"
|
||||||
|
>
|
||||||
|
Probeer opnieuw
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Message reactions -->
|
||||||
|
<div v-if="message.reactions && message.reactions.length" class="message-reactions">
|
||||||
|
<span
|
||||||
|
v-for="reaction in message.reactions"
|
||||||
|
:key="reaction.emoji"
|
||||||
|
class="reaction"
|
||||||
|
@click="reactToMessage(reaction.emoji)"
|
||||||
|
>
|
||||||
|
{{ reaction.emoji }} {{ reaction.count }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
110
eveai_chat_client/static/js/components/DynamicForm.js
Normal file
110
eveai_chat_client/static/js/components/DynamicForm.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
export const DynamicForm = {
|
||||||
|
name: 'DynamicForm',
|
||||||
|
props: {
|
||||||
|
formData: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
validator: (formData) => {
|
||||||
|
return formData.title && formData.fields && Array.isArray(formData.fields);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formValues: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isSubmitting: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['submit', 'cancel'],
|
||||||
|
methods: {
|
||||||
|
handleSubmit() {
|
||||||
|
// Basic validation
|
||||||
|
const requiredFields = this.formData.fields.filter(field => field.required);
|
||||||
|
const missingFields = requiredFields.filter(field => {
|
||||||
|
const value = this.formValues[field.name];
|
||||||
|
return !value || (typeof value === 'string' && !value.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
if (missingFields.length > 0) {
|
||||||
|
const fieldNames = missingFields.map(f => f.label).join(', ');
|
||||||
|
alert(`De volgende velden zijn verplicht: ${fieldNames}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('submit');
|
||||||
|
},
|
||||||
|
|
||||||
|
handleCancel() {
|
||||||
|
this.$emit('cancel');
|
||||||
|
},
|
||||||
|
|
||||||
|
updateFieldValue(fieldName, value) {
|
||||||
|
// Emit an update for reactive binding
|
||||||
|
this.$emit('update-field', fieldName, value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div class="dynamic-form">
|
||||||
|
<div class="form-title">{{ formData.title }}</div>
|
||||||
|
|
||||||
|
<div v-if="formData.description" class="form-description">
|
||||||
|
{{ formData.description }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleSubmit" novalidate>
|
||||||
|
<form-field
|
||||||
|
v-for="field in formData.fields"
|
||||||
|
:key="field.name"
|
||||||
|
:field="field"
|
||||||
|
:model-value="formValues[field.name]"
|
||||||
|
@update:model-value="formValues[field.name] = $event"
|
||||||
|
></form-field>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
:class="{ 'loading': isSubmitting }"
|
||||||
|
>
|
||||||
|
<span v-if="isSubmitting" class="spinner"></span>
|
||||||
|
{{ isSubmitting ? 'Verzenden...' : (formData.submitText || 'Versturen') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
@click="handleCancel"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
{{ formData.cancelText || 'Annuleren' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Optional reset button -->
|
||||||
|
<button
|
||||||
|
v-if="formData.showReset"
|
||||||
|
type="reset"
|
||||||
|
class="btn btn-outline"
|
||||||
|
@click="$emit('reset')"
|
||||||
|
:disabled="isSubmitting"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress indicator for multi-step forms -->
|
||||||
|
<div v-if="formData.steps && formData.currentStep" class="form-progress">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div
|
||||||
|
class="progress-fill"
|
||||||
|
:style="{ width: (formData.currentStep / formData.steps * 100) + '%' }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<small>Stap {{ formData.currentStep }} van {{ formData.steps }}</small>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
179
eveai_chat_client/static/js/components/FormField.js
Normal file
179
eveai_chat_client/static/js/components/FormField.js
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
export const FormField = {
|
||||||
|
name: 'FormField',
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
validator: (field) => {
|
||||||
|
return field.name && field.type && field.label;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
computed: {
|
||||||
|
value: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('update:modelValue', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleFileUpload(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
this.value = file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div class="form-field">
|
||||||
|
<label>
|
||||||
|
{{ field.label }}
|
||||||
|
<span v-if="field.required" class="required">*</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- Text/Email/Tel inputs -->
|
||||||
|
<input
|
||||||
|
v-if="['text', 'email', 'tel', 'url', 'password'].includes(field.type)"
|
||||||
|
:type="field.type"
|
||||||
|
v-model="value"
|
||||||
|
:required="field.required"
|
||||||
|
:placeholder="field.placeholder || ''"
|
||||||
|
:maxlength="field.maxLength"
|
||||||
|
:minlength="field.minLength"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- Number input -->
|
||||||
|
<input
|
||||||
|
v-if="field.type === 'number'"
|
||||||
|
type="number"
|
||||||
|
v-model.number="value"
|
||||||
|
:required="field.required"
|
||||||
|
:min="field.min"
|
||||||
|
:max="field.max"
|
||||||
|
:step="field.step || 1"
|
||||||
|
:placeholder="field.placeholder || ''"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- Date input -->
|
||||||
|
<input
|
||||||
|
v-if="field.type === 'date'"
|
||||||
|
type="date"
|
||||||
|
v-model="value"
|
||||||
|
:required="field.required"
|
||||||
|
:min="field.min"
|
||||||
|
:max="field.max"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- Time input -->
|
||||||
|
<input
|
||||||
|
v-if="field.type === 'time'"
|
||||||
|
type="time"
|
||||||
|
v-model="value"
|
||||||
|
:required="field.required"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- File input -->
|
||||||
|
<input
|
||||||
|
v-if="field.type === 'file'"
|
||||||
|
type="file"
|
||||||
|
@change="handleFileUpload"
|
||||||
|
:required="field.required"
|
||||||
|
:accept="field.accept"
|
||||||
|
:multiple="field.multiple"
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- Select dropdown -->
|
||||||
|
<select
|
||||||
|
v-if="field.type === 'select'"
|
||||||
|
v-model="value"
|
||||||
|
:required="field.required"
|
||||||
|
>
|
||||||
|
<option value="">{{ field.placeholder || 'Kies een optie' }}</option>
|
||||||
|
<option
|
||||||
|
v-for="option in field.options"
|
||||||
|
:key="option.value || option"
|
||||||
|
:value="option.value || option"
|
||||||
|
>
|
||||||
|
{{ option.label || option }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- Radio buttons -->
|
||||||
|
<div v-if="field.type === 'radio'" class="radio-group">
|
||||||
|
<label
|
||||||
|
v-for="option in field.options"
|
||||||
|
:key="option.value || option"
|
||||||
|
class="radio-label"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
:value="option.value || option"
|
||||||
|
v-model="value"
|
||||||
|
:required="field.required"
|
||||||
|
>
|
||||||
|
<span>{{ option.label || option }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Checkboxes -->
|
||||||
|
<div v-if="field.type === 'checkbox'" class="checkbox-group">
|
||||||
|
<label
|
||||||
|
v-for="option in field.options"
|
||||||
|
:key="option.value || option"
|
||||||
|
class="checkbox-label"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:value="option.value || option"
|
||||||
|
v-model="value"
|
||||||
|
>
|
||||||
|
<span>{{ option.label || option }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Single checkbox -->
|
||||||
|
<label v-if="field.type === 'single-checkbox'" class="checkbox-label">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="value"
|
||||||
|
:required="field.required"
|
||||||
|
>
|
||||||
|
<span>{{ field.checkboxText || field.label }}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- Textarea -->
|
||||||
|
<textarea
|
||||||
|
v-if="field.type === 'textarea'"
|
||||||
|
v-model="value"
|
||||||
|
:required="field.required"
|
||||||
|
:rows="field.rows || 3"
|
||||||
|
:placeholder="field.placeholder || ''"
|
||||||
|
:maxlength="field.maxLength"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<!-- Range slider -->
|
||||||
|
<div v-if="field.type === 'range'" class="range-field">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
v-model.number="value"
|
||||||
|
:min="field.min || 0"
|
||||||
|
:max="field.max || 100"
|
||||||
|
:step="field.step || 1"
|
||||||
|
>
|
||||||
|
<span class="range-value">{{ value }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Help text -->
|
||||||
|
<small v-if="field.helpText" class="help-text">
|
||||||
|
{{ field.helpText }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
147
eveai_chat_client/static/js/components/MessageHistory.js
Normal file
147
eveai_chat_client/static/js/components/MessageHistory.js
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
export const MessageHistory = {
|
||||||
|
name: 'MessageHistory',
|
||||||
|
props: {
|
||||||
|
messages: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
isTyping: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
formValues: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
isSubmittingForm: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
apiPrefix: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
autoScroll: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['submit-form', 'load-more', 'specialist-complete', 'specialist-error'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isAtBottom: true,
|
||||||
|
unreadCount: 0
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.scrollToBottom();
|
||||||
|
this.setupScrollListener();
|
||||||
|
},
|
||||||
|
updated() {
|
||||||
|
if (this.autoScroll && this.isAtBottom) {
|
||||||
|
this.$nextTick(() => this.scrollToBottom());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
scrollToBottom() {
|
||||||
|
const container = this.$refs.messagesContainer;
|
||||||
|
if (container) {
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
this.isAtBottom = true;
|
||||||
|
this.showScrollButton = false;
|
||||||
|
this.unreadCount = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setupScrollListener() {
|
||||||
|
const container = this.$refs.messagesContainer;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
container.addEventListener('scroll', this.handleScroll);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleScroll() {
|
||||||
|
const container = this.$refs.messagesContainer;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const threshold = 100; // pixels from bottom
|
||||||
|
const isNearBottom = container.scrollHeight - container.scrollTop - container.clientHeight < threshold;
|
||||||
|
|
||||||
|
this.isAtBottom = isNearBottom;
|
||||||
|
|
||||||
|
// Load more messages when scrolled to top
|
||||||
|
if (container.scrollTop === 0) {
|
||||||
|
this.$emit('load-more');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSubmitForm(formData, messageId) {
|
||||||
|
this.$emit('submit-form', formData, messageId);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleImageLoaded() {
|
||||||
|
// Auto-scroll when images load to maintain position
|
||||||
|
if (this.isAtBottom) {
|
||||||
|
this.$nextTick(() => this.scrollToBottom());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
searchMessages(query) {
|
||||||
|
// Simple message search
|
||||||
|
if (!query.trim()) return this.messages;
|
||||||
|
|
||||||
|
const searchTerm = query.toLowerCase();
|
||||||
|
return this.messages.filter(message =>
|
||||||
|
message.content &&
|
||||||
|
message.content.toLowerCase().includes(searchTerm)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
// Cleanup scroll listener
|
||||||
|
const container = this.$refs.messagesContainer;
|
||||||
|
if (container) {
|
||||||
|
container.removeEventListener('scroll', this.handleScroll);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div class="message-history-container">
|
||||||
|
<!-- Messages container -->
|
||||||
|
<div class="chat-messages" ref="messagesContainer">
|
||||||
|
<!-- Loading indicator for load more -->
|
||||||
|
<div v-if="$slots.loading" class="load-more-indicator">
|
||||||
|
<slot name="loading"></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty state -->
|
||||||
|
<div v-if="messages.length === 0" class="empty-state">
|
||||||
|
<div class="empty-icon">💬</div>
|
||||||
|
<div class="empty-text">Nog geen berichten</div>
|
||||||
|
<div class="empty-subtext">Start een gesprek door een bericht te typen!</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Message list -->
|
||||||
|
<template v-else>
|
||||||
|
<!-- Messages -->
|
||||||
|
<template v-for="(message, index) in messages" :key="message.id">
|
||||||
|
<!-- The actual message -->
|
||||||
|
<chat-message
|
||||||
|
:message="message"
|
||||||
|
:form-values="formValues"
|
||||||
|
:is-submitting-form="isSubmittingForm"
|
||||||
|
:api-prefix="apiPrefix"
|
||||||
|
@submit-form="handleSubmitForm"
|
||||||
|
@image-loaded="handleImageLoaded"
|
||||||
|
></chat-message>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Typing indicator -->
|
||||||
|
<typing-indicator v-if="isTyping"></typing-indicator>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
};
|
||||||
309
eveai_chat_client/static/js/components/ProgressTracker.js
Normal file
309
eveai_chat_client/static/js/components/ProgressTracker.js
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
export const ProgressTracker = {
|
||||||
|
name: 'ProgressTracker',
|
||||||
|
props: {
|
||||||
|
taskId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
apiPrefix: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['specialist-complete', 'progress-update', 'specialist-error'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isExpanded: false,
|
||||||
|
progressLines: [],
|
||||||
|
eventSource: null,
|
||||||
|
isCompleted: false,
|
||||||
|
lastLine: '',
|
||||||
|
error: null,
|
||||||
|
connecting: true,
|
||||||
|
finalAnswer: null,
|
||||||
|
hasError: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
progressEndpoint() {
|
||||||
|
return `${this.apiPrefix}/chat/api/task_progress/${this.taskId}`;
|
||||||
|
},
|
||||||
|
displayLines() {
|
||||||
|
return this.isExpanded ? this.progressLines : [
|
||||||
|
this.lastLine || 'Verbinden met taak...'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.connectToEventSource();
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.disconnectEventSource();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
connectToEventSource() {
|
||||||
|
try {
|
||||||
|
this.connecting = true;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
// Sluit eventuele bestaande verbinding
|
||||||
|
this.disconnectEventSource();
|
||||||
|
|
||||||
|
// Maak nieuwe SSE verbinding
|
||||||
|
this.eventSource = new EventSource(this.progressEndpoint);
|
||||||
|
|
||||||
|
// Algemene event handler
|
||||||
|
this.eventSource.onmessage = (event) => {
|
||||||
|
this.handleProgressUpdate(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Specifieke event handlers per type
|
||||||
|
this.eventSource.addEventListener('progress', (event) => {
|
||||||
|
this.handleProgressUpdate(event, 'progress');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventSource.addEventListener('EveAI Specialist Complete', (event) => {
|
||||||
|
console.log('Received EveAI Specialist Complete event');
|
||||||
|
this.handleProgressUpdate(event, 'EveAI Specialist Complete');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventSource.addEventListener('error', (event) => {
|
||||||
|
this.handleError(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Status handlers
|
||||||
|
this.eventSource.onopen = () => {
|
||||||
|
this.connecting = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventSource.onerror = (error) => {
|
||||||
|
console.error('SSE Connection error:', error);
|
||||||
|
this.error = 'Verbindingsfout. Probeer het later opnieuw.';
|
||||||
|
this.connecting = false;
|
||||||
|
|
||||||
|
// Probeer opnieuw te verbinden na 3 seconden
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.isCompleted && this.progressLines.length === 0) {
|
||||||
|
this.connectToEventSource();
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error setting up event source:', err);
|
||||||
|
this.error = 'Kan geen verbinding maken met de voortgangsupdates.';
|
||||||
|
this.connecting = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
disconnectEventSource() {
|
||||||
|
if (this.eventSource) {
|
||||||
|
this.eventSource.close();
|
||||||
|
this.eventSource = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleProgressUpdate(event, eventType = null) {
|
||||||
|
try {
|
||||||
|
const update = JSON.parse(event.data);
|
||||||
|
|
||||||
|
// Controleer op verschillende typen updates
|
||||||
|
const processingType = update.processing_type;
|
||||||
|
const data = update.data || {};
|
||||||
|
|
||||||
|
// Process based on processing type
|
||||||
|
let message = this.formatProgressMessage(processingType, data);
|
||||||
|
|
||||||
|
// Alleen bericht toevoegen als er daadwerkelijk een bericht is
|
||||||
|
if (message) {
|
||||||
|
this.progressLines.push(message);
|
||||||
|
this.lastLine = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit progress update voor parent component
|
||||||
|
this.$emit('progress-update', {
|
||||||
|
processingType,
|
||||||
|
data,
|
||||||
|
message
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle completion and errors
|
||||||
|
if (processingType === 'EveAI Specialist Complete') {
|
||||||
|
console.log('Processing EveAI Specialist Complete:', data);
|
||||||
|
this.handleSpecialistComplete(data);
|
||||||
|
} else if (processingType === 'EveAI Specialist Error') {
|
||||||
|
this.handleSpecialistError(data);
|
||||||
|
} else if (processingType === 'Task Complete' || processingType === 'Task Error') {
|
||||||
|
this.isCompleted = true;
|
||||||
|
this.disconnectEventSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll automatisch naar beneden als uitgevouwen
|
||||||
|
if (this.isExpanded) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const container = this.$refs.progressContainer;
|
||||||
|
if (container) {
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error parsing progress update:', err, event.data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
formatProgressMessage(processingType, data) {
|
||||||
|
// Lege data dictionary - toon enkel processing type
|
||||||
|
if (!data || Object.keys(data).length === 0) {
|
||||||
|
return processingType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specifiek bericht als er een message field is
|
||||||
|
if (data.message) {
|
||||||
|
return data.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processing type met name veld als dat bestaat
|
||||||
|
if (data.name) {
|
||||||
|
return `${processingType}: ${data.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stap informatie
|
||||||
|
if (data.step) {
|
||||||
|
return `Stap ${data.step}: ${data.description || ''}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Voor EveAI Specialist Complete - geen progress message
|
||||||
|
if (processingType === 'EveAI Specialist Complete') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: processing type + eventueel data als string
|
||||||
|
return processingType;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSpecialistComplete(data) {
|
||||||
|
this.isCompleted = true;
|
||||||
|
this.disconnectEventSource();
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
console.log('Specialist Complete Data:', data);
|
||||||
|
|
||||||
|
// Extract answer from data.result.answer
|
||||||
|
if (data.result && data.result.answer) {
|
||||||
|
this.finalAnswer = data.result.answer;
|
||||||
|
|
||||||
|
console.log('Final Answer:', this.finalAnswer);
|
||||||
|
|
||||||
|
// Direct update van de parent message als noodoplossing
|
||||||
|
try {
|
||||||
|
if (this.$parent && this.$parent.message) {
|
||||||
|
console.log('Direct update parent message');
|
||||||
|
this.$parent.message.content = data.result.answer;
|
||||||
|
}
|
||||||
|
} catch(err) {
|
||||||
|
console.error('Error updating parent message:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit event to parent met alle relevante data
|
||||||
|
this.$emit('specialist-complete', {
|
||||||
|
answer: data.result.answer,
|
||||||
|
result: data.result,
|
||||||
|
interactionId: data.interaction_id,
|
||||||
|
taskId: this.taskId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Missing result.answer in specialist complete data:', data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSpecialistError(data) {
|
||||||
|
this.isCompleted = true;
|
||||||
|
this.hasError = true;
|
||||||
|
this.disconnectEventSource();
|
||||||
|
|
||||||
|
// Zet gebruiksvriendelijke foutmelding
|
||||||
|
const errorMessage = "We could not process your request. Please try again later.";
|
||||||
|
this.error = errorMessage;
|
||||||
|
|
||||||
|
// Log de werkelijke fout voor debug doeleinden
|
||||||
|
if (data.Error) {
|
||||||
|
console.error('Specialist Error:', data.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit error event naar parent
|
||||||
|
this.$emit('specialist-error', {
|
||||||
|
message: errorMessage,
|
||||||
|
originalError: data.Error,
|
||||||
|
taskId: this.taskId
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleError(event) {
|
||||||
|
console.error('SSE Error event:', event);
|
||||||
|
this.error = 'Er is een fout opgetreden bij het verwerken van updates.';
|
||||||
|
|
||||||
|
// Probeer parse van foutgegevens
|
||||||
|
try {
|
||||||
|
const errorData = JSON.parse(event.data);
|
||||||
|
if (errorData && errorData.message) {
|
||||||
|
this.error = errorData.message;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Blijf bij algemene foutmelding als parsing mislukt
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleExpand() {
|
||||||
|
this.isExpanded = !this.isExpanded;
|
||||||
|
|
||||||
|
if (this.isExpanded) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const container = this.$refs.progressContainer;
|
||||||
|
if (container) {
|
||||||
|
container.scrollTop = container.scrollHeight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div class="progress-tracker" :class="{ 'expanded': isExpanded, 'completed': isCompleted && !hasError, 'error': error || hasError }">
|
||||||
|
<div
|
||||||
|
class="progress-header"
|
||||||
|
@click="toggleExpand"
|
||||||
|
:title="isExpanded ? 'Inklappen' : 'Uitklappen voor volledige voortgang'"
|
||||||
|
>
|
||||||
|
<div class="progress-title">
|
||||||
|
<span v-if="connecting" class="spinner"></span>
|
||||||
|
<span v-else-if="error" class="status-icon error">✗</span>
|
||||||
|
<span v-else-if="isCompleted" class="status-icon completed">✓</span>
|
||||||
|
<span v-else class="status-icon in-progress"></span>
|
||||||
|
<span v-if="error">Fout bij verwerking</span>
|
||||||
|
<span v-else-if="isCompleted">Verwerking voltooid</span>
|
||||||
|
<span v-else>Bezig met redeneren...</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-toggle">
|
||||||
|
{{ isExpanded ? '▲' : '▼' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="progress-error">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref="progressContainer"
|
||||||
|
class="progress-content"
|
||||||
|
:class="{ 'single-line': !isExpanded }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(line, index) in displayLines"
|
||||||
|
:key="index"
|
||||||
|
class="progress-line"
|
||||||
|
>
|
||||||
|
{{ line }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
10
eveai_chat_client/static/js/components/TypingIndicator.js
Normal file
10
eveai_chat_client/static/js/components/TypingIndicator.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export const TypingIndicator = {
|
||||||
|
name: 'TypingIndicator',
|
||||||
|
template: `
|
||||||
|
<div class="typing-indicator">
|
||||||
|
<div class="typing-dot"></div>
|
||||||
|
<div class="typing-dot"></div>
|
||||||
|
<div class="typing-dot"></div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<title>{% block title %}EveAI Chat{% endblock %}</title>
|
<title>{% block title %}EveAI Chat{% endblock %}</title>
|
||||||
|
|
||||||
<!-- CSS -->
|
<!-- CSS -->
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/chat.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/chat.css') }}">
|
||||||
|
|
||||||
<!-- Vue.js -->
|
<!-- Vue.js -->
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||||
@@ -99,9 +99,9 @@
|
|||||||
|
|
||||||
.chat-container {
|
.chat-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 20px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -129,7 +129,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
Vue.createApp({
|
// Create Vue app and make it available globally
|
||||||
|
window.__vueApp = Vue.createApp({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
explanation: `{{ customisation.sidebar_markdown|default('') }}`
|
explanation: `{{ customisation.sidebar_markdown|default('') }}`
|
||||||
@@ -148,7 +149,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).mount('#app');
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
|
|||||||
@@ -1,214 +1,43 @@
|
|||||||
|
<!-- chat.html - Clean componentized template -->
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Chat{% endblock %}
|
{% block title %}{{ tenant_make.name|default('EveAI') }} - AI Chat{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<!-- Chat specific CSS -->
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/chat-components.css') }}">
|
||||||
|
|
||||||
|
<!-- Pass server data to JavaScript -->
|
||||||
|
<script>
|
||||||
|
// Definieer chatConfig voordat componenten worden geladen
|
||||||
|
window.chatConfig = {
|
||||||
|
explanation: `{{ customisation.sidebar_markdown|default('') }}`,
|
||||||
|
conversationId: '{{ conversation_id|default("default") }}',
|
||||||
|
messages: {{ messages|tojson|safe }},
|
||||||
|
settings: {
|
||||||
|
maxMessageLength: {{ settings.max_message_length|default(2000) }},
|
||||||
|
allowFileUpload: {{ settings.allow_file_upload|default('true')|lower }},
|
||||||
|
allowVoiceMessage: {{ settings.allow_voice_message|default('false')|lower }},
|
||||||
|
autoScroll: {{ settings.auto_scroll|default('true')|lower }},
|
||||||
|
allowReactions: {{ settings.allow_reactions|default('true')|lower }}
|
||||||
|
},
|
||||||
|
apiPrefix: '{{ request.headers.get("X-Forwarded-Prefix", "") }}'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Debug info om te controleren of chatConfig correct is ingesteld
|
||||||
|
console.log('Chat configuration initialized:', window.chatConfig);
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="chat-container">
|
<!-- Gebruik het ChatApp component -->
|
||||||
<!-- Left sidebar with customizable content -->
|
<chat-app>
|
||||||
<div class="sidebar">
|
</chat-app>
|
||||||
{% 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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<!-- Import components and main app -->
|
||||||
// Store session information
|
<!-- Alle componenten worden geladen met absolute paden vanaf /static/ -->
|
||||||
const sessionInfo = {
|
<script type="module" src="{{ url_for('static', filename='assets/js/chat-app.js') }}"></script>
|
||||||
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 %}
|
{% endblock %}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from flask import Blueprint, render_template, request, session, current_app, jsonify, abort
|
from flask import Blueprint, render_template, request, session, current_app, jsonify, Response, stream_with_context
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
from common.extensions import db
|
from common.extensions import db
|
||||||
@@ -8,6 +8,7 @@ from common.models.interaction import SpecialistMagicLink, Specialist, ChatSessi
|
|||||||
from common.services.interaction.specialist_services import SpecialistServices
|
from common.services.interaction.specialist_services import SpecialistServices
|
||||||
from common.utils.database import Database
|
from common.utils.database import Database
|
||||||
from common.utils.chat_utils import get_default_chat_customisation
|
from common.utils.chat_utils import get_default_chat_customisation
|
||||||
|
from common.utils.execution_progress import ExecutionProgressTracker
|
||||||
|
|
||||||
chat_bp = Blueprint('chat_bp', __name__, url_prefix='/chat')
|
chat_bp = Blueprint('chat_bp', __name__, url_prefix='/chat')
|
||||||
|
|
||||||
@@ -82,6 +83,7 @@ def chat(magic_link_code):
|
|||||||
session['specialist'] = specialist.to_dict()
|
session['specialist'] = specialist.to_dict()
|
||||||
session['magic_link'] = specialist_ml.to_dict()
|
session['magic_link'] = specialist_ml.to_dict()
|
||||||
session['tenant_make'] = tenant_make.to_dict()
|
session['tenant_make'] = tenant_make.to_dict()
|
||||||
|
session['chat_session_id'] = SpecialistServices.start_session()
|
||||||
|
|
||||||
# Get customisation options with defaults
|
# Get customisation options with defaults
|
||||||
customisation = get_default_chat_customisation(tenant_make.chat_customisation_options)
|
customisation = get_default_chat_customisation(tenant_make.chat_customisation_options)
|
||||||
@@ -89,11 +91,20 @@ def chat(magic_link_code):
|
|||||||
# Start a new chat session
|
# Start a new chat session
|
||||||
session['chat_session_id'] = SpecialistServices.start_session()
|
session['chat_session_id'] = SpecialistServices.start_session()
|
||||||
|
|
||||||
|
# Define settings for the client
|
||||||
|
settings = {
|
||||||
|
"max_message_length": 2000,
|
||||||
|
"auto_scroll": True
|
||||||
|
}
|
||||||
|
|
||||||
return render_template('chat.html',
|
return render_template('chat.html',
|
||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
tenant_make=tenant_make,
|
tenant_make=tenant_make,
|
||||||
specialist=specialist,
|
specialist=specialist,
|
||||||
customisation=customisation)
|
customisation=customisation,
|
||||||
|
messages=[customisation['welcome_message']],
|
||||||
|
settings=settings
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"Error in chat view: {str(e)}", exc_info=True)
|
current_app.logger.error(f"Error in chat view: {str(e)}", exc_info=True)
|
||||||
@@ -111,10 +122,10 @@ def send_message():
|
|||||||
if not message:
|
if not message:
|
||||||
return jsonify({'error': 'No message provided'}), 400
|
return jsonify({'error': 'No message provided'}), 400
|
||||||
|
|
||||||
tenant_id = session.get('tenant_id')
|
tenant_id = session['tenant']['id']
|
||||||
specialist_id = session.get('specialist_id')
|
specialist_id = session['specialist']['id']
|
||||||
chat_session_id = session.get('chat_session_id')
|
chat_session_id = session.get('chat_session_id')
|
||||||
specialist_args = session.get('specialist_args', {})
|
specialist_args = session['magic_link'].get('specialist_args', {})
|
||||||
|
|
||||||
if not all([tenant_id, specialist_id, chat_session_id]):
|
if not all([tenant_id, specialist_id, chat_session_id]):
|
||||||
return jsonify({'error': 'Session expired or invalid'}), 400
|
return jsonify({'error': 'Session expired or invalid'}), 400
|
||||||
@@ -123,7 +134,11 @@ def send_message():
|
|||||||
Database(tenant_id).switch_schema()
|
Database(tenant_id).switch_schema()
|
||||||
|
|
||||||
# Add user message to specialist arguments
|
# Add user message to specialist arguments
|
||||||
specialist_args['user_message'] = message
|
specialist_args['question'] = message
|
||||||
|
|
||||||
|
current_app.logger.debug(f"Sending message to specialist: {specialist_id} for tenant {tenant_id}\n"
|
||||||
|
f" with args: {specialist_args}\n"
|
||||||
|
f"with session ID: {chat_session_id}")
|
||||||
|
|
||||||
# Execute specialist
|
# Execute specialist
|
||||||
result = SpecialistServices.execute_specialist(
|
result = SpecialistServices.execute_specialist(
|
||||||
@@ -134,12 +149,16 @@ def send_message():
|
|||||||
user_timezone=data.get('timezone', 'UTC')
|
user_timezone=data.get('timezone', 'UTC')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
current_app.logger.debug(f"Specialist execution result: {result}")
|
||||||
|
|
||||||
# Store the task ID for polling
|
# Store the task ID for polling
|
||||||
session['current_task_id'] = result['task_id']
|
session['current_task_id'] = result['task_id']
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'processing',
|
'status': 'processing',
|
||||||
'task_id': result['task_id']
|
'task_id': result['task_id'],
|
||||||
|
'content': 'Verwerking gestart...',
|
||||||
|
'type': 'text'
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -192,3 +211,34 @@ def check_status():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"Error checking status: {str(e)}", exc_info=True)
|
current_app.logger.error(f"Error checking status: {str(e)}", exc_info=True)
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@chat_bp.route('/api/task_progress/<task_id>')
|
||||||
|
def task_progress_stream(task_id):
|
||||||
|
"""
|
||||||
|
Server-Sent Events endpoint voor realtime voortgangsupdates
|
||||||
|
"""
|
||||||
|
current_app.logger.debug(f"Streaming updates for task ID: {task_id}")
|
||||||
|
try:
|
||||||
|
tracker = ExecutionProgressTracker()
|
||||||
|
|
||||||
|
def generate():
|
||||||
|
try:
|
||||||
|
for update in tracker.get_updates(task_id):
|
||||||
|
current_app.logger.debug(f"Progress update: {update}")
|
||||||
|
yield update
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Progress stream error: {str(e)}")
|
||||||
|
yield f"data: {{'error': '{str(e)}'}}\n\n"
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
stream_with_context(generate()),
|
||||||
|
mimetype='text/event-stream',
|
||||||
|
headers={
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'X-Accel-Buffering': 'no'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(f"Failed to start progress stream: {str(e)}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
from os import wait
|
from os import wait
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
|
from time import sleep
|
||||||
from crewai.flow.flow import start, listen, and_
|
from crewai.flow.flow import start, listen, and_
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
@@ -22,7 +22,7 @@ from common.services.interaction.specialist_services import SpecialistServices
|
|||||||
class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
|
class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
|
||||||
"""
|
"""
|
||||||
type: TRAICIE_SELECTION_SPECIALIST
|
type: TRAICIE_SELECTION_SPECIALIST
|
||||||
type_version: 1.0
|
type_version: 1.1
|
||||||
Traicie Selection Specialist Executor class
|
Traicie Selection Specialist Executor class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def type_version(self) -> str:
|
def type_version(self) -> str:
|
||||||
return "1.0"
|
return "1.1"
|
||||||
|
|
||||||
def _config_task_agents(self):
|
def _config_task_agents(self):
|
||||||
self._add_task_agent("traicie_get_competencies_task", "traicie_hr_bp_agent")
|
self._add_task_agent("traicie_get_competencies_task", "traicie_hr_bp_agent")
|
||||||
@@ -67,25 +67,34 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def execute(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult:
|
def execute(self, arguments: SpecialistArguments, formatted_context, citations) -> SpecialistResult:
|
||||||
self.log_tuning("Traicie Role Definition Specialist execution started", {})
|
self.log_tuning("Traicie Selection Specialist execution started", {})
|
||||||
|
|
||||||
flow_inputs = {
|
# flow_inputs = {
|
||||||
"vacancy_text": arguments.vacancy_text,
|
# "vacancy_text": arguments.vacancy_text,
|
||||||
"role_name": arguments.role_name,
|
# "role_name": arguments.role_name,
|
||||||
'role_reference': arguments.role_reference,
|
# '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
|
||||||
|
|
||||||
flow_results = self.flow.kickoff(inputs=flow_inputs)
|
# self.create_selection_specialist(arguments, flow_state.competencies)
|
||||||
|
for i in range(5):
|
||||||
|
sleep(3)
|
||||||
|
self.ept.send_update(self.task_id, "Traicie Selection Specialist Processing", {"name": f"Processing Iteration {i}"})
|
||||||
|
|
||||||
flow_state = self.flow.state
|
# flow_results = asyncio.run(self.flow.kickoff_async(inputs=arguments.model_dump()))
|
||||||
|
# flow_state = self.flow.state
|
||||||
|
# results = RoleDefinitionSpecialistResult.create_for_type(self.type, self.type_version)
|
||||||
|
results = SpecialistResult.create_for_type(self.type, self.type_version,
|
||||||
|
answer=f"Antwoord op uw vraag: {arguments.question}")
|
||||||
|
|
||||||
results = RoleDefinitionSpecialistResult.create_for_type(self.type, self.type_version)
|
self.log_tuning(f"Traicie Selection Specialist execution ended", {"Results": results.model_dump()})
|
||||||
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
|
return results
|
||||||
|
|
||||||
@@ -129,9 +138,7 @@ class SpecialistExecutor(CrewAIBaseSpecialistExecutor):
|
|||||||
current_app.logger.error(f"Error creating selection specialist: {str(e)}")
|
current_app.logger.error(f"Error creating selection specialist: {str(e)}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
SpecialistServices.initialize_specialist(new_specialist.id, "TRAICIE_SELECTION_SPECIALIST", "1.0")
|
SpecialistServices.initialize_specialist(new_specialist.id, self.type, self.type_version)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class RoleDefinitionSpecialistInput(BaseModel):
|
class RoleDefinitionSpecialistInput(BaseModel):
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from datetime import datetime as dt, timezone as tz
|
from datetime import datetime as dt, timezone as tz
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
|
import traceback
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
@@ -240,8 +241,9 @@ def execute_specialist(self, tenant_id: int, specialist_id: int, arguments: Dict
|
|||||||
# Get specialist from database
|
# Get specialist from database
|
||||||
specialist = Specialist.query.get_or_404(specialist_id)
|
specialist = Specialist.query.get_or_404(specialist_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
stacktrace = traceback.format_exc()
|
||||||
ept.send_update(task_id, "EveAI Specialist Error", {'Error': str(e)})
|
ept.send_update(task_id, "EveAI Specialist Error", {'Error': str(e)})
|
||||||
current_app.logger.error(f'execute_specialist: Error executing specialist: {e}')
|
current_app.logger.error(f'execute_specialist: Error executing specialist: {e}\n{stacktrace}')
|
||||||
raise
|
raise
|
||||||
|
|
||||||
with BusinessEvent("Execute Specialist",
|
with BusinessEvent("Execute Specialist",
|
||||||
@@ -272,7 +274,8 @@ def execute_specialist(self, tenant_id: int, specialist_id: int, arguments: Dict
|
|||||||
retriever_args=raw_arguments.get('retriever_arguments', {})
|
retriever_args=raw_arguments.get('retriever_arguments', {})
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
current_app.logger.error(f'execute_specialist: Error preparing arguments: {e}')
|
stacktrace = traceback.format_exc()
|
||||||
|
current_app.logger.error(f'execute_specialist: Error preparing arguments: {e}\n{stacktrace}')
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# Create new interaction record
|
# Create new interaction record
|
||||||
@@ -289,7 +292,8 @@ def execute_specialist(self, tenant_id: int, specialist_id: int, arguments: Dict
|
|||||||
event.update_attribute('interaction_id', new_interaction.id)
|
event.update_attribute('interaction_id', new_interaction.id)
|
||||||
|
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
current_app.logger.error(f'execute_specialist: Error creating interaction: {e}')
|
stacktrace = traceback.format_exc()
|
||||||
|
current_app.logger.error(f'execute_specialist: Error creating interaction: {e}\n{stacktrace}')
|
||||||
raise
|
raise
|
||||||
|
|
||||||
with current_event.create_span("Specialist invocation"):
|
with current_event.create_span("Specialist invocation"):
|
||||||
@@ -314,7 +318,8 @@ def execute_specialist(self, tenant_id: int, specialist_id: int, arguments: Dict
|
|||||||
db.session.add(new_interaction)
|
db.session.add(new_interaction)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
current_app.logger.error(f'execute_specialist: Error updating interaction: {e}')
|
stacktrace = traceback.format_exc()
|
||||||
|
current_app.logger.error(f'execute_specialist: Error updating interaction: {e}\n{stacktrace}')
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# Now that we have a complete interaction with an answer, add it to the cache
|
# Now that we have a complete interaction with an answer, add it to the cache
|
||||||
@@ -330,14 +335,16 @@ def execute_specialist(self, tenant_id: int, specialist_id: int, arguments: Dict
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
stacktrace = traceback.format_exc()
|
||||||
ept.send_update(task_id, "EveAI Specialist Error", {'Error': str(e)})
|
ept.send_update(task_id, "EveAI Specialist Error", {'Error': str(e)})
|
||||||
current_app.logger.error(f'execute_specialist: Error executing specialist: {e}')
|
current_app.logger.error(f'execute_specialist: Error executing specialist: {e}\n{stacktrace}')
|
||||||
new_interaction.processing_error = str(e)[:255]
|
new_interaction.processing_error = str(e)[:255]
|
||||||
try:
|
try:
|
||||||
db.session.add(new_interaction)
|
db.session.add(new_interaction)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
current_app.logger.error(f'execute_specialist: Error updating interaction: {e}')
|
stacktrace = traceback.format_exc()
|
||||||
|
current_app.logger.error(f'execute_specialist: Error updating interaction: {e}\n{stacktrace}')
|
||||||
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user