Changes for eveai_chat_client:

- Modal display of privacy statement & Terms & Conditions
- Consent-flag ==> check of privacy and Terms & Conditions
- customisation option added to show or hide DynamicForm titles
This commit is contained in:
Josako
2025-07-28 21:47:56 +02:00
parent ef138462d9
commit 5e81595622
28 changed files with 1609 additions and 2271 deletions

View File

@@ -0,0 +1,191 @@
// eveai_chat_client/static/assets/js/composables/useContentModal.js
import { ref, reactive, provide, inject } from 'vue';
// Injection key for the composable
export const CONTENT_MODAL_KEY = Symbol('contentModal');
// Create the composable
export function useContentModal() {
// Reactive state for modal
const modalState = reactive({
show: false,
title: '',
content: '',
version: '',
loading: false,
error: false,
errorMessage: '',
lastContentUrl: '' // Add this for retry functionality
});
// Content cache to avoid repeated API calls
const contentCache = ref({});
// Show modal with content
const showModal = async (options = {}) => {
const {
title = 'Content',
contentUrl = null,
content = null,
version = ''
} = options;
// Set initial state
modalState.show = true;
modalState.title = title;
modalState.content = content || '';
modalState.version = version;
modalState.loading = !!contentUrl; // Only show loading if we need to fetch content
modalState.error = false;
modalState.errorMessage = '';
modalState.lastContentUrl = contentUrl || ''; // Store for retry functionality
// If content is provided directly, no need to fetch
if (content) {
modalState.loading = false;
return;
}
// If contentUrl is provided, fetch the content
if (contentUrl) {
await loadContent(contentUrl);
}
};
// Hide modal
const hideModal = () => {
console.log('Hiding content modal');
modalState.show = false;
modalState.title = '';
modalState.content = '';
modalState.version = '';
modalState.loading = false;
modalState.error = false;
modalState.errorMessage = '';
modalState.lastContentUrl = ''; // Clear for next use
};
// Load content from API
const loadContent = async (contentUrl) => {
try {
console.log('Loading content from:', contentUrl);
modalState.loading = true;
modalState.error = false;
modalState.errorMessage = '';
// Check cache first
if (contentCache.value[contentUrl]) {
console.log('Content found in cache for:', contentUrl);
const cached = contentCache.value[contentUrl];
modalState.content = cached.content;
modalState.version = cached.version;
modalState.loading = false;
return;
}
// Fetch from API
console.log('Fetching content from API:', contentUrl);
const response = await fetch(contentUrl);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log('API response received:', {
hasSuccess: data.success !== undefined,
hasContent: data.content !== undefined,
hasError: data.error !== undefined,
contentLength: data.content ? data.content.length : 0
});
// Support both response formats
if (data.success !== undefined) {
// New format with success property
if (data.success) {
modalState.content = data.content || '';
modalState.version = data.version || '';
} else {
throw new Error(data.error || 'Onbekende fout bij het laden van content');
}
} else if (data.content !== undefined) {
// Legacy format without success property (current privacy/terms endpoints)
modalState.content = data.content || '';
modalState.version = data.version || '';
} else if (data.error) {
// Error response format
throw new Error(data.message || data.error || 'Er is een fout opgetreden');
} else {
throw new Error('Invalid response format: no content or success property found');
}
// Cache the result
contentCache.value[contentUrl] = {
content: modalState.content,
version: modalState.version,
timestamp: Date.now()
};
} catch (error) {
console.error('Error loading content:', error);
modalState.error = true;
modalState.errorMessage = error.message || 'Er is een fout opgetreden bij het laden van de content.';
} finally {
modalState.loading = false;
}
};
// Retry loading content
const retryLoad = async () => {
if (modalState.lastContentUrl) {
console.log('Retrying content load for:', modalState.lastContentUrl);
await loadContent(modalState.lastContentUrl);
} else {
console.warn('No content URL available for retry');
modalState.error = true;
modalState.errorMessage = 'Geen content URL beschikbaar voor opnieuw proberen.';
}
};
// Clear cache (useful for development or when content updates)
const clearCache = () => {
contentCache.value = {};
};
// Get cache size for debugging
const getCacheInfo = () => {
return {
size: Object.keys(contentCache.value).length,
entries: Object.keys(contentCache.value)
};
};
return {
// State
modalState,
// Methods
showModal,
hideModal,
loadContent,
retryLoad,
clearCache,
getCacheInfo
};
}
// Provider function to be used in the root component
export function provideContentModal() {
const contentModal = useContentModal();
provide(CONTENT_MODAL_KEY, contentModal);
return contentModal;
}
// Injector function to be used in child components
export function injectContentModal() {
const contentModal = inject(CONTENT_MODAL_KEY);
if (!contentModal) {
throw new Error('useContentModal must be provided before it can be injected');
}
return contentModal;
}

View File

@@ -211,7 +211,7 @@ export function useComponentTranslations(componentName, originalTexts) {
const translations = provider.registerComponent(componentName, originalTexts);
return {
translations: computed(() => translations.translated),
texts: computed(() => translations.translated),
isLoading: computed(() => translations.isLoading),
error: computed(() => translations.error),
currentLanguage: provider.currentLanguage

View File

@@ -33,6 +33,19 @@ active_text_color<template>
ref="chatInput"
class="chat-input-area"
></chat-input>
<!-- Content Modal - positioned at ChatApp level -->
<content-modal
:show="contentModal.modalState.show"
:title="contentModal.modalState.title"
:content="contentModal.modalState.content"
:version="contentModal.modalState.version"
:loading="contentModal.modalState.loading"
:error="contentModal.modalState.error"
:error-message="contentModal.modalState.errorMessage"
@close="contentModal.hideModal"
@retry="contentModal.retryLoad"
/>
</div>
</template>
@@ -46,9 +59,12 @@ import MessageHistory from './MessageHistory.vue';
import ProgressTracker from './ProgressTracker.vue';
import LanguageSelector from './LanguageSelector.vue';
import ChatInput from './ChatInput.vue';
import ContentModal from './ContentModal.vue';
// Import language provider
import { createLanguageProvider, LANGUAGE_PROVIDER_KEY } from '../js/services/LanguageProvider.js';
// Import content modal composable
import { provideContentModal } from '../js/composables/useContentModal.js';
import { provide } from 'vue';
export default {
@@ -60,7 +76,8 @@ export default {
ChatMessage,
MessageHistory,
ProgressTracker,
ChatInput
ChatInput,
ContentModal
},
setup() {
@@ -71,11 +88,15 @@ export default {
// Creëer language provider
const languageProvider = createLanguageProvider(initialLanguage, apiPrefix);
// Creëer en provide content modal
const contentModal = provideContentModal();
// Provide aan alle child components
provide(LANGUAGE_PROVIDER_KEY, languageProvider);
return {
languageProvider
languageProvider,
contentModal
};
},

View File

@@ -24,6 +24,7 @@
v-if="formData && formData.fields"
:form-data="formData"
:form-values="formValues"
:api-prefix="apiPrefix"
:is-submitting="isLoading"
:hide-actions="true"
@update:form-values="updateFormValues"

View File

@@ -19,6 +19,7 @@
<dynamic-form
:form-data="message.formData"
:form-values="message.formValues"
:api-prefix="apiPrefix"
:read-only="true"
hide-actions
class="message-form user-form"

View File

@@ -0,0 +1,412 @@
<template>
<div v-if="show" class="modal-overlay" @click="handleOverlayClick">
<div class="modal-dialog" @click.stop>
<!-- Modal Header -->
<div class="modal-header">
<h4 class="modal-title">{{ title }}</h4>
<button type="button" class="modal-close" @click="closeModal" aria-label="Close">
<span class="material-icons">close</span>
</button>
</div>
<!-- Modal Body -->
<div class="modal-body">
<!-- Loading State -->
<div v-if="loading" class="loading-container">
<div class="loading-spinner"></div>
<p>Content wordt geladen...</p>
</div>
<!-- Error State -->
<div v-else-if="error" class="error-container">
<div class="error-icon">
<span class="material-icons">error_outline</span>
</div>
<h5>Fout bij het laden van content</h5>
<p>{{ errorMessage }}</p>
<button type="button" class="btn btn-primary" @click="retryLoad">
Opnieuw proberen
</button>
</div>
<!-- Content Display -->
<div v-else-if="content" class="content-container">
<div class="content-body" v-html="renderedContent"></div>
<div v-if="version" class="content-version">
Versie: {{ version }}
</div>
</div>
<!-- Empty State -->
<div v-else class="empty-container">
<p>Geen content beschikbaar.</p>
</div>
</div>
<!-- Modal Footer -->
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @click="closeModal">
Sluiten
</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ContentModal',
props: {
show: {
type: Boolean,
default: false
},
title: {
type: String,
default: 'Content'
},
content: {
type: String,
default: ''
},
version: {
type: String,
default: ''
},
loading: {
type: Boolean,
default: false
},
error: {
type: Boolean,
default: false
},
errorMessage: {
type: String,
default: 'Er is een fout opgetreden bij het laden van de content.'
}
},
emits: ['close', 'retry'],
computed: {
renderedContent() {
if (!this.content) return '';
// Use marked library if available (same pattern as SideBarExplanation)
if (typeof window.marked === 'function') {
return window.marked(this.content);
} else if (window.marked && typeof window.marked.parse === 'function') {
return window.marked.parse(this.content);
} else {
console.warn('Marked library not available, falling back to basic parsing');
// Fallback to basic regex-based parsing if marked is not available
return this.content
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/^\* (.*$)/gim, '<li>$1</li>')
.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
.replace(/\*(.*)\*/gim, '<em>$1</em>')
.replace(/\n\n/gim, '</p><p>')
.replace(/^(?!<[h|l|p])/gim, '<p>')
.replace(/(?<![h|l|p]>)$/gim, '</p>')
.replace(/<p><\/p>/gim, '')
.replace(/<p>(<h[1-6]>)/gim, '$1')
.replace(/(<\/h[1-6]>)<\/p>/gim, '$1')
.replace(/<p>(<li>)/gim, '<ul>$1')
.replace(/(<\/li>)<\/p>/gim, '$1</ul>');
}
}
},
methods: {
closeModal() {
this.$emit('close');
},
handleOverlayClick() {
// Close modal when clicking on overlay (outside the dialog)
this.closeModal();
},
retryLoad() {
this.$emit('retry');
},
handleEscapeKey(event) {
if (event.key === 'Escape' && this.show) {
this.closeModal();
}
}
},
mounted() {
// Add escape key listener
document.addEventListener('keydown', this.handleEscapeKey);
},
beforeUnmount() {
// Remove escape key listener
document.removeEventListener('keydown', this.handleEscapeKey);
}
};
</script>
<style scoped>
/* Modal Overlay */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
padding: 20px;
box-sizing: border-box;
}
/* Modal Dialog */
.modal-dialog {
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
width: 80vw;
height: min(80vh, calc(100vh - 120px));
max-height: calc(100vh - 120px);
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Modal Header */
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #e0e0e0;
background-color: #f8f9fa;
}
.modal-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #333;
}
.modal-close {
background: none;
border: none;
cursor: pointer;
padding: 4px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: #666;
transition: background-color 0.2s, color 0.2s;
}
.modal-close:hover {
background-color: #e9ecef;
color: #333;
}
.modal-close .material-icons {
font-size: 20px;
}
/* Modal Body */
.modal-body {
flex: 1;
padding: 20px;
overflow-y: auto;
min-height: 200px;
}
/* Loading State */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
text-align: center;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Error State */
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
text-align: center;
}
.error-icon {
margin-bottom: 16px;
}
.error-icon .material-icons {
font-size: 48px;
color: #dc3545;
}
.error-container h5 {
margin: 0 0 12px 0;
color: #dc3545;
font-weight: 600;
}
.error-container p {
margin: 0 0 20px 0;
color: #666;
}
/* Content Display */
.content-container {
line-height: 1.6;
}
.content-body {
color: #333;
font-size: 14px;
}
.content-body h1,
.content-body h2,
.content-body h3 {
margin-top: 24px;
margin-bottom: 12px;
color: #333;
}
.content-body h1 {
font-size: 24px;
border-bottom: 2px solid #e0e0e0;
padding-bottom: 8px;
}
.content-body h2 {
font-size: 20px;
}
.content-body h3 {
font-size: 16px;
}
.content-body p {
margin-bottom: 12px;
}
.content-body ul {
margin: 12px 0;
padding-left: 20px;
}
.content-body li {
margin-bottom: 4px;
}
.content-body strong {
font-weight: 600;
}
.content-version {
margin-top: 20px;
padding-top: 12px;
border-top: 1px solid #e0e0e0;
font-size: 12px;
color: #666;
text-align: right;
}
/* Empty State */
.empty-container {
display: flex;
justify-content: center;
align-items: center;
padding: 40px 20px;
text-align: center;
color: #666;
}
/* Modal Footer */
.modal-footer {
padding: 16px 20px;
border-top: 1px solid #e0e0e0;
background-color: #f8f9fa;
display: flex;
justify-content: flex-end;
}
/* Button Styles */
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background-color 0.2s, border-color 0.2s;
text-decoration: none;
display: inline-block;
text-align: center;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #545b62;
}
/* Responsive Design */
@media (max-width: 768px) {
.modal-overlay {
padding: 10px;
bottom: 80px;
}
.modal-dialog {
width: 95vw;
height: min(95vh, calc(100vh - 60px));
max-height: calc(100vh - 60px);
}
.modal-header,
.modal-body,
.modal-footer {
padding: 12px 16px;
}
.modal-title {
font-size: 16px;
}
}
</style>

