syntaxmatrix 2.6.0__tar.gz → 2.6.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/PKG-INFO +1 -1
  2. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/SyntaxMatrix.egg-info/PKG-INFO +1 -1
  3. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/SyntaxMatrix.egg-info/SOURCES.txt +2 -1
  4. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/setup.py +1 -1
  5. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/core.py +59 -7
  6. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/db.py +46 -2
  7. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/routes.py +187 -6
  8. syntaxmatrix-2.6.2/syntaxmatrix/templates/admin_branding.html +104 -0
  9. syntaxmatrix-2.6.2/syntaxmatrix/templates/admin_features.html +63 -0
  10. syntaxmatrix-2.6.0/syntaxmatrix/static/icons/logo2.png +0 -0
  11. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/LICENSE.txt +0 -0
  12. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/README.md +0 -0
  13. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/SyntaxMatrix.egg-info/dependency_links.txt +0 -0
  14. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/SyntaxMatrix.egg-info/requires.txt +0 -0
  15. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/SyntaxMatrix.egg-info/top_level.txt +0 -0
  16. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/pyproject.toml +0 -0
  17. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/setup.cfg +0 -0
  18. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/__init__.py +0 -0
  19. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/agentic/__init__.py +0 -0
  20. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/agentic/agent_tools.py +0 -0
  21. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/agentic/agents.py +0 -0
  22. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/agentic/agents_orchestrer.py +0 -0
  23. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/agentic/code_tools_registry.py +0 -0
  24. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/agentic/model_templates.py +0 -0
  25. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/auth.py +0 -0
  26. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/bootstrap.py +0 -0
  27. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/commentary.py +0 -0
  28. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/dataset_preprocessing.py +0 -0
  29. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/display_html.py +0 -0
  30. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/emailer.py +0 -0
  31. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/file_processor.py +0 -0
  32. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/gpt_models_latest.py +0 -0
  33. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/history_store.py +0 -0
  34. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/kernel_manager.py +0 -0
  35. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/llm_store.py +0 -0
  36. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/media/__init__.py +0 -0
  37. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/media/media_pixabay.py +0 -0
  38. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/models.py +0 -0
  39. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/page_builder_defaults.py +0 -0
  40. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/page_builder_generation.py +0 -0
  41. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/page_layout_contract.py +0 -0
  42. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/page_patch_publish.py +0 -0
  43. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/plottings.py +0 -0
  44. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/preface.py +0 -0
  45. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/profiles.py +0 -0
  46. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/project_root.py +0 -0
  47. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/selftest_page_templates.py +0 -0
  48. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/session.py +0 -0
  49. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/settings/__init__.py +0 -0
  50. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/settings/client_items.py +0 -0
  51. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/settings/default.yaml +0 -0
  52. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/settings/logging.py +0 -0
  53. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/settings/model_map.py +0 -0
  54. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/settings/prompts.py +0 -0
  55. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/settings/string_navbar.py +0 -0
  56. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/smiv.py +0 -0
  57. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/smpv.py +0 -0
  58. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/static/assets/hero-default.svg +0 -0
  59. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/static/css/style.css +0 -0
  60. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/static/docs.md +0 -0
  61. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/static/icons/bot-icon.png +0 -0
  62. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/static/icons/favicon.png +0 -0
  63. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/static/icons/logo.png +0 -0
  64. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/static/icons/logo3.png +0 -0
  65. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/static/icons/svg_497526.svg +0 -0
  66. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/static/icons/svg_497528.svg +0 -0
  67. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/static/js/chat.js +0 -0
  68. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/static/js/sidebar.js +0 -0
  69. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/static/js/widgets.js +0 -0
  70. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/templates/admin_secretes.html +0 -0
  71. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/templates/change_password.html +0 -0
  72. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/templates/code_cell.html +0 -0
  73. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/templates/dashboard.html +0 -0
  74. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/templates/dataset_resize.html +0 -0
  75. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/templates/docs.html +0 -0
  76. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/templates/edit_page.html +0 -0
  77. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/templates/error.html +0 -0
  78. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/templates/login.html +0 -0
  79. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/templates/register.html +0 -0
  80. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/themes.py +0 -0
  81. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/ui_modes.py +0 -0
  82. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/utils.py +0 -0
  83. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/vector_db.py +0 -0
  84. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/vectordb/__init__.py +0 -0
  85. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/vectordb/adapters/__init__.py +0 -0
  86. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/vectordb/adapters/milvus_adapter.py +0 -0
  87. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/vectordb/adapters/pgvector_adapter.py +0 -0
  88. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/vectordb/adapters/sqlite_adapter.py +0 -0
  89. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/vectordb/base.py +0 -0
  90. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/vectordb/registry.py +0 -0
  91. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/vectorizer.py +0 -0
  92. {syntaxmatrix-2.6.0 → syntaxmatrix-2.6.2}/syntaxmatrix/workspace_db.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syntaxmatrix
