Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions application/single_app/functions_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"navLayout",
"darkModeEnabled",
"showTutorialButtons",
"desktopNotificationsEnabled",
"chatLayout",
"streamingEnabled",
"notifications_per_page",
Expand Down Expand Up @@ -1103,6 +1104,7 @@ def get_settings(use_cosmos=False, include_source=False):

# User Feedback / Conversation Archiving
'enable_user_feedback': True,
'enable_desktop_notifications': False,
'require_member_of_feedback_admin': False,
'enable_conversation_archiving': False,

Expand Down Expand Up @@ -2107,6 +2109,9 @@ def get_user_settings(user_id, allow_cross_user=False):
if 'showTutorialButtons' not in doc['settings']:
doc['settings']['showTutorialButtons'] = True
updated = True
if 'desktopNotificationsEnabled' not in doc['settings']:
doc['settings']['desktopNotificationsEnabled'] = True
updated = True

if should_sync_session_profile:
# Try to update email/display_name if missing and available in session
Expand Down Expand Up @@ -2150,6 +2155,7 @@ def get_user_settings(user_id, allow_cross_user=False):
doc = {"id": user_id, "settings": {}}
doc["settings"]["personal_model_endpoints"] = []
doc["settings"]["showTutorialButtons"] = True
doc["settings"]["desktopNotificationsEnabled"] = True
if should_sync_session_profile:
user = session.get("user", {})
email = user.get("preferred_username") or user.get("email")
Expand Down
6 changes: 6 additions & 0 deletions application/single_app/route_backend_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,8 @@
'ttsEnabled', 'ttsVoice', 'ttsSpeed', 'ttsAutoplay',
# Tutorial visibility settings
'showTutorialButtons',
# Desktop conversation notification settings

Check warning on line 523 in application/single_app/route_backend_users.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
'desktopNotificationsEnabled',
'recentCollaborators',
# Personal workspace settings managed by other backend/frontend flows
'personal_model_endpoints', 'tag_definitions',
Expand Down Expand Up @@ -560,6 +562,10 @@
if not isinstance(settings_to_update["conversationContentsDrawerEnabled"], bool):
return jsonify({"error": "Invalid conversation contents drawer preference"}), 400

if "desktopNotificationsEnabled" in settings_to_update:
if not isinstance(settings_to_update["desktopNotificationsEnabled"], bool):
return jsonify({"error": "Invalid desktop notification preference"}), 400

if AI_NOTICE_USER_SETTINGS_KEY in settings_to_update:
try:
settings_to_update[AI_NOTICE_USER_SETTINGS_KEY] = (
Expand Down
3 changes: 3 additions & 0 deletions application/single_app/route_frontend_admin_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,8 @@ def admin_settings():
settings['require_member_of_create_public_workspace'] = False
if 'enable_chat_file_uploads' not in settings:
settings['enable_chat_file_uploads'] = True
if 'enable_desktop_notifications' not in settings:
settings['enable_desktop_notifications'] = False
if 'enable_conversation_contents_drawer' not in settings:
settings['enable_conversation_contents_drawer'] = True
if 'require_member_of_chat_file_upload_user' not in settings:
Expand Down Expand Up @@ -2571,6 +2573,7 @@ def is_valid_url(url):

# Feedback, Archiving & Thoughts
'enable_user_feedback': form_data.get('enable_user_feedback') == 'on',
'enable_desktop_notifications': form_data.get('enable_desktop_notifications') == 'on',
'enable_conversation_archiving': form_data.get('enable_conversation_archiving') == 'on',
'enable_thoughts': form_data.get('enable_thoughts') == 'on',

Expand Down
5 changes: 5 additions & 0 deletions application/single_app/route_frontend_chats.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,10 @@ def chats():
enable_document_classification = public_settings.get("enable_document_classification", False)
enable_extract_meta_data = public_settings.get("enable_extract_meta_data", False)
enable_multi_model_endpoints = public_settings.get("enable_multi_model_endpoints", False)
desktop_notifications_enabled = bool(
public_settings.get("enable_desktop_notifications", False)
and user_settings_dict.get("desktopNotificationsEnabled", True)
)
active_group_id = user_settings_dict.get("activeGroupOid", "")
active_group_name = ""
if active_group_id:
Expand Down Expand Up @@ -881,6 +885,7 @@ def chats():
chat_model_options=chat_model_options,
initial_chat_model_selection=initial_chat_model_selection,
conversation_contents_drawer_enabled=conversation_contents_drawer_enabled,
desktop_notifications_enabled=desktop_notifications_enabled,
)

@bp.route('/workflow-activity', methods=['GET'])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// chat-desktop-notifications.js

const notifiedCompletionKeys = new Set();
let permissionRequestAttempted = false;

Check warning on line 4 in application/single_app/static/js/chat/chat-desktop-notifications.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.

function getDesktopNotificationConfig() {
const appSettings = window.appSettings || {};
return {
enabled: appSettings.enable_desktop_notifications === true
&& appSettings.desktop_notifications_enabled === true,
appTitle: String(appSettings.app_title || 'Simple Chat').trim() || 'Simple Chat'
};
}

function getConversationTitle(finalData = {}) {

Check warning on line 15 in application/single_app/static/js/chat/chat-desktop-notifications.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
const eventTitle = String(finalData.conversation_title || '').trim();

Check warning on line 16 in application/single_app/static/js/chat/chat-desktop-notifications.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
if (eventTitle) {
return eventTitle;
}

const currentTitle = document.getElementById('current-conversation-title')?.textContent?.trim();

Check warning on line 21 in application/single_app/static/js/chat/chat-desktop-notifications.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
return currentTitle || 'Conversation';

Check warning on line 22 in application/single_app/static/js/chat/chat-desktop-notifications.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
}

