- verbeteringen client
- removal of eveai_chat
This commit is contained in:
@@ -1,6 +1,18 @@
|
|||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
name: "Chat Client Customisation"
|
name: "Chat Client Customisation"
|
||||||
configuration:
|
configuration:
|
||||||
|
"sidebar_markdown":
|
||||||
|
name: "Sidebar Markdown"
|
||||||
|
description: "Sidebar Markdown-formatted Text"
|
||||||
|
type: "text"
|
||||||
|
required: false
|
||||||
|
"reasoning_visibility":
|
||||||
|
name: "Reasoning Visibility"
|
||||||
|
description: "Level of Reasoning showing in Chat Client"
|
||||||
|
type: "enum"
|
||||||
|
allowed_values: ["No Visibility", "Detailed Visibility"]
|
||||||
|
default: "Detailed Visibility"
|
||||||
|
required: false
|
||||||
"primary_color":
|
"primary_color":
|
||||||
name: "Primary Color"
|
name: "Primary Color"
|
||||||
description: "Primary Color"
|
description: "Primary Color"
|
||||||
@@ -51,11 +63,6 @@ configuration:
|
|||||||
description: "End Color for the gradient in the Chat Area"
|
description: "End Color for the gradient in the Chat Area"
|
||||||
type: "color"
|
type: "color"
|
||||||
required: false
|
required: false
|
||||||
"sidebar_markdown":
|
|
||||||
name: "Sidebar Markdown"
|
|
||||||
description: "Sidebar Markdown-formatted Text"
|
|
||||||
type: "text"
|
|
||||||
required: false
|
|
||||||
metadata:
|
metadata:
|
||||||
author: "Josako"
|
author: "Josako"
|
||||||
date_added: "2024-06-06"
|
date_added: "2024-06-06"
|
||||||
|
|||||||
@@ -144,40 +144,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- eveai-network
|
- eveai-network
|
||||||
|
|
||||||
# eveai_chat:
|
|
||||||
# image: josakola/eveai_chat:latest
|
|
||||||
# build:
|
|
||||||
# context: ..
|
|
||||||
# dockerfile: ./docker/eveai_chat/Dockerfile
|
|
||||||
# platforms:
|
|
||||||
# - linux/amd64
|
|
||||||
# - linux/arm64
|
|
||||||
# ports:
|
|
||||||
# - 5002:5002
|
|
||||||
# environment:
|
|
||||||
# <<: *common-variables
|
|
||||||
# COMPONENT_NAME: eveai_chat
|
|
||||||
# volumes:
|
|
||||||
# - ../eveai_chat:/app/eveai_chat
|
|
||||||
# - ../common:/app/common
|
|
||||||
# - ../config:/app/config
|
|
||||||
# - ../scripts:/app/scripts
|
|
||||||
# - ../patched_packages:/app/patched_packages
|
|
||||||
# - ./eveai_logs:/app/logs
|
|
||||||
# depends_on:
|
|
||||||
# db:
|
|
||||||
# condition: service_healthy
|
|
||||||
# redis:
|
|
||||||
# condition: service_healthy
|
|
||||||
# healthcheck:
|
|
||||||
# test: [ "CMD", "curl", "-f", "http://localhost:5002/healthz/ready" ] # Adjust based on your health endpoint
|
|
||||||
# interval: 30s
|
|
||||||
# timeout: 1s
|
|
||||||
# retries: 3
|
|
||||||
# start_period: 30s
|
|
||||||
# networks:
|
|
||||||
# - eveai-network
|
|
||||||
|
|
||||||
eveai_chat_client:
|
eveai_chat_client:
|
||||||
image: josakola/eveai_chat_client:latest
|
image: josakola/eveai_chat_client:latest
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
ARG PYTHON_VERSION=3.12.7
|
|
||||||
FROM python:${PYTHON_VERSION}-slim as base
|
|
||||||
|
|
||||||
# Prevents Python from writing pyc files.
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
|
||||||
|
|
||||||
# Keeps Python from buffering stdout and stderr to avoid situations where
|
|
||||||
# the application crashes without emitting any logs due to buffering.
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
|
||||||
|
|
||||||
# Create directory for patched packages and set permissions
|
|
||||||
RUN mkdir -p /app/patched_packages && \
|
|
||||||
chmod 777 /app/patched_packages
|
|
||||||
|
|
||||||
# Ensure patches are applied to the application.
|
|
||||||
ENV PYTHONPATH=/app/patched_packages:$PYTHONPATH
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Create a non-privileged user that the app will run under.
|
|
||||||
# See https://docs.docker.com/go/dockerfile-user-best-practices/
|
|
||||||
ARG UID=10001
|
|
||||||
RUN adduser \
|
|
||||||
--disabled-password \
|
|
||||||
--gecos "" \
|
|
||||||
--home "/nonexistent" \
|
|
||||||
--shell "/bin/bash" \
|
|
||||||
--no-create-home \
|
|
||||||
--uid "${UID}" \
|
|
||||||
appuser
|
|
||||||
|
|
||||||
# Install necessary packages and build tools
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
build-essential \
|
|
||||||
gcc \
|
|
||||||
postgresql-client \
|
|
||||||
curl \
|
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Create logs directory and set permissions
|
|
||||||
RUN mkdir -p /app/logs && chown -R appuser:appuser /app/logs
|
|
||||||
|
|
||||||
# Download dependencies as a separate step to take advantage of Docker's caching.
|
|
||||||
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
|
|
||||||
# Leverage a bind mount to requirements.txt to avoid having to copy them into
|
|
||||||
# into this layer.
|
|
||||||
|
|
||||||
COPY requirements.txt /app/
|
|
||||||
RUN python -m pip install -r requirements.txt
|
|
||||||
|
|
||||||
# Copy the source code into the container.
|
|
||||||
COPY eveai_chat /app/eveai_chat
|
|
||||||
COPY common /app/common
|
|
||||||
COPY config /app/config
|
|
||||||
COPY scripts /app/scripts
|
|
||||||
COPY patched_packages /app/patched_packages
|
|
||||||
|
|
||||||
# Set permissions for entrypoint script
|
|
||||||
RUN chmod 777 /app/scripts/entrypoint.sh
|
|
||||||
|
|
||||||
# Set ownership of the application directory to the non-privileged user
|
|
||||||
RUN chown -R appuser:appuser /app
|
|
||||||
|
|
||||||
# Expose the port that the application listens on.
|
|
||||||
EXPOSE 5002
|
|
||||||
|
|
||||||
# Set entrypoint and command
|
|
||||||
ENTRYPOINT ["/app/scripts/entrypoint.sh"]
|
|
||||||
CMD ["/app/scripts/start_eveai_chat.sh"]
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import logging
|
|
||||||
import logging.config
|
|
||||||
from flask import Flask, jsonify, request
|
|
||||||
import os
|
|
||||||
|
|
||||||
from flask_jwt_extended import verify_jwt_in_request, get_jwt_identity
|
|
||||||
|
|
||||||
from common.extensions import db, socketio, jwt, cors, session, simple_encryption, metrics
|
|
||||||
from config.logging_config import LOGGING
|
|
||||||
from eveai_chat.socket_handlers import chat_handler
|
|
||||||
from common.utils.cors_utils import create_cors_after_request, get_allowed_origins
|
|
||||||
from common.utils.celery_utils import make_celery, init_celery
|
|
||||||
from config.config import get_config
|
|
||||||
|
|
||||||
|
|
||||||
def create_app(config_file=None):
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
environment = os.getenv('FLASK_ENV', 'development')
|
|
||||||
|
|
||||||
match environment:
|
|
||||||
case 'development':
|
|
||||||
app.config.from_object(get_config('dev'))
|
|
||||||
case 'production':
|
|
||||||
app.config.from_object(get_config('prod'))
|
|
||||||
case _:
|
|
||||||
app.config.from_object(get_config('dev'))
|
|
||||||
|
|
||||||
app.config['SESSION_KEY_PREFIX'] = 'eveai_chat_'
|
|
||||||
|
|
||||||
logging.config.dictConfig(LOGGING)
|
|
||||||
register_extensions(app)
|
|
||||||
|
|
||||||
app.celery = make_celery(app.name, app.config)
|
|
||||||
init_celery(app.celery, app)
|
|
||||||
|
|
||||||
@app.before_request
|
|
||||||
def check_cors():
|
|
||||||
if request.method == 'OPTIONS':
|
|
||||||
app.logger.debug("Handling OPTIONS request")
|
|
||||||
return '', 200 # Allow OPTIONS to pass through
|
|
||||||
|
|
||||||
origin = request.headers.get('Origin')
|
|
||||||
if not origin:
|
|
||||||
return # Not a CORS request
|
|
||||||
|
|
||||||
# Get tenant ID from request
|
|
||||||
if verify_jwt_in_request():
|
|
||||||
tenant_id = get_jwt_identity()
|
|
||||||
if not tenant_id:
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check if origin is allowed for this tenant
|
|
||||||
allowed_origins = get_allowed_origins(tenant_id)
|
|
||||||
|
|
||||||
if origin not in allowed_origins:
|
|
||||||
app.logger.warning(f'Origin {origin} not allowed for tenant {tenant_id}')
|
|
||||||
return {'error': 'Origin not allowed'}, 403
|
|
||||||
|
|
||||||
app.logger.info("EveAI Chat Server Started Successfully")
|
|
||||||
app.logger.info("-------------------------------------------------------------------------------------------------")
|
|
||||||
return app
|
|
||||||
|
|
||||||
|
|
||||||
def register_extensions(app):
|
|
||||||
db.init_app(app)
|
|
||||||
socketio.init_app(app,
|
|
||||||
message_queue=app.config.get('SOCKETIO_MESSAGE_QUEUE'),
|
|
||||||
cors_allowed_origins=app.config.get('SOCKETIO_CORS_ALLOWED_ORIGINS'),
|
|
||||||
async_mode=app.config.get('SOCKETIO_ASYNC_MODE'),
|
|
||||||
logger=app.config.get('SOCKETIO_LOGGER'),
|
|
||||||
engineio_logger=app.config.get('SOCKETIO_ENGINEIO_LOGGER'),
|
|
||||||
path='/socket.io/',
|
|
||||||
ping_timeout=app.config.get('SOCKETIO_PING_TIMEOUT'),
|
|
||||||
ping_interval=app.config.get('SOCKETIO_PING_INTERVAL'),
|
|
||||||
)
|
|
||||||
jwt.init_app(app)
|
|
||||||
simple_encryption.init_app(app)
|
|
||||||
metrics.init_app(app)
|
|
||||||
|
|
||||||
# Cors setup
|
|
||||||
cors.init_app(app, resources={
|
|
||||||
r"/*": { # Make sure this matches your setup
|
|
||||||
"origins": "*",
|
|
||||||
"methods": ["GET", "POST", "PUT", "OPTIONS"],
|
|
||||||
"allow_headers": ["Content-Type", "Authorization", "X-Requested-With"],
|
|
||||||
"expose_headers": ["Content-Length", "Content-Range"],
|
|
||||||
"supports_credentials": True,
|
|
||||||
"max_age": 1728000,
|
|
||||||
"allow_credentials": True
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
session.init_app(app)
|
|
||||||
|
|
||||||
|
|
||||||
def register_blueprints(app):
|
|
||||||
from views.healthz_views import healthz_bp
|
|
||||||
app.register_blueprint(healthz_bp)
|
|
||||||
@@ -1,358 +0,0 @@
|
|||||||
import uuid
|
|
||||||
from functools import wraps
|
|
||||||
|
|
||||||
from flask_jwt_extended import create_access_token, get_jwt_identity, verify_jwt_in_request, decode_token
|
|
||||||
from flask_socketio import emit, disconnect, join_room, leave_room
|
|
||||||
from flask import current_app, request, session
|
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from prometheus_client import Counter, Histogram
|
|
||||||
from time import time
|
|
||||||
import re
|
|
||||||
|
|
||||||
from common.extensions import socketio, db, simple_encryption
|
|
||||||
from common.models.user import Tenant
|
|
||||||
from common.models.interaction import Interaction
|
|
||||||
from common.utils.celery_utils import current_celery
|
|
||||||
from common.utils.database import Database
|
|
||||||
from common.utils.token_validation import TokenValidator
|
|
||||||
from common.utils.eveai_exceptions import EveAISocketInputException
|
|
||||||
|
|
||||||
# Define custom metrics
|
|
||||||
socketio_message_counter = Counter('socketio_message_count', 'Count of SocketIO messages', ['event_type'])
|
|
||||||
socketio_message_latency = Histogram('socketio_message_latency_seconds', 'Latency of SocketIO message processing', ['event_type'])
|
|
||||||
|
|
||||||
|
|
||||||
class RoomManager:
|
|
||||||
def __init__(self):
|
|
||||||
self.active_rooms = {} # Store active room metadata
|
|
||||||
|
|
||||||
def validate_room_format(self, room_id: str) -> bool:
|
|
||||||
"""Validate room ID format: tenant_id_sessionid_timestamp"""
|
|
||||||
pattern = r'^\d+_[a-zA-Z0-9]+_\d+$'
|
|
||||||
return bool(re.match(pattern, room_id))
|
|
||||||
|
|
||||||
def is_room_active(self, room_id: str) -> bool:
|
|
||||||
return room_id in self.active_rooms
|
|
||||||
|
|
||||||
def validate_room_ownership(self, room_id: str, tenant_id: int, token: str) -> bool:
|
|
||||||
if not self.is_room_active(room_id):
|
|
||||||
return False
|
|
||||||
|
|
||||||
room_data = self.active_rooms[room_id]
|
|
||||||
return (room_data['tenant_id'] == tenant_id and
|
|
||||||
room_data['token'] == token)
|
|
||||||
|
|
||||||
def create_room(self, tenant_id: int, token: str) -> str:
|
|
||||||
"""Create new room with metadata"""
|
|
||||||
timestamp = int(datetime.now().timestamp())
|
|
||||||
room_id = f"{tenant_id}_{request.sid}_{timestamp}"
|
|
||||||
|
|
||||||
self.active_rooms[room_id] = {
|
|
||||||
'tenant_id': tenant_id,
|
|
||||||
'token': token,
|
|
||||||
'created_at': datetime.now(),
|
|
||||||
'last_activity': datetime.now()
|
|
||||||
}
|
|
||||||
|
|
||||||
return room_id
|
|
||||||
|
|
||||||
def update_room_activity(self, room_id: str):
|
|
||||||
"""Update room's last activity timestamp"""
|
|
||||||
if room_id in self.active_rooms:
|
|
||||||
self.active_rooms[room_id]['last_activity'] = datetime.now()
|
|
||||||
|
|
||||||
def cleanup_inactive_rooms(self, max_age_hours: int = 1):
|
|
||||||
"""Remove inactive rooms"""
|
|
||||||
now = datetime.now()
|
|
||||||
cutoff = now - timedelta(hours=max_age_hours)
|
|
||||||
|
|
||||||
inactive_rooms = [
|
|
||||||
room_id for room_id, data in self.active_rooms.items()
|
|
||||||
if data['last_activity'] < cutoff
|
|
||||||
]
|
|
||||||
|
|
||||||
for room_id in inactive_rooms:
|
|
||||||
del self.active_rooms[room_id]
|
|
||||||
|
|
||||||
|
|
||||||
room_manager = RoomManager()
|
|
||||||
|
|
||||||
|
|
||||||
# Decorator to measure SocketIO events
|
|
||||||
def track_socketio_event(func):
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
event_type = func.__name__
|
|
||||||
socketio_message_counter.labels(event_type=event_type).inc()
|
|
||||||
start_time = time()
|
|
||||||
result = func(*args, **kwargs)
|
|
||||||
latency = time() - start_time
|
|
||||||
socketio_message_latency.labels(event_type=event_type).observe(latency)
|
|
||||||
return result
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('connect')
|
|
||||||
@track_socketio_event
|
|
||||||
def handle_connect():
|
|
||||||
"""Handle incoming socket connections with enhanced security"""
|
|
||||||
try:
|
|
||||||
current_app.logger.debug('Handle Connection')
|
|
||||||
token = request.args.get('token')
|
|
||||||
if not token:
|
|
||||||
raise ValueError("Missing token")
|
|
||||||
|
|
||||||
current_app.logger.debug(f"Token received: {token}")
|
|
||||||
|
|
||||||
if not token:
|
|
||||||
raise ValueError("Missing token")
|
|
||||||
|
|
||||||
current_app.logger.info(f"Trying to connect with: {token}")
|
|
||||||
|
|
||||||
validator = TokenValidator()
|
|
||||||
validation_result = validator.validate_token(token)
|
|
||||||
|
|
||||||
if not validation_result.is_valid:
|
|
||||||
current_app.logger.error(f"Socket connection failed: {validation_result.error_message}")
|
|
||||||
emit('connect_error', {'error': validation_result.error_message})
|
|
||||||
disconnect()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Create room and setup session
|
|
||||||
room = room_manager.create_room(validation_result.tenant_id, token)
|
|
||||||
join_room(room)
|
|
||||||
|
|
||||||
session['session_id'] = str(uuid.uuid4())
|
|
||||||
session['last_activity'] = datetime.now()
|
|
||||||
session['room'] = room
|
|
||||||
|
|
||||||
# Emit success events
|
|
||||||
emit('connect', {
|
|
||||||
'status': 'Connected',
|
|
||||||
'tenant_id': validation_result.tenant_id,
|
|
||||||
'room': room
|
|
||||||
})
|
|
||||||
emit('authenticated', {'token': token, 'room': room})
|
|
||||||
current_app.logger.info(f"Socket connection succeeded: {token} / {room}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
current_app.logger.error(f"Socket connection failed: {str(e)}")
|
|
||||||
emit('connect_error', {'status': 'Connection Failed'})
|
|
||||||
disconnect()
|
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('rejoin_room')
|
|
||||||
def handle_room_rejoin(data):
|
|
||||||
try:
|
|
||||||
token = data.get('token')
|
|
||||||
tenant_id = data.get('tenant_id')
|
|
||||||
previous_room = data.get('previousRoom')
|
|
||||||
|
|
||||||
validator = TokenValidator()
|
|
||||||
validation_result = validator.validate_token(token, require_session=True)
|
|
||||||
if not validation_result.is_valid:
|
|
||||||
emit('room_rejoin_result', {'success': False, 'error': validation_result.error_message})
|
|
||||||
return
|
|
||||||
|
|
||||||
if not all([token, tenant_id, previous_room]):
|
|
||||||
raise ValueError("Missing required rejoin data")
|
|
||||||
|
|
||||||
# Validate room ownership
|
|
||||||
if not room_manager.validate_room_ownership(previous_room, tenant_id, token):
|
|
||||||
raise ValueError("Invalid room ownership")
|
|
||||||
|
|
||||||
# Rejoin room
|
|
||||||
join_room(previous_room)
|
|
||||||
session['room'] = previous_room
|
|
||||||
room_manager.update_room_activity(previous_room)
|
|
||||||
|
|
||||||
emit('room_rejoin_result', {
|
|
||||||
'success': True,
|
|
||||||
'room': previous_room
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
current_app.logger.error(f'Room rejoin failed: {e}')
|
|
||||||
emit('room_rejoin_result', {
|
|
||||||
'success': False,
|
|
||||||
'error': str(e)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('disconnect')
|
|
||||||
@track_socketio_event
|
|
||||||
def handle_disconnect():
|
|
||||||
room = session.get('room')
|
|
||||||
if room:
|
|
||||||
leave_room(room)
|
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('heartbeat')
|
|
||||||
def handle_heartbeat():
|
|
||||||
last_activity = session.get('last_activity')
|
|
||||||
if datetime.now() - last_activity > current_app.config.get('SOCKETIO_MAX_IDLE_TIME'):
|
|
||||||
disconnect()
|
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('user_message')
|
|
||||||
def handle_message(data):
|
|
||||||
current_app.logger.debug(f"SocketIO: Received message: {data}")
|
|
||||||
try:
|
|
||||||
validator = TokenValidator()
|
|
||||||
validation_result = validator.validate_token(data.get('token'))
|
|
||||||
|
|
||||||
if not validation_result.is_valid:
|
|
||||||
emit('error', {'message': validation_result.error_message})
|
|
||||||
return
|
|
||||||
|
|
||||||
current_app.logger.debug(f"SocketIO: token validated: {validation_result}")
|
|
||||||
|
|
||||||
room = session.get('room')
|
|
||||||
current_app.logger.debug(f"SocketIO: Room in session: {room}, Room in arguments: {data.get('room')}")
|
|
||||||
|
|
||||||
current_app.logger.debug(f"SocketIO: Room: {room}")
|
|
||||||
if not room or not room_manager.is_room_active(room):
|
|
||||||
raise Exception("Invalid or inactive room")
|
|
||||||
current_app.logger.debug(f"SocketIO: Room active: {room}")
|
|
||||||
|
|
||||||
if not room_manager.validate_room_ownership(room, data['tenant_id'], data['token']):
|
|
||||||
raise Exception("Room ownership validation failed")
|
|
||||||
current_app.logger.debug(f"SocketIO: Room ownership validated: {room}")
|
|
||||||
|
|
||||||
room_manager.update_room_activity(room)
|
|
||||||
current_app.logger.debug(f"SocketIO: Room activity updated: {room}")
|
|
||||||
session['last_activity'] = datetime.now()
|
|
||||||
current_tenant_id = validate_incoming_data(data)
|
|
||||||
|
|
||||||
current_app.logger.debug(f"SocketIO: Incoming data validated: {current_tenant_id}")
|
|
||||||
|
|
||||||
# Offload actual processing of question
|
|
||||||
task = current_celery.send_task('execute_specialist',
|
|
||||||
queue='llm_interactions',
|
|
||||||
args=[
|
|
||||||
current_tenant_id,
|
|
||||||
data['specialist_id'],
|
|
||||||
data['arguments'],
|
|
||||||
session['session_id'],
|
|
||||||
data['timezone'],
|
|
||||||
room
|
|
||||||
])
|
|
||||||
response = {
|
|
||||||
'tenantId': current_tenant_id,
|
|
||||||
'message': f'Processing question ... Session ID = {session["session_id"]}',
|
|
||||||
'taskId': task.id,
|
|
||||||
'room': room,
|
|
||||||
}
|
|
||||||
current_app.logger.debug(f"SocketIO: Sent response {response}")
|
|
||||||
emit('bot_response', response, room=room)
|
|
||||||
except Exception as e:
|
|
||||||
current_app.logger.error(f'SocketIO: Message handling failed: {str(e)}')
|
|
||||||
emit('error', {'message': 'Failed to process message'}, room=room)
|
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('check_task_status')
|
|
||||||
def check_task_status(data):
|
|
||||||
current_app.logger.debug(f'SocketIO: Checking Task Status ... {data}')
|
|
||||||
|
|
||||||
validator = TokenValidator()
|
|
||||||
validation_result = validator.validate_token(data.get('token'))
|
|
||||||
|
|
||||||
if not validation_result.is_valid:
|
|
||||||
emit('feedback_received', {'status': 'error', 'error': validation_result.error_message})
|
|
||||||
return
|
|
||||||
|
|
||||||
task_id = data.get('task_id')
|
|
||||||
room = session.get('room')
|
|
||||||
if not task_id:
|
|
||||||
emit('task_status', {'status': 'error', 'message': 'Missing task ID'}, room=room)
|
|
||||||
return
|
|
||||||
|
|
||||||
task_result = current_celery.AsyncResult(task_id)
|
|
||||||
if task_result.state == 'PENDING':
|
|
||||||
emit('task_status', {'status': 'pending', 'taskId': task_id}, room=room)
|
|
||||||
elif task_result.state == 'SUCCESS':
|
|
||||||
result = task_result.result
|
|
||||||
current_app.logger.debug(f'SocketIO: Task {task_id} returned: {result}')
|
|
||||||
|
|
||||||
# Access the result structure correctly
|
|
||||||
specialist_result = result['result'] # This contains the SpecialistResult model_dump
|
|
||||||
response = {
|
|
||||||
'status': 'success',
|
|
||||||
'taskId': task_id,
|
|
||||||
'results': {
|
|
||||||
'answer': specialist_result.get('answer'),
|
|
||||||
'citations': specialist_result.get('citations', []),
|
|
||||||
'insufficient_info': specialist_result.get('insufficient_info', False)
|
|
||||||
},
|
|
||||||
'interaction_id': result['interaction_id'],
|
|
||||||
'room': room
|
|
||||||
}
|
|
||||||
emit('task_status', response, room=room)
|
|
||||||
else:
|
|
||||||
current_app.logger.error(f'SocketIO: Task {task_id} has failed. Error: {task_result.info}')
|
|
||||||
emit('task_status', {'status': task_result.state, 'message': str(task_result.info)}, room=room)
|
|
||||||
|
|
||||||
@socketio.on('feedback')
|
|
||||||
def handle_feedback(data):
|
|
||||||
current_app.logger.debug(f'SocketIO: Received feedback: {data}')
|
|
||||||
try:
|
|
||||||
validator = TokenValidator()
|
|
||||||
validation_result = validator.validate_token(data.get('token'))
|
|
||||||
|
|
||||||
if not validation_result.is_valid:
|
|
||||||
emit('feedback_received', {'status': 'error', 'error': validation_result.error_message})
|
|
||||||
return
|
|
||||||
|
|
||||||
current_tenant_id = validate_incoming_data(data)
|
|
||||||
|
|
||||||
interaction_id = data.get('interactionId')
|
|
||||||
feedback = data.get('feedback') # 'up' or 'down'
|
|
||||||
|
|
||||||
Database(current_tenant_id).switch_schema()
|
|
||||||
|
|
||||||
interaction = Interaction.query.get_or_404(interaction_id)
|
|
||||||
interaction.appreciation = 0 if feedback == 'down' else 100
|
|
||||||
|
|
||||||
room = session.get('room')
|
|
||||||
if not room:
|
|
||||||
emit('feedback_received', {'status': 'error', 'message': 'No active room'})
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
db.session.commit()
|
|
||||||
emit('feedback_received', {'status': 'success', 'interaction_id': interaction_id, 'room': room}, room=room)
|
|
||||||
except SQLAlchemyError as e:
|
|
||||||
current_app.logger.error(f'SocketIO: Feedback handling failed: {e}')
|
|
||||||
db.session.rollback()
|
|
||||||
emit('feedback_received', {'status': 'Could not register feedback', 'interaction_id': interaction_id})
|
|
||||||
raise e
|
|
||||||
except Exception as e:
|
|
||||||
current_app.logger.error(f'SocketIO: Feedback handling failed: {e}')
|
|
||||||
disconnect()
|
|
||||||
|
|
||||||
|
|
||||||
def validate_api_key(tenant_id, api_key):
|
|
||||||
tenant = Tenant.query.get_or_404(tenant_id)
|
|
||||||
decrypted_api_key = simple_encryption.decrypt_api_key(tenant.encrypted_chat_api_key)
|
|
||||||
|
|
||||||
return decrypted_api_key == api_key
|
|
||||||
|
|
||||||
|
|
||||||
def validate_incoming_data(data):
|
|
||||||
current_app.logger.debug(f'SocketIO: Validating incoming data: {data}')
|
|
||||||
token = data.get('token')
|
|
||||||
if not token:
|
|
||||||
raise EveAISocketInputException("SocketIO: Missing token in input")
|
|
||||||
|
|
||||||
decoded_token = decode_token(token)
|
|
||||||
if not decoded_token:
|
|
||||||
raise EveAISocketInputException("SocketIO: Invalid token in input")
|
|
||||||
|
|
||||||
current_app.logger.debug(f'SocketIO: Decoded token: {decoded_token}')
|
|
||||||
|
|
||||||
current_tenant_id = decoded_token.get('sub')
|
|
||||||
|
|
||||||
if not current_tenant_id:
|
|
||||||
raise EveAISocketInputException("SocketIO: Missing tenant_id (sub) in input")
|
|
||||||
|
|
||||||
return current_tenant_id
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Chat Client</title>
|
|
||||||
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Chat Client</h1>
|
|
||||||
<button onclick="registerClient()">Register Client</button>
|
|
||||||
<div>
|
|
||||||
<input type="text" id="message" placeholder="Enter your message">
|
|
||||||
<button onclick="sendMessage()">Send Message</button>
|
|
||||||
</div>
|
|
||||||
<div id="messages"></div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
let socket;
|
|
||||||
|
|
||||||
async function registerClient() {
|
|
||||||
const tenantId = '1';
|
|
||||||
const apiKey = 'EveAI-CHAT-8553-7987-2800-9115-6454';
|
|
||||||
|
|
||||||
const response = await fetch('http://127.0.0.1:5001/chat/register_client', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ tenant_id: tenantId, api_key: apiKey })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
const token = data.token;
|
|
||||||
|
|
||||||
socket = io('http://127.0.0.1:5001/chat', {
|
|
||||||
auth: {
|
|
||||||
token: `Bearer ${token}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('connect', () => {
|
|
||||||
console.log('Connected to server');
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('response', (msg) => {
|
|
||||||
const messagesDiv = document.getElementById('messages');
|
|
||||||
const messageElement = document.createElement('div');
|
|
||||||
messageElement.innerText = `Response: ${msg.data}`;
|
|
||||||
messagesDiv.appendChild(messageElement);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error('Registration failed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendMessage() {
|
|
||||||
const message = document.getElementById('message').value;
|
|
||||||
if (socket) {
|
|
||||||
socket.emit('message', { content: message });
|
|
||||||
} else {
|
|
||||||
console.error('Socket not connected');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Chat Client</title>
|
|
||||||
<link rel="stylesheet" href="css/eveai-chat-style.css">
|
|
||||||
<script src="js/eveai-sdk.js" defer></script>
|
|
||||||
<script src="js/eveai-chat-widget.js" defer></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="chat-container"></div>
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const eveAI = new EveAI('tenant-id', 'EVEAI-CHAT-xxxx-xxxx-xxxx-xxxx-xxxx');
|
|
||||||
eveAI.initializeChat('chat-container');
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
from flask import Blueprint, current_app, request
|
|
||||||
from flask_healthz import HealthError
|
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
|
||||||
from celery.exceptions import TimeoutError as CeleryTimeoutError
|
|
||||||
from common.extensions import db, metrics, minio_client
|
|
||||||
from common.utils.celery_utils import current_celery
|
|
||||||
from eveai_chat.socket_handlers.chat_handler import socketio_message_counter, socketio_message_latency
|
|
||||||
|
|
||||||
healthz_bp = Blueprint('healthz', __name__, url_prefix='/_healthz')
|
|
||||||
|
|
||||||
|
|
||||||
def liveness():
|
|
||||||
try:
|
|
||||||
# Basic check to see if the app is running
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
raise HealthError("Liveness check failed")
|
|
||||||
|
|
||||||
|
|
||||||
def readiness():
|
|
||||||
checks = {
|
|
||||||
"database": check_database(),
|
|
||||||
"celery": check_celery(),
|
|
||||||
# Add more checks as needed
|
|
||||||
}
|
|
||||||
|
|
||||||
if not all(checks.values()):
|
|
||||||
raise HealthError("Readiness check failed")
|
|
||||||
|
|
||||||
|
|
||||||
def check_database():
|
|
||||||
try:
|
|
||||||
# Perform a simple database query
|
|
||||||
db.session.execute("SELECT 1")
|
|
||||||
return True
|
|
||||||
except SQLAlchemyError:
|
|
||||||
current_app.logger.error("Database check failed", exc_info=True)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def check_celery():
|
|
||||||
try:
|
|
||||||
# Send a simple task to Celery
|
|
||||||
result = current_celery.send_task('ping', queue='llm_interactions')
|
|
||||||
response = result.get(timeout=10) # Wait for up to 10 seconds for a response
|
|
||||||
return response == 'pong'
|
|
||||||
except CeleryTimeoutError:
|
|
||||||
current_app.logger.error("Celery check timed out", exc_info=True)
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
current_app.logger.error(f"Celery check failed: {str(e)}", exc_info=True)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@healthz_bp.route('/metrics')
|
|
||||||
@metrics.do_not_track()
|
|
||||||
def prometheus_metrics():
|
|
||||||
return metrics.generate_latest()
|
|
||||||
|
|
||||||
|
|
||||||
def init_healtz(app):
|
|
||||||
app.config.update(
|
|
||||||
HEALTHZ={
|
|
||||||
"live": "healthz_views.liveness",
|
|
||||||
"ready": "healthz_views.readiness",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
# Register SocketIO metrics with Prometheus
|
|
||||||
metrics.register(socketio_message_counter)
|
|
||||||
metrics.register(socketio_message_latency)
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
></progress-tracker>
|
></progress-tracker>
|
||||||
|
|
||||||
<!-- Form data display if available (alleen in user messages) -->
|
<!-- Form data display if available (alleen in user messages) -->
|
||||||
<div v-if="message.formValues && message.sender === 'user'" class="form-display user-form-values">
|
<div v-if="hasMeaningfulFormValues(message)" class="form-display user-form-values">
|
||||||
<dynamic-form
|
<dynamic-form
|
||||||
:form-data="message.formData"
|
:form-data="message.formData"
|
||||||
:form-values="message.formValues"
|
:form-values="message.formValues"
|
||||||
@@ -181,6 +181,39 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
hasMeaningfulFormValues(message) {
|
||||||
|
// Check if message is user message and has formValues
|
||||||
|
if (!message || message.sender !== 'user' || !message.formValues) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if formData exists and has fields
|
||||||
|
if (!message.formData || !message.formData.fields || Object.keys(message.formData.fields).length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if formValues object exists and isn't empty
|
||||||
|
if (!message.formValues || Object.keys(message.formValues).length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any formValues have actual meaningful content
|
||||||
|
const hasActualValues = Object.entries(message.formValues).some(([key, value]) => {
|
||||||
|
// Skip if the field doesn't exist in formData
|
||||||
|
if (!message.formData.fields[key]) return false;
|
||||||
|
|
||||||
|
// Check for meaningful values
|
||||||
|
if (value === null || value === undefined) return false;
|
||||||
|
if (typeof value === 'string' && value.trim() === '') return false;
|
||||||
|
if (typeof value === 'boolean') return true; // Boolean values are always meaningful
|
||||||
|
if (Array.isArray(value) && value.length === 0) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasActualValues;
|
||||||
|
},
|
||||||
|
|
||||||
async handleLanguageChange(event) {
|
async handleLanguageChange(event) {
|
||||||
// Controleer of dit het eerste bericht is in een gesprek met maar één bericht
|
// Controleer of dit het eerste bericht is in een gesprek met maar één bericht
|
||||||
// Dit wordt al afgehandeld door MessageHistory component, dus we hoeven hier niets te doen
|
// Dit wordt al afgehandeld door MessageHistory component, dus we hoeven hier niets te doen
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
<div class="form-title">{{ formData.title || formData.name }}</div>
|
<div class="form-title">{{ formData.title || formData.name }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Form fields -->
|
<!-- Form fields (alleen tonen als NIET read-only) -->
|
||||||
<div class="form-fields">
|
<div v-if="!readOnly" class="form-fields">
|
||||||
<template v-if="Array.isArray(formData.fields)">
|
<template v-if="Array.isArray(formData.fields)">
|
||||||
<form-field
|
<form-field
|
||||||
v-for="field in formData.fields"
|
v-for="field in formData.fields"
|
||||||
|
|||||||
@@ -29,7 +29,31 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hasFormData() {
|
hasFormData() {
|
||||||
return this.formData && this.formData.fields && Object.keys(this.formData.fields).length > 0;
|
// Check if formData and fields exist
|
||||||
|
if (!this.formData || !this.formData.fields || Object.keys(this.formData.fields).length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if formValues exist and contain meaningful data
|
||||||
|
if (!this.formValues || Object.keys(this.formValues).length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any formValues have actual content (not empty, null, or undefined)
|
||||||
|
const hasActualValues = Object.entries(this.formValues).some(([key, value]) => {
|
||||||
|
// Skip if the field doesn't exist in formData
|
||||||
|
if (!this.formData.fields[key]) return false;
|
||||||
|
|
||||||
|
// Check for meaningful values
|
||||||
|
if (value === null || value === undefined) return false;
|
||||||
|
if (typeof value === 'string' && value.trim() === '') return false;
|
||||||
|
if (typeof value === 'boolean') return true; // Boolean values are always meaningful
|
||||||
|
if (Array.isArray(value) && value.length === 0) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasActualValues;
|
||||||
},
|
},
|
||||||
formattedFields() {
|
formattedFields() {
|
||||||
if (!this.hasFormData) return [];
|
if (!this.hasFormData) return [];
|
||||||
@@ -267,7 +291,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* High contrast mode */
|
/* High contrast mode */
|
||||||
@media (prefers-contrast: high) {
|
@media (prefers-contrast: more) {
|
||||||
.form-message {
|
.form-message {
|
||||||
border: 2px solid #000;
|
border: 2px solid #000;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
// Test script to verify LanguageSelector component functionality
|
|
||||||
// This script tests if the component can be imported and has the correct structure
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
console.log('🔍 Testing Vue component refactoring...');
|
|
||||||
|
|
||||||
// Read the LanguageSelector component file
|
|
||||||
const languageSelectorPath = path.join(__dirname, 'eveai_chat_client/static/assets/js/components/LanguageSelector.js');
|
|
||||||
|
|
||||||
if (!fs.existsSync(languageSelectorPath)) {
|
|
||||||
console.error('❌ LanguageSelector.js file not found!');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const componentContent = fs.readFileSync(languageSelectorPath, 'utf8');
|
|
||||||
|
|
||||||
// Test 1: Check that renderComponent method is removed
|
|
||||||
console.log('🔍 Test 1: Checking renderComponent method is removed...');
|
|
||||||
if (componentContent.includes('renderComponent()')) {
|
|
||||||
console.error('❌ FAIL: renderComponent method still exists!');
|
|
||||||
process.exit(1);
|
|
||||||
} else {
|
|
||||||
console.log('✅ PASS: renderComponent method successfully removed');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 2: Check that render() fallback method is removed
|
|
||||||
console.log('🔍 Test 2: Checking render() fallback method is removed...');
|
|
||||||
if (componentContent.includes('render()')) {
|
|
||||||
console.error('❌ FAIL: render() fallback method still exists!');
|
|
||||||
process.exit(1);
|
|
||||||
} else {
|
|
||||||
console.log('✅ PASS: render() fallback method successfully removed');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 3: Check that Vue template exists
|
|
||||||
console.log('🔍 Test 3: Checking Vue template exists...');
|
|
||||||
if (componentContent.includes('template:') && componentContent.includes('v-model="selectedLanguage"')) {
|
|
||||||
console.log('✅ PASS: Vue template with v-model exists');
|
|
||||||
} else {
|
|
||||||
console.error('❌ FAIL: Vue template missing or incorrect!');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 4: Check that component has proper Vue structure
|
|
||||||
console.log('🔍 Test 4: Checking Vue component structure...');
|
|
||||||
const hasName = componentContent.includes("name: 'LanguageSelector'");
|
|
||||||
const hasProps = componentContent.includes('props:');
|
|
||||||
const hasData = componentContent.includes('data()');
|
|
||||||
const hasMethods = componentContent.includes('methods:');
|
|
||||||
const hasMounted = componentContent.includes('mounted()');
|
|
||||||
|
|
||||||
if (hasName && hasProps && hasData && hasMethods && hasMounted) {
|
|
||||||
console.log('✅ PASS: Component has proper Vue structure');
|
|
||||||
} else {
|
|
||||||
console.error('❌ FAIL: Component missing Vue structure elements');
|
|
||||||
console.error(` - name: ${hasName}`);
|
|
||||||
console.error(` - props: ${hasProps}`);
|
|
||||||
console.error(` - data: ${hasData}`);
|
|
||||||
console.error(` - methods: ${hasMethods}`);
|
|
||||||
console.error(` - mounted: ${hasMounted}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 5: Check that chat-client.js fallback logic is removed
|
|
||||||
console.log('🔍 Test 5: Checking chat-client.js fallback logic is removed...');
|
|
||||||
const chatClientPath = path.join(__dirname, 'nginx/frontend_src/js/chat-client.js');
|
|
||||||
|
|
||||||
if (!fs.existsSync(chatClientPath)) {
|
|
||||||
console.error('❌ chat-client.js file not found!');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatClientContent = fs.readFileSync(chatClientPath, 'utf8');
|
|
||||||
|
|
||||||
if (chatClientContent.includes('LanguageSelector.renderComponent') ||
|
|
||||||
chatClientContent.includes('Noodoplossing: Handmatige DOM manipulatie')) {
|
|
||||||
console.error('❌ FAIL: Fallback logic still exists in chat-client.js!');
|
|
||||||
process.exit(1);
|
|
||||||
} else {
|
|
||||||
console.log('✅ PASS: Fallback logic successfully removed from chat-client.js');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 6: Check build output exists
|
|
||||||
console.log('🔍 Test 6: Checking build output exists...');
|
|
||||||
const chatClientBundlePath = path.join(__dirname, 'nginx/static/dist/chat-client.js');
|
|
||||||
|
|
||||||
if (!fs.existsSync(chatClientBundlePath)) {
|
|
||||||
console.error('❌ FAIL: chat-client.js bundle not found!');
|
|
||||||
process.exit(1);
|
|
||||||
} else {
|
|
||||||
const bundleStats = fs.statSync(chatClientBundlePath);
|
|
||||||
console.log(`✅ PASS: chat-client.js bundle exists (${Math.round(bundleStats.size / 1024)} KB)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 7: Check ChatInput component
|
|
||||||
console.log('🔍 Test 7: Checking ChatInput component refactoring...');
|
|
||||||
const chatInputPath = path.join(__dirname, 'eveai_chat_client/static/assets/js/components/ChatInput.js');
|
|
||||||
|
|
||||||
if (!fs.existsSync(chatInputPath)) {
|
|
||||||
console.error('❌ ChatInput.js file not found!');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatInputContent = fs.readFileSync(chatInputPath, 'utf8');
|
|
||||||
|
|
||||||
if (chatInputContent.includes('renderComponent(container, props, app)')) {
|
|
||||||
console.error('❌ FAIL: ChatInput renderComponent method still exists!');
|
|
||||||
process.exit(1);
|
|
||||||
} else {
|
|
||||||
console.log('✅ PASS: ChatInput renderComponent method successfully removed');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check ChatInput has proper Vue template
|
|
||||||
if (chatInputContent.includes('template:') && chatInputContent.includes('v-model="localMessage"')) {
|
|
||||||
console.log('✅ PASS: ChatInput Vue template with v-model exists');
|
|
||||||
} else {
|
|
||||||
console.error('❌ FAIL: ChatInput Vue template missing or incorrect!');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n🎉 All tests passed! LanguageSelector and ChatInput components successfully refactored to use pure Vue templates.');
|
|
||||||
console.log('\n📋 Summary of changes:');
|
|
||||||
console.log(' ✅ LanguageSelector: Removed renderComponent() method');
|
|
||||||
console.log(' ✅ LanguageSelector: Removed render() fallback method');
|
|
||||||
console.log(' ✅ LanguageSelector: Removed renderComponent() call from mounted()');
|
|
||||||
console.log(' ✅ ChatInput: Removed renderComponent() method');
|
|
||||||
console.log(' ✅ Removed fallback logic from chat-client.js');
|
|
||||||
console.log(' ✅ Kept clean Vue templates with proper reactivity');
|
|
||||||
console.log(' ✅ Build completed successfully');
|
|
||||||
console.log('\n🔧 Next components to refactor:');
|
|
||||||
console.log(' - MessageHistory.js');
|
|
||||||
console.log(' - ChatMessage.js');
|
|
||||||
console.log(' - TypingIndicator.js');
|
|
||||||
console.log(' - ProgressTracker.js');
|
|
||||||
console.log(' - DynamicForm.js');
|
|
||||||
console.log(' - FormField.js');
|
|
||||||
console.log(' - FormMessage.js');
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script to verify the new list view implementation works correctly.
|
|
||||||
This script tests the specialists view to ensure it returns the expected data format.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Add the project root to the Python path
|
|
||||||
project_root = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
sys.path.insert(0, project_root)
|
|
||||||
|
|
||||||
def test_specialists_view_data_format():
|
|
||||||
"""Test that the specialists view returns data in the expected format for Tabulator."""
|
|
||||||
try:
|
|
||||||
# Import the Flask app and required modules
|
|
||||||
from eveai_app import create_app
|
|
||||||
from common.models.interaction import Specialist
|
|
||||||
from flask import url_for
|
|
||||||
|
|
||||||
# Create the Flask app
|
|
||||||
app = create_app()
|
|
||||||
|
|
||||||
with app.app_context():
|
|
||||||
# Test the data format that would be returned by the specialists view
|
|
||||||
specialists_query = Specialist.query.order_by(Specialist.id)
|
|
||||||
all_specialists = specialists_query.all()
|
|
||||||
|
|
||||||
# Prepare data for Tabulator (same logic as in the view)
|
|
||||||
specialists_data = []
|
|
||||||
for specialist in all_specialists:
|
|
||||||
specialists_data.append({
|
|
||||||
'id': specialist.id,
|
|
||||||
'name': specialist.name,
|
|
||||||
'type': specialist.type,
|
|
||||||
'type_version': specialist.type_version,
|
|
||||||
'active': specialist.active
|
|
||||||
})
|
|
||||||
|
|
||||||
# Column definitions
|
|
||||||
columns = [
|
|
||||||
{'title': 'ID', 'field': 'id', 'type': 'number', 'width': 80},
|
|
||||||
{'title': 'Name', 'field': 'name', 'type': 'text', 'dynamicValues': True},
|
|
||||||
{'title': 'Type', 'field': 'type', 'type': 'text', 'dynamicValues': True},
|
|
||||||
{'title': 'Type Version', 'field': 'type_version', 'type': 'text'},
|
|
||||||
{'title': 'Active', 'field': 'active', 'type': 'boolean', 'formatter': 'tickCross'}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Action definitions
|
|
||||||
actions = [
|
|
||||||
{'value': 'edit_specialist', 'text': 'Edit Specialist', 'class': 'btn-primary', 'requiresSelection': True},
|
|
||||||
{'value': 'execute_specialist', 'text': 'Execute Specialist', 'class': 'btn-primary', 'requiresSelection': True},
|
|
||||||
{'value': 'create_specialist', 'text': 'Register Specialist', 'class': 'btn-success', 'position': 'right', 'requiresSelection': False}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Initial sort configuration
|
|
||||||
initial_sort = [{'column': 'id', 'dir': 'asc'}]
|
|
||||||
|
|
||||||
# Test that data can be serialized to JSON (required for template)
|
|
||||||
try:
|
|
||||||
json.dumps(specialists_data)
|
|
||||||
json.dumps(columns)
|
|
||||||
json.dumps(actions)
|
|
||||||
json.dumps(initial_sort)
|
|
||||||
print("✓ All data structures can be serialized to JSON")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ JSON serialization failed: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Test data structure validation
|
|
||||||
if not isinstance(specialists_data, list):
|
|
||||||
print("✗ specialists_data should be a list")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if specialists_data and not isinstance(specialists_data[0], dict):
|
|
||||||
print("✗ specialists_data items should be dictionaries")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not isinstance(columns, list):
|
|
||||||
print("✗ columns should be a list")
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not isinstance(actions, list):
|
|
||||||
print("✗ actions should be a list")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print(f"✓ Found {len(specialists_data)} specialists")
|
|
||||||
print(f"✓ Configured {len(columns)} columns")
|
|
||||||
print(f"✓ Configured {len(actions)} actions")
|
|
||||||
print("✓ Data structure validation passed")
|
|
||||||
|
|
||||||
# Test that required fields are present in data
|
|
||||||
if specialists_data:
|
|
||||||
required_fields = ['id', 'name', 'type', 'type_version', 'active']
|
|
||||||
sample_data = specialists_data[0]
|
|
||||||
for field in required_fields:
|
|
||||||
if field not in sample_data:
|
|
||||||
print(f"✗ Required field '{field}' missing from data")
|
|
||||||
return False
|
|
||||||
print("✓ All required fields present in data")
|
|
||||||
|
|
||||||
# Test column configuration
|
|
||||||
for column in columns:
|
|
||||||
if 'title' not in column or 'field' not in column:
|
|
||||||
print(f"✗ Column missing required 'title' or 'field': {column}")
|
|
||||||
return False
|
|
||||||
print("✓ Column configuration validation passed")
|
|
||||||
|
|
||||||
# Test action configuration
|
|
||||||
for action in actions:
|
|
||||||
required_action_fields = ['value', 'text']
|
|
||||||
for field in required_action_fields:
|
|
||||||
if field not in action:
|
|
||||||
print(f"✗ Action missing required field '{field}': {action}")
|
|
||||||
return False
|
|
||||||
print("✓ Action configuration validation passed")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Test failed with exception: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
return False
|
|
||||||
|
|
||||||
def test_template_files_exist():
|
|
||||||
"""Test that the required template files exist."""
|
|
||||||
template_files = [
|
|
||||||
'eveai_app/templates/eveai_list_view.html',
|
|
||||||
'eveai_app/templates/interaction/specialists.html'
|
|
||||||
]
|
|
||||||
|
|
||||||
for template_file in template_files:
|
|
||||||
full_path = os.path.join(project_root, template_file)
|
|
||||||
if not os.path.exists(full_path):
|
|
||||||
print(f"✗ Template file missing: {template_file}")
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
print(f"✓ Template file exists: {template_file}")
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Run all tests."""
|
|
||||||
print("Testing new list view implementation...")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
# Test template files
|
|
||||||
print("\n1. Testing template files...")
|
|
||||||
template_test_passed = test_template_files_exist()
|
|
||||||
|
|
||||||
# Test data format
|
|
||||||
print("\n2. Testing data format...")
|
|
||||||
data_test_passed = test_specialists_view_data_format()
|
|
||||||
|
|
||||||
# Summary
|
|
||||||
print("\n" + "=" * 50)
|
|
||||||
if template_test_passed and data_test_passed:
|
|
||||||
print("✓ All tests passed! The list view implementation appears to be working correctly.")
|
|
||||||
return 0
|
|
||||||
else:
|
|
||||||
print("✗ Some tests failed. Please check the implementation.")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
// Test script for SideBar Vue components
|
|
||||||
// This can be run in the browser console when the chat client is loaded
|
|
||||||
|
|
||||||
console.log('Testing SideBar Vue components...');
|
|
||||||
|
|
||||||
// Test if all components are available
|
|
||||||
function testComponentAvailability() {
|
|
||||||
console.log('🧪 Testing component availability...');
|
|
||||||
|
|
||||||
// Check if Vue is available
|
|
||||||
if (!window.Vue || !window.Vue.createApp) {
|
|
||||||
console.error('❌ Vue is not available');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
console.log('✅ Vue is available');
|
|
||||||
|
|
||||||
// Check if chatConfig is available
|
|
||||||
if (!window.chatConfig) {
|
|
||||||
console.error('❌ window.chatConfig is not available');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
console.log('✅ window.chatConfig is available');
|
|
||||||
|
|
||||||
// Check if sidebar container exists
|
|
||||||
const container = document.getElementById('sidebar-container');
|
|
||||||
if (!container) {
|
|
||||||
console.error('❌ #sidebar-container not found in DOM');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
console.log('✅ #sidebar-container found in DOM');
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test sidebar component props
|
|
||||||
function testSidebarProps() {
|
|
||||||
console.log('🧪 Testing sidebar props...');
|
|
||||||
|
|
||||||
const expectedProps = {
|
|
||||||
tenantMake: window.chatConfig.tenantMake || {},
|
|
||||||
explanationText: window.chatConfig.explanation || '',
|
|
||||||
initialLanguage: window.chatConfig.language || 'nl',
|
|
||||||
supportedLanguageDetails: window.chatConfig.supportedLanguageDetails || {},
|
|
||||||
allowedLanguages: window.chatConfig.allowedLanguages || ['nl', 'en', 'fr', 'de'],
|
|
||||||
apiPrefix: window.chatConfig.apiPrefix || ''
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('📋 Expected props:', expectedProps);
|
|
||||||
|
|
||||||
// Validate props
|
|
||||||
if (!expectedProps.tenantMake.name) {
|
|
||||||
console.warn('⚠️ tenantMake.name is empty');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!expectedProps.explanationText) {
|
|
||||||
console.warn('⚠️ explanationText is empty');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!expectedProps.supportedLanguageDetails || Object.keys(expectedProps.supportedLanguageDetails).length === 0) {
|
|
||||||
console.warn('⚠️ supportedLanguageDetails is empty, using defaults');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ Props validation completed');
|
|
||||||
return expectedProps;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test if old sidebar elements are removed
|
|
||||||
function testOldSidebarCleanup() {
|
|
||||||
console.log('🧪 Testing old sidebar cleanup...');
|
|
||||||
|
|
||||||
const oldElements = [
|
|
||||||
'sidebar-explanation',
|
|
||||||
'language-selector-container'
|
|
||||||
];
|
|
||||||
|
|
||||||
let foundOldElements = false;
|
|
||||||
oldElements.forEach(id => {
|
|
||||||
const element = document.getElementById(id);
|
|
||||||
if (element) {
|
|
||||||
console.warn(`⚠️ Old element #${id} still exists in DOM`);
|
|
||||||
foundOldElements = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!foundOldElements) {
|
|
||||||
console.log('✅ No old sidebar elements found - cleanup successful');
|
|
||||||
}
|
|
||||||
|
|
||||||
return !foundOldElements;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test translation functionality
|
|
||||||
async function testTranslationIntegration() {
|
|
||||||
console.log('🧪 Testing translation integration...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Test if translation API is accessible
|
|
||||||
const response = await fetch('/api/translate', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Requested-With': 'XMLHttpRequest'
|
|
||||||
},
|
|
||||||
credentials: 'same-origin',
|
|
||||||
body: JSON.stringify({
|
|
||||||
text: 'Test translation',
|
|
||||||
target_lang: 'nl',
|
|
||||||
context: 'sidebar_test'
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const result = await response.json();
|
|
||||||
console.log('✅ Translation API is accessible:', result.success ? 'Success' : 'Failed');
|
|
||||||
return result.success;
|
|
||||||
} else {
|
|
||||||
console.warn('⚠️ Translation API returned error:', response.status);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('⚠️ Translation API test failed:', error.message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run all tests
|
|
||||||
async function runAllSidebarTests() {
|
|
||||||
console.log('🚀 Starting SideBar component tests...');
|
|
||||||
|
|
||||||
console.log('\n1. Testing component availability...');
|
|
||||||
const availabilityTest = testComponentAvailability();
|
|
||||||
|
|
||||||
console.log('\n2. Testing sidebar props...');
|
|
||||||
const props = testSidebarProps();
|
|
||||||
|
|
||||||
console.log('\n3. Testing old sidebar cleanup...');
|
|
||||||
const cleanupTest = testOldSidebarCleanup();
|
|
||||||
|
|
||||||
console.log('\n4. Testing translation integration...');
|
|
||||||
const translationTest = await testTranslationIntegration();
|
|
||||||
|
|
||||||
console.log('\n📊 Test Results Summary:');
|
|
||||||
console.log(`Component Availability: ${availabilityTest ? '✅' : '❌'}`);
|
|
||||||
console.log(`Props Validation: ✅`);
|
|
||||||
console.log(`Old Sidebar Cleanup: ${cleanupTest ? '✅' : '⚠️'}`);
|
|
||||||
console.log(`Translation Integration: ${translationTest ? '✅' : '⚠️'}`);
|
|
||||||
|
|
||||||
const allPassed = availabilityTest && cleanupTest && translationTest;
|
|
||||||
console.log(`\n🎯 Overall Status: ${allPassed ? '✅ All tests passed!' : '⚠️ Some tests need attention'}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
availability: availabilityTest,
|
|
||||||
cleanup: cleanupTest,
|
|
||||||
translation: translationTest,
|
|
||||||
props: props,
|
|
||||||
overall: allPassed
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export for manual testing
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.testComponentAvailability = testComponentAvailability;
|
|
||||||
window.testSidebarProps = testSidebarProps;
|
|
||||||
window.testOldSidebarCleanup = testOldSidebarCleanup;
|
|
||||||
window.testTranslationIntegration = testTranslationIntegration;
|
|
||||||
window.runAllSidebarTests = runAllSidebarTests;
|
|
||||||
|
|
||||||
console.log('SideBar test functions available:');
|
|
||||||
console.log('- testComponentAvailability()');
|
|
||||||
console.log('- testSidebarProps()');
|
|
||||||
console.log('- testOldSidebarCleanup()');
|
|
||||||
console.log('- testTranslationIntegration()');
|
|
||||||
console.log('- runAllSidebarTests()');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-run if in Node.js environment (for CI/CD)
|
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
|
||||||
module.exports = {
|
|
||||||
testComponentAvailability,
|
|
||||||
testSidebarProps,
|
|
||||||
testOldSidebarCleanup,
|
|
||||||
testTranslationIntegration,
|
|
||||||
runAllSidebarTests
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script to verify that the Tabulator library is properly loaded and the table displays correctly.
|
|
||||||
This script will test the specialists view to ensure the table formatting is fixed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Add the project root to the Python path
|
|
||||||
project_root = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
sys.path.insert(0, project_root)
|
|
||||||
|
|
||||||
def test_tabulator_library_inclusion():
|
|
||||||
"""Test that Tabulator library is included in the templates."""
|
|
||||||
|
|
||||||
# Check head.html for Tabulator CSS
|
|
||||||
head_file = os.path.join(project_root, 'eveai_app/templates/head.html')
|
|
||||||
with open(head_file, 'r') as f:
|
|
||||||
head_content = f.read()
|
|
||||||
|
|
||||||
if 'tabulator' in head_content.lower():
|
|
||||||
print("✓ Tabulator CSS found in head.html")
|
|
||||||
else:
|
|
||||||
print("✗ Tabulator CSS not found in head.html")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Check scripts.html for Tabulator JavaScript
|
|
||||||
scripts_file = os.path.join(project_root, 'eveai_app/templates/scripts.html')
|
|
||||||
with open(scripts_file, 'r') as f:
|
|
||||||
scripts_content = f.read()
|
|
||||||
|
|
||||||
if 'tabulator' in scripts_content.lower():
|
|
||||||
print("✓ Tabulator JavaScript found in scripts.html")
|
|
||||||
else:
|
|
||||||
print("✗ Tabulator JavaScript not found in scripts.html")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def test_list_view_template():
|
|
||||||
"""Test that the list view template has proper column configuration."""
|
|
||||||
|
|
||||||
list_view_file = os.path.join(project_root, 'eveai_app/templates/eveai_list_view.html')
|
|
||||||
with open(list_view_file, 'r') as f:
|
|
||||||
template_content = f.read()
|
|
||||||
|
|
||||||
# Check for key Tabulator configuration elements
|
|
||||||
required_elements = [
|
|
||||||
'_buildColumns',
|
|
||||||
'new Tabulator',
|
|
||||||
'layout: "fitColumns"',
|
|
||||||
'columns:',
|
|
||||||
'data:'
|
|
||||||
]
|
|
||||||
|
|
||||||
for element in required_elements:
|
|
||||||
if element in template_content:
|
|
||||||
print(f"✓ Found required element: {element}")
|
|
||||||
else:
|
|
||||||
print(f"✗ Missing required element: {element}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def test_specialists_template():
|
|
||||||
"""Test that the specialists template properly initializes the list view."""
|
|
||||||
|
|
||||||
specialists_file = os.path.join(project_root, 'eveai_app/templates/interaction/specialists.html')
|
|
||||||
with open(specialists_file, 'r') as f:
|
|
||||||
template_content = f.read()
|
|
||||||
|
|
||||||
# Check for proper initialization
|
|
||||||
required_elements = [
|
|
||||||
'EveAI.ListView.initialize',
|
|
||||||
'specialists-list-view',
|
|
||||||
'data: {{ specialists_data | tojson }}',
|
|
||||||
'columns: {{ columns | tojson }}',
|
|
||||||
'selectable: true'
|
|
||||||
]
|
|
||||||
|
|
||||||
for element in required_elements:
|
|
||||||
if element in template_content:
|
|
||||||
print(f"✓ Found required element: {element}")
|
|
||||||
else:
|
|
||||||
print(f"✗ Missing required element: {element}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def test_column_configuration():
|
|
||||||
"""Test that the column configuration in the view is correct."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Import the view function
|
|
||||||
from eveai_app.views.interaction_views import specialists
|
|
||||||
|
|
||||||
# Test the column configuration structure
|
|
||||||
sample_columns = [
|
|
||||||
{'title': 'ID', 'field': 'id', 'type': 'number', 'width': 80},
|
|
||||||
{'title': 'Name', 'field': 'name', 'type': 'text', 'dynamicValues': True},
|
|
||||||
{'title': 'Type', 'field': 'type', 'type': 'text', 'dynamicValues': True},
|
|
||||||
{'title': 'Type Version', 'field': 'type_version', 'type': 'text'},
|
|
||||||
{'title': 'Active', 'field': 'active', 'type': 'boolean', 'formatter': 'tickCross'}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Validate column structure
|
|
||||||
for i, column in enumerate(sample_columns):
|
|
||||||
if 'title' in column and 'field' in column:
|
|
||||||
print(f"✓ Column {i+1} ({column['title']}) has required title and field")
|
|
||||||
else:
|
|
||||||
print(f"✗ Column {i+1} missing title or field")
|
|
||||||
return False
|
|
||||||
|
|
||||||
print("✓ Column configuration structure is correct")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Error testing column configuration: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Run all tests."""
|
|
||||||
print("Testing Tabulator table formatting fix...")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
# Test Tabulator library inclusion
|
|
||||||
print("\n1. Testing Tabulator library inclusion...")
|
|
||||||
tabulator_test_passed = test_tabulator_library_inclusion()
|
|
||||||
|
|
||||||
# Test list view template
|
|
||||||
print("\n2. Testing list view template...")
|
|
||||||
list_view_test_passed = test_list_view_template()
|
|
||||||
|
|
||||||
# Test specialists template
|
|
||||||
print("\n3. Testing specialists template...")
|
|
||||||
specialists_test_passed = test_specialists_template()
|
|
||||||
|
|
||||||
# Test column configuration
|
|
||||||
print("\n4. Testing column configuration...")
|
|
||||||
column_test_passed = test_column_configuration()
|
|
||||||
|
|
||||||
# Summary
|
|
||||||
print("\n" + "=" * 50)
|
|
||||||
if all([tabulator_test_passed, list_view_test_passed, specialists_test_passed, column_test_passed]):
|
|
||||||
print("✓ All tests passed! The table formatting should now work correctly.")
|
|
||||||
print("\nThe table should now display with proper columns:")
|
|
||||||
print("- ID column (80px width)")
|
|
||||||
print("- Name column (with dynamic filtering)")
|
|
||||||
print("- Type column (with dynamic filtering)")
|
|
||||||
print("- Type Version column")
|
|
||||||
print("- Active column (with tick/cross formatter)")
|
|
||||||
return 0
|
|
||||||
else:
|
|
||||||
print("✗ Some tests failed. Please check the implementation.")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
// Simple test script for the updated useTranslation composable
|
|
||||||
// This can be run in the browser console when the chat client is loaded
|
|
||||||
|
|
||||||
console.log('Testing updated useTranslation composable...');
|
|
||||||
|
|
||||||
// Test basic translation functionality
|
|
||||||
async function testTranslation() {
|
|
||||||
try {
|
|
||||||
// Test direct API call to /api/translate
|
|
||||||
const response = await fetch('/api/translate', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Requested-With': 'XMLHttpRequest'
|
|
||||||
},
|
|
||||||
credentials: 'same-origin',
|
|
||||||
body: JSON.stringify({
|
|
||||||
text: 'Hello world',
|
|
||||||
target_lang: 'nl',
|
|
||||||
source_lang: 'en',
|
|
||||||
context: 'test'
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
console.log('✅ Direct API test successful:', result);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
console.log('✅ Translation successful:', result.translated_text);
|
|
||||||
} else {
|
|
||||||
console.log('❌ Translation failed:', result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Direct API test failed:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test empty text handling
|
|
||||||
async function testEmptyText() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/translate', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Requested-With': 'XMLHttpRequest'
|
|
||||||
},
|
|
||||||
credentials: 'same-origin',
|
|
||||||
body: JSON.stringify({
|
|
||||||
text: '',
|
|
||||||
target_lang: 'nl'
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
console.log('✅ Empty text test result:', result);
|
|
||||||
|
|
||||||
if (!result.success && result.error.includes('empty')) {
|
|
||||||
console.log('✅ Empty text validation working correctly');
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Empty text test failed:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test missing parameters
|
|
||||||
async function testMissingParams() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/translate', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Requested-With': 'XMLHttpRequest'
|
|
||||||
},
|
|
||||||
credentials: 'same-origin',
|
|
||||||
body: JSON.stringify({
|
|
||||||
text: 'Hello world'
|
|
||||||
// Missing target_lang
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
console.log('✅ Missing params test result:', result);
|
|
||||||
|
|
||||||
if (!result.success && result.error.includes('missing')) {
|
|
||||||
console.log('✅ Parameter validation working correctly');
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Missing params test failed:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run all tests
|
|
||||||
async function runAllTests() {
|
|
||||||
console.log('🚀 Starting translation API tests...');
|
|
||||||
|
|
||||||
console.log('\n1. Testing basic translation...');
|
|
||||||
await testTranslation();
|
|
||||||
|
|
||||||
console.log('\n2. Testing empty text validation...');
|
|
||||||
await testEmptyText();
|
|
||||||
|
|
||||||
console.log('\n3. Testing missing parameters validation...');
|
|
||||||
await testMissingParams();
|
|
||||||
|
|
||||||
console.log('\n✅ All tests completed!');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export for manual testing
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.testTranslation = testTranslation;
|
|
||||||
window.testEmptyText = testEmptyText;
|
|
||||||
window.testMissingParams = testMissingParams;
|
|
||||||
window.runAllTests = runAllTests;
|
|
||||||
|
|
||||||
console.log('Test functions available:');
|
|
||||||
console.log('- testTranslation()');
|
|
||||||
console.log('- testEmptyText()');
|
|
||||||
console.log('- testMissingParams()');
|
|
||||||
console.log('- runAllTests()');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-run if in Node.js environment (for CI/CD)
|
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
|
||||||
module.exports = {
|
|
||||||
testTranslation,
|
|
||||||
testEmptyText,
|
|
||||||
testMissingParams,
|
|
||||||
runAllTests
|
|
||||||
};
|
|
||||||
}
|
|
||||||
139
test_ui_fixes.js
139
test_ui_fixes.js
@@ -1,139 +0,0 @@
|
|||||||
// Test script for UI fixes: SideBarExplanation height and DynamicForm single column
|
|
||||||
// This can be run in the browser console when the chat client is loaded
|
|
||||||
|
|
||||||
console.log('Testing UI fixes...');
|
|
||||||
|
|
||||||
// Test SideBarExplanation height fix
|
|
||||||
function testSideBarExplanationHeight() {
|
|
||||||
console.log('🧪 Testing SideBarExplanation height fix...');
|
|
||||||
|
|
||||||
const sidebarExplanation = document.querySelector('.sidebar-explanation');
|
|
||||||
if (!sidebarExplanation) {
|
|
||||||
console.warn('⚠️ SideBarExplanation element not found');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const computedStyle = window.getComputedStyle(sidebarExplanation);
|
|
||||||
const flexValue = computedStyle.getPropertyValue('flex');
|
|
||||||
|
|
||||||
console.log('📏 SideBarExplanation flex value:', flexValue);
|
|
||||||
|
|
||||||
if (flexValue === '1 1 0%' || flexValue === '1') {
|
|
||||||
console.error('❌ SideBarExplanation still has flex: 1 - height fix failed');
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ SideBarExplanation height fix successful - no flex: 1 found');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test DynamicForm single column layout
|
|
||||||
function testDynamicFormSingleColumn() {
|
|
||||||
console.log('🧪 Testing DynamicForm single column layout...');
|
|
||||||
|
|
||||||
const formFields = document.querySelector('.form-fields');
|
|
||||||
if (!formFields) {
|
|
||||||
console.warn('⚠️ DynamicForm .form-fields element not found');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const computedStyle = window.getComputedStyle(formFields);
|
|
||||||
const gridColumns = computedStyle.getPropertyValue('grid-template-columns');
|
|
||||||
|
|
||||||
console.log('📐 DynamicForm grid-template-columns:', gridColumns);
|
|
||||||
|
|
||||||
// Check if it's single column (should be "1fr" or similar)
|
|
||||||
if (gridColumns.includes('1fr 1fr') || gridColumns.includes('repeat(2, 1fr)')) {
|
|
||||||
console.error('❌ DynamicForm still has 2-column layout - single column fix failed');
|
|
||||||
return false;
|
|
||||||
} else if (gridColumns === '1fr' || gridColumns.includes('1fr') && !gridColumns.includes('1fr 1fr')) {
|
|
||||||
console.log('✅ DynamicForm single column fix successful');
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
console.warn('⚠️ DynamicForm has unexpected grid layout:', gridColumns);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test CSS media query removal
|
|
||||||
function testMediaQueryRemoval() {
|
|
||||||
console.log('🧪 Testing media query removal...');
|
|
||||||
|
|
||||||
// Create a test element to check if media queries still apply
|
|
||||||
const testDiv = document.createElement('div');
|
|
||||||
testDiv.className = 'form-fields';
|
|
||||||
testDiv.style.display = 'none';
|
|
||||||
document.body.appendChild(testDiv);
|
|
||||||
|
|
||||||
// Force a larger screen width for testing
|
|
||||||
const originalWidth = window.innerWidth;
|
|
||||||
|
|
||||||
// Check computed style at different screen sizes
|
|
||||||
const computedStyle = window.getComputedStyle(testDiv);
|
|
||||||
const gridColumns = computedStyle.getPropertyValue('grid-template-columns');
|
|
||||||
|
|
||||||
document.body.removeChild(testDiv);
|
|
||||||
|
|
||||||
console.log('📱 Media query test - grid columns:', gridColumns);
|
|
||||||
|
|
||||||
if (gridColumns === '1fr') {
|
|
||||||
console.log('✅ Media query removal successful - single column maintained');
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
console.warn('⚠️ Media query might still be active:', gridColumns);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run all tests
|
|
||||||
function runAllUITests() {
|
|
||||||
console.log('🚀 Starting UI fixes tests...');
|
|
||||||
|
|
||||||
console.log('\n1. Testing SideBarExplanation height fix...');
|
|
||||||
const heightTest = testSideBarExplanationHeight();
|
|
||||||
|
|
||||||
console.log('\n2. Testing DynamicForm single column layout...');
|
|
||||||
const columnTest = testDynamicFormSingleColumn();
|
|
||||||
|
|
||||||
console.log('\n3. Testing media query removal...');
|
|
||||||
const mediaTest = testMediaQueryRemoval();
|
|
||||||
|
|
||||||
console.log('\n📊 Test Results Summary:');
|
|
||||||
console.log(`SideBarExplanation Height Fix: ${heightTest ? '✅' : '❌'}`);
|
|
||||||
console.log(`DynamicForm Single Column: ${columnTest ? '✅' : '❌'}`);
|
|
||||||
console.log(`Media Query Removal: ${mediaTest ? '✅' : '⚠️'}`);
|
|
||||||
|
|
||||||
const allPassed = heightTest && columnTest;
|
|
||||||
console.log(`\n🎯 Overall Status: ${allPassed ? '✅ All critical tests passed!' : '⚠️ Some tests need attention'}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
heightFix: heightTest,
|
|
||||||
singleColumn: columnTest,
|
|
||||||
mediaQuery: mediaTest,
|
|
||||||
overall: allPassed
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export for manual testing
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.testSideBarExplanationHeight = testSideBarExplanationHeight;
|
|
||||||
window.testDynamicFormSingleColumn = testDynamicFormSingleColumn;
|
|
||||||
window.testMediaQueryRemoval = testMediaQueryRemoval;
|
|
||||||
window.runAllUITests = runAllUITests;
|
|
||||||
|
|
||||||
console.log('UI test functions available:');
|
|
||||||
console.log('- testSideBarExplanationHeight()');
|
|
||||||
console.log('- testDynamicFormSingleColumn()');
|
|
||||||
console.log('- testMediaQueryRemoval()');
|
|
||||||
console.log('- runAllUITests()');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-run if in Node.js environment (for CI/CD)
|
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
|
||||||
module.exports = {
|
|
||||||
testSideBarExplanationHeight,
|
|
||||||
testDynamicFormSingleColumn,
|
|
||||||
testMediaQueryRemoval,
|
|
||||||
runAllUITests
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user