diff --git a/application/single_app/config.py b/application/single_app/config.py
index 87fb07e7..624ae367 100644
--- a/application/single_app/config.py
+++ b/application/single_app/config.py
@@ -94,7 +94,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
-VERSION = "0.241.001"
+VERSION = "0.241.002"
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')
diff --git a/application/single_app/route_backend_chats.py b/application/single_app/route_backend_chats.py
index cac4ee8d..c6e99a62 100644
--- a/application/single_app/route_backend_chats.py
+++ b/application/single_app/route_backend_chats.py
@@ -578,6 +578,33 @@ def _load_user_message_response_context(
return response_context
+def _initialize_assistant_response_tracking(
+ conversation_id,
+ user_message_id,
+ current_user_thread_id,
+ previous_thread_id,
+ retry_thread_attempt,
+ is_retry,
+ user_id,
+):
+ """Create assistant response tracking state for both new and retry/edit flows."""
+ assistant_message_id = f"{conversation_id}_assistant_{int(time.time())}_{random.randint(1000,9999)}"
+ thought_tracker = ThoughtTracker(
+ conversation_id=conversation_id,
+ message_id=assistant_message_id,
+ thread_id=current_user_thread_id,
+ user_id=user_id,
+ )
+ assistant_thread_attempt = retry_thread_attempt if is_retry and retry_thread_attempt is not None else 1
+ response_message_context = _load_user_message_response_context(
+ conversation_id=conversation_id,
+ user_message_id=user_message_id,
+ fallback_thread_id=current_user_thread_id,
+ fallback_previous_thread_id=previous_thread_id,
+ )
+ return assistant_message_id, thought_tracker, assistant_thread_attempt, response_message_context
+
+
def _build_safety_message_doc(
conversation_id,
message_id,
@@ -5383,6 +5410,29 @@ def resolve_foundry_scope_for_auth(auth_settings, endpoint=None):
return 'https://ai.azure.com/.default'
+def get_foundry_api_version_candidates(primary_version, settings):
+ """Return distinct Foundry API versions to try for inference compatibility."""
+ settings = settings or {}
+ candidates = [
+ str(primary_version or '').strip(),
+ str(settings.get('azure_openai_gpt_api_version') or '').strip(),
+ '2024-10-01-preview',
+ '2024-07-01-preview',
+ '2024-05-01-preview',
+ '2024-02-01',
+ ]
+
+ unique_candidates = []
+ seen_candidates = set()
+ for candidate in candidates:
+ if not candidate or candidate in seen_candidates:
+ continue
+ seen_candidates.add(candidate)
+ unique_candidates.append(candidate)
+
+ return unique_candidates
+
+
def build_streaming_multi_endpoint_client(auth_settings, provider, endpoint, api_version):
"""Create an inference client for a resolved streaming model endpoint."""
auth_settings = auth_settings or {}
@@ -5990,21 +6040,16 @@ def result_requires_message_reload(result: Any) -> bool:
)
try:
multi_endpoint_config = None
- if should_use_default_model:
- try:
- multi_endpoint_config = resolve_default_model_gpt_config(settings)
- if multi_endpoint_config:
- debug_print("[GPTClient] Using default multi-endpoint model for agent request.")
- except Exception as default_exc:
- log_event(
- f"[GPTClient] Default model selection unavailable: {default_exc}",
- level=logging.WARNING,
- exceptionTraceback=True
- )
- if multi_endpoint_config is None and request_agent_info:
- debug_print("[GPTClient] Skipping multi-endpoint resolution because agent_info is provided.")
- elif multi_endpoint_config is None:
- multi_endpoint_config = resolve_multi_endpoint_gpt_config(settings, data, enable_gpt_apim)
+ if settings.get('enable_multi_model_endpoints', False):
+ multi_endpoint_config = resolve_streaming_multi_endpoint_gpt_config(
+ settings,
+ data,
+ user_id,
+ active_group_ids=active_group_ids,
+ allow_default_selection=should_use_default_model,
+ )
+ if multi_endpoint_config and should_use_default_model and not data.get('model_endpoint_id'):
+ debug_print("[GPTClient] Using default multi-endpoint model for agent request.")
if multi_endpoint_config:
gpt_client, gpt_model, gpt_provider, gpt_endpoint, gpt_auth, gpt_api_version = multi_endpoint_config
elif enable_gpt_apim:
@@ -6487,26 +6532,18 @@ def result_requires_message_reload(result: Any) -> bool:
conversation_item['last_updated'] = datetime.utcnow().isoformat()
cosmos_conversations_container.upsert_item(conversation_item) # Update timestamp and potentially title
- # Generate assistant_message_id early for thought tracking
- assistant_message_id = f"{conversation_id}_assistant_{int(time.time())}_{random.randint(1000,9999)}"
-
- # Initialize thought tracker
- thought_tracker = ThoughtTracker(
- conversation_id=conversation_id,
- message_id=assistant_message_id,
- thread_id=current_user_thread_id,
- user_id=user_id
- )
- assistant_thread_attempt = retry_thread_attempt if is_retry else 1
- response_message_context = _load_user_message_response_context(
- conversation_id=conversation_id,
- user_message_id=user_message_id,
- fallback_thread_id=current_user_thread_id,
- fallback_previous_thread_id=previous_thread_id,
- )
- user_info_for_assistant = response_message_context.get('user_info')
- user_thread_id = response_message_context.get('thread_id')
- user_previous_thread_id = response_message_context.get('previous_thread_id')
+ assistant_message_id, thought_tracker, assistant_thread_attempt, response_message_context = _initialize_assistant_response_tracking(
+ conversation_id=conversation_id,
+ user_message_id=user_message_id,
+ current_user_thread_id=current_user_thread_id,
+ previous_thread_id=previous_thread_id,
+ retry_thread_attempt=retry_thread_attempt,
+ is_retry=is_retry,
+ user_id=user_id,
+ )
+ user_info_for_assistant = response_message_context.get('user_info')
+ user_thread_id = response_message_context.get('thread_id')
+ user_previous_thread_id = response_message_context.get('previous_thread_id')
# region 3 - Content Safety
# ---------------------------------------------------------------------
@@ -8417,7 +8454,12 @@ def invoke_gpt_fallback():
continue
try:
debug_print(f"[SKChat] Foundry retry api_version={candidate}")
- retry_client = build_multi_endpoint_client(gpt_auth or {}, gpt_provider, gpt_endpoint, candidate)
+ retry_client = build_streaming_multi_endpoint_client(
+ gpt_auth or {},
+ gpt_provider,
+ gpt_endpoint,
+ candidate,
+ )
response = retry_client.chat.completions.create(**api_params)
break
except Exception as retry_exc:
@@ -9405,22 +9447,14 @@ def generate(publish_background_event=None):
conversation_item['last_updated'] = datetime.utcnow().isoformat()
cosmos_conversations_container.upsert_item(conversation_item)
- # Generate assistant_message_id early for thought tracking
- assistant_message_id = f"{conversation_id}_assistant_{int(time.time())}_{random.randint(1000,9999)}"
-
- # Initialize thought tracker for streaming path
- thought_tracker = ThoughtTracker(
- conversation_id=conversation_id,
- message_id=assistant_message_id,
- thread_id=current_user_thread_id,
- user_id=user_id
- )
- assistant_thread_attempt = retry_thread_attempt if is_retry else 1
- response_message_context = _load_user_message_response_context(
+ assistant_message_id, thought_tracker, assistant_thread_attempt, response_message_context = _initialize_assistant_response_tracking(
conversation_id=conversation_id,
user_message_id=user_message_id,
- fallback_thread_id=current_user_thread_id,
- fallback_previous_thread_id=previous_thread_id,
+ current_user_thread_id=current_user_thread_id,
+ previous_thread_id=previous_thread_id,
+ retry_thread_attempt=retry_thread_attempt,
+ is_retry=is_retry,
+ user_id=user_id,
)
user_info_for_assistant = response_message_context.get('user_info')
user_thread_id = response_message_context.get('thread_id')
diff --git a/application/single_app/route_backend_settings.py b/application/single_app/route_backend_settings.py
index 34c9f8d1..3ad09217 100644
--- a/application/single_app/route_backend_settings.py
+++ b/application/single_app/route_backend_settings.py
@@ -542,7 +542,8 @@ def send_support_feedback_email():
user_email = user.get('preferred_username', user.get('email', reporter_email))
feedback_label = 'Bug Report' if feedback_type == 'bug_report' else 'Feature Request'
- subject_line = f'[SimpleChat User Support] {feedback_label} - {organization}'
+ application_title = str(settings.get('app_title') or '').strip() or 'Simple Chat'
+ subject_line = f'[{application_title} User Support] {feedback_label} - {organization}'
log_user_support_feedback_email_submission(
user_id=user_id,
diff --git a/application/single_app/support_menu_config.py b/application/single_app/support_menu_config.py
index d020daa8..fa5411da 100644
--- a/application/single_app/support_menu_config.py
+++ b/application/single_app/support_menu_config.py
@@ -7,6 +7,29 @@
_SUPPORT_LATEST_FEATURE_DOCS_SETTING_KEY = 'enable_support_latest_feature_documentation_links'
+def _resolve_support_application_title(settings):
+ """Return the application title used for user-facing support copy."""
+ app_title = str((settings or {}).get('app_title') or '').strip()
+ return app_title or 'Simple Chat'
+
+
+def _apply_support_application_title(value, app_title):
+ """Replace hard-coded product naming in user-facing support metadata."""
+ if isinstance(value, str):
+ return value.replace('{app_title}', app_title).replace('SimpleChat', app_title)
+
+ if isinstance(value, list):
+ return [_apply_support_application_title(item, app_title) for item in value]
+
+ if isinstance(value, dict):
+ return {
+ key: _apply_support_application_title(item, app_title)
+ for key, item in value.items()
+ }
+
+ return value
+
+
_SUPPORT_LATEST_FEATURE_CATALOG = [
{
'id': 'guided_tutorials',
@@ -145,7 +168,7 @@
'title': 'Tabular Analysis',
'icon': 'bi-table',
'summary': 'Spreadsheet and table workflows continue to improve for exploration, filtering, and grounded follow-up questions.',
- 'details': 'Tabular Analysis improves how SimpleChat works with CSV and spreadsheet files for filtering, comparisons, and grounded follow-up questions.',
+ 'details': 'Tabular Analysis improves how {app_title} works with CSV and spreadsheet files for filtering, comparisons, and grounded follow-up questions.',
'why': 'You get the most value after the file is uploaded, because the assistant can reason over the stored rows and columns instead of only whatever is pasted into one message.',
'guidance': [
'Upload your CSV or XLSX to Personal Workspace if it is enabled, or add the file directly to Chat when you want a quicker one-off analysis.',
@@ -501,7 +524,7 @@
'id': 'send_feedback',
'title': 'Send Feedback',
'icon': 'bi-envelope-paper',
- 'summary': 'End users can prepare bug reports and feature requests for their SimpleChat admins directly from the Support menu.',
+ 'summary': 'End users can prepare bug reports and feature requests for their {app_title} admins directly from the Support menu.',
'details': 'Send Feedback opens a guided, text-only email draft workflow so you can report issues or request improvements without leaving the app.',
'why': 'That gives your admins a cleaner starting point for triage than a vague message without context or reproduction details.',
'guidance': [
@@ -577,7 +600,7 @@
'icon': 'bi-download',
'summary': 'Export one or multiple conversations from Chat in JSON or Markdown without carrying internal-only metadata into the downloaded package.',
'details': 'Conversation Export adds a guided workflow for choosing format, packaging, and download options when you need to reuse or archive chat history outside the app.',
- 'why': 'This matters because users often need to share, archive, or reuse a conversation without copying raw chat text by hand or exposing internal metadata that should stay inside SimpleChat.',
+ 'why': 'This matters because users often need to share, archive, or reuse a conversation without copying raw chat text by hand or exposing internal metadata that should stay inside {app_title}.',
'guidance': [
'Open an existing conversation from Chat when you want to export content that already has enough context to share.',
'Choose JSON when you want a machine-readable export and Markdown when you want something easier for people to review directly.',
@@ -975,6 +998,7 @@ def get_visible_support_latest_features(settings):
normalized_visibility = normalize_support_latest_features_visibility(
(settings or {}).get('support_latest_features_visibility', {})
)
+ app_title = _resolve_support_application_title(settings)
visible_items = []
for item in _SUPPORT_LATEST_FEATURE_CATALOG:
@@ -984,6 +1008,7 @@ def get_visible_support_latest_features(settings):
action for action in visible_item.get('actions', [])
if _action_enabled(action, settings)
]
+ visible_item = _apply_support_application_title(visible_item, app_title)
_normalize_feature_media(visible_item)
visible_items.append(visible_item)
@@ -995,6 +1020,7 @@ def get_visible_support_latest_feature_groups(settings):
normalized_visibility = normalize_support_latest_features_visibility(
(settings or {}).get('support_latest_features_visibility', {})
)
+ app_title = _resolve_support_application_title(settings)
visible_groups = []
for feature_group in _SUPPORT_LATEST_FEATURE_RELEASE_GROUPS:
@@ -1008,12 +1034,14 @@ def get_visible_support_latest_feature_groups(settings):
action for action in visible_feature.get('actions', [])
if _action_enabled(action, settings)
]
+ visible_feature = _apply_support_application_title(visible_feature, app_title)
_normalize_feature_media(visible_feature)
visible_features.append(visible_feature)
if visible_features:
visible_group = deepcopy(feature_group)
visible_group['features'] = visible_features
+ visible_group = _apply_support_application_title(visible_group, app_title)
visible_groups.append(visible_group)
return visible_groups
@@ -1022,6 +1050,7 @@ def get_visible_support_latest_feature_groups(settings):
def get_support_latest_feature_release_groups_for_settings(settings):
"""Return grouped latest-feature metadata with actions filtered for the current settings."""
filtered_groups = deepcopy(_SUPPORT_LATEST_FEATURE_RELEASE_GROUPS)
+ app_title = _resolve_support_application_title(settings)
for feature_group in filtered_groups:
for feature in feature_group.get('features', []):
@@ -1029,8 +1058,11 @@ def get_support_latest_feature_release_groups_for_settings(settings):
action for action in feature.get('actions', [])
if _action_enabled(action, settings)
]
+ feature.update(_apply_support_application_title(feature, app_title))
_normalize_feature_media(feature)
+ feature_group.update(_apply_support_application_title(feature_group, app_title))
+
return filtered_groups
diff --git a/application/single_app/templates/profile.html b/application/single_app/templates/profile.html
index 5e6dd0a9..87985b55 100644
--- a/application/single_app/templates/profile.html
+++ b/application/single_app/templates/profile.html
@@ -1103,7 +1103,6 @@
-
-
-
{% endblock %}
diff --git a/application/single_app/templates/support_send_feedback.html b/application/single_app/templates/support_send_feedback.html
index 7ac3e021..a31314cd 100644
--- a/application/single_app/templates/support_send_feedback.html
+++ b/application/single_app/templates/support_send_feedback.html
@@ -8,7 +8,7 @@
Send Feedback
-
Prepare a support email for your SimpleChat administrators. This opens a text-only draft in your local mail client.
+
Prepare a support email for your {{ app_settings.app_title }} administrators. This opens a text-only draft in your local mail client.
diff --git a/docs/_layouts/libdoc/page.html b/docs/_layouts/libdoc/page.html
new file mode 100644
index 00000000..83935c1a
--- /dev/null
+++ b/docs/_layouts/libdoc/page.html
@@ -0,0 +1,5 @@
+---
+layout: page
+---
+
+{{ content }}
\ No newline at end of file
diff --git a/docs/explanation/fixes/v0.241.001/NEW_FOUNDRY_UI_VISIBILITY_FIX.md b/docs/explanation/fixes/v0.241.001/NEW_FOUNDRY_UI_VISIBILITY_FIX.md
index 2056127f..ec11c7a3 100644
--- a/docs/explanation/fixes/v0.241.001/NEW_FOUNDRY_UI_VISIBILITY_FIX.md
+++ b/docs/explanation/fixes/v0.241.001/NEW_FOUNDRY_UI_VISIBILITY_FIX.md
@@ -10,7 +10,7 @@ New Foundry had already been wired into backend fetch and streaming paths, but t
## Root Cause
-- The New Foundry agent type radio in `_agent_modal.html` was wrapped in a disabled `{% if false %}` block.
+- The New Foundry agent type radio in `_agent_modal.html` was wrapped in a disabled {% raw %}{% if false %}{% endraw %} block.
- `_multiendpoint_modal.html` only exposed `aoai` and classic Foundry in the provider selector.
- `is_frontend_visible_model_endpoint_provider()` in `functions_settings.py` still treated `new_foundry` as unsupported for frontend use.
diff --git a/docs/explanation/fixes/v0.241.001/SINGLE_APP_TEMPLATE_JSON_BOOTSTRAP_FIX.md b/docs/explanation/fixes/v0.241.001/SINGLE_APP_TEMPLATE_JSON_BOOTSTRAP_FIX.md
index 02baafc4..d89c6a5d 100644
--- a/docs/explanation/fixes/v0.241.001/SINGLE_APP_TEMPLATE_JSON_BOOTSTRAP_FIX.md
+++ b/docs/explanation/fixes/v0.241.001/SINGLE_APP_TEMPLATE_JSON_BOOTSTRAP_FIX.md
@@ -4,7 +4,7 @@ Fixed/Implemented in version: **0.240.020**
## Issue Description
-Several `single_app` templates were still bootstrapping server-side JSON with patterns such as `JSON.parse('{{ value|tojson }}')`. That left workspace and admin pages vulnerable to the same control-character and quoting failures already fixed in the chat template.
+Several `single_app` templates were still bootstrapping server-side JSON with patterns such as `JSON.parse('{% raw %}{{ value|tojson }}{% endraw %}')`. That left workspace and admin pages vulnerable to the same control-character and quoting failures already fixed in the chat template.
## Root Cause Analysis
@@ -47,4 +47,4 @@ Several `single_app` templates were still bootstrapping server-side JSON with pa
- The affected templates now emit direct JavaScript literals from Jinja `tojson` output.
- Escaped values remain encoded within the serialized payload instead of being reinterpreted by an intermediate JavaScript string literal.
-- The added regression tests help prevent `JSON.parse('{{ ...|tojson ... }}')` from being reintroduced in these templates.
\ No newline at end of file
+- The added regression tests help prevent `JSON.parse('{% raw %}{{ ...|tojson ... }}{% endraw %}')` from being reintroduced in these templates.
diff --git a/docs/explanation/fixes/v0.241.002/SUPPORT_APPLICATION_TITLE_COPY_FIX.md b/docs/explanation/fixes/v0.241.002/SUPPORT_APPLICATION_TITLE_COPY_FIX.md
new file mode 100644
index 00000000..b6264f58
--- /dev/null
+++ b/docs/explanation/fixes/v0.241.002/SUPPORT_APPLICATION_TITLE_COPY_FIX.md
@@ -0,0 +1,51 @@
+# Support Application Title Copy Fix
+
+Fixed in version: **0.241.002**
+
+## Overview
+
+User-facing Support content now uses the configured application title instead of hard-coded `SimpleChat` branding.
+
+## Issue Description
+
+The Send Feedback page and parts of the Support Latest Features catalog still referenced `SimpleChat` directly. That caused user-facing copy in customized environments to ignore `app_title` from `config.py` and Admin Settings.
+
+## Root Cause
+
+The support feature catalog stored user-facing copy as static Python strings, and the Send Feedback experience also used fixed product naming in both the page copy and the generated mail draft subject.
+
+## Technical Details
+
+### Files Modified
+
+- `application/single_app/support_menu_config.py`
+- `application/single_app/templates/support_send_feedback.html`
+- `application/single_app/route_backend_settings.py`
+- `functional_tests/test_support_menu_user_feature.py`
+- `functional_tests/test_support_app_title_personalization.py`
+- `ui_tests/test_support_latest_features_image_modal.py`
+- `ui_tests/test_support_send_feedback_field_selection.py`
+- `application/single_app/config.py`
+
+### Code Changes Summary
+
+- Added support-copy personalization helpers that replace `SimpleChat` with the configured `app_title` when building user-facing latest-feature data.
+- Updated the Send Feedback page intro text to reference `app_settings.app_title`.
+- Updated the user-facing Send Feedback mail draft subject to use the configured application title.
+- Added regression coverage for both rendered support metadata and the Send Feedback flow.
+
+## Testing Approach
+
+- Added `functional_tests/test_support_app_title_personalization.py` to validate personalized support content and dynamic Send Feedback subject generation.
+- Updated existing support functional coverage in `functional_tests/test_support_menu_user_feature.py`.
+- Updated support UI tests to assert Send Feedback copy reflects the application title and that support latest-features text no longer leaks hard-coded `SimpleChat` copy.
+
+## Impact Analysis
+
+- Customized deployments now present consistent product naming across Support Latest Features, Previous Release Features, and Send Feedback.
+- Default deployments continue to work without extra configuration because the fallback title remains `Simple Chat`.
+
+## Validation
+
+- Before: Support copy could display `SimpleChat` even when the app title had been customized.
+- After: Support copy and the user-visible feedback draft subject resolve the configured application title at runtime.
\ No newline at end of file
diff --git a/docs/explanation/fixes/v0.241.003/CHAT_STREAM_RETRY_MULTI_ENDPOINT_RESOLUTION_FIX.md b/docs/explanation/fixes/v0.241.003/CHAT_STREAM_RETRY_MULTI_ENDPOINT_RESOLUTION_FIX.md
new file mode 100644
index 00000000..8c7af852
--- /dev/null
+++ b/docs/explanation/fixes/v0.241.003/CHAT_STREAM_RETRY_MULTI_ENDPOINT_RESOLUTION_FIX.md
@@ -0,0 +1,53 @@
+# Chat Stream Retry Multi Endpoint Resolution Fix
+
+Fixed/Implemented in version: **0.241.003**
+
+Related config.py update: `VERSION = "0.241.003"`
+
+## Overview
+
+Streaming retries that route through the legacy compatibility bridge now reuse the in-app multi-endpoint GPT resolver instead of failing during model initialization with undefined helper names.
+
+## Issue Description
+
+When `/api/chat/stream` handled a retry request, it switched into the compatibility bridge and delegated the actual response generation to `/api/chat`. In multi-endpoint environments, that path could fail before any streamed content was returned with `Failed to initialize AI model: name 'resolve_multi_endpoint_gpt_config' is not defined`.
+
+## Root Cause Analysis
+
+`application/single_app/route_backend_chats.py` had been updated to use in-app streaming helpers for live streaming requests, but the legacy `/api/chat` path still referenced script-only helper names that were never defined in the Flask app module:
+
+- `resolve_default_model_gpt_config`
+- `resolve_multi_endpoint_gpt_config`
+- `build_multi_endpoint_client`
+- `get_foundry_api_version_candidates`
+
+That left the compatibility bridge with a split implementation where normal streaming requests worked, but retry/edit compatibility requests could still raise `NameError` during GPT client setup.
+
+## Technical Details
+
+### Files Modified
+
+- `application/single_app/route_backend_chats.py`
+- `application/single_app/config.py`
+- `functional_tests/test_chat_stream_retry_multiendpoint_resolution_fix.py`
+
+### Code Changes Summary
+
+- Updated `/api/chat` to call `resolve_streaming_multi_endpoint_gpt_config(...)` with the current `user_id`, validated `active_group_ids`, and default-model fallback flag so the compatibility path shares the same endpoint/model resolution logic as `/api/chat/stream`.
+- Added an in-app `get_foundry_api_version_candidates(...)` helper so Foundry compatibility retries can enumerate fallback API versions without importing script-only code.
+- Updated the non-streaming Foundry retry block to use `build_streaming_multi_endpoint_client(...)` for in-app client reconstruction.
+
+## Testing Approach
+
+- Added `functional_tests/test_chat_stream_retry_multiendpoint_resolution_fix.py` to verify that `/api/chat` now uses the shared multi-endpoint resolver, no longer references the removed helper names, and keeps the fix documentation/version aligned.
+
+## Impact Analysis
+
+- Retry and edit flows in `/api/chat/stream` keep working when they fall back to the compatibility bridge.
+- Multi-endpoint GPT selection is now resolved consistently between `/api/chat` and `/api/chat/stream`.
+- Foundry compatibility retries no longer depend on helper functions that only exist in `scripts/resolve_multiendpoint_gpt.py`.
+
+## Validation
+
+- Before: compatibility-mode streaming retries could terminate immediately with undefined-name errors before any SSE content arrived.
+- After: compatibility-mode retries reuse the in-app model-resolution and Foundry fallback helpers, so GPT initialization proceeds through the same supported code paths as the primary streaming route.
\ No newline at end of file
diff --git a/docs/explanation/fixes/v0.241.003/PROFILE_FACT_MEMORY_SCRIPT_DEDUP_FIX.md b/docs/explanation/fixes/v0.241.003/PROFILE_FACT_MEMORY_SCRIPT_DEDUP_FIX.md
new file mode 100644
index 00000000..a086aa17
--- /dev/null
+++ b/docs/explanation/fixes/v0.241.003/PROFILE_FACT_MEMORY_SCRIPT_DEDUP_FIX.md
@@ -0,0 +1,48 @@
+# Profile Fact Memory Script Dedup Fix
+
+Fixed in version: **0.241.003**
+
+Related config.py update: `VERSION = "0.241.004"`
+
+## Overview
+
+The profile page no longer ships duplicate fact-memory inline helpers or a duplicate Chart.js include that caused browser parsing to fail before the fact-memory UI could initialize.
+
+## Issue Description
+
+Opening the profile page could trigger `Uncaught SyntaxError: Identifier 'factMemoryEntries' has already been declared` because the template contained two copies of the same fact-memory declarations and helper functions.
+
+## Root Cause
+
+`application/single_app/templates/profile.html` contained a duplicated inline-script segment for the fact-memory editor, including repeated `let` declarations and helper functions, plus a second Chart.js script tag.
+
+## Technical Details
+
+### Files Modified
+
+- `application/single_app/templates/profile.html`
+- `functional_tests/test_profile_fact_memory_script_dedup.py`
+- `ui_tests/test_profile_fact_memory_editor.py`
+- `application/single_app/config.py`
+
+### Code Changes Summary
+
+- Removed the duplicate Chart.js script include from the profile template.
+- Removed the second copy of the fact-memory/tutoral helper declarations and functions from the profile inline script.
+- Added a functional regression test that counts key profile-script markers and fails if any duplicate block returns.
+- Hardened the profile fact-memory UI test to fail on browser page errors and duplicate-declaration console errors during load.
+
+## Testing Approach
+
+- Added `functional_tests/test_profile_fact_memory_script_dedup.py` to validate that the profile template contains only one copy of the relevant fact-memory markers.
+- Updated `ui_tests/test_profile_fact_memory_editor.py` so the Playwright workflow fails immediately when the profile page raises browser parse/runtime errors.
+
+## Impact Analysis
+
+- The profile fact-memory editor loads normally again.
+- Browser-side failures caused by duplicate inline declarations are now covered by both source-level and UI-level regression checks.
+
+## Validation
+
+- Before: The profile page could stop executing its inline script with a duplicate declaration error for `factMemoryEntries`.
+- After: The template declares those fact-memory symbols once, and the regression tests guard the page against the same merge-style duplication.
\ No newline at end of file
diff --git a/docs/explanation/fixes/v0.241.004/CHAT_RETRY_THOUGHT_TRACKER_INIT_FIX.md b/docs/explanation/fixes/v0.241.004/CHAT_RETRY_THOUGHT_TRACKER_INIT_FIX.md
new file mode 100644
index 00000000..bbcd8393
--- /dev/null
+++ b/docs/explanation/fixes/v0.241.004/CHAT_RETRY_THOUGHT_TRACKER_INIT_FIX.md
@@ -0,0 +1,47 @@
+# Chat Retry Thought Tracker Init Fix
+
+Fixed/Implemented in version: **0.241.004**
+
+Related config.py update: `VERSION = "0.241.004"`
+
+## Overview
+
+Retry and edit requests that fall back to the compatibility bridge now initialize assistant response tracking before content safety runs, so they no longer fail with an unbound `thought_tracker` variable.
+
+## Issue Description
+
+When `/api/chat/stream` handled a retry or edit request, it routed through the compatibility bridge into `/api/chat`. That path could read the existing user message successfully and then fail during content safety with `UnboundLocalError: cannot access local variable 'thought_tracker' where it is not associated with a value`.
+
+## Root Cause Analysis
+
+`application/single_app/route_backend_chats.py` initialized `assistant_message_id`, `thought_tracker`, `assistant_thread_attempt`, and `response_message_context` only inside the normal new-message branch. Retry and edit paths skipped that block because they reused an existing user message, but the later content-safety block still assumed `thought_tracker` had already been created.
+
+## Technical Details
+
+### Files Modified
+
+- `application/single_app/route_backend_chats.py`
+- `application/single_app/config.py`
+- `functional_tests/test_chat_retry_thought_tracker_init_fix.py`
+- `functional_tests/test_chat_stream_retry_multiendpoint_resolution_fix.py`
+
+### Code Changes Summary
+
+- Added `_initialize_assistant_response_tracking(...)` to centralize assistant message ID creation, thought-tracker setup, retry attempt handling, and response-context loading.
+- Updated `/api/chat` to call the shared helper after both new-message and retry/edit branches complete, so compatibility bridge flows always have assistant tracking state before content safety.
+- Updated the live streaming generator to reuse the same shared helper so the assistant-tracking setup stays consistent across both chat backends.
+
+## Testing Approach
+
+- Added `functional_tests/test_chat_retry_thought_tracker_init_fix.py` to verify the shared helper exists, `/api/chat` initializes assistant tracking before content safety, and the streaming generator also reuses the helper.
+- Updated `functional_tests/test_chat_stream_retry_multiendpoint_resolution_fix.py` so it remains valid after later version bumps while still checking the earlier compatibility resolver fix.
+
+## Impact Analysis
+
+- Retry and edit requests in streaming compatibility mode now reach content safety, search, and generation without crashing on uninitialized assistant tracking state.
+- The assistant tracking setup is now shared instead of duplicated, which reduces drift between `/api/chat` and `/api/chat/stream`.
+
+## Validation
+
+- Before: retry and edit compatibility requests could fail immediately in `/api/chat` once content safety tried to call `thought_tracker.add_thought(...)`.
+- After: retry and edit requests initialize assistant tracking before content safety runs, so the shared compatibility path remains usable.
\ No newline at end of file
diff --git a/docs/explanation/release_notes.md b/docs/explanation/release_notes.md
index 4acc038d..1092c4c8 100644
--- a/docs/explanation/release_notes.md
+++ b/docs/explanation/release_notes.md
@@ -1,9 +1,34 @@
-This page tracks notable Simple Chat releases and organizes the detailed change log by version. The timeline below provides a quick visual overview of the current release progression through v0.240.001, and the per-version entries continue immediately after it.
+This page tracks notable Simple Chat releases and organizes the detailed change log by version. The timeline below provides a quick visual overview of the current release progression through v0.240.002, and the per-version entries continue immediately after it.
For feature-focused and fix-focused drill-downs by version, see [Features by Version](/explanation/features/) and [Fixes by Version](/explanation/fixes/).
+### **(v0.241.002)**
+
+#### Bug Fixes
+
+* **Support Pages Respect Custom Application Titles**
+ * Fixed user-facing Support copy so Latest Features, Previous Release Features, and Send Feedback no longer fall back to the default `SimpleChat` name in customized deployments.
+ * Support feedback email drafts now also use the configured application title, keeping the user-facing support flow consistent with branded environments.
+ * (Ref: `support_menu_config.py`, `support_send_feedback.html`, `route_backend_settings.py`, support application-title personalization)
+
+* **Streaming Retry and Edit Thought Tracking**
+ * Fixed retry and edit requests in streaming chat when they fall back to the compatibility bridge and continue through the legacy `/api/chat` path.
+ * Assistant response tracking is now initialized for both new-message and retry/edit flows before content safety runs, preventing compatibility-mode failures caused by an uninitialized `ThoughtTracker`.
+ * (Ref: `route_backend_chats.py`, `ThoughtTracker`, `/api/chat/stream`, `/api/chat`, retry/edit compatibility bridge)
+
+* **Streaming Retry and Edit Multi-Endpoint Model Resolution**
+ * Fixed streaming retry and edit requests that route through the compatibility bridge so they no longer fail during AI model initialization in multi-endpoint environments.
+ * The compatibility path now reuses the in-app multi-endpoint GPT resolver and Foundry fallback helpers instead of depending on script-only helper functions that were not available inside the Flask runtime.
+ * (Ref: `route_backend_chats.py`, `/api/chat/stream`, `/api/chat`, multi-endpoint model resolution, Foundry fallback helpers)
+
+* **Profile Fact Memory Script Deduplication**
+ * Fixed a profile-page load failure where duplicate inline Fact Memory and tutorial script blocks could trigger browser parse errors such as `Identifier 'factMemorySearchInput' has already been declared`.
+ * Removed duplicated profile sections, modal markup, and shadowing helper definitions so Fact Memory, tutorial preferences, and retention settings now initialize from one canonical script path.
+ * Added source-level and UI regression coverage so duplicate profile blocks and page-load JavaScript errors are caught earlier.
+ * (Ref: `profile.html`, `test_profile_fact_memory_script_dedup.py`, `test_profile_fact_memory_editor.py`, profile page script initialization)
+
### **(v0.241.001)**
#### New Features
diff --git a/docs/explanation/running_simplechat_azure_production.md b/docs/explanation/running_simplechat_azure_production.md
index 7e6e3841..b3042591 100644
--- a/docs/explanation/running_simplechat_azure_production.md
+++ b/docs/explanation/running_simplechat_azure_production.md
@@ -8,7 +8,7 @@ category: Explanation
This guide explains the supported production startup patterns for Simple Chat in Azure.
-Current documentation version: 0.239.139
+Current documentation version: 0.241.002
## Default Azure Production Model in This Repo
diff --git a/docs/explanation/running_simplechat_locally.md b/docs/explanation/running_simplechat_locally.md
index 163c728f..cc0d2537 100644
--- a/docs/explanation/running_simplechat_locally.md
+++ b/docs/explanation/running_simplechat_locally.md
@@ -8,7 +8,7 @@ category: Explanation
This guide explains the recommended local developer workflow for Simple Chat.
-Current documentation version: 0.240.002
+Current documentation version: 0.241.002
## VS Code Python 3.12 Setup
diff --git a/functional_tests/test_chat_retry_thought_tracker_init_fix.py b/functional_tests/test_chat_retry_thought_tracker_init_fix.py
new file mode 100644
index 00000000..33b60614
--- /dev/null
+++ b/functional_tests/test_chat_retry_thought_tracker_init_fix.py
@@ -0,0 +1,111 @@
+# test_chat_retry_thought_tracker_init_fix.py
+#!/usr/bin/env python3
+"""
+Functional test for chat retry thought tracker initialization.
+Version: 0.241.004
+Implemented in: 0.241.004
+
+This test ensures retry and edit flows that route through the compatibility
+bridge initialize assistant response tracking before content safety uses the
+thought tracker.
+"""
+
+import os
+
+
+ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+ROUTE_FILE = os.path.join(ROOT_DIR, 'application', 'single_app', 'route_backend_chats.py')
+CONFIG_FILE = os.path.join(ROOT_DIR, 'application', 'single_app', 'config.py')
+FIX_DOC = os.path.join(
+ ROOT_DIR,
+ 'docs',
+ 'explanation',
+ 'fixes',
+ 'v0.241.004',
+ 'CHAT_RETRY_THOUGHT_TRACKER_INIT_FIX.md',
+)
+
+
+def read_file_text(file_path):
+ with open(file_path, 'r', encoding='utf-8') as file_handle:
+ return file_handle.read()
+
+
+def read_config_version():
+ for line in read_file_text(CONFIG_FILE).splitlines():
+ if line.startswith('VERSION = '):
+ return line.split('=', 1)[1].strip().strip('"')
+ raise AssertionError('VERSION assignment not found in config.py')
+
+
+def test_retry_and_edit_paths_initialize_thought_tracker_before_content_safety():
+ """Verify chat_api uses shared assistant tracking setup before content safety."""
+ print('๐ Testing retry/edit thought-tracker initialization...')
+
+ route_source = read_file_text(ROUTE_FILE)
+ chat_route_marker = "@app.route('/api/chat', methods=['POST'])"
+ chat_stream_marker = "@app.route('/api/chat/stream', methods=['POST'])"
+ chat_route_index = route_source.find(chat_route_marker)
+ chat_stream_index = route_source.find(chat_stream_marker)
+
+ assert chat_route_index != -1, 'Expected to find the /api/chat route definition.'
+ assert chat_stream_index != -1, 'Expected to find the /api/chat/stream route definition.'
+
+ chat_api_source = route_source[chat_route_index:chat_stream_index]
+
+ helper_marker = 'def _initialize_assistant_response_tracking('
+ shared_init_marker = 'assistant_message_id, thought_tracker, assistant_thread_attempt, response_message_context = _initialize_assistant_response_tracking('
+ content_safety_marker = "thought_tracker.add_thought('content_safety', 'Checking content safety...')"
+
+ assert helper_marker in route_source, 'Expected shared assistant-response tracking helper in route_backend_chats.py.'
+ assert "retry_user_message_id = data.get('retry_user_message_id') or data.get('edited_user_message_id')" in chat_api_source
+ assert shared_init_marker in chat_api_source, 'Expected chat_api to initialize assistant tracking through the shared helper.'
+ assert content_safety_marker in chat_api_source, 'Expected content safety thought logging in chat_api.'
+ assert chat_api_source.find(shared_init_marker) < chat_api_source.find(content_safety_marker), (
+ 'Expected assistant tracking initialization before content safety uses thought_tracker.'
+ )
+
+ stream_source = route_source[chat_stream_index:]
+ assert shared_init_marker in stream_source, 'Expected the streaming generator to reuse the shared assistant tracking helper.'
+
+ print('โ
Retry/edit thought-tracker initialization passed')
+
+
+def test_version_and_fix_documentation_alignment():
+ """Verify version bump and fix documentation stay aligned."""
+ print('๐ Testing version and fix documentation alignment...')
+
+ fix_doc_content = read_file_text(FIX_DOC)
+
+ assert read_config_version() == '0.241.004'
+ assert 'Fixed/Implemented in version: **0.241.004**' in fix_doc_content
+ assert 'Related config.py update: `VERSION = "0.241.004"`' in fix_doc_content
+ assert 'thought_tracker' in fix_doc_content
+ assert '/api/chat' in fix_doc_content
+ assert 'content safety' in fix_doc_content.lower()
+ assert 'retry and edit' in fix_doc_content.lower()
+
+ print('โ
Version and fix documentation alignment passed')
+
+
+if __name__ == '__main__':
+ tests = [
+ test_retry_and_edit_paths_initialize_thought_tracker_before_content_safety,
+ test_version_and_fix_documentation_alignment,
+ ]
+
+ results = []
+ for test in tests:
+ print(f'\n๐งช Running {test.__name__}...')
+ try:
+ test()
+ results.append(True)
+ except Exception as exc:
+ print(f'โ {test.__name__} failed: {exc}')
+ import traceback
+ traceback.print_exc()
+ results.append(False)
+
+ success = all(results)
+ print(f'\n๐ Results: {sum(results)}/{len(results)} tests passed')
+ raise SystemExit(0 if success else 1)
\ No newline at end of file
diff --git a/functional_tests/test_chat_stream_retry_multiendpoint_resolution_fix.py b/functional_tests/test_chat_stream_retry_multiendpoint_resolution_fix.py
new file mode 100644
index 00000000..22429164
--- /dev/null
+++ b/functional_tests/test_chat_stream_retry_multiendpoint_resolution_fix.py
@@ -0,0 +1,123 @@
+# test_chat_stream_retry_multiendpoint_resolution_fix.py
+#!/usr/bin/env python3
+"""
+Functional test for chat stream retry multi-endpoint resolution.
+Version: 0.241.004
+Implemented in: 0.241.003
+
+This test ensures the compatibility retry path reuses the in-app multi-endpoint
+resolver and Foundry fallback helpers instead of calling undefined script-only
+functions during GPT initialization.
+"""
+
+import os
+
+
+ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+ROUTE_FILE = os.path.join(ROOT_DIR, 'application', 'single_app', 'route_backend_chats.py')
+CONFIG_FILE = os.path.join(ROOT_DIR, 'application', 'single_app', 'config.py')
+FIX_DOC = os.path.join(
+ ROOT_DIR,
+ 'docs',
+ 'explanation',
+ 'fixes',
+ 'v0.241.003',
+ 'CHAT_STREAM_RETRY_MULTI_ENDPOINT_RESOLUTION_FIX.md',
+)
+
+
+def read_file_text(file_path):
+ with open(file_path, 'r', encoding='utf-8') as file_handle:
+ return file_handle.read()
+
+
+def read_config_version():
+ for line in read_file_text(CONFIG_FILE).splitlines():
+ if line.startswith('VERSION = '):
+ return line.split('=', 1)[1].strip().strip('"')
+ raise AssertionError('VERSION assignment not found in config.py')
+
+
+def parse_version(version_text):
+ return tuple(int(part) for part in str(version_text).split('.'))
+
+
+def test_chat_api_uses_shared_multi_endpoint_resolution_for_retry_compatibility():
+ """Verify compatibility chat requests reuse the in-app multi-endpoint resolver."""
+ print('๐ Testing compatibility retry multi-endpoint resolution wiring...')
+
+ route_source = read_file_text(ROUTE_FILE)
+ chat_route_marker = "@app.route('/api/chat', methods=['POST'])"
+ chat_stream_marker = "@app.route('/api/chat/stream', methods=['POST'])"
+
+ chat_route_index = route_source.find(chat_route_marker)
+ chat_stream_index = route_source.find(chat_stream_marker)
+ assert chat_route_index != -1, 'Expected to find the /api/chat route definition.'
+ assert chat_stream_index != -1, 'Expected to find the /api/chat/stream route definition.'
+
+ chat_api_source = route_source[chat_route_index:chat_stream_index]
+
+ assert 'resolve_streaming_multi_endpoint_gpt_config(' in chat_api_source, (
+ 'Expected /api/chat to reuse the in-app multi-endpoint resolver.'
+ )
+ assert 'active_group_ids=active_group_ids' in chat_api_source, (
+ 'Expected /api/chat to pass validated group scope into multi-endpoint resolution.'
+ )
+ assert 'allow_default_selection=should_use_default_model' in chat_api_source, (
+ 'Expected /api/chat retry compatibility requests to keep default-model fallback wiring.'
+ )
+ assert 'resolve_default_model_gpt_config(settings)' not in chat_api_source, (
+ 'Expected /api/chat to stop calling the removed default-model helper.'
+ )
+ assert 'resolve_multi_endpoint_gpt_config(settings, data, enable_gpt_apim)' not in chat_api_source, (
+ 'Expected /api/chat to stop calling the undefined script-only multi-endpoint resolver.'
+ )
+
+ assert 'def get_foundry_api_version_candidates(' in route_source, (
+ 'Expected route_backend_chats.py to define Foundry API-version fallback candidates in-app.'
+ )
+ assert 'retry_client = build_streaming_multi_endpoint_client(' in route_source, (
+ 'Expected Foundry fallback retries to reuse the in-app multi-endpoint client builder.'
+ )
+
+ print('โ
Compatibility retry multi-endpoint resolution wiring passed')
+
+
+def test_version_and_fix_documentation_alignment():
+ """Verify version bump and fix documentation stay aligned."""
+ print('๐ Testing version and fix documentation alignment...')
+
+ fix_doc_content = read_file_text(FIX_DOC)
+
+ assert parse_version(read_config_version()) >= (0, 241, 3)
+ assert 'Fixed/Implemented in version: **0.241.003**' in fix_doc_content
+ assert 'Related config.py update: `VERSION = "0.241.003"`' in fix_doc_content
+ assert 'resolve_multi_endpoint_gpt_config' in fix_doc_content
+ assert 'build_multi_endpoint_client' in fix_doc_content
+ assert '/api/chat/stream' in fix_doc_content
+ assert 'compatibility bridge' in fix_doc_content
+
+ print('โ
Version and fix documentation alignment passed')
+
+
+if __name__ == '__main__':
+ tests = [
+ test_chat_api_uses_shared_multi_endpoint_resolution_for_retry_compatibility,
+ test_version_and_fix_documentation_alignment,
+ ]
+
+ results = []
+ for test in tests:
+ print(f'\n๐งช Running {test.__name__}...')
+ try:
+ test()
+ results.append(True)
+ except Exception as exc:
+ print(f'โ {test.__name__} failed: {exc}')
+ import traceback
+ traceback.print_exc()
+ results.append(False)
+
+ success = all(results)
+ print(f'\n๐ Results: {sum(results)}/{len(results)} tests passed')
+ raise SystemExit(0 if success else 1)
\ No newline at end of file
diff --git a/functional_tests/test_profile_fact_memory_script_dedup.py b/functional_tests/test_profile_fact_memory_script_dedup.py
new file mode 100644
index 00000000..0cfbb48b
--- /dev/null
+++ b/functional_tests/test_profile_fact_memory_script_dedup.py
@@ -0,0 +1,67 @@
+# test_profile_fact_memory_script_dedup.py
+"""
+Functional test for the profile fact-memory script deduplication fix.
+Version: 0.241.004
+Implemented in: 0.241.003; 0.241.004
+
+This test ensures the profile page only includes one Chart.js script tag and one
+copy of the fact-memory inline helpers so browser parsing does not fail.
+"""
+
+from pathlib import Path
+
+
+ROOT_DIR = Path(__file__).resolve().parent.parent
+PROFILE_TEMPLATE = ROOT_DIR / 'application' / 'single_app' / 'templates' / 'profile.html'
+
+
+def read_profile_template():
+ return PROFILE_TEMPLATE.read_text(encoding='utf-8')
+
+
+def assert_occurs_once(source_text, marker):
+ occurrence_count = source_text.count(marker)
+ assert occurrence_count == 1, (
+ f'Expected marker to appear once, found {occurrence_count}: {marker}'
+ )
+
+
+def test_profile_fact_memory_script_blocks_are_not_duplicated():
+ """Verify the profile template keeps fact-memory script markers unique."""
+ print('๐ Testing profile fact-memory script deduplication...')
+
+ profile_template = read_profile_template()
+
+ unique_markers = [
+ "",
+ '
',
+ '
',
+ '
',
+ '
',
+ 'let factMemoryEntries = [];',
+ 'let filteredFactMemoryEntries = [];',
+ 'let factMemoryCurrentPage = 1;',
+ 'const FACT_MEMORY_PAGE_SIZE = 5;',
+ "const factMemorySearchInput = document.getElementById('fact-memory-search-input');",
+ "function updateFactMemoryStatus(message, type = 'muted') {",
+ 'function getFilteredFactMemoryEntries() {',
+ 'async function loadFactMemory() {',
+ 'async function createFactMemory() {',
+ 'async function saveFactMemory(factId) {',
+ 'async function confirmDeleteFactMemory() {',
+ 'async function loadRetentionSettings() {',
+ 'function saveRetentionSettings() {',
+ 'function showSuccessToast(message) {',
+ 'function loadTutorialPreferences() {',
+ 'function saveTutorialPreferences() {',
+ ]
+
+ for marker in unique_markers:
+ assert_occurs_once(profile_template, marker)
+
+ print('โ
Profile fact-memory script markers are unique')
+
+
+if __name__ == '__main__':
+ test_profile_fact_memory_script_blocks_are_not_duplicated()
+ print('๐ Results: 1/1 tests passed')
\ No newline at end of file
diff --git a/functional_tests/test_support_app_title_personalization.py b/functional_tests/test_support_app_title_personalization.py
new file mode 100644
index 00000000..d4838e46
--- /dev/null
+++ b/functional_tests/test_support_app_title_personalization.py
@@ -0,0 +1,115 @@
+# test_support_app_title_personalization.py
+"""
+Functional test for support application-title personalization.
+Version: 0.241.002
+Implemented in: 0.241.002
+
+This test ensures that user-facing Support pages replace hard-coded SimpleChat
+copy with the configured application title for latest-feature content and the
+Send Feedback experience.
+"""
+
+import importlib.util
+import os
+import sys
+
+
+CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
+REPO_ROOT = os.path.abspath(os.path.join(CURRENT_DIR, '..'))
+
+SUPPORT_CONFIG = os.path.join(REPO_ROOT, 'application', 'single_app', 'support_menu_config.py')
+SUPPORT_TEMPLATE = os.path.join(REPO_ROOT, 'application', 'single_app', 'templates', 'support_send_feedback.html')
+BACKEND_SETTINGS = os.path.join(REPO_ROOT, 'application', 'single_app', 'route_backend_settings.py')
+
+
+def read_text(path):
+ with open(path, 'r', encoding='utf-8') as file_handle:
+ return file_handle.read()
+
+
+def load_module(path, module_name):
+ spec = importlib.util.spec_from_file_location(module_name, path)
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+
+def _collect_strings(value):
+ if isinstance(value, str):
+ return [value]
+
+ if isinstance(value, list):
+ strings = []
+ for item in value:
+ strings.extend(_collect_strings(item))
+ return strings
+
+ if isinstance(value, dict):
+ strings = []
+ for item in value.values():
+ strings.extend(_collect_strings(item))
+ return strings
+
+ return []
+
+
+def test_support_latest_features_use_application_title():
+ print('๐ Testing support latest-features app-title personalization...')
+
+ support_config = load_module(SUPPORT_CONFIG, 'support_app_title_personalization_test')
+ settings = {
+ 'app_title': 'Contoso Assist',
+ 'enable_support_send_feedback': True,
+ 'enable_support_latest_feature_documentation_links': True,
+ 'enable_user_workspace': True,
+ 'enable_semantic_kernel': True,
+ 'per_user_semantic_kernel': True,
+ 'support_latest_features_visibility': support_config.get_default_support_latest_features_visibility(),
+ }
+
+ visible_groups = support_config.get_visible_support_latest_feature_groups(settings)
+ visible_strings = _collect_strings(visible_groups)
+
+ assert visible_groups, 'Expected visible support feature groups for personalized copy testing'
+ assert any('Contoso Assist admins' in value for value in visible_strings), 'Expected Send Feedback copy to use the configured application title'
+ assert any('inside Contoso Assist' in value for value in visible_strings), 'Expected previous-release copy to use the configured application title'
+ assert not any('SimpleChat administrators' in value for value in visible_strings), 'Found hard-coded SimpleChat administrators copy in visible support features'
+ assert not any('inside SimpleChat' in value for value in visible_strings), 'Found hard-coded SimpleChat copy in previous-release support features'
+
+ print('โ
Support latest-features content uses the configured application title')
+
+
+def test_support_send_feedback_template_and_subject_are_dynamic():
+ print('๐ Testing Send Feedback application-title markers...')
+
+ template_content = read_text(SUPPORT_TEMPLATE)
+ backend_content = read_text(BACKEND_SETTINGS)
+
+ assert '{{ app_settings.app_title }} administrators' in template_content, 'Send Feedback template should use the configured application title in its intro copy'
+ assert "application_title = str(settings.get('app_title') or '').strip() or 'Simple Chat'" in backend_content, 'Support feedback mailto generation should resolve the configured application title'
+ assert "subject_line = f'[{application_title} User Support] {feedback_label} - {organization}'" in backend_content, 'Support feedback subject should use the configured application title'
+
+ print('โ
Send Feedback template and draft subject use the configured application title')
+
+
+if __name__ == '__main__':
+ tests = [
+ test_support_latest_features_use_application_title,
+ test_support_send_feedback_template_and_subject_are_dynamic,
+ ]
+
+ results = []
+ for test in tests:
+ try:
+ test()
+ results.append(True)
+ except Exception as exc:
+ print(f'โ {test.__name__} failed: {exc}')
+ import traceback
+ traceback.print_exc()
+ results.append(False)
+ print()
+
+ passed = sum(1 for result in results if result)
+ print(f'๐ Results: {passed}/{len(results)} tests passed')
+ sys.exit(0 if all(results) else 1)
\ No newline at end of file
diff --git a/functional_tests/test_support_menu_user_feature.py b/functional_tests/test_support_menu_user_feature.py
index 78bb3256..f98d1fbd 100644
--- a/functional_tests/test_support_menu_user_feature.py
+++ b/functional_tests/test_support_menu_user_feature.py
@@ -274,8 +274,9 @@ def test_support_menu_feedback_backend_and_templates():
"@app.route('/api/support/send_feedback_email', methods=['POST'])",
'def send_support_feedback_email():',
"return jsonify({'error': 'Support menu is available to signed-in app users only'}), 403",
+ "application_title = str(settings.get('app_title') or '').strip() or 'Simple Chat'",
'log_user_support_feedback_email_submission(',
- "'[SimpleChat User Support]",
+ "subject_line = f'[{application_title} User Support] {feedback_label} - {organization}'",
]
missing_backend = [marker for marker in backend_markers if marker not in backend_content]
assert not missing_backend, f'Missing support feedback backend markers: {missing_backend}'
diff --git a/ui_tests/test_profile_fact_memory_editor.py b/ui_tests/test_profile_fact_memory_editor.py
index a58a9261..5937acdb 100644
--- a/ui_tests/test_profile_fact_memory_editor.py
+++ b/ui_tests/test_profile_fact_memory_editor.py
@@ -1,11 +1,12 @@
# test_profile_fact_memory_editor.py
"""
UI test for the profile fact-memory editor.
-Version: 0.240.083
-Implemented in: 0.240.079; 0.240.082; 0.240.083
+Version: 0.241.004
+Implemented in: 0.240.079; 0.240.082; 0.240.083; 0.241.003; 0.241.004
This test ensures a signed-in user can create, edit, retag, and delete
-fact-memory entries from the profile page using the compact summary and modal editor.
+fact-memory entries from the profile page using the compact summary and modal editor
+without browser parse or runtime errors breaking the workflow.
"""
import os
@@ -49,6 +50,15 @@ def test_profile_fact_memory_editor(playwright):
created_fact_id = None
try:
page = context.new_page()
+ page_errors = []
+ console_errors = []
+
+ page.on('pageerror', lambda error: page_errors.append(str(error)))
+ page.on(
+ 'console',
+ lambda message: console_errors.append(message.text) if message.type == 'error' else None,
+ )
+
response = page.goto(f'{BASE_URL}/profile', wait_until='domcontentloaded')
assert response is not None, 'Expected a navigation response when loading /profile.'
if response.status in {401, 403, 404}:
@@ -57,6 +67,23 @@ def test_profile_fact_memory_editor(playwright):
assert response.ok, f'Expected /profile to load successfully, got HTTP {response.status}.'
expect(page.get_by_role('heading', name='Fact Memory')).to_be_visible()
expect(page.locator('#fact-memory-status')).to_contain_text(re.compile(r'Fact memory is'))
+ expect(page.locator('#tutorial-preferences')).to_have_count(1)
+ expect(page.locator('#fact-memory-settings')).to_have_count(1)
+ expect(page.locator('#factMemoryDeleteModal')).to_have_count(1)
+ expect(page.locator('#factMemoryManagerModal')).to_have_count(1)
+ assert not page_errors, f'Unexpected profile page errors: {page_errors}'
+ duplicate_declaration_errors = [
+ message for message in console_errors
+ if 'factMemoryEntries' in message
+ or 'factMemorySearchInput' in message
+ or 'already been declared' in message
+ or 'Identifier' in message
+ or 'SyntaxError' in message
+ ]
+ assert not duplicate_declaration_errors, (
+ 'Unexpected profile console errors during page load: '
+ f'{duplicate_declaration_errors}'
+ )
count_before_text = page.locator('#fact-memory-count').text_content() or '0'
initial_count = int(count_before_text.strip())
diff --git a/ui_tests/test_support_latest_features_image_modal.py b/ui_tests/test_support_latest_features_image_modal.py
index b41bf0e6..0782ca21 100644
--- a/ui_tests/test_support_latest_features_image_modal.py
+++ b/ui_tests/test_support_latest_features_image_modal.py
@@ -54,6 +54,12 @@ def test_support_latest_features_image_modal(playwright):
assert response.ok, f"Expected /support/latest-features to load successfully, got HTTP {response.status}."
expect(page.get_by_role("heading", name="Latest Features")).to_be_visible()
+ page_title = page.title()
+ assert page_title.startswith("Latest Features - "), f"Unexpected page title: {page_title}"
+ app_title = page_title.replace("Latest Features - ", "", 1).strip()
+ main_text = page.locator("main").text_content() or ""
+ if app_title != "SimpleChat":
+ assert "SimpleChat" not in main_text, "Visible Latest Features copy should use the configured application title."
expect(page.locator(".support-feature-card")).to_have_count(page.locator(".support-feature-card").count())
expect(page.locator(".support-feature-callout").first).to_be_visible()
expect(page.locator(".support-feature-action-card").first).to_be_visible()
diff --git a/ui_tests/test_support_send_feedback_field_selection.py b/ui_tests/test_support_send_feedback_field_selection.py
index f20e3a6f..59dbf864 100644
--- a/ui_tests/test_support_send_feedback_field_selection.py
+++ b/ui_tests/test_support_send_feedback_field_selection.py
@@ -1,8 +1,8 @@
# test_support_send_feedback_field_selection.py
"""
UI test for support Send Feedback field selection stability.
-Version: 0.240.064
-Implemented in: 0.240.064
+Version: 0.241.002
+Implemented in: 0.240.064; 0.241.002
This test ensures the Send Feedback page targets fields by stable form metadata
even if an extra text input is inserted ahead of the intended controls.
@@ -53,6 +53,11 @@ def test_support_send_feedback_uses_stable_field_selectors(playwright):
assert response.ok, f"Expected /support/send-feedback to load successfully, got HTTP {response.status}."
expect(page.get_by_role("heading", name="Send Feedback")).to_be_visible()
+ page_title = page.title()
+ assert page_title.startswith("Send Feedback - "), f"Unexpected page title: {page_title}"
+ app_title = page_title.replace("Send Feedback - ", "", 1).strip()
+ intro_text = page.locator("#support-send-feedback-pane .text-muted")
+ expect(intro_text).to_contain_text(app_title)
captured_payload = {}