diff --git a/application/single_app/requirements.txt b/application/single_app/requirements.txt index ceff7d77a..4ed0f99dd 100644 --- a/application/single_app/requirements.txt +++ b/application/single_app/requirements.txt @@ -64,6 +64,8 @@ psycopg2-binary==2.9.10 cython==3.2.4 pyyaml==6.0.2 aiohttp==3.14.3 +# AnyIO 4.11+ requires typing_extensions.sentinel, which older App Service agents can shadow. +anyio==4.10.0 html2text==2025.4.15 matplotlib==3.10.7 azure-cognitiveservices-speech==1.48.2 diff --git a/application/single_app/route_backend_conversations.py b/application/single_app/route_backend_conversations.py index 476356249..440077665 100644 --- a/application/single_app/route_backend_conversations.py +++ b/application/single_app/route_backend_conversations.py @@ -237,6 +237,41 @@ def _build_replayed_document_context(original_metadata): 'active_public_workspace_ids': workspace_search.get('active_public_workspace_ids') or [], } +def _build_replayed_model_context(original_metadata, request_data=None): + """Rebuild model endpoint routing from stored metadata for retry and edit.""" + metadata = original_metadata if isinstance(original_metadata, dict) else {} + model_selection = metadata.get('model_selection') + model_selection = model_selection if isinstance(model_selection, dict) else {} + request_data = request_data if isinstance(request_data, dict) else {} + + stored_request_model = str( + model_selection.get('frontend_requested_model') + or model_selection.get('selected_model') + or '' + ).strip() + stored_selected_model = str(model_selection.get('selected_model') or '').strip() + requested_model = str(request_data.get('model') or '').strip() + replayed_model = requested_model or stored_request_model + reuses_stored_selection = not requested_model or requested_model in { + stored_request_model, + stored_selected_model, + } + + def requested_or_stored(request_key, metadata_key): + requested_value = request_data.get(request_key) + if requested_value not in (None, ''): + return requested_value + if reuses_stored_selection: + return model_selection.get(metadata_key) + return None + + return { + 'model_deployment': replayed_model or None, + 'model_id': requested_or_stored('model_id', 'model_id'), + 'model_endpoint_id': requested_or_stored('model_endpoint_id', 'model_endpoint_id'), + 'model_provider': requested_or_stored('model_provider', 'model_provider'), + 'model_icon': requested_or_stored('model_icon', 'model_icon'), + } def _get_requested_workspace_document_delete_ids_for_conversation(payload, conversation_id): if not isinstance(payload, dict): @@ -2758,7 +2793,6 @@ def retry_message(message_id): try: data = request.get_json() or {} - selected_model = data.get('model') reasoning_effort = data.get('reasoning_effort') agent_info = data.get('agent_info') # Get agent info if provided @@ -2915,10 +2949,11 @@ def retry_message(message_id): ) # Build chat request parameters from original message metadata replayed_document_context = _build_replayed_document_context(original_metadata) + replayed_model_context = _build_replayed_model_context(original_metadata, data) chat_request = { 'message': user_content, 'conversation_id': conversation_id, - 'model_deployment': selected_model or original_metadata.get('model_selection', {}).get('selected_model'), + **replayed_model_context, 'reasoning_effort': reasoning_effort or original_metadata.get('reasoning_effort'), **replayed_document_context, 'image_generation': original_metadata.get('image_generation', {}).get('enabled', False), @@ -3139,10 +3174,11 @@ def edit_message(message_id): # Build chat request parameters from original message metadata # Keep all original settings (model, reasoning, doc search, etc.) replayed_document_context = _build_replayed_document_context(original_metadata) + replayed_model_context = _build_replayed_model_context(original_metadata) chat_request = { 'message': edited_content, # Use edited content 'conversation_id': conversation_id, - 'model_deployment': original_metadata.get('model_selection', {}).get('selected_model'), + **replayed_model_context, 'reasoning_effort': original_metadata.get('reasoning_effort'), **replayed_document_context, 'image_generation': original_metadata.get('image_generation', {}).get('enabled', False), diff --git a/application/single_app/route_backend_retention_policy.py b/application/single_app/route_backend_retention_policy.py index 6384760bb..6779daf17 100644 --- a/application/single_app/route_backend_retention_policy.py +++ b/application/single_app/route_backend_retention_policy.py @@ -410,7 +410,7 @@ def update_user_retention_settings(): # Validate and parse conversation retention if 'conversation_retention_days' in data: conv_retention = data['conversation_retention_days'] - if conv_retention == 'none' or conv_retention is None: + if conv_retention == 'none' or conv_retention == 'default' or conv_retention is None: retention_settings['conversation_retention_days'] = 'none' else: try: @@ -435,7 +435,7 @@ def update_user_retention_settings(): # Validate and parse document retention if 'document_retention_days' in data: doc_retention = data['document_retention_days'] - if doc_retention == 'none' or doc_retention is None: + if doc_retention == 'none' or doc_retention == 'default' or doc_retention is None: retention_settings['document_retention_days'] = 'none' else: try: @@ -519,7 +519,7 @@ def update_group_retention_settings(group_id): # Validate and parse conversation retention if 'conversation_retention_days' in data: conv_retention = data['conversation_retention_days'] - if conv_retention == 'none' or conv_retention is None: + if conv_retention == 'none' or conv_retention == 'default' or conv_retention is None: retention_settings['conversation_retention_days'] = 'none' else: try: @@ -544,7 +544,7 @@ def update_group_retention_settings(group_id): # Validate and parse document retention if 'document_retention_days' in data: doc_retention = data['document_retention_days'] - if doc_retention == 'none' or doc_retention is None: + if doc_retention == 'none' or doc_retention == 'default' or doc_retention is None: retention_settings['document_retention_days'] = 'none' else: try: @@ -629,7 +629,7 @@ def update_public_workspace_retention_settings(public_workspace_id): # Validate and parse conversation retention if 'conversation_retention_days' in data: conv_retention = data['conversation_retention_days'] - if conv_retention == 'none' or conv_retention is None: + if conv_retention == 'none' or conv_retention == 'default' or conv_retention is None: retention_settings['conversation_retention_days'] = 'none' else: try: @@ -654,7 +654,7 @@ def update_public_workspace_retention_settings(public_workspace_id): # Validate and parse document retention if 'document_retention_days' in data: doc_retention = data['document_retention_days'] - if doc_retention == 'none' or doc_retention is None: + if doc_retention == 'none' or doc_retention == 'default' or doc_retention is None: retention_settings['document_retention_days'] = 'none' else: try: diff --git a/application/single_app/static/css/styles.css b/application/single_app/static/css/styles.css index 611092e26..ef2643874 100644 --- a/application/single_app/static/css/styles.css +++ b/application/single_app/static/css/styles.css @@ -124,18 +124,21 @@ main { color: #e9ecef !important; } -/* Fix for group dropdown in dark mode */ -[data-bs-theme="dark"] #group-dropdown .group-search-container { +/* Fix for workspace selector dropdowns in dark mode */ +[data-bs-theme="dark"] #group-dropdown .group-search-container, +[data-bs-theme="dark"] #public-dropdown .public-search-container { background-color: #343a40 !important; border-bottom: 1px solid #495057 !important; } -[data-bs-theme="dark"] #group-dropdown .dropdown-item:hover { +[data-bs-theme="dark"] #group-dropdown .dropdown-item:hover, +[data-bs-theme="dark"] #public-dropdown .dropdown-item:hover { background-color: #495057 !important; color: #e9ecef !important; } -[data-bs-theme="dark"] #group-dropdown .dropdown-menu { +[data-bs-theme="dark"] #group-dropdown .dropdown-menu, +[data-bs-theme="dark"] #public-dropdown .dropdown-menu { background-color: #343a40 !important; color: #e9ecef !important; border-color: #495057 !important; diff --git a/application/single_app/static/js/chat/chat-input-actions.js b/application/single_app/static/js/chat/chat-input-actions.js index bc8c9d830..517be4d33 100644 --- a/application/single_app/static/js/chat/chat-input-actions.js +++ b/application/single_app/static/js/chat/chat-input-actions.js @@ -303,6 +303,18 @@ function buildUploadFileList(filesLike, fallbackPrefix = "clipboard_upload") { .filter(Boolean); } +async function ensureUploadFileIsReadable(file) { + const fileName = String(file?.name || "selected file").trim() || "selected file"; + + try { + await file.slice(0, Math.min(file.size, 1)).arrayBuffer(); + } catch (error) { + throw new Error( + `The browser could not read "${fileName}". Close the file in other apps then try again.` + ); + } +} + function hasNamedFile(files) { return Array.from(files || []).some((file) => String(file?.name || "").trim()); } @@ -684,6 +696,7 @@ export async function uploadFileToConversation(file) { let uploadingIndicatorEl = null; try { + await ensureUploadFileIsReadable(file); const groupUploadContext = await resolveGroupUploadContext(); uploadingIndicatorEl = showFileUploadingMessage(); @@ -706,10 +719,18 @@ export async function uploadFileToConversation(file) { }); } - const response = await fetch("/upload", { - method: "POST", - body: formData, - }); + let response; + try { + response = await fetch("/upload", { + method: "POST", + body: formData, + }); + } catch (error) { + const fileName = String(file?.name || "selected file").trim() || "selected file"; + throw new Error( + `The browser could not upload "${fileName}". The file may be open in another app, unavailable from cloud storage, or the network connection may have been interrupted. Ensure the file is closed and then try again.` + ); + } hideFileUploadingMessage(uploadingIndicatorEl); uploadingIndicatorEl = null; diff --git a/application/single_app/static/js/chat/chat-retry.js b/application/single_app/static/js/chat/chat-retry.js index 99d3cfdf3..be2612b05 100644 --- a/application/single_app/static/js/chat/chat-retry.js +++ b/application/single_app/static/js/chat/chat-retry.js @@ -102,7 +102,8 @@ export async function handleRetryButtonClick(messageDiv, messageId, messageType) if (modelSelect && retryModelSelect) { // Clone model options from main select - retryModelSelect.innerHTML = modelSelect.innerHTML; + const clonedOptions = Array.from(modelSelect.options || []).map((option) => option.cloneNode(true)); + retryModelSelect.replaceChildren(...clonedOptions); retryModelSelect.value = modelSelect.value; // Set to currently selected model } @@ -188,14 +189,26 @@ export async function handleRetryButtonClick(messageDiv, messageId, messageType) levels.forEach(level => { const div = document.createElement('div'); div.className = 'form-check'; - div.innerHTML = ` - - - `; + + const input = document.createElement('input'); + input.className = 'form-check-input'; + input.type = 'radio'; + input.name = 'retry-reasoning-effort'; + input.id = `retry-reasoning-${level.value}`; + input.value = level.value; + input.checked = level.value === 'medium'; + + const label = document.createElement('label'); + label.className = 'form-check-label'; + label.setAttribute('for', input.id); + + const strong = document.createElement('strong'); + strong.textContent = level.label; + + label.appendChild(strong); + label.appendChild(document.createTextNode(` - ${level.description}`)); + div.appendChild(input); + div.appendChild(label); retryReasoningLevels.appendChild(div); }); } @@ -264,8 +277,18 @@ window.executeMessageRetry = function() { // Model mode - get model and reasoning effort const retryModelSelect = document.getElementById('retry-model-select'); const selectedOption = retryModelSelect ? retryModelSelect.options[retryModelSelect.selectedIndex] : null; - const selectedModel = selectedOption?.dataset?.deploymentName || (retryModelSelect ? retryModelSelect.value : null); + const selectedModel = selectedOption?.dataset?.requestModel + || selectedOption?.dataset?.deploymentName + || (retryModelSelect ? retryModelSelect.value : null); requestBody.model = selectedModel; + requestBody.model_id = selectedOption?.dataset?.modelId || null; + requestBody.model_endpoint_id = selectedOption?.dataset?.endpointId || null; + requestBody.model_provider = selectedOption?.dataset?.provider || null; + try { + requestBody.model_icon = JSON.parse(selectedOption?.dataset?.modelIcon || '{}'); + } catch (error) { + requestBody.model_icon = {}; + } let reasoningEffort = null; const retryReasoningContainer = document.getElementById('retry-reasoning-container'); diff --git a/application/single_app/templates/public_workspaces.html b/application/single_app/templates/public_workspaces.html index ac3864f6b..979ec94f3 100644 --- a/application/single_app/templates/public_workspaces.html +++ b/application/single_app/templates/public_workspaces.html @@ -213,7 +213,7 @@
{% include "_semantic_search_health_warning.html" %} -