diff --git a/application/single_app/functions_ai_notice.py b/application/single_app/functions_ai_notice.py
new file mode 100644
index 000000000..4da8719da
--- /dev/null
+++ b/application/single_app/functions_ai_notice.py
@@ -0,0 +1,112 @@
+# functions_ai_notice.py
+"""Helpers for the configurable AI notice shown below the chat composer."""
+
+import hashlib
+import json
+import re
+from datetime import datetime, timezone
+
+
+AI_NOTICE_FREQUENCIES = ("non_dismissible", "every_session", "daily", "once")
+AI_NOTICE_DEFAULT_FREQUENCY = "non_dismissible"
+AI_NOTICE_MAX_MESSAGE_LENGTH = 1000
+AI_NOTICE_USER_SETTINGS_KEY = "aiNoticeDismissal"
+_AI_NOTICE_HASH_PATTERN = re.compile(r"^[0-9a-f]{64}$")
+
+
+def normalize_ai_notice_frequency(value):
+ """Normalize the configured AI notice display frequency."""
+ normalized_value = str(value or "").strip().lower().replace("-", "_")
+ aliases = {
+ "always": "non_dismissible",
+ "non_dismissible": "non_dismissible",
+ "persistent": "non_dismissible",
+ "session": "every_session",
+ "every_session": "every_session",
+ "per_session": "every_session",
+ "daily": "daily",
+ "once_per_day": "daily",
+ "per_day": "daily",
+ "once": "once",
+ "one_time": "once",
+ "just_once": "once",
+ }
+ return aliases.get(normalized_value, AI_NOTICE_DEFAULT_FREQUENCY)
+
+
+def normalize_ai_notice_message(value):
+ """Normalize administrator-entered AI notice text."""
+ return (
+ str(value or "")
+ .replace("\r\n", "\n")
+ .replace("\r", "\n")
+ .strip()[:AI_NOTICE_MAX_MESSAGE_LENGTH]
+ )
+
+
+def compute_ai_notice_hash(message, frequency):
+ """Compute the version hash used to invalidate stale dismissals."""
+ payload = {
+ "message": normalize_ai_notice_message(message),
+ "frequency": normalize_ai_notice_frequency(frequency),
+ }
+ encoded_payload = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
+ return hashlib.sha256(encoded_payload).hexdigest()
+
+
+def get_ai_notice_config(settings):
+ """Build normalized browser-safe AI notice configuration."""
+ source_settings = settings or {}
+ message = normalize_ai_notice_message(source_settings.get("ai_notice_message"))
+ frequency = normalize_ai_notice_frequency(source_settings.get("ai_notice_frequency"))
+ return {
+ "enabled": bool(source_settings.get("enable_ai_notice", False) and message),
+ "message": message,
+ "frequency": frequency,
+ "hash": compute_ai_notice_hash(message, frequency),
+ }
+
+
+def build_ai_notice_dismissal_record(value, dismissed_at=None):
+ """Validate a requested dismissal and create a server-timestamped record."""
+ if not isinstance(value, dict):
+ raise ValueError("AI notice dismissal must be an object.")
+
+ notice_hash = str(value.get("hash") or "").strip().lower()
+ if not _AI_NOTICE_HASH_PATTERN.fullmatch(notice_hash):
+ raise ValueError("AI notice dismissal hash is invalid.")
+
+ frequency = normalize_ai_notice_frequency(value.get("frequency"))
+ if frequency not in {"daily", "once"}:
+ raise ValueError("AI notice dismissal frequency is invalid.")
+
+ dismissed_at = dismissed_at or datetime.now(timezone.utc)
+ return {
+ "hash": notice_hash,
+ "frequency": frequency,
+ "dismissed_at": dismissed_at.isoformat(),
+ "dismissed_date": dismissed_at.strftime("%Y-%m-%d"),
+ }
+
+
+def is_ai_notice_dismissed(ai_notice_config, user_settings, current_time=None):
+ """Return whether the current persisted dismissal hides this notice."""
+ if not ai_notice_config.get("enabled"):
+ return True
+
+ frequency = ai_notice_config.get("frequency")
+ if frequency not in {"daily", "once"}:
+ return False
+
+ dismissal = (user_settings or {}).get(AI_NOTICE_USER_SETTINGS_KEY, {})
+ if not isinstance(dismissal, dict):
+ return False
+ if dismissal.get("hash") != ai_notice_config.get("hash"):
+ return False
+ if dismissal.get("frequency") != frequency:
+ return False
+ if frequency == "once":
+ return True
+
+ current_time = current_time or datetime.now(timezone.utc)
+ return dismissal.get("dismissed_date") == current_time.strftime("%Y-%m-%d")
diff --git a/application/single_app/functions_settings.py b/application/single_app/functions_settings.py
index bcebb15e1..4eb0ca77f 100644
--- a/application/single_app/functions_settings.py
+++ b/application/single_app/functions_settings.py
@@ -862,6 +862,9 @@ def get_settings(use_cosmos=False, include_source=False):
'terms_of_use_decline_redirect_url': '/',
'terms_of_use_accept_button_text': 'Accept and continue',
'terms_of_use_decline_button_text': 'Cancel',
+ 'enable_ai_notice': False,
+ 'ai_notice_message': '',
+ 'ai_notice_frequency': 'non_dismissible',
'enable_dark_mode_default': False,
'enable_left_nav_default': True,
'release_notifications_registered': False,
diff --git a/application/single_app/route_backend_users.py b/application/single_app/route_backend_users.py
index 20d5cf1f9..1c9363357 100644
--- a/application/single_app/route_backend_users.py
+++ b/application/single_app/route_backend_users.py
@@ -11,6 +11,10 @@
normalize_collaboration_user,
)
from functions_appinsights import log_event
+from functions_ai_notice import (
+ AI_NOTICE_USER_SETTINGS_KEY,
+ build_ai_notice_dismissal_record,
+)
from functions_authentication import *
from functions_group import (
check_group_status_allows_operation,
@@ -506,6 +510,7 @@ def user_settings():
# Chat UI settings
'navbar_layout', 'chatLayout', 'showChatTitle', 'chatSplitSizes',
'deepResearchDefaultEnabled',
+ 'aiNoticeDismissal',
'sidebarToggleStyle', 'sidebarMenuState', 'fontSizePreference',
'conversationContentsDrawerEnabled',
LATEST_FEATURES_HIDDEN_VERSION_SETTING,
@@ -555,6 +560,16 @@ def user_settings():
if not isinstance(settings_to_update["conversationContentsDrawerEnabled"], bool):
return jsonify({"error": "Invalid conversation contents drawer preference"}), 400
+ if AI_NOTICE_USER_SETTINGS_KEY in settings_to_update:
+ try:
+ settings_to_update[AI_NOTICE_USER_SETTINGS_KEY] = (
+ build_ai_notice_dismissal_record(
+ settings_to_update[AI_NOTICE_USER_SETTINGS_KEY]
+ )
+ )
+ except ValueError:
+ return jsonify({"error": "Invalid AI notice dismissal"}), 400
+
if "sidebarMenuState" in settings_to_update:
sidebar_menu_state = settings_to_update.get("sidebarMenuState")
allowed_sidebar_menu_keys = {
diff --git a/application/single_app/route_frontend_admin_settings.py b/application/single_app/route_frontend_admin_settings.py
index a2b8f6b08..1cc72aeb4 100644
--- a/application/single_app/route_frontend_admin_settings.py
+++ b/application/single_app/route_frontend_admin_settings.py
@@ -41,6 +41,10 @@
from functions_logging import *
from functions_document_actions import normalize_document_action_capabilities
from functions_model_capabilities import is_vision_capable_model
+from functions_ai_notice import (
+ normalize_ai_notice_frequency,
+ normalize_ai_notice_message,
+)
from functions_terms_of_use import (
TERMS_OF_USE_DEFAULT_REDIRECT,
TERMS_OF_USE_MAX_BUTTON_TEXT_LENGTH,
@@ -841,6 +845,14 @@ def admin_settings():
settings['terms_of_use_accept_button_text'] = 'Accept and continue'
if 'terms_of_use_decline_button_text' not in settings:
settings['terms_of_use_decline_button_text'] = 'Cancel'
+ if 'enable_ai_notice' not in settings:
+ settings['enable_ai_notice'] = False
+ settings['ai_notice_message'] = normalize_ai_notice_message(
+ settings.get('ai_notice_message')
+ )
+ settings['ai_notice_frequency'] = normalize_ai_notice_frequency(
+ settings.get('ai_notice_frequency')
+ )
# --- Add defaults for user agreement ---
if 'enable_user_agreement' not in settings:
settings['enable_user_agreement'] = False
@@ -2077,6 +2089,18 @@ def parse_admin_int(raw_value, fallback_value, field_name="unknown", hard_defaul
flash('Terms of Use message is required when the feature is enabled.', 'danger')
return redirect(url_for('frontend_admin_settings.admin_settings'))
+ # --- AI Notice Settings ---
+ enable_ai_notice = form_data.get('enable_ai_notice') == 'on'
+ ai_notice_message = normalize_ai_notice_message(
+ form_data.get('ai_notice_message')
+ )
+ ai_notice_frequency = normalize_ai_notice_frequency(
+ form_data.get('ai_notice_frequency')
+ )
+ if enable_ai_notice and not ai_notice_message:
+ flash('AI notice message is required when the feature is enabled.', 'danger')
+ return redirect(url_for('frontend_admin_settings.admin_settings'))
+
# --- Authentication & Redirect Settings ---
enable_front_door = form_data.get('enable_front_door') == 'on'
front_door_url = form_data.get('front_door_url', '').strip()
@@ -2460,6 +2484,11 @@ def is_valid_url(url):
'terms_of_use_accept_button_text': terms_of_use_accept_button_text,
'terms_of_use_decline_button_text': terms_of_use_decline_button_text,
+ # AI Notice
+ 'enable_ai_notice': enable_ai_notice,
+ 'ai_notice_message': ai_notice_message,
+ 'ai_notice_frequency': ai_notice_frequency,
+
# Multimedia & Metadata
'enable_video_file_support': enable_video_file_support,
'enable_audio_file_support': enable_audio_file_support,
diff --git a/application/single_app/route_frontend_chats.py b/application/single_app/route_frontend_chats.py
index 8f81fcc23..340032513 100644
--- a/application/single_app/route_frontend_chats.py
+++ b/application/single_app/route_frontend_chats.py
@@ -6,6 +6,7 @@
from functions_content import *
from functions_settings import *
from functions_agent_catalog import build_accessible_agent_catalog
+from functions_ai_notice import get_ai_notice_config, is_ai_notice_dismissed
from functions_collaboration import (
assert_user_can_participate_in_collaboration_conversation,
create_collaboration_message_notifications,
@@ -677,6 +678,11 @@ def chats():
user_settings = get_user_settings(user_id)
user_settings_dict = user_settings.get("settings", {}) if isinstance(user_settings, dict) else {}
public_settings = sanitize_settings_for_user(settings)
+ ai_notice = get_ai_notice_config(public_settings)
+ ai_notice['dismissed'] = is_ai_notice_dismissed(
+ ai_notice,
+ user_settings_dict,
+ )
current_user_info = get_current_user_info() or {}
current_user_roles = (session.get('user') or {}).get('roles', [])
user_workflows_enabled_for_user = is_user_workflows_enabled_for_user(
@@ -855,6 +861,7 @@ def chats():
return render_template(
'chats.html',
settings=public_settings,
+ ai_notice=ai_notice,
enable_user_feedback=enable_user_feedback,
active_group_id=active_group_id,
active_group_name=active_group_name,
diff --git a/application/single_app/static/css/chats.css b/application/single_app/static/css/chats.css
index b40a02717..23462a751 100644
--- a/application/single_app/static/css/chats.css
+++ b/application/single_app/static/css/chats.css
@@ -4351,4 +4351,37 @@ mark.search-highlight {
[data-bs-theme="dark"] .thoughts-container {
border-top-color: #495057 !important;
+}
+
+.ai-notice {
+ align-items: flex-start;
+ gap: 0.5rem;
+ padding: 0.5rem 0.25rem 0.125rem;
+ color: var(--bs-secondary-color);
+ font-size: 0.78rem;
+ line-height: 1.4;
+}
+
+.ai-notice:not(.d-none) {
+ display: flex;
+}
+
+.ai-notice-icon {
+ flex: 0 0 auto;
+ margin-top: 0.1rem;
+ color: var(--bs-primary);
+}
+
+.ai-notice-message {
+ flex: 1 1 auto;
+ min-width: 0;
+ white-space: pre-line;
+}
+
+.ai-notice-dismiss {
+ flex: 0 0 auto;
+ width: 0.7rem;
+ height: 0.7rem;
+ margin-top: 0.15rem;
+ font-size: 0.6rem;
}
\ No newline at end of file
diff --git a/application/single_app/static/js/admin/admin_settings.js b/application/single_app/static/js/admin/admin_settings.js
index f25a9ddcd..6da18300a 100644
--- a/application/single_app/static/js/admin/admin_settings.js
+++ b/application/single_app/static/js/admin/admin_settings.js
@@ -6793,6 +6793,16 @@ function setupToggles() {
});
}
+ const enableAiNotice = document.getElementById('enable_ai_notice');
+ const aiNoticeSettings = document.getElementById('ai_notice_settings');
+ if (enableAiNotice && aiNoticeSettings) {
+ toggleVisibility(aiNoticeSettings, enableAiNotice.checked);
+ enableAiNotice.addEventListener('change', function() {
+ toggleVisibility(aiNoticeSettings, this.checked);
+ markFormAsModified();
+ });
+ }
+
const enableUrlAccess = document.getElementById('enable_url_access');
const urlAccessSettings = document.getElementById('url_access_settings');
const applyUrlAccessDefaults = () => {
diff --git a/application/single_app/static/js/chat/chat-ai-notice.js b/application/single_app/static/js/chat/chat-ai-notice.js
new file mode 100644
index 000000000..12b161175
--- /dev/null
+++ b/application/single_app/static/js/chat/chat-ai-notice.js
@@ -0,0 +1,89 @@
+// chat-ai-notice.js
+
+import { saveUserSetting } from './chat-layout.js';
+import { showToast } from './chat-toast.js';
+
+
+const aiNotice = document.getElementById('ai-notice');
+const aiNoticeDismiss = document.getElementById('ai-notice-dismiss');
+const aiNoticeUserSettingKey = 'aiNoticeDismissal';
+const aiNoticeSessionStorageKey = 'simplechat.aiNoticeDismissal';
+
+
+function getSessionDismissalKey(noticeHash) {
+ return `${aiNoticeSessionStorageKey}.${noticeHash}`;
+}
+
+
+function isSessionNoticeDismissed(noticeHash) {
+ try {
+ return sessionStorage.getItem(getSessionDismissalKey(noticeHash)) === 'true';
+ } catch (error) {
+ if (!(error instanceof DOMException)) {
+ throw error;
+ }
+ console.warn('Session storage is unavailable; the AI notice will remain visible.', error);
+ return false;
+ }
+}
+
+
+function showAiNotice() {
+ aiNotice?.classList.remove('d-none');
+}
+
+
+function hideAiNotice() {
+ aiNotice?.classList.add('d-none');
+}
+
+
+async function dismissAiNotice() {
+ if (!aiNotice || !aiNoticeDismiss) {
+ return;
+ }
+
+ const noticeFrequency = aiNotice.dataset.noticeFrequency || '';
+ const noticeHash = aiNotice.dataset.noticeHash || '';
+ if (noticeFrequency === 'every_session') {
+ try {
+ sessionStorage.setItem(getSessionDismissalKey(noticeHash), 'true');
+ } catch (error) {
+ if (!(error instanceof DOMException)) {
+ throw error;
+ }
+ showToast('This browser cannot save the session dismissal.', 'warning');
+ return;
+ }
+ hideAiNotice();
+ return;
+ }
+
+ aiNoticeDismiss.disabled = true;
+ const saved = await saveUserSetting({
+ [aiNoticeUserSettingKey]: {
+ hash: noticeHash,
+ frequency: noticeFrequency
+ }
+ });
+ if (saved) {
+ hideAiNotice();
+ return;
+ }
+
+ aiNoticeDismiss.disabled = false;
+ showToast('The AI notice could not be dismissed. Please try again.', 'warning');
+}
+
+
+if (aiNotice) {
+ const noticeFrequency = aiNotice.dataset.noticeFrequency || '';
+ const noticeHash = aiNotice.dataset.noticeHash || '';
+ const dismissedForSession = noticeFrequency === 'every_session'
+ && isSessionNoticeDismissed(noticeHash);
+
+ if (!dismissedForSession) {
+ showAiNotice();
+ }
+ aiNoticeDismiss?.addEventListener('click', dismissAiNotice);
+}
diff --git a/application/single_app/templates/admin_settings.html b/application/single_app/templates/admin_settings.html
index 4f2c9a9cf..4f2b53181 100644
--- a/application/single_app/templates/admin_settings.html
+++ b/application/single_app/templates/admin_settings.html
@@ -2861,6 +2861,51 @@
+
+
+ Chat AI Notice
+
+
+ Display an administrator-provided reminder directly below the chat input, such as guidance that responses are AI-generated and should be reviewed.
+
+
+
+
+
+
+
+
+
+
Plain text only. Line breaks are preserved.
+
+
+
+
+
Changing the notice text or display behavior creates a new message version and shows it again.
+
+
+
+
Terms of Use
diff --git a/application/single_app/templates/chats.html b/application/single_app/templates/chats.html
index 17c7ae3b4..b33edb413 100644
--- a/application/single_app/templates/chats.html
+++ b/application/single_app/templates/chats.html
@@ -803,6 +803,28 @@ Grounded Search
+ {% if ai_notice.enabled and not ai_notice.dismissed %}
+
+
+ {{ ai_notice.message }}
+ {% if ai_notice.frequency != 'non_dismissible' %}
+
+ {% endif %}
+
+ {% endif %}
{% if conversation_contents_drawer_enabled %}