From 02f11dc1cb17ccc4acb7db9cda10dc955dabebb6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 03:11:12 +0000 Subject: [PATCH 1/3] Add splash screen feature with configurable policies and dismissal tracking Co-authored-by: garland3 <1162675+garland3@users.noreply.github.com> --- backend/modules/config/config_manager.py | 1 + backend/routes/config_routes.py | 72 +++++++++++ config/defaults/splash-config.json | 16 +++ config/overrides/splash-config.json | 36 ++++++ frontend/src/App.jsx | 25 ++++ frontend/src/components/SplashScreen.jsx | 149 +++++++++++++++++++++++ 6 files changed, 299 insertions(+) create mode 100644 config/defaults/splash-config.json create mode 100644 config/overrides/splash-config.json create mode 100644 frontend/src/components/SplashScreen.jsx diff --git a/backend/modules/config/config_manager.py b/backend/modules/config/config_manager.py index 5e41f04..efb3110 100644 --- a/backend/modules/config/config_manager.py +++ b/backend/modules/config/config_manager.py @@ -271,6 +271,7 @@ def agent_mode_available(self) -> bool: help_config_file: str = Field(default="help-config.json", validation_alias="HELP_CONFIG_FILE") messages_config_file: str = Field(default="messages.txt", validation_alias="MESSAGES_CONFIG_FILE") tool_approvals_config_file: str = Field(default="tool-approvals.json", validation_alias="TOOL_APPROVALS_CONFIG_FILE") + splash_config_file: str = Field(default="splash-config.json", validation_alias="SPLASH_CONFIG_FILE") # Config directory paths app_config_overrides: str = Field(default="config/overrides", validation_alias="APP_CONFIG_OVERRIDES") diff --git a/backend/routes/config_routes.py b/backend/routes/config_routes.py index 8200ec2..5b1d301 100644 --- a/backend/routes/config_routes.py +++ b/backend/routes/config_routes.py @@ -340,6 +340,78 @@ async def get_compliance_levels(current_user: str = Depends(get_current_user)): } +@router.get("/splash") +async def get_splash_config(current_user: str = Depends(get_current_user)): + """Get splash screen configuration.""" + config_manager = app_factory.get_config_manager() + app_settings = config_manager.app_settings + + # Read splash screen configuration + splash_config = {} + import json + splash_config_filename = app_settings.splash_config_file + splash_paths = [] + try: + # Reuse config manager search logic + try: + splash_paths = config_manager._search_paths(splash_config_filename) # type: ignore[attr-defined] + except AttributeError: + # Fallback minimal search if method renamed/removed + from pathlib import Path + backend_root = Path(__file__).parent.parent + project_root = backend_root.parent + splash_paths = [ + project_root / "config" / "overrides" / splash_config_filename, + project_root / "config" / "defaults" / splash_config_filename, + backend_root / "configfilesadmin" / splash_config_filename, + backend_root / "configfiles" / splash_config_filename, + backend_root / splash_config_filename, + project_root / splash_config_filename, + ] + + found_path = None + for p in splash_paths: + if p.exists(): + found_path = p + break + if found_path: + with open(found_path, "r", encoding="utf-8") as f: + splash_config = json.load(f) + logger.info(f"Loaded splash config from {found_path}") + else: + logger.info( + "Splash config not found in any of these locations: %s", + [str(p) for p in splash_paths] + ) + # Return default disabled config + splash_config = { + "enabled": False, + "title": "", + "messages": [], + "dismissible": True, + "require_accept": False, + "dismiss_duration_days": 30, + "accept_button_text": "Accept", + "dismiss_button_text": "Dismiss", + "show_on_every_visit": False + } + except Exception as e: + logger.warning(f"Error loading splash config: {e}") + splash_config = { + "enabled": False, + "title": "", + "messages": [], + "dismissible": True, + "require_accept": False, + "dismiss_duration_days": 30, + "accept_button_text": "Accept", + "dismiss_button_text": "Dismiss", + "show_on_every_visit": False + } + + return splash_config + + # @router.get("/sessions") # async def get_session_info(current_user: str = Depends(get_current_user)): # """Get session information for the current user.""" diff --git a/config/defaults/splash-config.json b/config/defaults/splash-config.json new file mode 100644 index 0000000..d397a38 --- /dev/null +++ b/config/defaults/splash-config.json @@ -0,0 +1,16 @@ +{ + "enabled": false, + "title": "Welcome to Chat UI", + "messages": [ + { + "type": "text", + "content": "Welcome! Please review our policies before continuing." + } + ], + "dismissible": true, + "require_accept": false, + "dismiss_duration_days": 30, + "accept_button_text": "Accept", + "dismiss_button_text": "Dismiss", + "show_on_every_visit": false +} diff --git a/config/overrides/splash-config.json b/config/overrides/splash-config.json new file mode 100644 index 0000000..f8f35ea --- /dev/null +++ b/config/overrides/splash-config.json @@ -0,0 +1,36 @@ +{ + "enabled": true, + "title": "Important Policies and Information", + "messages": [ + { + "type": "heading", + "content": "Cookie Policy" + }, + { + "type": "text", + "content": "This application uses cookies to enhance your experience and maintain your session. By continuing to use this application, you consent to our use of cookies." + }, + { + "type": "heading", + "content": "Acceptable Use Policy" + }, + { + "type": "text", + "content": "This system is for authorized use only. Users must comply with all applicable policies and regulations. Unauthorized access or misuse of this system may result in disciplinary action and/or legal prosecution." + }, + { + "type": "heading", + "content": "Data Privacy" + }, + { + "type": "text", + "content": "Your conversations and data are processed in accordance with our privacy policy. Please do not share sensitive or confidential information unless explicitly authorized." + } + ], + "dismissible": true, + "require_accept": true, + "dismiss_duration_days": 30, + "accept_button_text": "I Accept", + "dismiss_button_text": "Close", + "show_on_every_visit": false +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 2974344..4280496 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -18,6 +18,7 @@ import LogViewer from './components/LogViewer' // Import LogViewer import FeedbackButton from './components/FeedbackButton' import FileManagerPanel from './components/FileManagerPanel' import FilesPage from './components/FilesPage' +import SplashScreen from './components/SplashScreen' function ChatInterface() { const [toolsPanelOpen, setToolsPanelOpen] = useState(false) @@ -174,11 +175,35 @@ function AppRoutes() { } function App() { + const [splashConfig, setSplashConfig] = useState(null) + + // Fetch splash screen configuration on app load + useEffect(() => { + const fetchSplashConfig = async () => { + try { + const response = await fetch('/api/splash') + if (response.ok) { + const config = await response.json() + setSplashConfig(config) + } else { + console.warn('Failed to fetch splash configuration') + setSplashConfig({ enabled: false }) + } + } catch (error) { + console.error('Error fetching splash configuration:', error) + setSplashConfig({ enabled: false }) + } + } + + fetchSplashConfig() + }, []) + return ( + diff --git a/frontend/src/components/SplashScreen.jsx b/frontend/src/components/SplashScreen.jsx new file mode 100644 index 0000000..e6e2819 --- /dev/null +++ b/frontend/src/components/SplashScreen.jsx @@ -0,0 +1,149 @@ +import { useState, useEffect } from 'react' +import { X, CheckCircle } from 'lucide-react' + +/** + * SplashScreen component that displays important policies and information. + * + * Features: + * - Displays configurable messages (text and headings) + * - Can be dismissed or require acceptance + * - Tracks dismissal using localStorage for N days + * - Fully configurable via backend API + */ +const SplashScreen = ({ config, onClose }) => { + const [isVisible, setIsVisible] = useState(false) + + useEffect(() => { + // Don't show if not enabled + if (!config || !config.enabled) { + return + } + + // Check if already dismissed + const dismissKey = 'splash-screen-dismissed' + const dismissedData = localStorage.getItem(dismissKey) + + if (dismissedData && !config.show_on_every_visit) { + try { + const { timestamp } = JSON.parse(dismissedData) + const dismissedDate = new Date(timestamp) + const now = new Date() + const daysSinceDismiss = (now - dismissedDate) / (1000 * 60 * 60 * 24) + + // If within the dismiss duration, don't show + if (daysSinceDismiss < config.dismiss_duration_days) { + return + } + } catch (e) { + // Invalid data, show splash screen + console.warn('Invalid splash screen dismissal data:', e) + } + } + + // Show splash screen + setIsVisible(true) + }, [config]) + + const handleDismiss = () => { + if (config.dismissible) { + // Save dismissal to localStorage + const dismissKey = 'splash-screen-dismissed' + const dismissalData = { + timestamp: new Date().toISOString(), + version: config.title || 'default' + } + localStorage.setItem(dismissKey, JSON.stringify(dismissalData)) + + setIsVisible(false) + if (onClose) { + onClose() + } + } + } + + const handleAccept = () => { + // Save acceptance to localStorage + const dismissKey = 'splash-screen-dismissed' + const dismissalData = { + timestamp: new Date().toISOString(), + version: config.title || 'default', + accepted: true + } + localStorage.setItem(dismissKey, JSON.stringify(dismissalData)) + + setIsVisible(false) + if (onClose) { + onClose() + } + } + + if (!isVisible || !config || !config.enabled) { + return null + } + + return ( +
+
+ {/* Header */} +
+