View File

@@ -2,7 +2,7 @@
<div class="dynamic-form-container">
<div class="dynamic-form" :class="{ 'readonly': readOnly, 'edit': !readOnly }">
<!-- Form header with icon and title -->
<div v-if="formData.title || formData.name || formData.icon" class="form-header">
<div v-if="shouldShowFormHeader" class="form-header">
<div v-if="formData.icon" class="form-icon">
<span class="material-symbols-outlined">{{ formData.icon }}</span>
</div>
@@ -19,6 +19,8 @@
:field-id="field.id || field.name"
:model-value="localFormValues[field.id || field.name]"
@update:model-value="updateFieldValue(field.id || field.name, $event)"
@open-privacy-modal="openPrivacyModal"
@open-terms-modal="openTermsModal"
/>
</template>
<template v-else-if="typeof formData.fields === 'object'">
@@ -29,6 +31,8 @@
:field-id="fieldId"
:model-value="localFormValues[fieldId]"
@update:model-value="updateFieldValue(fieldId, $event)"
@open-privacy-modal="openPrivacyModal"
@open-terms-modal="openTermsModal"
/>
</template>
</div>
@@ -68,12 +72,14 @@
</div>
</div>
</div>
</div>
</template>
<script>
import FormField from './FormField.vue';
import { useIconManager } from '../js/composables/useIconManager.js';
import { injectContentModal } from '../js/composables/useContentModal.js';
export default {
name: 'DynamicForm',
@@ -82,11 +88,14 @@ export default {
},
setup(props) {
const { watchIcon } = useIconManager();
const contentModal = injectContentModal();
// Watch formData.icon for automatic icon loading
watchIcon(() => props.formData?.icon);
return {};
return {
contentModal
};
},
props: {
formData: {
@@ -136,6 +145,10 @@ export default {
hideActions: {
type: Boolean,
default: false
},
apiPrefix: {
type: String,
required: true
}
},
emits: ['submit', 'cancel', 'update:formValues'],
@@ -195,6 +208,16 @@ export default {
}
return missingFields.length === 0;
},
// Title display mode configuration
titleDisplayMode() {
console.log('Title display mode:', window.chatConfig?.form_title_display || 'Full Title');
return window.chatConfig?.form_title_display || 'Full Title';
},
// Determine if form header should be shown
shouldShowFormHeader() {
const hasContent = this.formData.title || this.formData.name || this.formData.icon;
return hasContent && this.titleDisplayMode !== 'No Title';
}
},
watch: {
@@ -393,6 +416,40 @@ export default {
}
return value.toString();
},
// Modal handling methods
openPrivacyModal() {
this.loadContent('privacy');
},
openTermsModal() {
this.loadContent('terms');
},
closeModal() {
this.contentModal.hideModal();
},
retryLoad() {
// Retry loading the last requested content type
const currentTitle = this.contentModal.modalState.title.toLowerCase();
if (currentTitle.includes('privacy')) {
this.loadContent('privacy');
} else if (currentTitle.includes('terms')) {
this.loadContent('terms');
}
},
async loadContent(contentType) {
const title = contentType === 'privacy' ? 'Privacy Statement' : 'Terms & Conditions';
const contentUrl = `${this.apiPrefix}/${contentType}`;
// Use the composable to show modal and load content
await this.contentModal.showModal({
title: title,
contentUrl: contentUrl
});
}
}
};

View File

@@ -99,7 +99,16 @@
:required="field.required"
style="margin-right: 8px;"
>
<span class="checkbox-text">{{ field.name }}</span>
<!-- Regular checkbox label -->
<span v-if="!isConsentField" class="checkbox-text">{{ field.name }}</span>
<!-- Consent field with privacy and terms links -->
<span v-else class="checkbox-text consent-text">
{{ texts.consentPrefix }}
<a href="#" @click="openPrivacyModal" class="consent-link">{{ texts.privacyLink }}</a>
{{ texts.consentMiddle }}
<a href="#" @click="openTermsModal" class="consent-link">{{ texts.termsLink }}</a>
{{ texts.consentSuffix }}
</span>
<span v-if="field.required" class="required" style="color: #d93025; margin-left: 2px;">*</span>
</label>
</div>
@@ -167,6 +176,8 @@
</template>
<script>
import { useComponentTranslations } from '../js/services/LanguageProvider.js';
export default {
name: 'FormField',
props: {
@@ -185,8 +196,57 @@ export default {
default: null
}
},
emits: ['update:modelValue'],
emits: ['update:modelValue', 'open-privacy-modal', 'open-terms-modal'],
setup() {
// Consent text constants (English base)
const consentTexts = {
consentPrefix: "I agree with the",
consentMiddle: "and",
consentSuffix: "of AskEveAI",
privacyLink: "privacy statement",
termsLink: "terms and conditions"
};
try {
// Initialize translations for this component
const { texts } = useComponentTranslations('FormField', consentTexts);
return {
translatedTexts: texts,
fallbackTexts: consentTexts
};
} catch (error) {
console.error('FormField setup(): Error in useComponentTranslations:', error);
// Return fallback texts if LanguageProvider fails
return {
translatedTexts: null,
fallbackTexts: consentTexts
};
}
},
computed: {
texts() {
// Robust consent texts that always return valid values
// Use translated texts if available and valid, otherwise use fallback
if (this.translatedTexts && typeof this.translatedTexts === 'object') {
const translated = this.translatedTexts;
// Check if translated texts have all required properties
if (translated.consentPrefix && translated.consentMiddle && translated.consentSuffix &&
translated.privacyLink && translated.termsLink) {
return translated;
}
}
// Fallback to English texts
return this.fallbackTexts || {
consentPrefix: "I agree with the",
consentMiddle: "and",
consentSuffix: "of AskEveAI",
privacyLink: "privacy statement",
termsLink: "terms and conditions"
};
},
value: {
get() {
// Gebruik default waarde als modelValue undefined is
@@ -239,6 +299,12 @@ export default {
},
description() {
return this.field.description || '';
},
isConsentField() {
// Detect consent fields by fieldId (key in dictionary) only, not by name (translated label)
return this.field.type === 'boolean' &&
(this.fieldId === 'consent' ||
this.fieldId.toLowerCase().includes('consent'));
}
},
methods: {
@@ -247,6 +313,14 @@ export default {
if (file) {
this.value = file;
}
},
openPrivacyModal(event) {
event.preventDefault();
this.$emit('open-privacy-modal');
},
openTermsModal(event) {
event.preventDefault();
this.$emit('open-terms-modal');
}
}
};
@@ -399,6 +473,29 @@ export default {
margin-left: 2px;
}
/* Consent field styling */
.consent-text {
line-height: 1.4;
}
.consent-link {
color: #007bff;
text-decoration: underline;
cursor: pointer;
transition: color 0.2s ease;
}
.consent-link:hover {
color: #0056b3;
text-decoration: underline;
}
.consent-link:focus {
outline: 2px solid #007bff;
outline-offset: 2px;
border-radius: 2px;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.form-field {

View File

@@ -34,17 +34,17 @@ const props = defineProps({
}
});
// Use component translations from provider
const originalTexts = computed(() => ({
// Use component translations from provider - fix: use reactive object instead of computed
const originalTexts = {
explanation: props.originalText || ''
}));
};
const { translations, isLoading, error, currentLanguage } = useComponentTranslations(
const { texts: translations, isLoading, error, currentLanguage } = useComponentTranslations(
'sidebar_explanation',
originalTexts.value
originalTexts
);
const translatedText = computed(() => translations.value.explanation || props.originalText);
const translatedText = computed(() => translations.value?.explanation || props.originalText);
// Render markdown content
const renderedExplanation = computed(() => {
@@ -61,12 +61,11 @@ const renderedExplanation = computed(() => {
});
// Watch for text changes to update the provider
watch(() => props.originalText, () => {
// Update original texts when prop changes
originalTexts.value = {
explanation: props.originalText || ''
};
});
watch(() => props.originalText, (newText) => {
// Re-register component with new text if needed
// The LanguageProvider will handle the update automatically
console.log('SideBarExplanation: Original text changed to:', newText);
}, { immediate: true });
</script>
<style scoped>

View File

@@ -11,6 +11,7 @@
window.chatConfig = {
explanation: `{{ customisation.sidebar_markdown|default('') }}`,
progress_tracker_insights: `{{ customisation.progress_tracker_insights|default('No Information') }}`,
form_title_display: `{{ customisation.form_title_display|default('Full Title') }}`,
conversationId: '{{ conversation_id|default("default") }}',
messages: {{ messages|tojson|safe }},
settings: {

View File

@@ -3,7 +3,7 @@ import uuid
from flask import Blueprint, render_template, request, session, current_app, jsonify, Response, stream_with_context
from sqlalchemy.exc import SQLAlchemyError
from common.extensions import db
from common.extensions import db, content_manager
from common.models.user import Tenant, SpecialistMagicLinkTenant, TenantMake
from common.models.interaction import SpecialistMagicLink, Specialist, ChatSession, Interaction
from common.services.interaction.specialist_services import SpecialistServices
@@ -377,4 +377,71 @@ def translate():
'success': False,
'error': f"Error translating: {str(e)}"
}), 500
@chat_bp.route('/privacy', methods=['GET'])
def privacy_statement():
"""
Public AJAX endpoint for privacy statement content
Returns JSON response suitable for modal display
"""
try:
# Use content_manager to get the latest privacy content
content_data = content_manager.read_content('privacy')
if not content_data:
current_app.logger.error("Privacy statement content not found")
return jsonify({
'error': 'Privacy statement not available',
'message': 'The privacy statement could not be loaded at this time.'
}), 404
current_app.logger.debug(f"Content data: {content_data}")
# Return JSON response for AJAX consumption
return jsonify({
'title': 'Privacy Statement',
'content': content_data['content'],
'version': content_data['version'],
'content_type': content_data['content_type']
}), 200
except Exception as e:
current_app.logger.error(f"Error loading privacy statement: {str(e)}")
return jsonify({
'error': 'Server error',
'message': 'An error occurred while loading the privacy statement.'
}), 500
@chat_bp.route('/terms', methods=['GET'])
def terms_conditions():
"""
Public AJAX endpoint for terms & conditions content
Returns JSON response suitable for modal display
"""
try:
# Use content_manager to get the latest terms content
content_data = content_manager.read_content('terms')
if not content_data:
current_app.logger.error("Terms & conditions content not found")
return jsonify({
'error': 'Terms & conditions not available',
'message': 'The terms & conditions could not be loaded at this time.'
}), 404
# Return JSON response for AJAX consumption
return jsonify({
'title': 'Terms & Conditions',
'content': content_data['content'],
'version': content_data['version'],
'content_type': content_data['content_type']
}), 200
except Exception as e:
current_app.logger.error(f"Error loading terms & conditions: {str(e)}")
return jsonify({
'error': 'Server error',
'message': 'An error occurred while loading the terms & conditions.'
}), 500