3
- Version: 2.6.0
3
+ Version: 2.6.2
4
4
  Summary: SyntaxMUI: A customizable framework for Python AI Assistant Projects.
5
5
  Author: Bob Nti
6
6
  Author-email: bob.nti@syntaxmatrix.net
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syntaxmatrix
3
- Version: 2.6.0
3
+ Version: 2.6.2
4
4
  Summary: SyntaxMUI: A customizable framework for Python AI Assistant Projects.
5
5
  Author: Bob Nti
6
6
  Author-email: bob.nti@syntaxmatrix.net
@@ -67,13 +67,14 @@ syntaxmatrix/static/css/style.css
67
67
  syntaxmatrix/static/icons/bot-icon.png
68
68
  syntaxmatrix/static/icons/favicon.png
69
69
  syntaxmatrix/static/icons/logo.png
70
- syntaxmatrix/static/icons/logo2.png
71
70
  syntaxmatrix/static/icons/logo3.png
72
71
  syntaxmatrix/static/icons/svg_497526.svg
73
72
  syntaxmatrix/static/icons/svg_497528.svg
74
73
  syntaxmatrix/static/js/chat.js
75
74
  syntaxmatrix/static/js/sidebar.js
76
75
  syntaxmatrix/static/js/widgets.js
76
+ syntaxmatrix/templates/admin_branding.html
77
+ syntaxmatrix/templates/admin_features.html
77
78
  syntaxmatrix/templates/admin_secretes.html
78
79
  syntaxmatrix/templates/change_password.html
79
80
  syntaxmatrix/templates/code_cell.html
@@ -8,7 +8,7 @@ with open(os.path.join(this_directory, "README.md"), encoding="utf-8") as f:
8
8
 