function getCompletionKey(finalData = {}) {
const messageId = String(finalData.message_id || '').trim();
if (messageId) {
return `message:${messageId}`;
}

const conversationId = String(finalData.conversation_id || '').trim();

Check warning on line 31 in application/single_app/static/js/chat/chat-desktop-notifications.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
return conversationId ? `conversation:${conversationId}` : '';

Check warning on line 32 in application/single_app/static/js/chat/chat-desktop-notifications.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
}

export function requestDesktopNotificationPermissionIfNeeded() {

Check warning on line 35 in application/single_app/static/js/chat/chat-desktop-notifications.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
const config = getDesktopNotificationConfig();
if (!config.enabled || !('Notification' in window) || Notification.permission !== 'default') {

Check warning on line 37 in application/single_app/static/js/chat/chat-desktop-notifications.js

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
return Promise.resolve(window.Notification?.permission || 'unsupported');
}
if (permissionRequestAttempted) {
return Promise.resolve(Notification.permission);
}

permissionRequestAttempted = true;
return Notification.requestPermission().catch(error => {
console.warn('Desktop notification permission request failed:', error);
return Notification.permission;
});
}

export function showDesktopConversationNotification(finalData = {}) {
const config = getDesktopNotificationConfig();
if (
!config.enabled
|| finalData.blocked === true
|| finalData.role === 'safety'
|| !('Notification' in window)
|| Notification.permission !== 'granted'
|| (document.visibilityState !== 'hidden' && document.hasFocus())
) {
return null;
}

const completionKey = getCompletionKey(finalData);
if (completionKey && notifiedCompletionKeys.has(completionKey)) {
return null;
}

try {
const conversationId = String(finalData.conversation_id || '').trim();
const notification = new Notification(config.appTitle, {
body: getConversationTitle(finalData),
tag: conversationId ? `simplechat-conversation-${conversationId}` : undefined
});

if (completionKey) {
notifiedCompletionKeys.add(completionKey);
}

notification.addEventListener('click', () => {
window.focus();
notification.close();
});

return notification;
} catch (error) {
console.warn('Desktop conversation notification could not be shown:', error);
return null;
}
}

export function resetDesktopNotificationCompletionKeysForTesting() {
notifiedCompletionKeys.clear();
permissionRequestAttempted = false;
}
3 changes: 3 additions & 0 deletions application/single_app/static/js/chat/chat-streaming.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { beginStreamingThoughtSession, clearStreamingThoughtSession, handleStrea
import { destroyInlineCharts, hydrateInlineCharts } from './chat-inline-charts.js';
import { hydrateInlineImageProposals } from './chat-inline-image-proposals.js';
import { escapeHtml } from './chat-utils.js';
import { requestDesktopNotificationPermissionIfNeeded, showDesktopConversationNotification } from './chat-desktop-notifications.js';

let currentStreamController = null;
let currentStreamContext = null;
Expand Down Expand Up @@ -667,6 +668,7 @@ function consumeStreamingResponse(requestFactory, tempAiMessageId, tempUserMessa
data,
fallbackAgentInfo
);
showDesktopConversationNotification(data);

if (typeof onDone === 'function') {
onDone(data);
Expand Down Expand Up @@ -913,6 +915,7 @@ function consumeStreamingResponse(requestFactory, tempAiMessageId, tempUserMessa

export function sendMessageWithStreaming(messageData, tempUserMessageId, currentConversationId, options = {}) {
const { endpoint = '/api/chat/stream' } = options;
void requestDesktopNotificationPermissionIfNeeded();
const tempAiMessageId = createStreamingPlaceholder();
const recoveryConversationId = currentConversationId || messageData?.conversation_id || window.currentConversationId || null;

Expand Down
24 changes: 24 additions & 0 deletions application/single_app/templates/admin_settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -9982,6 +9982,30 @@ <h5>
</div>
</div>

<div class="card p-3 mb-3" id="desktop-notifications-section">
<h5>
<i class="bi bi-bell me-2"></i>Desktop Conversation Notifications
</h5>
<p class="text-muted">
Allow users to receive an operating system notification when an AI response finishes while SimpleChat is open in a hidden or unfocused browser tab.
</p>
<div class="form-group form-check form-switch mb-2">
<input
type="checkbox"
class="form-check-input"
id="enable_desktop_notifications"
name="enable_desktop_notifications"
{% if settings.enable_desktop_notifications %}checked{% endif %}
>
<label class="form-check-label ms-2" for="enable_desktop_notifications">
Enable Desktop Conversation Notifications
</label>
</div>
<p class="form-text text-muted mb-0">
Users can turn notifications off from Profile. Browser permission is required, and notifications stop when the SimpleChat tab is closed.
</p>
</div>


<div class="card p-3 mb-3" id="permissions-section">
<h5>
Expand Down
3 changes: 3 additions & 0 deletions application/single_app/templates/chats.html
Original file line number Diff line number Diff line change
Expand Up @@ -1579,6 +1579,9 @@ <h5 class="modal-title" id="scopeLockModalLabel">
enable_multi_model_endpoints: {{ enable_multi_model_endpoints|tojson }},
enable_thoughts: {{ settings.enable_thoughts|tojson }},
enable_collaborative_conversations: {{ settings.enable_collaborative_conversations|tojson }},
enable_desktop_notifications: {{ settings.enable_desktop_notifications|default(false, true)|tojson }},
desktop_notifications_enabled: {{ desktop_notifications_enabled|default(false, true)|tojson }},
app_title: {{ settings.app_title|default('Simple Chat', true)|tojson }},
documentActionCapabilities: {{ settings.document_action_capabilities|default({}, true)|tojson|safe }}
};

Expand Down
Loading
Loading