- Allow and improve viewing of different content types. First type implemented: changelog
This commit is contained in:
@@ -11,6 +11,7 @@ from flask_restx import Api
|
|||||||
from prometheus_flask_exporter import PrometheusMetrics
|
from prometheus_flask_exporter import PrometheusMetrics
|
||||||
|
|
||||||
from .utils.cache.eveai_cache_manager import EveAICacheManager
|
from .utils.cache.eveai_cache_manager import EveAICacheManager
|
||||||
|
from .utils.content_utils import ContentManager
|
||||||
from .utils.simple_encryption import SimpleEncryption
|
from .utils.simple_encryption import SimpleEncryption
|
||||||
from .utils.minio_utils import MinioClient
|
from .utils.minio_utils import MinioClient
|
||||||
|
|
||||||
@@ -30,4 +31,5 @@ simple_encryption = SimpleEncryption()
|
|||||||
minio_client = MinioClient()
|
minio_client = MinioClient()
|
||||||
metrics = PrometheusMetrics.for_app_factory()
|
metrics = PrometheusMetrics.for_app_factory()
|
||||||
cache_manager = EveAICacheManager()
|
cache_manager = EveAICacheManager()
|
||||||
|
content_manager = ContentManager()
|
||||||
|
|
||||||
|
|||||||
215
common/utils/content_utils.py
Normal file
215
common/utils/content_utils.py
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
from packaging import version
|
||||||
|
from flask import current_app
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class ContentManager:
|
||||||
|
def __init__(self, app=None):
|
||||||
|
self.app = app
|
||||||
|
if app:
|
||||||
|
self.init_app(app)
|
||||||
|
|
||||||
|
def init_app(self, app):
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
# Controleer of het pad bestaat
|
||||||
|
if not os.path.exists(app.config['CONTENT_DIR']):
|
||||||
|
logger.warning(f"Content directory not found at: {app.config['CONTENT_DIR']}")
|
||||||
|
else:
|
||||||
|
logger.info(f"Content directory configured at: {app.config['CONTENT_DIR']}")
|
||||||
|
|
||||||
|
def get_content_path(self, content_type, major_minor=None, patch=None):
|
||||||
|
"""
|
||||||
|
Geef het volledige pad naar een contentbestand
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content_type (str): Type content (bv. 'changelog', 'terms')
|
||||||
|
major_minor (str, optional): Major.Minor versie (bv. '1.0')
|
||||||
|
patch (str, optional): Patchnummer (bv. '5')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Volledige pad naar de content map of bestand
|
||||||
|
"""
|
||||||
|
content_path = os.path.join(self.app.config['CONTENT_DIR'], content_type)
|
||||||
|
|
||||||
|
if major_minor:
|
||||||
|
content_path = os.path.join(content_path, major_minor)
|
||||||
|
|
||||||
|
if patch:
|
||||||
|
content_path = os.path.join(content_path, f"{major_minor}.{patch}.md")
|
||||||
|
|
||||||
|
return content_path
|
||||||
|
|
||||||
|
def _parse_version(self, filename):
|
||||||
|
"""Parse een versienummer uit een bestandsnaam"""
|
||||||
|
match = re.match(r'(\d+\.\d+)\.(\d+)\.md', filename)
|
||||||
|
if match:
|
||||||
|
return match.group(1), match.group(2)
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def get_latest_version(self, content_type, major_minor=None):
|
||||||
|
"""
|
||||||
|
Verkrijg de laatste versie van een bepaald contenttype
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content_type (str): Type content (bv. 'changelog', 'terms')
|
||||||
|
major_minor (str, optional): Specifieke major.minor versie, anders de hoogste
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (major_minor, patch, full_version) of None als niet gevonden
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Basispad voor dit contenttype
|
||||||
|
content_path = os.path.join(self.app.config['CONTENT_DIR'], content_type)
|
||||||
|
|
||||||
|
if not os.path.exists(content_path):
|
||||||
|
logger.error(f"Content path does not exist: {content_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Als geen major_minor opgegeven, vind de hoogste
|
||||||
|
if not major_minor:
|
||||||
|
available_versions = os.listdir(content_path)
|
||||||
|
if not available_versions:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Sorteer op versienummer (major.minor)
|
||||||
|
available_versions.sort(key=lambda v: version.parse(v))
|
||||||
|
major_minor = available_versions[-1]
|
||||||
|
|
||||||
|
# Nu we major_minor hebben, zoek de hoogste patch
|
||||||
|
major_minor_path = os.path.join(content_path, major_minor)
|
||||||
|
|
||||||
|
if not os.path.exists(major_minor_path):
|
||||||
|
logger.error(f"Version path does not exist: {major_minor_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
files = os.listdir(major_minor_path)
|
||||||
|
version_files = []
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
mm, p = self._parse_version(file)
|
||||||
|
if mm == major_minor and p:
|
||||||
|
version_files.append((mm, p, f"{mm}.{p}"))
|
||||||
|
|
||||||
|
if not version_files:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Sorteer op patch nummer
|
||||||
|
version_files.sort(key=lambda v: int(v[1]))
|
||||||
|
return version_files[-1]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error finding latest version for {content_type}: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def read_content(self, content_type, major_minor=None, patch=None):
|
||||||
|
"""
|
||||||
|
Lees content met versieondersteuning
|
||||||
|
|
||||||
|
Als major_minor en patch niet zijn opgegeven, wordt de laatste versie gebruikt.
|
||||||
|
Als alleen major_minor is opgegeven, wordt de laatste patch van die versie gebruikt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content_type (str): Type content (bv. 'changelog', 'terms')
|
||||||
|
major_minor (str, optional): Major.Minor versie (bv. '1.0')
|
||||||
|
patch (str, optional): Patchnummer (bv. '5')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: {
|
||||||
|
'content': str,
|
||||||
|
'version': str,
|
||||||
|
'content_type': str
|
||||||
|
} of None bij fout
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Als geen versie opgegeven, vind de laatste
|
||||||
|
if not major_minor:
|
||||||
|
version_info = self.get_latest_version(content_type)
|
||||||
|
if not version_info:
|
||||||
|
logger.error(f"No versions found for {content_type}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
major_minor, patch, full_version = version_info
|
||||||
|
|
||||||
|
# Als geen patch opgegeven, vind de laatste patch voor deze major_minor
|
||||||
|
elif not patch:
|
||||||
|
version_info = self.get_latest_version(content_type, major_minor)
|
||||||
|
if not version_info:
|
||||||
|
logger.error(f"No versions found for {content_type} {major_minor}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
major_minor, patch, full_version = version_info
|
||||||
|
else:
|
||||||
|
full_version = f"{major_minor}.{patch}"
|
||||||
|
|
||||||
|
# Nu hebben we major_minor en patch, lees het bestand
|
||||||
|
file_path = self.get_content_path(content_type, major_minor, patch)
|
||||||
|
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
logger.error(f"Content file does not exist: {file_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as file:
|
||||||
|
content = file.read()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'content': content,
|
||||||
|
'version': full_version,
|
||||||
|
'content_type': content_type
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error reading content {content_type} {major_minor}.{patch}: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def list_content_types(self):
|
||||||
|
"""Lijst alle beschikbare contenttypes op"""
|
||||||
|
try:
|
||||||
|
return [d for d in os.listdir(self.app.config['CONTENT_DIR'])
|
||||||
|
if os.path.isdir(os.path.join(self.app.config['CONTENT_DIR'], d))]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error listing content types: {str(e)}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def list_versions(self, content_type):
|
||||||
|
"""
|
||||||
|
Lijst alle beschikbare versies voor een contenttype
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: Lijst van dicts met versie-informatie
|
||||||
|
[{'version': '1.0.0', 'path': '/path/to/file', 'date_modified': datetime}]
|
||||||
|
"""
|
||||||
|
versions = []
|
||||||
|
try:
|
||||||
|
content_path = os.path.join(self.app.config['CONTENT_DIR'], content_type)
|
||||||
|
|
||||||
|
if not os.path.exists(content_path):
|
||||||
|
return []
|
||||||
|
|
||||||
|
for major_minor in os.listdir(content_path):
|
||||||
|
major_minor_path = os.path.join(content_path, major_minor)
|
||||||
|
|
||||||
|
if not os.path.isdir(major_minor_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for file in os.listdir(major_minor_path):
|
||||||
|
mm, p = self._parse_version(file)
|
||||||
|
if mm and p:
|
||||||
|
file_path = os.path.join(major_minor_path, file)
|
||||||
|
mod_time = os.path.getmtime(file_path)
|
||||||
|
versions.append({
|
||||||
|
'version': f"{mm}.{p}",
|
||||||
|
'path': file_path,
|
||||||
|
'date_modified': mod_time
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sorteer op versienummer
|
||||||
|
versions.sort(key=lambda v: version.parse(v['version']))
|
||||||
|
return versions
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error listing versions for {content_type}: {str(e)}")
|
||||||
|
return []
|
||||||
@@ -172,6 +172,9 @@ class Config(object):
|
|||||||
# Entitlement Constants
|
# Entitlement Constants
|
||||||
ENTITLEMENTS_MAX_PENDING_DAYS = 5 # Defines the maximum number of days a pending entitlement can be active
|
ENTITLEMENTS_MAX_PENDING_DAYS = 5 # Defines the maximum number of days a pending entitlement can be active
|
||||||
|
|
||||||
|
# Content Directory for static content like the changelog, terms & conditions, privacy statement, ...
|
||||||
|
CONTENT_DIR = '/app/content'
|
||||||
|
|
||||||
|
|
||||||
class DevConfig(Config):
|
class DevConfig(Config):
|
||||||
DEVELOPMENT = True
|
DEVELOPMENT = True
|
||||||
|
|||||||
37
content/privacy/1.0/1.0.0.md
Normal file
37
content/privacy/1.0/1.0.0.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Privacy Policy
|
||||||
|
|
||||||
|
## Version 1.0.0
|
||||||
|
|
||||||
|
*Effective Date: 2025-06-03*
|
||||||
|
|
||||||
|
### 1. Introduction
|
||||||
|
|
||||||
|
This Privacy Policy describes how EveAI collects, uses, and discloses your information when you use our services.
|
||||||
|
|
||||||
|
### 2. Information We Collect
|
||||||
|
|
||||||
|
We collect information you provide directly to us, such as account information, content you process through our services, and communication data.
|
||||||
|
|
||||||
|
### 3. How We Use Your Information
|
||||||
|
|
||||||
|
We use your information to provide, maintain, and improve our services, process transactions, send communications, and comply with legal obligations.
|
||||||
|
|
||||||
|
### 4. Data Security
|
||||||
|
|
||||||
|
We implement appropriate security measures to protect your personal information against unauthorized access, alteration, disclosure, or destruction.
|
||||||
|
|
||||||
|
### 5. International Data Transfers
|
||||||
|
|
||||||
|
Your information may be transferred to and processed in countries other than the country you reside in, where data protection laws may differ.
|
||||||
|
|
||||||
|
### 6. Your Rights
|
||||||
|
|
||||||
|
Depending on your location, you may have certain rights regarding your personal information, such as access, correction, deletion, or restriction of processing.
|
||||||
|
|
||||||
|
### 7. Changes to This Policy
|
||||||
|
|
||||||
|
We may update this Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page.
|
||||||
|
|
||||||
|
### 8. Contact Us
|
||||||
|
|
||||||
|
If you have any questions about this Privacy Policy, please contact us at privacy@askeveai.be.
|
||||||
37
content/terms/1.0/1.0.0.md
Normal file
37
content/terms/1.0/1.0.0.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Terms of Service
|
||||||
|
|
||||||
|
## Version 1.0.0
|
||||||
|
|
||||||
|
*Effective Date: 2025-06-03*
|
||||||
|
|
||||||
|
### 1. Introduction
|
||||||
|
|
||||||
|
Welcome to EveAI. By accessing or using our services, you agree to be bound by these Terms of Service.
|
||||||
|
|
||||||
|
### 2. Service Description
|
||||||
|
|
||||||
|
EveAI provides AI-powered solutions for businesses to optimize their operations through intelligent document processing and specialist execution.
|
||||||
|
|
||||||
|
### 3. User Accounts
|
||||||
|
|
||||||
|
To access certain features of the Service, you must register for an account. You are responsible for maintaining the confidentiality of your account information.
|
||||||
|
|
||||||
|
### 4. Privacy
|
||||||
|
|
||||||
|
Your use of the Service is also governed by our Privacy Policy, which can be found [here](/content/privacy).
|
||||||
|
|
||||||
|
### 5. Intellectual Property
|
||||||
|
|
||||||
|
All content, features, and functionality of the Service are owned by EveAI and are protected by international copyright, trademark, and other intellectual property laws.
|
||||||
|
|
||||||
|
### 6. Limitation of Liability
|
||||||
|
|
||||||
|
In no event shall EveAI be liable for any indirect, incidental, special, consequential or punitive damages.
|
||||||
|
|
||||||
|
### 7. Changes to Terms
|
||||||
|
|
||||||
|
We reserve the right to modify these Terms at any time. Your continued use of the Service after such modifications will constitute your acceptance of the new Terms.
|
||||||
|
|
||||||
|
### 8. Governing Law
|
||||||
|
|
||||||
|
These Terms shall be governed by the laws of Belgium.
|
||||||
@@ -91,6 +91,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ../eveai_app:/app/eveai_app
|
- ../eveai_app:/app/eveai_app
|
||||||
- ../common:/app/common
|
- ../common:/app/common
|
||||||
|
- ../content:/app/content
|
||||||
- ../config:/app/config
|
- ../config:/app/config
|
||||||
- ../migrations:/app/migrations
|
- ../migrations:/app/migrations
|
||||||
- ../scripts:/app/scripts
|
- ../scripts:/app/scripts
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ COPY config /app/config
|
|||||||
COPY migrations /app/migrations
|
COPY migrations /app/migrations
|
||||||
COPY scripts /app/scripts
|
COPY scripts /app/scripts
|
||||||
COPY patched_packages /app/patched_packages
|
COPY patched_packages /app/patched_packages
|
||||||
|
COPY content /app/content
|
||||||
|
|
||||||
# Set permissions for entrypoint script
|
# Set permissions for entrypoint script
|
||||||
RUN chmod 777 /app/scripts/entrypoint.sh
|
RUN chmod 777 /app/scripts/entrypoint.sh
|
||||||
|
|||||||
296
documentation/Eveai Chat Client Developer Documentation.md
Normal file
296
documentation/Eveai Chat Client Developer Documentation.md
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
# Evie Chat Client - Developer Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Evie Chat Client is a modern, customizable chat interface for interacting with eveai specialists. It supports both anonymous and authenticated modes, with initial focus on anonymous mode. The client provides real-time interaction with AI specialists, customizable tenant branding, and European-compliant analytics tracking.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
- **Anonymous Mode**: Public access with tenant UUID and API key authentication
|
||||||
|
- **Real-time Communication**: Server-Sent Events (SSE) for live updates and intermediate states
|
||||||
|
- **Tenant Customization**: Simple CSS variable-based theming with visual editor
|
||||||
|
- **Multiple Choice Options**: Dynamic button/dropdown responses from specialists
|
||||||
|
- **Chat History**: Persistent ChatSession and Interaction storage
|
||||||
|
- **File Upload Support**: Planned for future implementation
|
||||||
|
- **European Analytics**: Umami integration for GDPR-compliant tracking
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Component Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
eveai_chat_client/
|
||||||
|
├── app.py # Flask app entry point
|
||||||
|
├── routes/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── chat_routes.py # Main chat interface routes
|
||||||
|
│ └── api_routes.py # SSE/API endpoints
|
||||||
|
├── services/
|
||||||
|
│ ├── chat_service.py # Chat session management
|
||||||
|
│ ├── specialist_service.py # Specialist interaction wrapper
|
||||||
|
│ └── tenant_service.py # Tenant config & theming
|
||||||
|
├── templates/
|
||||||
|
│ ├── base.html # Base template
|
||||||
|
│ ├── chat.html # Main chat interface
|
||||||
|
│ └── components/
|
||||||
|
│ ├── message.html # Individual message component
|
||||||
|
│ ├── options.html # Multiple choice options
|
||||||
|
│ └── thinking.html # Intermediate states display
|
||||||
|
└── utils/
|
||||||
|
├── auth.py # API key validation
|
||||||
|
└── tracking.py # Umami analytics integration
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Approach
|
||||||
|
|
||||||
|
- **Services Layer**: Direct integration with existing eveai services (not API) for better performance
|
||||||
|
- **Database**: Utilizes existing ChatSession and Interaction models
|
||||||
|
- **Caching**: Leverages existing Redis setup
|
||||||
|
- **Static Files**: Uses existing nginx/static structure
|
||||||
|
|
||||||
|
## URL Structure & Parameters
|
||||||
|
|
||||||
|
### Main Chat Interface
|
||||||
|
```
|
||||||
|
GET /chat/{tenant_code}/{specialist_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `api_key` (required): Tenant API key for authentication
|
||||||
|
- `utm_source`, `utm_campaign`, `utm_medium` (optional): Analytics tracking
|
||||||
|
- Other tracking parameters as needed
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```
|
||||||
|
/chat/550e8400-e29b-41d4-a716-446655440000/document-analyzer?api_key=xxx&utm_source=email
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
```
|
||||||
|
POST /api/chat/{tenant_code}/interact # Send message to specialist
|
||||||
|
GET /api/chat/{tenant_code}/status/{session_id} # SSE endpoint for updates
|
||||||
|
GET /api/tenant/{tenant_code}/theme.css # Dynamic tenant CSS (if needed)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authentication & Security
|
||||||
|
|
||||||
|
### Anonymous Mode
|
||||||
|
- **Tenant Identification**: UUID-based tenant codes (not sequential IDs)
|
||||||
|
- **API Key Validation**: Required for all anonymous interactions
|
||||||
|
- **Rate Limiting**: Implement per-tenant/IP rate limiting
|
||||||
|
- **Input Validation**: Sanitize all user inputs and parameters
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
- Use tenant UUIDs to prevent enumeration attacks
|
||||||
|
- Validate API keys against tenant database
|
||||||
|
- Implement CORS policies for cross-origin requests
|
||||||
|
- Sanitize all user messages and file uploads
|
||||||
|
|
||||||
|
## Real-time Communication
|
||||||
|
|
||||||
|
### Server-Sent Events (SSE)
|
||||||
|
- **Connection**: Long-lived SSE connection per chat session
|
||||||
|
- **Message Types**:
|
||||||
|
- `message`: Complete specialist response
|
||||||
|
- `thinking`: Intermediate processing states
|
||||||
|
- `options`: Multiple choice response options
|
||||||
|
- `error`: Error messages
|
||||||
|
- `complete`: Interaction completion
|
||||||
|
|
||||||
|
### SSE Message Format
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "thinking",
|
||||||
|
"data": {
|
||||||
|
"message": "Analyzing your request...",
|
||||||
|
"step": 1,
|
||||||
|
"total_steps": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tenant Customization
|
||||||
|
|
||||||
|
### Theme Configuration
|
||||||
|
Stored in tenant table as JSONB column:
|
||||||
|
```sql
|
||||||
|
ALTER TABLE tenants ADD COLUMN theme_config JSONB;
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS Variables Approach
|
||||||
|
Inline CSS variables in chat template:
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
/* Brand Colors */
|
||||||
|
--primary-color: {{ tenant.theme_config.primary_color or '#007bff' }};
|
||||||
|
--secondary-color: {{ tenant.theme_config.secondary_color or '#6c757d' }};
|
||||||
|
--accent-color: {{ tenant.theme_config.accent_color or '#28a745' }};
|
||||||
|
|
||||||
|
/* Chat Interface */
|
||||||
|
--user-message-bg: {{ tenant.theme_config.user_message_bg or 'var(--primary-color)' }};
|
||||||
|
--bot-message-bg: {{ tenant.theme_config.bot_message_bg or '#f8f9fa' }};
|
||||||
|
--chat-bg: {{ tenant.theme_config.chat_bg or '#ffffff' }};
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
--font-family: {{ tenant.theme_config.font_family or 'system-ui, -apple-system, sans-serif' }};
|
||||||
|
--font-size-base: {{ tenant.theme_config.font_size or '16px' }};
|
||||||
|
|
||||||
|
/* Branding */
|
||||||
|
--logo-url: url('/api/tenant/{{ tenant.code }}/logo');
|
||||||
|
--header-bg: {{ tenant.theme_config.header_bg or 'var(--primary-color)' }};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Theme Editor (eveai_app)
|
||||||
|
Simple form interface with:
|
||||||
|
- Color pickers for brand colors
|
||||||
|
- Font selection dropdown
|
||||||
|
- Logo upload functionality
|
||||||
|
- Live preview of chat interface
|
||||||
|
- Reset to defaults option
|
||||||
|
|
||||||
|
## Multiple Choice Options
|
||||||
|
|
||||||
|
### Dynamic Rendering Logic
|
||||||
|
```python
|
||||||
|
def render_options(options_list):
|
||||||
|
if len(options_list) <= 3:
|
||||||
|
return render_template('components/options.html',
|
||||||
|
display_type='buttons',
|
||||||
|
options=options_list)
|
||||||
|
else:
|
||||||
|
return render_template('components/options.html',
|
||||||
|
display_type='dropdown',
|
||||||
|
options=options_list)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option Data Structure
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "options",
|
||||||
|
"data": {
|
||||||
|
"question": "How would you like to proceed?",
|
||||||
|
"options": [
|
||||||
|
{"id": "option1", "text": "Continue analysis", "value": "continue"},
|
||||||
|
{"id": "option2", "text": "Generate report", "value": "report"},
|
||||||
|
{"id": "option3", "text": "Start over", "value": "restart"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Analytics Integration
|
||||||
|
|
||||||
|
### Umami Setup
|
||||||
|
- **European Hosting**: Self-hosted Umami instance
|
||||||
|
- **Privacy Compliant**: No cookies, GDPR compliant by design
|
||||||
|
- **Tracking Events**:
|
||||||
|
- Chat session start
|
||||||
|
- Message sent
|
||||||
|
- Option selected
|
||||||
|
- Session duration
|
||||||
|
- Specialist interaction completion
|
||||||
|
|
||||||
|
### Tracking Implementation
|
||||||
|
```javascript
|
||||||
|
// Track chat events
|
||||||
|
function trackEvent(eventName, eventData) {
|
||||||
|
if (window.umami) {
|
||||||
|
umami.track(eventName, eventData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Upload Support (Future)
|
||||||
|
|
||||||
|
### Planned Implementation
|
||||||
|
- **Multipart Upload**: Standard HTML5 file upload
|
||||||
|
- **File Types**: Documents, images, spreadsheets
|
||||||
|
- **Storage**: Tenant-specific S3 buckets
|
||||||
|
- **Processing**: Integration with existing document processing pipeline
|
||||||
|
- **UI**: Drag-and-drop interface with progress indicators
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
- File type validation
|
||||||
|
- Size limits per tenant
|
||||||
|
- Virus scanning integration
|
||||||
|
- Temporary file cleanup
|
||||||
|
|
||||||
|
## Development Guidelines
|
||||||
|
|
||||||
|
### Code Organization
|
||||||
|
- Follow existing eveai project structure and conventions
|
||||||
|
- Use existing common/services and common/utils where applicable
|
||||||
|
- Maintain multi-tenant data isolation
|
||||||
|
- Implement proper error handling and logging
|
||||||
|
|
||||||
|
### Testing Strategy
|
||||||
|
- Unit tests for services and utilities
|
||||||
|
- Integration tests for chat flow
|
||||||
|
- UI tests for theme customization
|
||||||
|
- Load testing for SSE connections
|
||||||
|
- Cross-browser compatibility testing
|
||||||
|
|
||||||
|
### Performance Considerations
|
||||||
|
- Cache tenant configurations in Redis
|
||||||
|
- Optimize SSE connection management
|
||||||
|
- Implement connection pooling for database
|
||||||
|
- Use CDN for static assets
|
||||||
|
- Monitor real-time connection limits
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Container Configuration
|
||||||
|
- New `eveai_chat_client` container
|
||||||
|
- Integration with existing docker setup
|
||||||
|
- Environment configuration for tenant isolation
|
||||||
|
- Load balancer configuration for SSE connections
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- Flask and Flask-restx (existing)
|
||||||
|
- Celery integration (existing)
|
||||||
|
- PostgreSQL and Redis (existing)
|
||||||
|
- Umami analytics client library
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### Authenticated Mode
|
||||||
|
- User login integration
|
||||||
|
- Session persistence across devices
|
||||||
|
- Advanced specialist access controls
|
||||||
|
- User-specific chat history
|
||||||
|
|
||||||
|
### Advanced Features
|
||||||
|
- Voice message support
|
||||||
|
- Screen sharing capabilities
|
||||||
|
- Collaborative chat sessions
|
||||||
|
- Advanced analytics dashboard
|
||||||
|
- Mobile app integration
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
```bash
|
||||||
|
CHAT_CLIENT_PORT=5000
|
||||||
|
TENANT_API_VALIDATION_CACHE_TTL=3600
|
||||||
|
SSE_CONNECTION_TIMEOUT=300
|
||||||
|
UMAMI_WEBSITE_ID=your-website-id
|
||||||
|
UMAMI_SCRIPT_URL=https://your-umami.domain/script.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Theme Configuration
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"primary_color": "#2563eb",
|
||||||
|
"secondary_color": "#64748b",
|
||||||
|
"accent_color": "#059669",
|
||||||
|
"user_message_bg": "#2563eb",
|
||||||
|
"bot_message_bg": "#f1f5f9",
|
||||||
|
"chat_bg": "#ffffff",
|
||||||
|
"font_family": "Inter, system-ui, sans-serif",
|
||||||
|
"font_size": "16px",
|
||||||
|
"header_bg": "#1e40af"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This documentation provides a comprehensive foundation for developing the Evie Chat Client while maintaining consistency with the existing eveai architecture and meeting the specific requirements for anonymous mode interactions with customizable tenant branding.
|
||||||
@@ -7,7 +7,7 @@ from werkzeug.middleware.proxy_fix import ProxyFix
|
|||||||
import logging.config
|
import logging.config
|
||||||
|
|
||||||
from common.extensions import (db, migrate, bootstrap, security, login_manager, cors, csrf, session,
|
from common.extensions import (db, migrate, bootstrap, security, login_manager, cors, csrf, session,
|
||||||
minio_client, simple_encryption, metrics, cache_manager)
|
minio_client, simple_encryption, metrics, cache_manager, content_manager)
|
||||||
from common.models.user import User, Role, Tenant, TenantDomain
|
from common.models.user import User, Role, Tenant, TenantDomain
|
||||||
import common.models.interaction
|
import common.models.interaction
|
||||||
import common.models.entitlements
|
import common.models.entitlements
|
||||||
@@ -124,6 +124,7 @@ def register_extensions(app):
|
|||||||
minio_client.init_app(app)
|
minio_client.init_app(app)
|
||||||
cache_manager.init_app(app)
|
cache_manager.init_app(app)
|
||||||
metrics.init_app(app)
|
metrics.init_app(app)
|
||||||
|
content_manager.init_app(app)
|
||||||
|
|
||||||
|
|
||||||
def register_blueprints(app):
|
def register_blueprints(app):
|
||||||
|
|||||||
102
eveai_app/templates/basic/view_markdown.html
Normal file
102
eveai_app/templates/basic/view_markdown.html
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{{ title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content_title %}{{ title }}{% endblock %}
|
||||||
|
{% block content_description %}{{ description }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-5">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" id="showRaw">Show Raw</button>
|
||||||
|
<button class="btn btn-sm btn-outline-primary active" id="showRendered">Show Rendered</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Raw markdown view (hidden by default) -->
|
||||||
|
<div id="rawMarkdown" class="code-wrapper" style="display: none;">
|
||||||
|
<pre><code class="language-markdown">{{ markdown_content }}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rendered markdown view -->
|
||||||
|
<div id="renderedMarkdown" class="markdown-body">
|
||||||
|
{{ markdown_content | markdown }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
{{ super() }}
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@4.0.0/github-markdown.min.css">
|
||||||
|
<style>
|
||||||
|
pre, code {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
white-space: pre-wrap !important;
|
||||||
|
word-wrap: break-word !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code {
|
||||||
|
padding: 1rem !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body {
|
||||||
|
padding: 1rem;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode styling (optional) */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.markdown-body {
|
||||||
|
color: #c9d1d9;
|
||||||
|
background-color: #0d1117;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{{ super() }}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize syntax highlighting
|
||||||
|
document.querySelectorAll('pre code').forEach((block) => {
|
||||||
|
hljs.highlightElement(block);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle buttons for display
|
||||||
|
const showRawBtn = document.getElementById('showRaw');
|
||||||
|
const showRenderedBtn = document.getElementById('showRendered');
|
||||||
|
const rawMarkdown = document.getElementById('rawMarkdown');
|
||||||
|
const renderedMarkdown = document.getElementById('renderedMarkdown');
|
||||||
|
|
||||||
|
showRawBtn.addEventListener('click', function() {
|
||||||
|
rawMarkdown.style.display = 'block';
|
||||||
|
renderedMarkdown.style.display = 'none';
|
||||||
|
showRawBtn.classList.add('active');
|
||||||
|
showRenderedBtn.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
showRenderedBtn.addEventListener('click', function() {
|
||||||
|
rawMarkdown.style.display = 'none';
|
||||||
|
renderedMarkdown.style.display = 'block';
|
||||||
|
showRawBtn.classList.remove('active');
|
||||||
|
showRenderedBtn.classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -9,9 +9,17 @@ from common.models.user import Tenant
|
|||||||
from common.utils.database import Database
|
from common.utils.database import Database
|
||||||
from common.utils.nginx_utils import prefixed_url_for
|
from common.utils.nginx_utils import prefixed_url_for
|
||||||
from .basic_forms import SessionDefaultsForm
|
from .basic_forms import SessionDefaultsForm
|
||||||
|
from common.extensions import content_manager
|
||||||
|
|
||||||
|
import markdown
|
||||||
|
|
||||||
basic_bp = Blueprint('basic_bp', __name__)
|
basic_bp = Blueprint('basic_bp', __name__)
|
||||||
|
|
||||||
|
# Markdown filter toevoegen aan Jinja2
|
||||||
|
@basic_bp.app_template_filter('markdown')
|
||||||
|
def render_markdown(text):
|
||||||
|
return markdown.markdown(text, extensions=['tables', 'fenced_code'])
|
||||||
|
|
||||||
|
|
||||||
@basic_bp.before_request
|
@basic_bp.before_request
|
||||||
def log_before_request():
|
def log_before_request():
|
||||||
@@ -104,28 +112,57 @@ def check_csrf():
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@basic_bp.route('/release_notes', methods=['GET'])
|
@basic_bp.route('/content/<content_type>', methods=['GET'])
|
||||||
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
def release_notes():
|
def view_content(content_type):
|
||||||
"""Display the CHANGELOG.md file."""
|
"""
|
||||||
|
Show content like release notes, terms of use, etc.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content_type (str): Type content (eg. 'changelog', 'terms', 'privacy')
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
# Construct the URL to the CHANGELOG.md file in the static directory
|
current_app.logger.debug(f"Showing content {content_type}")
|
||||||
static_url = url_for('static', filename='docs/CHANGELOG.md', _external=True)
|
major_minor = request.args.get('version')
|
||||||
|
patch = request.args.get('patch')
|
||||||
|
|
||||||
# Make a request to get the content of the CHANGELOG.md file
|
# Gebruik de ContentManager om de content op te halen
|
||||||
response = requests.get(static_url)
|
content_data = content_manager.read_content(content_type, major_minor, patch)
|
||||||
response.raise_for_status() # Raise an exception for HTTP errors
|
|
||||||
|
|
||||||
# Get the content of the response
|
if not content_data:
|
||||||
markdown_content = response.text
|
flash(f'Content van type {content_type} werd niet gevonden.', 'danger')
|
||||||
|
return redirect(prefixed_url_for('basic_bp.index'))
|
||||||
|
|
||||||
|
# Titels en beschrijvingen per contenttype
|
||||||
|
titles = {
|
||||||
|
'changelog': 'Release Notes',
|
||||||
|
'terms': 'Terms & Conditions',
|
||||||
|
'privacy': 'Privacy Statement',
|
||||||
|
# Voeg andere types toe indien nodig
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptions = {
|
||||||
|
'changelog': 'EveAI Release Notes',
|
||||||
|
'terms': "Terms & Conditions for using AskEveAI's Evie",
|
||||||
|
'privacy': "Privacy Statement for AskEveAI's Evie",
|
||||||
|
# Voeg andere types toe indien nodig
|
||||||
|
}
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'basic/view_markdown.html',
|
'basic/view_markdown.html',
|
||||||
title='Release Notes',
|
title=titles.get(content_type, content_type.capitalize()),
|
||||||
description='EveAI Release Notes and Change History',
|
description=descriptions.get(content_type, ''),
|
||||||
markdown_content=markdown_content
|
markdown_content=content_data['content'],
|
||||||
|
version=content_data['version']
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(f"Error displaying release notes: {str(e)}")
|
current_app.logger.error(f"Error displaying content {content_type}: {str(e)}")
|
||||||
flash(f'Error displaying release notes: {str(e)}', 'danger')
|
flash(f'Error displaying content: {str(e)}', 'danger')
|
||||||
return redirect(prefixed_url_for('basic_bp.index'))
|
return redirect(prefixed_url_for('basic_bp.index'))
|
||||||
|
|
||||||
|
@basic_bp.route('/release_notes', methods=['GET'])
|
||||||
|
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
|
||||||
|
def release_notes():
|
||||||
|
"""Doorverwijzen naar de nieuwe content view voor changelog"""
|
||||||
|
current_app.logger.debug(f"Redirecting to content viewer")
|
||||||
|
return redirect(prefixed_url_for('basic_bp.view_content', content_type='changelog'))
|
||||||
|
|||||||
Reference in New Issue
Block a user