9
9
  setup(
10
10
  name="syntaxmatrix",
11
- version="2.6.0",
11
+ version="2.6.2",
12
12
  author="Bob Nti",
13
13
  author_email="bob.nti@syntaxmatrix.net",
14
14
  description="SyntaxMUI: A customizable framework for Python AI Assistant Projects.",
@@ -40,6 +40,9 @@ _CLIENT_DIR = detect_project_root()
40
40
  _HISTORY_DIR = os.path.join(_CLIENT_DIR, "smx_history")
41
41
  os.makedirs(_HISTORY_DIR, exist_ok=True)
42
42
 
43
+ _BRANDING_DIR = os.path.join(_CLIENT_DIR, "branding")
44
+ os.makedirs(_BRANDING_DIR, exist_ok=True)
45
+
43
46
  _SECRET_PATH = os.path.join(_CLIENT_DIR, ".smx_secret_key")
44
47
 
45
48
  # OPENAI_API_KEY = getenv_api_key(_CLIENT_DIR, "OPENAI_API_KEY"))
@@ -72,6 +75,8 @@ class SyntaxMUI:
72
75
  self.bot_icon = bot_icon
73
76
  self.site_logo = site_logo
74
77
  self.favicon = favicon
78
+ self._default_site_logo = site_logo
79
+ self._default_favicon = favicon
75
80
  self.site_title = site_title
76
81
  self.project_name = project_name
77
82
  self.ui_mode = ui_mode
@@ -99,6 +104,9 @@ class SyntaxMUI:
99
104
  self._last_llm_usage = None
100
105
  routes.setup_routes(self)
101
106
 
107
+ # Apply client branding overrides if present on disk
108
+ self._apply_branding_from_disk()
109
+
102
110
  # LLM Profiles
103
111
  self.admin_profile = {}
104
112
  self.chat_profile = {}
@@ -107,11 +115,12 @@ class SyntaxMUI:
107
115
  self.coder_profile = {}
108
116
  self.imagetexter_profile = {}
109
117
  self.textimager_profile = {}
110
- self.imagereditor_profile = {}
118
+ self.imageeditor_profile = {}
111
119
 
112
120
  self._gpt_models_latest_prev_resp_ids = {}
113
121
  self.is_streaming = False
114
122
  self.stream_args = {}
123
+ self._apply_feature_flags_from_db()
115
124
 
116
125
  self._recent_visual_summaries = []
117
126
 
@@ -321,6 +330,24 @@ class SyntaxMUI:
321
330
 
322
331
  def enable_registration(self):
323
332
  self.registration_enabled = True
333
+
334
+ def _apply_feature_flags_from_db(self):
335
+ """
336
+ Pull persisted toggles from app_settings.
337
+ """
338
+ def _truthy(v):
339
+ return str(v or "").strip().lower() in ("1", "true", "yes", "on")
340
+
341
+ try:
342
+ stream_v = db.get_setting("feature.stream_mode", "0")
343
+ user_files_v = db.get_setting("feature.user_files", "0")
344
+
345
+ self.is_streaming = _truthy(stream_v)
346
+ self.user_files_enabled = _truthy(user_files_v)
347
+ except Exception:
348
+ # Keep defaults if DB isn't ready for any reason
349
+ pass
350
+
324
351
 
325
352
  @staticmethod
326
353
  def columns(components):
@@ -330,24 +357,49 @@ class SyntaxMUI:
330
357
  col_html += "</div>"
331
358
  return col_html
332
359
 
360
+ # Site Branding
333
361
  def set_site_title(self, title):
334
362
  self.site_title = title
335
-
336
363
  def set_project_name(self, project_name):
337
364
  self.project_name = project_name
338
-
339
365
  def set_favicon(self, icon):
340
366
  self.favicon = icon
341
-
342
-
343
367
  def set_site_logo(self, logo):
344
368
  self.site_logo = logo
345
-
346
369
  def set_user_icon(self, icon):
347
370
  self.user_icon = icon
348
-
349
371
  def set_bot_icon(self, icon):
350
372
  self.bot_icon = icon
373
+
374
+ def _apply_branding_from_disk(self):
375
+ """
376
+ If a client logo/favicon exists in syntaxmatrixdir/branding/,
377
+ use it; otherwise keep the framework defaults.
378
+ """
379
+ branding_dir = os.path.join(_CLIENT_DIR, "branding")
380
+
381
+ def _pick(basename: str):
382
+ for ext in (".png", ".jpg", ".jpeg"):
383
+ fn = f"{basename}{ext}"
384
+ p = os.path.join(branding_dir, fn)
385
+ if os.path.exists(p):
386
+ return fn
387
+ return None
388
+
389
+ logo_fn = _pick("logo")
390
+ fav_fn = _pick("favicon")
391
+
392
+ if logo_fn:
393
+ # Use client-served endpoint (added in routes.py below)
394
+ self.site_logo = f"<img src='/branding/{logo_fn}' width='45' alt='logo'/>"
395
+ else:
396
+ self.site_logo = getattr(self, "_default_site_logo", self.site_logo)
397
+
398
+ if fav_fn:
399
+ self.favicon = f"/branding/{fav_fn}"
400
+ else:
401
+ self.favicon = getattr(self, "_default_favicon", self.favicon)
402
+
351
403
 
352
404
  def text_input(self, key, id, label, placeholder=""):
353
405
  if not placeholder:
@@ -68,6 +68,14 @@ def init_db():
68
68
  )