+ {config.title || 'Welcome'} +

+ {config.dismissible && !config.require_accept && ( + + )} +
+ + {/* Content */} +
+
+ {config.messages && config.messages.map((message, index) => { + if (message.type === 'heading') { + return ( +

+ {message.content} +

+ ) + } else if (message.type === 'text') { + return ( +

+ {message.content} +

+ ) + } + return null + })} +
+
+ + {/* Footer */} +
+ {config.require_accept ? ( + + ) : config.dismissible ? ( + + ) : null} +
+
+
+ ) +} + +export default SplashScreen From 10ef4be498814f65a36ee7944fdc9f99dd7e589a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 03:17:01 +0000 Subject: [PATCH 2/3] Add frontend tests for splash screen feature Co-authored-by: garland3 <1162675+garland3@users.noreply.github.com> --- config/overrides/splash-config.json | 2 +- frontend/src/test/splash-screen.test.jsx | 252 +++++++++++++++++++++++ 2 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 frontend/src/test/splash-screen.test.jsx diff --git a/config/overrides/splash-config.json b/config/overrides/splash-config.json index f8f35ea..0ee4a50 100644 --- a/config/overrides/splash-config.json +++ b/config/overrides/splash-config.json @@ -28,7 +28,7 @@ } ], "dismissible": true, - "require_accept": true, + "require_accept": false, "dismiss_duration_days": 30, "accept_button_text": "I Accept", "dismiss_button_text": "Close", diff --git a/frontend/src/test/splash-screen.test.jsx b/frontend/src/test/splash-screen.test.jsx new file mode 100644 index 0000000..45a1fc7 --- /dev/null +++ b/frontend/src/test/splash-screen.test.jsx @@ -0,0 +1,252 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { render, screen, fireEvent, waitFor } from '@testing-library/react' +import SplashScreen from '../components/SplashScreen' + +describe('SplashScreen', () => { + beforeEach(() => { + // Clear localStorage before each test + localStorage.clear() + vi.clearAllMocks() + }) + + afterEach(() => { + localStorage.clear() + }) + + it('should not render when config is null', () => { + const { container } = render() + expect(container.firstChild).toBeNull() + }) + + it('should not render when config.enabled is false', () => { + const config = { + enabled: false, + title: 'Test Title', + messages: [{ type: 'text', content: 'Test message' }] + } + const { container } = render() + expect(container.firstChild).toBeNull() + }) + + it('should render splash screen when enabled', () => { + const config = { + enabled: true, + title: 'Welcome', + messages: [{ type: 'text', content: 'Welcome to the app' }], + dismissible: true, + require_accept: false, + dismiss_button_text: 'Close' + } + render() + + expect(screen.getByText('Welcome')).toBeInTheDocument() + expect(screen.getByText('Welcome to the app')).toBeInTheDocument() + expect(screen.getByText('Close')).toBeInTheDocument() + }) + + it('should render multiple messages with headings and text', () => { + const config = { + enabled: true, + title: 'Policies', + messages: [ + { type: 'heading', content: 'Cookie Policy' }, + { type: 'text', content: 'We use cookies.' }, + { type: 'heading', content: 'Privacy Policy' }, + { type: 'text', content: 'Your data is secure.' } + ], + dismissible: true + } + render() + + expect(screen.getByText('Cookie Policy')).toBeInTheDocument() + expect(screen.getByText('We use cookies.')).toBeInTheDocument() + expect(screen.getByText('Privacy Policy')).toBeInTheDocument() + expect(screen.getByText('Your data is secure.')).toBeInTheDocument() + }) + + it('should show Accept button when require_accept is true', () => { + const config = { + enabled: true, + title: 'Terms', + messages: [{ type: 'text', content: 'Please accept the terms' }], + require_accept: true, + accept_button_text: 'I Accept' + } + render() + + expect(screen.getByText('I Accept')).toBeInTheDocument() + }) + + it('should show Dismiss button when dismissible is true and require_accept is false', () => { + const config = { + enabled: true, + title: 'Info', + messages: [{ type: 'text', content: 'Some info' }], + dismissible: true, + require_accept: false, + dismiss_button_text: 'Close' + } + render() + + expect(screen.getByText('Close')).toBeInTheDocument() + }) + + it('should call onClose when dismiss button is clicked', () => { + const onClose = vi.fn() + const config = { + enabled: true, + title: 'Info', + messages: [{ type: 'text', content: 'Some info' }], + dismissible: true, + require_accept: false, + dismiss_button_text: 'Close', + dismiss_duration_days: 30 + } + render() + + const closeButton = screen.getByText('Close') + fireEvent.click(closeButton) + + expect(onClose).toHaveBeenCalledTimes(1) + }) + + it('should save dismissal to localStorage when dismissed', () => { + const config = { + enabled: true, + title: 'Info', + messages: [{ type: 'text', content: 'Some info' }], + dismissible: true, + require_accept: false, + dismiss_button_text: 'Close', + dismiss_duration_days: 30 + } + render() + + const closeButton = screen.getByText('Close') + fireEvent.click(closeButton) + + const dismissedData = localStorage.getItem('splash-screen-dismissed') + expect(dismissedData).not.toBeNull() + + const parsed = JSON.parse(dismissedData) + expect(parsed).toHaveProperty('timestamp') + expect(parsed).toHaveProperty('version') + }) + + it('should save acceptance to localStorage when accepted', () => { + const config = { + enabled: true, + title: 'Terms', + messages: [{ type: 'text', content: 'Accept terms' }], + require_accept: true, + accept_button_text: 'I Accept', + dismiss_duration_days: 30 + } + render() + + const acceptButton = screen.getByText('I Accept') + fireEvent.click(acceptButton) + + const dismissedData = localStorage.getItem('splash-screen-dismissed') + expect(dismissedData).not.toBeNull() + + const parsed = JSON.parse(dismissedData) + expect(parsed).toHaveProperty('timestamp') + expect(parsed).toHaveProperty('accepted', true) + }) + + it('should not render if dismissed within duration', () => { + // Pre-populate localStorage with recent dismissal + const dismissedData = { + timestamp: new Date().toISOString(), + version: 'Test' + } + localStorage.setItem('splash-screen-dismissed', JSON.stringify(dismissedData)) + + const config = { + enabled: true, + title: 'Info', + messages: [{ type: 'text', content: 'Should not show' }], + dismissible: true, + dismiss_duration_days: 30, + show_on_every_visit: false + } + + const { container } = render() + expect(container.firstChild).toBeNull() + }) + + it('should render if dismissed outside duration', () => { + // Pre-populate localStorage with old dismissal (40 days ago) + const oldDate = new Date() + oldDate.setDate(oldDate.getDate() - 40) + const dismissedData = { + timestamp: oldDate.toISOString(), + version: 'Test' + } + localStorage.setItem('splash-screen-dismissed', JSON.stringify(dismissedData)) + + const config = { + enabled: true, + title: 'Info', + messages: [{ type: 'text', content: 'Should show again' }], + dismissible: true, + dismiss_duration_days: 30, + show_on_every_visit: false + } + + render() + expect(screen.getByText('Should show again')).toBeInTheDocument() + }) + + it('should render every time if show_on_every_visit is true', () => { + // Pre-populate localStorage with recent dismissal + const dismissedData = { + timestamp: new Date().toISOString(), + version: 'Test' + } + localStorage.setItem('splash-screen-dismissed', JSON.stringify(dismissedData)) + + const config = { + enabled: true, + title: 'Info', + messages: [{ type: 'text', content: 'Always show' }], + dismissible: true, + dismiss_duration_days: 30, + show_on_every_visit: true + } + + render() + expect(screen.getByText('Always show')).toBeInTheDocument() + }) + + it('should show close button (X) when dismissible and not require_accept', () => { + const config = { + enabled: true, + title: 'Info', + messages: [{ type: 'text', content: 'Some info' }], + dismissible: true, + require_accept: false + } + render() + + // Check for close button (X icon) in the header + const closeButton = screen.getByLabelText('Close') + expect(closeButton).toBeInTheDocument() + }) + + it('should not show close button (X) when require_accept is true', () => { + const config = { + enabled: true, + title: 'Terms', + messages: [{ type: 'text', content: 'Must accept' }], + dismissible: true, + require_accept: true + } + render() + + // Close button should not be in the document + const closeButton = screen.queryByLabelText('Close') + expect(closeButton).not.toBeInTheDocument() + }) +}) From 5e695628f35a99ba0b01ef7ce4a8b739a24c3bb0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 18 Nov 2025 04:35:50 +0000 Subject: [PATCH 3/3] Add feature flag and documentation for splash screen Co-authored-by: garland3 <1162675+garland3@users.noreply.github.com> --- .env.example | 11 +++++ backend/modules/config/config_manager.py | 7 +++ backend/routes/config_routes.py | 17 ++++++- docs/02_admin_guide.md | 59 ++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 0d4d1e3..2654269 100644 --- a/.env.example +++ b/.env.example @@ -67,9 +67,20 @@ FEATURE_MARKETPLACE_ENABLED=true # Marketplace browsing (disabled) FEATURE_FILES_PANEL_ENABLED=true # Uploaded/session files panel FEATURE_CHAT_HISTORY_ENABLED=false # Previous chat history list FEATURE_COMPLIANCE_LEVELS_ENABLED=false # Compliance level filtering for MCP servers and data sources +FEATURE_SPLASH_SCREEN_ENABLED=false # Startup splash screen for displaying policies and information # (Adjust above to stage rollouts. For a bare-bones chat set them all to false.) +############################################# +# Configuration File Names +# Override the default names for configuration files. +# Useful for testing or managing multiple configurations. +############################################# +# SPLASH_CONFIG_FILE=splash-config.json # Splash screen configuration file name +# MCP_CONFIG_FILE=mcp.json # MCP servers configuration file name +# LLM_CONFIG_FILE=llmconfig.yml # LLM models configuration file name +# HELP_CONFIG_FILE=help-config.json # Help page configuration file name + # ths might be need for mcp serves to know where to download the files. # CHATUI_BACKEND_BASE_URL=http://127.0.0.1:8000 diff --git a/backend/modules/config/config_manager.py b/backend/modules/config/config_manager.py index efb3110..ebf1f0b 100644 --- a/backend/modules/config/config_manager.py +++ b/backend/modules/config/config_manager.py @@ -163,6 +163,13 @@ class AppSettings(BaseSettings): # Banner settings banner_enabled: bool = False + # Splash screen settings + feature_splash_screen_enabled: bool = Field( + False, + description="Enable startup splash screen for displaying policies and information", + validation_alias=AliasChoices("FEATURE_SPLASH_SCREEN_ENABLED", "SPLASH_SCREEN_ENABLED"), + ) + # Agent settings # Renamed to feature_agent_mode_available to align with other FEATURE_* flags. feature_agent_mode_available: bool = Field( diff --git a/backend/routes/config_routes.py b/backend/routes/config_routes.py index 5b1d301..ecabd6c 100644 --- a/backend/routes/config_routes.py +++ b/backend/routes/config_routes.py @@ -304,7 +304,8 @@ async def get_config( "marketplace": app_settings.feature_marketplace_enabled, "files_panel": app_settings.feature_files_panel_enabled, "chat_history": app_settings.feature_chat_history_enabled, - "compliance_levels": app_settings.feature_compliance_levels_enabled + "compliance_levels": app_settings.feature_compliance_levels_enabled, + "splash_screen": app_settings.feature_splash_screen_enabled } } @@ -346,6 +347,20 @@ async def get_splash_config(current_user: str = Depends(get_current_user)): config_manager = app_factory.get_config_manager() app_settings = config_manager.app_settings + # Check if splash screen feature is enabled + if not app_settings.feature_splash_screen_enabled: + return { + "enabled": False, + "title": "", + "messages": [], + "dismissible": True, + "require_accept": False, + "dismiss_duration_days": 30, + "accept_button_text": "Accept", + "dismiss_button_text": "Dismiss", + "show_on_every_visit": False + } + # Read splash screen configuration splash_config = {} import json diff --git a/docs/02_admin_guide.md b/docs/02_admin_guide.md index a9f1c10..d8e37e9 100644 --- a/docs/02_admin_guide.md +++ b/docs/02_admin_guide.md @@ -20,6 +20,7 @@ To customize your instance, you will place your own versions of the configuratio * **`llmconfig.yml`**: Defines the list of available Large Language Models and their connection details. * **`compliance-levels.json`**: Defines the security compliance levels (e.g., Public, Internal, HIPAA) and the rules for how they can interact. * **`help-config.json`**: Populates the content of the "Help" modal in the user interface. +* **`splash-config.json`**: Configures the startup splash screen for displaying policies and information to users. * **`messages.txt`**: Defines the text for system-wide banner messages that can be displayed to all users. ### Customizing the Help Modal (`help-config.json`) @@ -51,6 +52,64 @@ The file consists of a title and a list of sections, each with a title and conte } ``` +### Configuring the Splash Screen (`splash-config.json`) + +The splash screen feature allows you to display important policies and information to users when they first access the application. This is commonly used for displaying cookie policies, acceptable use policies, and other legal or organizational information. + +* **Location**: Place your custom file at `config/overrides/splash-config.json`. +* **Feature Flag**: Enable the splash screen by setting `FEATURE_SPLASH_SCREEN_ENABLED=true` in your `.env` file. + +The splash screen supports two operational modes: + +1. **Accept Mode** (`require_accept: true`): Users must explicitly click "I Accept" to proceed. The close (X) button is hidden. +2. **Dismiss Mode** (`require_accept: false`): Users can dismiss the screen by clicking "Close" or the X button in the header. + +User dismissals are tracked in the browser's local storage and will not show again until the configured duration expires (default: 30 days). + +**Example `splash-config.json`:** +```json +{ + "enabled": true, + "title": "Important Policies and Information", + "messages": [ + { + "type": "heading", + "content": "Cookie Policy" + }, + { + "type": "text", + "content": "This application uses cookies to enhance your experience and maintain your session. By continuing to use this application, you consent to our use of cookies." + }, + { + "type": "heading", + "content": "Acceptable Use Policy" + }, + { + "type": "text", + "content": "This system is for authorized use only. Users must comply with all applicable policies and regulations. Unauthorized access or misuse of this system may result in disciplinary action and/or legal prosecution." + } + ], + "dismissible": true, + "require_accept": true, + "dismiss_duration_days": 30, + "accept_button_text": "I Accept", + "dismiss_button_text": "Close", + "show_on_every_visit": false +} +``` + +**Configuration Fields:** + +* **`enabled`**: (boolean) Whether the splash screen is shown. Must be `true` and `FEATURE_SPLASH_SCREEN_ENABLED=true` in `.env`. +* **`title`**: (string) The title displayed at the top of the splash screen modal. +* **`messages`**: (array) A list of message objects. Each message has a `type` (`"heading"` or `"text"`) and `content` (string). +* **`dismissible`**: (boolean) Whether users can dismiss the splash screen. +* **`require_accept`**: (boolean) If `true`, users must click the accept button. If `false`, users can dismiss casually. +* **`dismiss_duration_days`**: (number) Number of days before showing the splash screen again after dismissal. +* **`accept_button_text`**: (string) Text for the accept button (shown when `require_accept` is `true`). +* **`dismiss_button_text`**: (string) Text for the dismiss button (shown when `require_accept` is `false`). +* **`show_on_every_visit`**: (boolean) If `true`, the splash screen will show every time, ignoring dismissal tracking. + ### The `.env` File This file is crucial for setting up your instance. Start by copying the example file: