- Allow and improve viewing of different content types. First type implemented: changelog

This commit is contained in:
Josako
2025-06-03 09:48:50 +02:00
parent 67078ce925
commit b4e58659a8
12 changed files with 748 additions and 16 deletions

View File

@@ -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()

View 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 []

View File

@@ -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

View 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.

View 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.

View File

@@ -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

View File

@@ -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

View 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.

View File

@@ -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):

View 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 %}

View File

@@ -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'))