69
69
  """)
70
70
 
71
+ conn.execute("""
72
+ CREATE TABLE IF NOT EXISTS app_settings (
73
+ key TEXT PRIMARY KEY,
74
+ value TEXT NOT NULL,
75
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
76
+ )
77
+ """)
78
+
71
79
  conn.commit()
72
80
  conn.close()
73
81
 
@@ -447,7 +455,7 @@ def get_page_layout(page_name: str) -> dict | None:
447
455
 
448
456
 
449
457
  def _init_media_assets_table() -> None:
450
- import sqlite3
458
+
451
459
  conn = sqlite3.connect(DB_PATH)
452
460
  try:
453
461
  with conn:
@@ -495,7 +503,7 @@ def upsert_media_asset(
495
503
  if not rel_path:
496
504
  return
497
505
  _init_media_assets_table()
498
- import sqlite3
506
+
499
507
  conn = sqlite3.connect(DB_PATH)
500
508
  try:
501
509
  with conn:
@@ -564,3 +572,39 @@ def get_media_asset_by_rel_path(rel_path: str) -> dict | None:
564
572
  }
565
573
  finally:
566
574
  conn.close()
575
+
576
+ # ***************************************
577
+ # App Settings Helpers (feature toggles)
578
+ # ***************************************
579
+ def set_setting(key: str, value: str) -> None:
580
+ if not key:
581
+ return
582
+ key = key.strip()
583
+ value = "" if value is None else str(value)
584
+ conn = sqlite3.connect(DB_PATH)
585
+ try:
586
+ with conn:
587
+ conn.execute(
588
+ """
589
+ INSERT INTO app_settings (key, value, updated_at)
590
+ VALUES (?, ?, CURRENT_TIMESTAMP)
591
+ ON CONFLICT(key) DO UPDATE SET
592
+ value = excluded.value,
593
+ updated_at = CURRENT_TIMESTAMP
594
+ """,
595
+ (key, value),
596
+ )
597
+ finally:
598
+ conn.close()
599
+
600
+
601
+ def get_setting(key: str, default: str | None = None) -> str | None:
602
+ if not key:
603
+ return default
604
+ key = key.strip()
605
+ conn = sqlite3.connect(DB_PATH)
606
+ try:
607
+ row = conn.execute("SELECT value FROM app_settings WHERE key = ?", (key,)).fetchone()
608
+ return row[0] if row else default
609
+ finally:
610
+ conn.close()
@@ -3326,6 +3326,10 @@ def setup_routes(smx):
3326
3326
  def upload_user_file():
3327
3327
  import uuid
3328
3328
  from flask import jsonify
3329
+
3330
+ if not getattr(smx, "user_files_enabled", False):
3331
+ return jsonify({"error": "user_files_disabled"}), 403
3332
+
3329
3333
  # Define the upload folder for user files.
3330
3334
  upload_folder = os.path.join(_CLIENT_DIR, "uploads", "user")
3331
3335
  if not os.path.exists(upload_folder):
@@ -4501,15 +4505,21 @@ def setup_routes(smx):
4501
4505
  <ul class="catalog-list" style="padding-left:1rem; margin-bottom:0;">
4502
4506
  {profile_items or "<li class='li-row'>No profiles yet.</li>"}
4503
4507
  </ul>
4508
+
4509
+ <!-- Refresh button (reload admin page; anchor back to Models section) -->
4510
+ <div style="display:flex; justify-content:flex-end; margin-top:10px;">
4511
+ <a class="btn" href="/admin?refresh=profiles#models" title="Reload to refresh profiles list">Refresh</a>
4512
+ </div>
4504
4513
  </div>
4505
4514
  """
4506
4515
 
4516
+
4507
4517
  # ────────────────────────────────────────────────────────────────────────────────
4508
4518
  # SYSTEM FILES
4509
4519
  # ────────────────────────────────────────────────────────────────────────────────
4510
4520
  sys_files_card = f"""
4511
- <div class="card span-4">
4512
- <h4>Upload System Files (PDFs only)</h4>
4521
+ <div class="card span-3">
4522
+ <h4>Upload System Files<br>(PDFs only)</h4>
4513
4523
  <form id="form-upload" method="post" enctype="multipart/form-data" style="display:inline-block;">
4514
4524
  <input type="file" name="upload_files" accept=".pdf" multiple>
4515
4525
  <button type="submit" name="action" value="upload_files">Upload</button>
@@ -4540,7 +4550,7 @@ def setup_routes(smx):
4540
4550
  """
4541
4551
 
4542
4552
  manage_sys_files_card = f"""
4543
- <div class='card span-4'>
4553
+ <div class='card span-3'>
4544
4554
  <h4>Manage Company Files</h4>
4545
4555
  <ul class="catalog-list" style="list-style:none; padding-left:0; margin:0;">
4546
4556
  {sys_files_html or "<li>No company file has been uploaded yet.</li>"}
@@ -4964,7 +4974,7 @@ def setup_routes(smx):
4964
4974
  pixabay_saved = bool(os.environ.get("PIXABAY_API_KEY"))
4965
4975
 
4966
4976
  secretes_link_card = f"""
4967
- <div class="card span-4">
4977
+ <div class="card span-3">
4968
4978
  <h4>Integrations (Secrets)</h4>
4969
4979
  <div style="font-size:.72rem;color:#555;margin-top:-6px;margin-bottom:10px;line-height:1.35;">
4970
4980
  Store secrete credentials.
@@ -4973,17 +4983,39 @@ def setup_routes(smx):
4973
4983
  </div>
4974
4984
  """
4975
4985
 
4986
+ features_link_card = f"""
4987
+ <div class="card span-4">
4988
+ <h4>Feature toggles</h4>
4989
+ <div style="font-size:.72rem;color:#555;margin-top:-6px;margin-bottom:10px;line-height:1.35;">
4990
+ Turn streaming on/off and allow user file uploads in chat.
4991
+ </div>
4992
+ <a href="{url_for('admin_features')}" class="btn">Manage features</a>
4993
+ </div>
4994
+ """
4995
+
4996
+ branding_link_card = f"""
4997
+ <div class="card span-3">
4998
+ <h4>Branding</h4>
4999
+ <div style="font-size:.72rem;color:#555;margin-top:-6px;margin-bottom:10px;line-height:1.35;">
5000
+ Upload your company logo and favicon (PNG/JPG). Defaults are used if nothing is uploaded.
5001
+ </div>
5002
+ <a href="{url_for('admin_branding')}" class="btn">Manage branding</a>
5003
+ </div>
5004
+ """
5005
+
4976
5006
  system_section = f"""
4977
5007
  <section id="system" class="section">
4978
5008
  <h2>System</h2>
4979
5009
  <div class="admin-grid">
4980
5010
  {secretes_link_card}
5011
+ {branding_link_card}
5012
+ {features_link_card}
4981
5013
  {sys_files_card}
4982
5014
  {manage_sys_files_card}
4983
5015
  </div>
5016
+
4984
5017
  </section>
4985
5018
  """
4986
-
4987
5019
  users_section = f"""
4988
5020
  <section id="users" class="section">
4989
5021
  <h2>Users</h2>
@@ -5518,6 +5550,148 @@ def setup_routes(smx):
5518
5550
  return render_template("admin_secretes.html", secret_names=names)
5519
5551
 
5520
5552
 
5553
+ @smx.app.route("/admin/branding", methods=["GET", "POST"])
5554
+ @admin_required
5555
+ def admin_branding():
5556
+ branding_dir = os.path.join(_CLIENT_DIR, "branding")
5557
+ os.makedirs(branding_dir, exist_ok=True)
5558
+
5559
+ allowed_ext = {".png", ".jpg", ".jpeg"}
5560
+ max_logo_bytes = 5 * 1024 * 1024 # 5 MB
5561
+ max_favicon_bytes = 1 * 1024 * 1024 # 1 MB
5562
+
5563
+ def _find(base: str):
5564
+ for ext in (".png", ".jpg", ".jpeg"):
5565
+ p = os.path.join(branding_dir, f"{base}{ext}")
5566
+ if os.path.exists(p):
5567
+ return f"{base}{ext}"
5568
+ return None
5569
+
5570
+ def _delete_all(base: str):
5571
+ for ext in (".png", ".jpg", ".jpeg"):
5572
+ p = os.path.join(branding_dir, f"{base}{ext}")
5573
+ if os.path.exists(p):
5574
+ try:
5575
+ os.remove(p)
5576
+ except Exception:
5577
+ pass
5578
+
5579
+ def _save_upload(field_name: str, base: str, max_bytes: int):
5580
+ f = request.files.get(field_name)
5581
+ if not f or not f.filename:
5582
+ return False, None
5583
+
5584
+ ext = os.path.splitext(f.filename.lower())[1].strip()
5585
+ if ext not in allowed_ext:
5586
+ return False, f"Invalid file type for {base}. Use PNG or JPG."
5587
+
5588
+ # size check
5589
+ try:
5590
+ f.stream.seek(0, os.SEEK_END)
5591
+ size = f.stream.tell()
5592
+ f.stream.seek(0)
5593
+ except Exception:
5594
+ size = None
5595
+
5596
+ if size is not None and size > max_bytes:
5597
+ return False, f"{base.capitalize()} is too large. Max {max_bytes // (1024*1024)} MB."
5598
+
5599
+ # Replace existing logo.* / favicon.*
5600
+ _delete_all(base)
5601
+
5602
+ out_path = os.path.join(branding_dir, f"{base}{ext}")
5603
+ try:
5604
+ f.save(out_path)
5605
+ except Exception as e:
5606
+ return False, f"Failed to save {base}: {e}"
5607
+
5608
+ return True, None
5609
+
5610
+ # POST actions
5611
+ if request.method == "POST":
5612
+ action = (request.form.get("action") or "upload").strip().lower()
5613
+
5614
+ if action == "reset":
5615
+ _delete_all("logo")
5616
+ _delete_all("favicon")
5617
+ try:
5618
+ smx._apply_branding_from_disk()
5619
+ except Exception:
5620
+ pass
5621
+ flash("Branding reset to defaults ✓")
5622
+ return redirect(url_for("admin_branding"))
5623
+
5624
+ ok1, err1 = _save_upload("logo_file", "logo", max_logo_bytes)
5625
+ ok2, err2 = _save_upload("favicon_file", "favicon", max_favicon_bytes)
5626
+
5627
+ if err1:
5628
+ flash(err1, "error")
5629
+ if err2:
5630
+ flash(err2, "error")
5631
+
5632
+ if ok1 or ok2:
5633
+ try:
5634
+ smx._apply_branding_from_disk()
5635
+ except Exception:
5636
+ pass
5637
+ flash("Branding updated ✓")
5638
+
5639
+ return redirect(url_for("admin_branding"))
5640
+
5641
+ # GET: show current status
5642
+ logo_fn = _find("logo")
5643
+ fav_fn = _find("favicon")
5644
+
5645
+ cache_bust = int(time.time())
5646
+
5647
+ logo_url = f"/branding/{logo_fn}?v={cache_bust}" if logo_fn else None
5648
+ favicon_url = f"/branding/{fav_fn}?v={cache_bust}" if fav_fn else None
5649
+
5650
+ default_logo_html = getattr(smx, "_default_site_logo", smx.site_logo)
5651
+ default_favicon_url = getattr(smx, "_default_favicon", smx.favicon)
5652
+
5653
+ return render_template(
5654
+ "admin_branding.html",
5655
+ logo_url=logo_url,
5656
+ favicon_url=favicon_url,
5657
+ default_logo_html=Markup(default_logo_html),
5658
+ default_favicon_url=default_favicon_url,
5659
+ )
5660
+
5661
+
5662
+ @smx.app.route("/admin/features", methods=["GET", "POST"])
5663
+ @admin_required
5664
+ def admin_features():
5665
+ # Defaults from DB (or fall back)
5666
+ def _truthy(v):
5667
+ return str(v or "").strip().lower() in ("1", "true", "yes", "on")
5668
+
5669
+ if request.method == "POST":
5670
+ stream_on = "1" if request.form.get("stream_mode") == "on" else "0"
5671
+ user_files_on = "1" if request.form.get("user_files") == "on" else "0"
5672
+
5673
+ db.set_setting("feature.stream_mode", stream_on)
5674
+ db.set_setting("feature.user_files", user_files_on)
5675
+
5676
+ # Apply immediately (no restart)
5677
+ try:
5678
+ smx._apply_feature_flags_from_db()
5679
+ except Exception:
5680
+ pass
5681
+
5682
+ flash("Settings updated ✓")
5683
+ return redirect(url_for("admin_features"))
5684
+
5685
+ stream_mode = _truthy(db.get_setting("feature.stream_mode", "0"))
5686
+ user_files = _truthy(db.get_setting("feature.user_files", "0"))
5687
+
5688
+ return render_template(
5689
+ "admin_features.html",
5690
+ stream_mode=stream_mode,
5691
+ user_files=user_files,
5692
+ )
5693
+
5694
+
5521
5695
  @smx.app.route("/admin/delete.json", methods=["POST"])
5522
5696
  def admin_delete_universal():
5523
5697
 
@@ -6407,13 +6581,20 @@ def setup_routes(smx):
6407
6581
 
6408
6582
  return jsonify({"file_paths": file_paths})
6409
6583
 
6410
-
6584
+
6411
6585
  # Serve the raw media files
6412
6586
  @smx.app.route('/uploads/media/<path:filename>')
6413
6587
  def serve_media(filename):
6414
6588
  media_dir = os.path.join(_CLIENT_DIR, 'uploads', 'media')
6415
6589
  return send_from_directory(media_dir, filename)
6416
6590
 
6591
+
6592
+ @smx.app.route("/branding/<path:filename>")
6593
+ def serve_branding(filename):
6594
+ branding_dir = os.path.join(_CLIENT_DIR, "branding")
6595
+ return send_from_directory(branding_dir, filename)
6596
+
6597
+
6417
6598
  # ────────────────────────────────────────────────────────────────────────────────────────
6418
6599
  # DASHBOARD
6419
6600
  # ────────────────────────────────────────────────────────────────────────────────────────
@@ -0,0 +1,104 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Branding</title>
7
+ <style>
8
+ body{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;margin:0;background:#0b1224;color:#e5e7eb;}
9
+ .wrap{max-width:980px;margin:0 auto;padding:18px;}
10
+ .top{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:14px;}
11
+ .top h1{margin:0;font-size:1.1rem;}
12
+ .btn{display:inline-block;padding:8px 12px;border-radius:999px;border:1px solid rgba(148,163,184,.35);color:#e5e7eb;text-decoration:none;background:transparent;cursor:pointer;font-weight:650;font-size:.85rem;}
13
+ .btn:hover{border-color:rgba(56,189,248,.6);color:#38bdf8;}
14
+ .card{background:rgba(2,6,23,.65);border:1px solid rgba(148,163,184,.25);border-radius:14px;padding:14px;box-shadow:0 18px 36px rgba(15,23,42,.6);margin-bottom:12px;}
15
+ label{display:block;font-size:.78rem;font-weight:750;margin-bottom:6px;color:#d1d5db;}
16
+ input[type="file"]{width:100%;padding:10px 11px;border-radius:12px;border:1px solid rgba(148,163,184,.25);background:rgba(2,6,23,.8);color:#e5e7eb;outline:none;}
17
+ .row{display:grid;grid-template-columns:1fr 1fr;gap:10px;}
18
+ @media (max-width:760px){.row{grid-template-columns:1fr;}}
19
+ .hint{color:#9ca3af;font-size:.78rem;line-height:1.35;margin-top:8px;}
20
+ .flash{background:rgba(34,197,94,.12);border:1px solid rgba(34,197,94,.35);padding:10px;border-radius:12px;margin-bottom:10px;color:#bbf7d0;font-size:.85rem;}
21
+ .flash.warn{background:rgba(239,68,68,.10);border-color:rgba(239,68,68,.30);color:#fecaca;}
22
+ .preview{display:flex;gap:14px;align-items:center;flex-wrap:wrap;margin-top:10px;}
23
+ .chip{padding:6px 10px;border-radius:999px;border:1px solid rgba(148,163,184,.25);font-size:.8rem;color:#cbd5e1;}
24
+ .danger{border-color:rgba(239,68,68,.55);color:#fecaca;}
25
+ .danger:hover{border-color:rgba(239,68,68,.9);color:#fff;}
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <div class="wrap">
30
+ <div class="top">
31
+ <h1>Branding</h1>
32
+ <a class="btn" href="{{ url_for('admin_panel') }}#system">Back to Admin</a>
33
+ </div>
34
+
35
+ {% with messages = get_flashed_messages(with_categories=True) %}
36
+ {% if messages %}
37
+ {% for cat, m in messages %}
38
+ <div class="flash {{ 'warn' if cat == 'error' else '' }}">{{ m }}</div>
39
+ {% endfor %}
40
+ {% endif %}
41
+ {% endwith %}
42
+
43
+ <div class="card">
44
+ <h2 style="margin:0 0 10px;font-size:.95rem;">Upload logo and favicon</h2>
45
+
46
+ <form method="post" enctype="multipart/form-data">
47
+ <input type="hidden" name="action" value="upload">
48
+
49
+ <div class="row">
50
+ <div>
51
+ <label>Logo (PNG/JPG)</label>
52
+ <input type="file" name="logo_file" accept=".png,.jpg,.jpeg,image/png,image/jpeg">
53
+ <div class="hint">Tip: a wide logo works best. Up to 5 MB.</div>
54
+ </div>
55
+
56
+ <div>
57
+ <label>Favicon (PNG/JPG)</label>
58
+ <input type="file" name="favicon_file" accept=".png,.jpg,.jpeg,image/png,image/jpeg">
59
+ <div class="hint">Tip: square image (32×32 or 48×48). Up to 1 MB.</div>
60
+ </div>
61
+ </div>
62
+
63
+ <div style="margin-top:12px;display:flex;justify-content:flex-end;gap:10px;">
64
+ <button class="btn" type="submit">Save</button>
65
+ </div>
66
+ </form>
67
+
68
+ <div class="preview">
69
+ <div>
70
+ <div class="chip">Current logo</div>
71
+ <div style="margin-top:8px;">
72
+ {% if logo_url %}
73
+ <img src="{{ logo_url }}" alt="logo" style="max-height:44px;max-width:240px;">
74
+ {% else %}
75
+ {{ default_logo_html|safe }}
76
+ {% endif %}
77
+ </div>
78
+ </div>
79
+
80
+ <div>
81
+ <div class="chip">Current favicon</div>
82
+ <div style="margin-top:8px;">
83
+ {% if favicon_url %}
84
+ <img src="{{ favicon_url }}" alt="favicon" style="width:32px;height:32px;border-radius:6px;">
85
+ {% else %}
86
+ <img src="{{ default_favicon_url }}" alt="favicon" style="width:32px;height:32px;border-radius:6px;">
87
+ {% endif %}
88
+ </div>
89
+ </div>
90
+ </div>
91
+ </div>
92
+
93
+ <div class="card">
94
+ <h2 style="margin:0 0 10px;font-size:.95rem;">Reset</h2>
95
+ <div class="hint">Removes the uploaded logo/favicon so the app falls back to the SyntaxMatrix defaults.</div>
96
+
97
+ <form method="post" onsubmit="return confirm('Reset branding to defaults?');" style="margin-top:10px;">
98
+ <input type="hidden" name="action" value="reset">
99
+ <button class="btn danger" type="submit">Reset to defaults</button>
100
+ </form>
101
+ </div>
102
+ </div>
103
+ </body>
104
+ </html>
@@ -0,0 +1,63 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Feature toggles</title>
7
+ <style>
8
+ body{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;margin:0;background:#0b1224;color:#e5e7eb;}
9
+ .wrap{max-width:980px;margin:0 auto;padding:18px;}
10
+ .top{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:14px;}
11
+ .top h1{margin:0;font-size:1.1rem;}
12
+ .btn{display:inline-block;padding:8px 12px;border-radius:999px;border:1px solid rgba(148,163,184,.35);color:#e5e7eb;text-decoration:none;background:transparent;cursor:pointer;font-weight:650;font-size:.85rem;}
13
+ .btn:hover{border-color:rgba(56,189,248,.6);color:#38bdf8;}
14
+ .card{background:rgba(2,6,23,.65);border:1px solid rgba(148,163,184,.25);border-radius:14px;padding:14px;box-shadow:0 18px 36px rgba(15,23,42,.6);margin-bottom:12px;}
15
+ .flash{background:rgba(34,197,94,.12);border:1px solid rgba(34,197,94,.35);padding:10px;border-radius:12px;margin-bottom:10px;color:#bbf7d0;font-size:.85rem;}
16
+ .hint{color:#9ca3af;font-size:.78rem;line-height:1.35;margin-top:8px;}
17
+ .row{display:flex;align-items:flex-start;gap:10px;padding:10px 0;border-top:1px solid rgba(148,163,184,.15);}
18
+ .row:first-child{border-top:none;}
19
+ .label{font-weight:750;}
20
+ input[type="checkbox"]{transform: translateY(2px); width:16px; height:16px;}
21
+ </style>
22
+ </head>
23
+ <body>
24
+ <div class="wrap">
25
+ <div class="top">
26
+ <h1>Feature toggles</h1>
27
+ <a class="btn" href="{{ url_for('admin_panel') }}#system">Back to Admin</a>
28
+ </div>
29
+
30
+ {% with messages = get_flashed_messages(with_categories=False) %}
31
+ {% if messages %}
32
+ {% for m in messages %}
33
+ <div class="flash">{{ m }}</div>
34
+ {% endfor %}
35
+ {% endif %}
36
+ {% endwith %}
37
+
38
+ <div class="card">
39
+ <form method="post">
40
+ <div class="row">
41
+ <input type="checkbox" id="stream_mode" name="stream_mode" {% if stream_mode %}checked{% endif %}>
42
+ <div>
43
+ <div class="label">Enable stream mode</div>
44
+ <div class="hint">Streams assistant responses in real time (SSE). Applies to the chat UI immediately.</div>
45
+ </div>
46
+ </div>
47
+
48
+ <div class="row">
49
+ <input type="checkbox" id="user_files" name="user_files" {% if user_files %}checked{% endif %}>
50
+ <div>
51
+ <div class="label">Enable user files</div>
52
+ <div class="hint">Shows the “➕” upload icon and allows users to upload PDFs for that chat session.</div>
53
+ </div>
54
+ </div>
55
+
56
+ <div style="margin-top:12px;display:flex;justify-content:flex-end;gap:10px;">
57
+ <button class="btn" type="submit">Save</button>
58
+ </div>
59
+ </form>
60
+ </div>
61
+ </div>
62
+ </body>
63
+ </html>
File without changes
File without changes
File without changes