syntaxmatrix 2.5.8.2__py3-none-any.whl → 2.6.0__py3-none-any.whl

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 (39) hide show
  1. syntaxmatrix/agentic/agents.py +1149 -54
  2. syntaxmatrix/agentic/agents_orchestrer.py +326 -0
  3. syntaxmatrix/agentic/code_tools_registry.py +27 -32
  4. syntaxmatrix/commentary.py +16 -16
  5. syntaxmatrix/core.py +107 -70
  6. syntaxmatrix/db.py +416 -4
  7. syntaxmatrix/{display.py → display_html.py} +2 -6
  8. syntaxmatrix/gpt_models_latest.py +1 -1
  9. syntaxmatrix/media/__init__.py +0 -0
  10. syntaxmatrix/media/media_pixabay.py +277 -0
  11. syntaxmatrix/models.py +1 -1
  12. syntaxmatrix/page_builder_defaults.py +183 -0
  13. syntaxmatrix/page_builder_generation.py +1122 -0
  14. syntaxmatrix/page_layout_contract.py +644 -0
  15. syntaxmatrix/page_patch_publish.py +1471 -0
  16. syntaxmatrix/preface.py +128 -8
  17. syntaxmatrix/profiles.py +26 -13
  18. syntaxmatrix/routes.py +1347 -427
  19. syntaxmatrix/selftest_page_templates.py +360 -0
  20. syntaxmatrix/settings/client_items.py +28 -0
  21. syntaxmatrix/settings/model_map.py +1022 -208
  22. syntaxmatrix/settings/prompts.py +328 -130
  23. syntaxmatrix/static/assets/hero-default.svg +22 -0
  24. syntaxmatrix/static/icons/bot-icon.png +0 -0
  25. syntaxmatrix/static/icons/favicon.png +0 -0
  26. syntaxmatrix/static/icons/logo.png +0 -0
  27. syntaxmatrix/static/icons/logo2.png +0 -0
  28. syntaxmatrix/static/icons/logo3.png +0 -0
  29. syntaxmatrix/templates/admin_secretes.html +108 -0
  30. syntaxmatrix/templates/dashboard.html +116 -72
  31. syntaxmatrix/templates/edit_page.html +2535 -0
  32. syntaxmatrix/utils.py +2365 -2411
  33. {syntaxmatrix-2.5.8.2.dist-info → syntaxmatrix-2.6.0.dist-info}/METADATA +6 -2
  34. {syntaxmatrix-2.5.8.2.dist-info → syntaxmatrix-2.6.0.dist-info}/RECORD +37 -24
  35. syntaxmatrix/generate_page.py +0 -644
  36. syntaxmatrix/static/icons/hero_bg.jpg +0 -0
  37. {syntaxmatrix-2.5.8.2.dist-info → syntaxmatrix-2.6.0.dist-info}/WHEEL +0 -0
  38. {syntaxmatrix-2.5.8.2.dist-info → syntaxmatrix-2.6.0.dist-info}/licenses/LICENSE.txt +0 -0
  39. {syntaxmatrix-2.5.8.2.dist-info → syntaxmatrix-2.6.0.dist-info}/top_level.txt +0 -0
syntaxmatrix/routes.py CHANGED
@@ -1,7 +1,7 @@
1
- import os, zipfile, time, uuid, werkzeug, queue, html, ast, re
2
- import threading, textwrap, json, pandas as pd
3
- import contextlib
4
-
1
+ from math import floor
2
+ import os, time, uuid, queue, html, re
3
+ import json, pandas as pd
4
+ import contextlib, werkzeug
5
5
  import io as _std_io
6
6
 
7
7
  from io import BytesIO
@@ -9,18 +9,15 @@ from scipy import io
9
9
  from flask import Blueprint, Response, request, send_file, session
10
10
  from flask import render_template, render_template_string, url_for, redirect, g
11
11
  from flask import flash, jsonify, send_from_directory, get_flashed_messages, stream_with_context
12
-
12
+ from syntaxmatrix.page_patch_publish import patch_page_publish, ensure_sections_exist
13
13
  from flask_login import current_user
14
-
14
+ from syntaxmatrix.page_layout_contract import normalise_layout, validate_layout, validate_compiled_html
15
15
  from PyPDF2 import PdfReader
16
16
  from markupsafe import Markup
17
17
  from urllib.parse import quote
18
18
  from datetime import datetime
19
- from prompt_toolkit import HTML
20
19
  from PyPDF2.errors import EmptyFileError
21
- import numpy as np
22
- from .auth import register_user, authenticate, login_required, admin_required, superadmin_required
23
-
20
+ import numpy as np
24
21
  from syntaxmatrix.themes import DEFAULT_THEMES
25
22
  from syntaxmatrix import db
26
23
  from syntaxmatrix.vector_db import add_pdf_chunk
@@ -32,15 +29,32 @@ from syntaxmatrix.history_store import SQLHistoryStore, PersistentHistoryStore
32
29
  from syntaxmatrix.kernel_manager import SyntaxMatrixKernelManager, execute_code_in_kernel
33
30
  from syntaxmatrix.vector_db import *
34
31
  from syntaxmatrix.settings.string_navbar import string_navbar_items
35
- from syntaxmatrix.settings.model_map import GPT_MODELS_LATEST, PROVIDERS_MODELS, MODEL_DESCRIPTIONS, PURPOSE_TAGS, EMBEDDING_MODELS
36
32
  from syntaxmatrix.project_root import detect_project_root
37
- from syntaxmatrix import generate_page as _genpage
33
+ from syntaxmatrix.page_builder_defaults import make_default_layout, layout_to_html
38
34
  from syntaxmatrix import auth as _auth
39
- from .auth import register_user, authenticate, login_required, admin_required, superadmin_required, update_password
40
-
35
+ from syntaxmatrix.auth import register_user, authenticate, login_required, admin_required, superadmin_required, update_password
41
36
  from syntaxmatrix import profiles as _prof
42
37
  from syntaxmatrix.gpt_models_latest import set_args, extract_output_text as _out
43
- from syntaxmatrix.agentic.agents import classify_ml_job_agent, refine_question_agent, text_formatter_agent
38
+ from syntaxmatrix.agentic.agent_tools import ToolRunner
39
+ from syntaxmatrix.settings.model_map import(
40
+ GPT_MODELS_LATEST,
41
+ PROVIDERS_MODELS,
42
+ MODEL_DESCRIPTIONS,
43
+ PURPOSE_TAGS,
44
+ EMBEDDING_MODELS
45
+ )
46
+ from syntaxmatrix.agentic.agents import (
47
+ classify_ml_job_agent, context_compatibility,
48
+ agentic_generate_page,
49
+ )
50
+
51
+ from syntaxmatrix.page_builder_generation import (
52
+ build_layout_for_page,
53
+ fill_layout_images_from_pixabay,
54
+ compile_layout_to_html,
55
+ patch_page_from_layout,
56
+ patch_section_titles_and_intros,
57
+ )
44
58
 
45
59
  try:
46
60
  from pygments import highlight as _hl
@@ -50,17 +64,6 @@ try:
50
64
  except Exception:
51
65
  _HAVE_PYGMENTS = False
52
66
 
53
- # from syntaxmatrix.utils import *
54
- from syntaxmatrix.utils import (
55
- auto_inject_template, drop_bad_classification_metrics, ensure_accuracy_block,
56
- ensure_image_output, ensure_output, fix_plain_prints, fix_print_html, patch_fix_sentinel_plot_calls,
57
- patch_pairplot, fix_to_datetime_errors, harden_ai_code, patch_ensure_seaborn_import, get_plotting_imports,
58
- patch_fix_seaborn_palette_calls, patch_quiet_specific_warnings, fix_seaborn_barplot_nameerror, fix_seaborn_boxplot_nameerror, ensure_matplotlib_title, patch_plot_code, patch_prefix_seaborn_calls, fix_scatter_and_summary, inject_auto_preprocessing, fix_importance_groupby, patch_pie_chart, patch_rmse_calls, clean_llm_code
59
- )
60
-
61
- from syntaxmatrix.agentic.agent_tools import ToolRunner
62
- from syntaxmatrix.agentic.code_tools_registry import EARLY_SANITIZERS, SYNTAX_AND_REPAIR
63
-
64
67
  _CLIENT_DIR = detect_project_root()
65
68
  _stream_q = queue.Queue()
66
69
  _stream_cancelled = {}
@@ -123,6 +126,7 @@ def get_contrast_color(hex_color: str) -> str:
123
126
  def render_chat_history(smx):
124
127
  plottings_html = smx.get_plottings()
125
128
  messages = smx.get_chat_history() or []
129
+
126
130
  chat_html = ""
127
131
  if not messages and not plottings_html:
128
132
  chat_html += f"""
@@ -172,27 +176,43 @@ def setup_routes(smx):
172
176
  os.makedirs(DATA_FOLDER, exist_ok=True)
173
177
 
174
178
  MEDIA_FOLDER = os.path.join(_CLIENT_DIR, "uploads", "media")
175
- if not os.path.exists(MEDIA_FOLDER):
176
- os.makedirs(MEDIA_FOLDER)
179
+
180
+ # Ensure media subfolders (images/videos + generated)
181
+ MEDIA_IMAGES_UPLOADED = os.path.join(MEDIA_FOLDER, "images", "uploaded")
182
+ MEDIA_IMAGES_GENERATED = os.path.join(MEDIA_FOLDER, "images", "generated")
183
+ MEDIA_IMAGES_GENERATED_ICONS = os.path.join(MEDIA_IMAGES_GENERATED, "icons")
184
+ MEDIA_IMAGES_THUMBS = os.path.join(MEDIA_IMAGES_GENERATED, "thumbs")
185
+
186
+ MEDIA_VIDEOS_UPLOADED = os.path.join(MEDIA_FOLDER, "videos", "uploaded")
187
+ MEDIA_FILES_UPLOADED = os.path.join(MEDIA_FOLDER, "files", "uploaded")
188
+
189
+ for _p in [MEDIA_IMAGES_UPLOADED,
190
+ MEDIA_IMAGES_GENERATED,
191
+ MEDIA_IMAGES_GENERATED_ICONS,
192
+ MEDIA_IMAGES_THUMBS,
193
+ MEDIA_VIDEOS_UPLOADED,
194
+ MEDIA_FILES_UPLOADED,
195
+ ]:
196
+ os.makedirs(_p, exist_ok=True)
177
197
 
178
198
  def _evict_profile_caches_by_name(prof_name: str) -> None:
179
- """
180
- Clear any in-memory profile cache on `smx` that points to the deleted profile.
181
- Future-proof: it scans all attributes and clears any dict whose 'name' matches.
182
- """
183
- if not prof_name:
184
- return
185
- try:
186
- for attr in dir(smx):
187
- # be generous: match anything that mentions 'profile' in its name
188
- if "profile" not in attr.lower():
189
- continue
190
- val = getattr(smx, attr, None)
191
- if isinstance(val, dict) and val.get("name") == prof_name:
192
- setattr(smx, attr, {}) # drop just this one; others untouched
193
- except Exception:
194
- # never let cache eviction break the request path
195
- pass
199
+ """
200
+ Clear any in-memory profile cache on `smx` that points to the deleted profile.
201
+ Future-proof: it scans all attributes and clears any dict whose 'name' matches.
202
+ """
203
+ if not prof_name:
204
+ return
205
+ try:
206
+ for attr in dir(smx):
207
+ # be generous: match anything that mentions 'profile' in its name
208
+ if "profile" not in attr.lower():
209
+ continue
210
+ val = getattr(smx, attr, None)
211
+ if isinstance(val, dict) and val.get("name") == prof_name:
212
+ setattr(smx, attr, {}) # drop just this one; others untouched
213
+ except Exception:
214
+ # never let cache eviction break the request path
215
+ pass
196
216
 
197
217
  @smx.app.after_request
198
218
  def _set_session_cookie(resp):
@@ -264,10 +284,7 @@ def setup_routes(smx):
264
284
  font-size: clamp(1.4rem, 1.8vw, 1.8rem);
265
285
  margin-right: 0;
266
286
  }}
267
- .logo img {{
268
- display: block;
269
- width: clamp(1.4rem, 1.8vw, 1.8rem);
270
- }}
287
+
271
288
  .nav-left a {{
272
289
  color: {smx.theme["nav_text"]};
273
290
  text-decoration: none;
@@ -292,9 +309,13 @@ def setup_routes(smx):
292
309
  }}
293
310
  /* Hamburger button (hidden on desktop) */
294
311
  #hamburger-btn {{
295
- display: none;
296
- width: clamp(140px, 20vw, 240px);
312
+ display: none; /* shown only in mobile media query */
313
+ width: auto;
314
+ height: 40px;
315
+ margin-left: auto; /* push it to the far right */
316
+ padding: 0;
297
317
  font-size: 2rem;
318
+ line-height: 1;
298
319
  background: none;
299
320
  border: none;
300
321
  color: {smx.theme["nav_text"]};
@@ -498,6 +519,11 @@ def setup_routes(smx):
498
519
  box-sizing: border-box;
499
520
  }}
500
521
  }}
522
+ @media (max-width:900px){{
523
+ #chat-history {{
524
+ padding-top: 62px;
525
+ }}
526
+ }}
501
527
  </style>
502
528
 
503
529
  <!-- Add MathJax -->
@@ -592,32 +618,87 @@ def setup_routes(smx):
592
618
  dst = (href or "/").rstrip("/") or "/"
593
619
  return cur == dst or cur.startswith(dst + "/")
594
620
 
621
+ # Pull nav metadata from DB. Fail-soft if anything goes wrong.
622
+ try:
623
+ nav_meta = db.get_page_nav_map()
624
+ except Exception as e:
625
+ smx.warning(f"get_page_nav_map failed: {e}")
626
+ nav_meta = {}
627
+
628
+ def _page_label(name: str) -> str:
629
+ meta = nav_meta.get(name.lower()) or {}
630
+ label = (meta.get("nav_label") or "").strip()
631
+ return label or name.capitalize()
632
+
633
+ def _page_visible(name: str) -> bool:
634
+ meta = nav_meta.get(name.lower())
635
+ # Default behaviour: if there's no row, we show it.
636
+ if not meta:
637
+ return True
638
+ return bool(meta.get("show_in_nav", True))
639
+
640
+ def _page_order(name: str) -> int:
641
+ meta = nav_meta.get(name.lower()) or {}
642
+ order_val = meta.get("nav_order")
643
+ try:
644
+ return int(order_val)
645
+ except (TypeError, ValueError):
646
+ # Pages without explicit order go to the end, sorted by label
647
+ return 10_000
648
+
595
649
  # Build nav links with active class
596
650
  nav_items = []
597
- for page in smx.pages:
651
+
652
+ # Sort pages by nav_order first, then by label
653
+ pages_sorted = sorted(
654
+ smx.pages,
655
+ key=lambda nm: (_page_order(nm), _page_label(nm).lower()),
656
+ )
657
+
658
+ for page in pages_sorted:
659
+ if not _page_visible(page):
660
+ continue
598
661
  href = f"/page/{page}"
599
662
  active = " active" if _is_active(href) else ""
600
663
  aria = ' aria-current="page"' if active else ""
601
- nav_items.append(f'<a href="{href}" class="{active.strip()}"{aria}>{page.capitalize()}</a>')
664
+ label = _page_label(page)
665
+ nav_items.append(
666
+ f'<a href="{href}" class="{active.strip()}"{aria}>{label}</a>'
667
+ )
602
668
 
669
+ # # 1) Custom pages from smx.pages, filtered by show_in_nav
670
+ # for page in smx.pages:
671
+ # if not _page_visible(page):
672
+ # continue
673
+ # href = f"/page/{page}"
674
+ # active = " active" if _is_active(href) else ""
675
+ # aria = ' aria-current="page"' if active else ""
676
+ # label = _page_label(page)
677
+ # nav_items.append(
678
+ # f'<a href="{href}" class="{active.strip()}"{aria}>{label}</a>'
679
+ # )
680
+
681
+ # 2) Fixed items from string_navbar_items (unchanged, except Dashboard label)
603
682
  for st in string_navbar_items:
604
683
  slug = st.lower().replace(" ", "_")
605
684
  href = f"/{slug}"
606
685
  active = " active" if _is_active(href) else ""
607
686
  aria = ' aria-current="page"' if active else ""
608
- if st == "Dashboard":
609
- st = "MLearning"
687
+ label = "MLearning" if st == "Dashboard" else st
610
688
 
611
689
  # Only show Admin link to admins/superadmins
612
690
  if slug in ("admin", "admin_panel", "adminpanel"):
613
691
  role = session.get("role")
614
692
  if role not in ("admin", "superadmin"):
615
693
  continue
616
- nav_items.append(f'<a href="{href}" class="{active.strip()}"{aria}>{st}</a>')
694
+
695
+ nav_items.append(
696
+ f'<a href="{href}" class="{active.strip()}"{aria}>{label}</a>'
697
+ )
617
698
 
618
699
  nav_links = "".join(nav_items)
619
700
 
620
- theme_link = ''
701
+ theme_link = ""
621
702
  if smx.theme_toggle_enabled:
622
703
  theme_link = '<a href="/toggle_theme">Theme</a>'
623
704
 
@@ -630,7 +711,6 @@ def setup_routes(smx):
630
711
  '</form>'
631
712
  )
632
713
  else:
633
- # Only show Register link if the consumer app explicitly enabled it.
634
714
  reg_link = ""
635
715
  if getattr(smx, "registration_enabled", False):
636
716
  reg_link = f'|<a href="{url_for("register")}" class="nav-link">Register</a>'
@@ -668,7 +748,6 @@ def setup_routes(smx):
668
748
  {hamburger_btn}
669
749
  </nav>
670
750
  {mobile_nav}
671
- {hamburger_btn}
672
751
  """
673
752
 
674
753
  def footer_html():
@@ -3079,7 +3158,7 @@ def setup_routes(smx):
3079
3158
  yield "data: " + json.dumps({ "event": "cancelled" }) + "\n\n"
3080
3159
 
3081
3160
  try:
3082
- gen = smx.process_query_stream(**sa) # <- yes, this is where streaming is used
3161
+ gen = smx.process_query_stream(**sa)
3083
3162
  except Exception as e:
3084
3163
  smx.error(f"Could not start stream: {e}")
3085
3164
  return jsonify({"error": "stream_start_failed", "message": str(e)})
@@ -3351,24 +3430,45 @@ def setup_routes(smx):
3351
3430
  }
3352
3431
  .admin-sidenav a:hover,.admin-sidenav a.active{background:#DADADA}
3353
3432
 
3354
- /* Main content with balanced margins */
3355
- @media (min-width: 901px){
3356
- .admin-main{
3357
- margin-left: calc(var(--sidenav-w) + 3px); /* 1px for the border */
3358
- margin-top: var(--nav-h);
3359
- margin-bottom: 0;
3360
- padding: 0 10px; /* keep your left gutter */
3361
- margin-right: 0 !important; /* stop over-wide total */
3362
- width: calc(100% - var(--sidenav-w)) !important; /* % not vw */
3363
- padding-right: var(--right) !important; /* keep your right gutter */
3364
- box-sizing: border-box;
3365
- max-width: 100%;
3433
+ /* Admin overlay + toggle (desktop: hidden) */
3434
+ .admin-scrim{
3435
+ position: fixed;
3436
+ inset: 0;
3437
+ background: rgba(0,0,0,.25);
3438
+ z-index: 1000;
3439
+ opacity: 0;
3440
+ pointer-events: none;
3441
+ transition: opacity .2s ease;
3442
+ }
3443
+ .admin-scrim.show{
3444
+ opacity: 1;
3445
+ pointer-events: auto;
3446
+ }
3447
+
3448
+ .admin-sidebar-toggle{
3449
+ display: none; /* only visible on mobile */
3366
3450
  }
3367
- @media (max-width: 1200px) {
3368
- body {
3369
- padding-top: 0;
3451
+
3452
+ /* shared with dashboard drawer logic */
3453
+ body.no-scroll{
3454
+ overflow: hidden;
3455
+ }
3456
+
3457
+ /* Main content with balanced margins (desktop ≥ 901px) */
3458
+ @media (min-width: 901px){
3459
+ .admin-main{
3460
+ margin-left: calc(var(--sidenav-w) + 3px); /* 1px for the border */
3461
+ margin-top: var(--nav-h);
3462
+ margin-bottom: 0;
3463
+ padding: 0 10px; /* keep your left gutter */
3464
+ margin-right: 0 !important; /* stop over-wide total */
3465
+ width: calc(100% - var(--sidenav-w)) !important; /* % not vw */
3466
+ padding-right: var(--right) !important; /* keep your right gutter */
3467
+ box-sizing: border-box;
3468
+ max-width: 100%;
3370
3469
  }
3371
3470
  }
3471
+
3372
3472
  /* Section demarcation */
3373
3473
  .section{
3374
3474
  background: var(--section-bg);
@@ -3424,7 +3524,7 @@ def setup_routes(smx):
3424
3524
  .span-12 { grid-column: span 12; }
3425
3525
 
3426
3526
  /* Lists */
3427
- .catalog-list{max-height:120px;overflow:auto;margin:0;padding:0;list-style:none}
3527
+ .catalog-list{max-height:200px;overflow:auto;margin:0;padding:0;list-style:none}
3428
3528
  .catalog-list li{
3429
3529
  display:flex;align-items:center;justify-content:space-between;gap:4px;
3430
3530
  padding:1px 2px;border-bottom:1px solid #eee;font-size:.7rem;
@@ -3468,22 +3568,67 @@ def setup_routes(smx):
3468
3568
  }
3469
3569
  }
3470
3570
 
3471
- /* Mobile */
3571
+ /* Mobile: off-canvas drawer from the left (like dashboard) */
3472
3572
  @media (max-width: 900px){
3473
- .admin-sidenav{ width: var(--sidenav-w-sm); }
3573
+ .admin-sidenav{
3574
+ position: fixed;
3575
+ top: var(--nav-h);
3576
+ left: 0;
3577
+ width: 24vw; /* narrower drawer */
3578
+ max-width: 96px; /* cap on larger phones */
3579
+ height: calc(100vh - var(--nav-h));
3580
+ transform: translateX(-100%);
3581
+ transition: transform .28s ease;
3582
+ z-index: 1100;
3583
+ border-radius: 0 10px 10px 0;
3584
+ }
3585
+ .admin-sidenav.open{
3586
+ transform: translateX(0);
3587
+ }
3588
+
3474
3589
  .admin-main{
3475
- margin-top: var(--nav-h);
3476
- margin-left: calc(var(--sidenav-w-sm) - 1px); /* 1px for the border */
3477
- margin-right: 4px;
3478
- width: calc(100% - var(--sidenav-w-sm));
3479
- padding: 0;
3590
+ margin-left: 0;
3591
+ margin-right: 0;
3592
+ width: 100%;
3593
+ padding: 8px 8px 16px;
3480
3594
  box-sizing: border-box;
3481
- max-width: 100%;
3595
+ max-width: 100%;
3596
+ }
3597
+
3598
+ /* Floating blue toggle button (hamburger / close) */
3599
+ .admin-sidebar-toggle{
3600
+ position: fixed;
3601
+ top: calc(var(--nav-h) + 8px); /* sit just below the blue header */
3602
+ left: 10px;
3603
+ z-index: 1200;
3604
+ display: inline-flex;
3605
+ align-items: center;
3606
+ justify-content: center;
3607
+ width: 40px;
3608
+ height: 40px;
3609
+ border: 0;
3610
+ border-radius: 10px;
3611
+ background: #0d6efd;
3612
+ color: #fff;
3613
+ box-shadow: 0 4px 14px rgba(0,0,0,.18);
3614
+ cursor: pointer;
3615
+ }
3616
+ .admin-sidebar-toggle::before{
3617
+ content: "☰";
3618
+ font-size: 22px;
3619
+ line-height: 1;
3620
+ }
3621
+ .admin-sidebar-toggle.is-open::before{
3622
+ content: "✕";
3623
+ }
3624
+
3625
+ /* Stack cards one per row on narrow screens */
3626
+ .span-2, .span-3, .span-4, .span-5, .span-6, .span-7,
3627
+ .span-8, .span-9, .span-10, .span-12 {
3628
+ grid-column: span 12;
3482
3629
  }
3483
-
3484
- /* force all grid items to stack */
3485
- .span-3, .span-4, .span-6, .span-8, .span-12 { grid-column: span 12; }
3486
3630
  }
3631
+
3487
3632
  /* Prevent any inner block from insisting on a width that causes overflow */
3488
3633
  .admin-shell .card, .admin-grid { min-width: 0; }
3489
3634
 
@@ -3559,12 +3704,13 @@ def setup_routes(smx):
3559
3704
  .catalog-list li:hover {
3560
3705
  background: #D3E3D3;
3561
3706
  }
3562
- #users > div > div > ul > li > form > button {
3707
+ #users > div > div > ul > li > form > button {
3563
3708
  font-size: 0.7rem;
3564
3709
  margin: 0;
3565
3710
  padding: 0 !important;
3566
3711
  border: 0.5px dashed gray;
3567
3712
  }
3713
+
3568
3714
  /* Fix: stop inputs/selects inside cards spilling out (desktop & tablet) */
3569
3715
  .admin-shell .card > * { min-width: 0; }
3570
3716
  .admin-shell .card input,
@@ -3577,13 +3723,53 @@ def setup_routes(smx):
3577
3723
  }
3578
3724
  .admin-shell .card input:not([type="checkbox"]):not([type="radio"]),
3579
3725
  .admin-shell .card select,
3580
- .admin-shell .card textarea{
3726
+ .admin-shell .card textarea {
3581
3727
  display:block;
3582
3728
  width:100%;
3583
3729
  max-width:100%;
3584
3730
  box-sizing:border-box;
3585
3731
  }
3586
3732
 
3733
+ /* ── Manage Pages overrides: compact single-row controls inside the list ── */
3734
+ #pages .catalog-list li {
3735
+ align-items: center;
3736
+ }
3737
+
3738
+ #pages .catalog-list li form {
3739
+ display: flex;
3740
+ align-items: center;
3741
+ justify-content: space-between;
3742
+ gap: 0.4rem;
3743
+ width: 100%;
3744
+ flex-wrap: nowrap;
3745
+ }
3746
+
3747
+ #pages .catalog-list li form input,
3748
+ #pages .catalog-list li form select,
3749
+ #pages .catalog-list li form button {
3750
+ display: inline-block;
3751
+ width: auto;
3752
+ max-width: 10rem;
3753
+ box-sizing: border-box;
3754
+ }
3755
+
3756
+ #pages .catalog-list li form input[type="text"] {
3757
+ flex: 1 1 160px; /* nav label / title can grow */
3758
+ }
3759
+
3760
+ #pages .catalog-list li form input[type="number"] {
3761
+ width: 3rem;
3762
+ flex: 0 0 auto; /* small fixed width for order */
3763
+ }
3764
+
3765
+ #pages .catalog-list li form label {
3766
+ display: inline-flex;
3767
+ align-items: center;
3768
+ gap: 0.3rem;
3769
+ white-space: nowrap;
3770
+ margin: 0;
3771
+ }
3772
+
3587
3773
  /* Restore normal checkbox/radio sizing & alignment */
3588
3774
  .admin-shell .card input[type="checkbox"],
3589
3775
  .admin-shell .card input[type="radio"]{
@@ -3603,18 +3789,13 @@ def setup_routes(smx):
3603
3789
  }
3604
3790
  /* If fixed and its height is constant (e.g., 56px) */
3605
3791
  body { padding-top: 46px; } /* make room for the bar */
3606
- .admin-main { margin-top: 0; } /* remove the manual bump */
3607
- .admin-sidenav { /* keep the sidebar aligned */
3608
- top: 56px;
3609
- height: calc(100vh - 56px);
3610
- }
3792
+
3611
3793
  #del-embed-btn, .del-btn {
3612
3794
  padding: 0;
3613
3795
  font-size: 0.6rem;
3614
3796
  border: none;
3615
3797
  text-decoration: none;
3616
3798
  }
3617
-
3618
3799
  </style>
3619
3800
  """
3620
3801
 
@@ -3666,15 +3847,199 @@ def setup_routes(smx):
3666
3847
  f"Generated {total_chunks} chunk(s)."
3667
3848
  )
3668
3849
 
3850
+
3669
3851
  elif action == "add_page":
3670
- page_name = request.form.get("page_name", "").strip()
3671
- page_name = page_name.lower()
3672
- site_desc = request.form.get("site_desc", "").strip()
3673
- if site_desc != "":
3852
+ # Core fields
3853
+ page_name = (request.form.get("page_name") or "").strip().lower()
3854
+
3855
+ def _slugify(s: str) -> str:
3856
+ s = (s or "").strip().lower()
3857
+ s = s.replace("_", "-")
3858
+ s = re.sub(r"\s+", "-", s)
3859
+ s = re.sub(r"[^a-z0-9\-]+", "", s)
3860
+ s = re.sub(r"-{2,}", "-", s).strip("-")
3861
+ return s or "page"
3862
+ requested_slug = _slugify(page_name)
3863
+ base_slug = requested_slug
3864
+
3865
+ # Find a free slug (auto-suffix)
3866
+ final_slug = base_slug
3867
+ n = 2
3868
+ while final_slug in (smx.pages or {}):
3869
+ final_slug = f"{base_slug}-{n}"
3870
+ n += 1
3871
+ page_name = final_slug
3872
+
3873
+ site_desc = (request.form.get("site_desc") or "").strip()
3874
+
3875
+ # Nav-related fields from the form
3876
+ show_in_nav_raw = request.form.get("show_in_nav")
3877
+ show_in_nav = bool(show_in_nav_raw)
3878
+ nav_label = (request.form.get("nav_label") or "").strip()
3879
+
3880
+
3881
+ # Compile to modern HTML with icons + animations
3882
+ # Use instance website description unless the form provides a new one
3883
+ if site_desc:
3674
3884
  smx.set_website_description(site_desc)
3675
- page_content_html = _genpage.generate_page_html(page_name, smx.website_description)
3676
- if page_name and page_name not in smx.pages:
3677
- db.add_page(page_name, page_content_html)
3885
+
3886
+ base_slug = (page_name or "").strip().lower()
3887
+ if not base_slug:
3888
+ flash("Page name is required.", "error")
3889
+ return redirect(url_for("admin_panel"))
3890
+
3891
+ # Auto-suffix if slug clashes
3892
+ final_slug = base_slug
3893
+ if final_slug in (smx.pages or {}):
3894
+ n = 2
3895
+ while f"{base_slug}-{n}" in (smx.pages or {}):
3896
+ n += 1
3897
+ final_slug = f"{base_slug}-{n}"
3898
+
3899
+ # Pull Pixabay key if you have it in DB (best-effort)
3900
+ pixabay_key = ""
3901
+ try:
3902
+ if hasattr(db, "get_secret"):
3903
+ pixabay_key = db.get_secret("PIXABAY_API_KEY") or ""
3904
+ except Exception:
3905
+ pixabay_key = ""
3906
+
3907
+ # Agentic generation (Gemini → plan → validate → Pixabay → compile)
3908
+ result = agentic_generate_page(
3909
+ page_slug=final_slug,
3910
+ website_description=smx.website_description,
3911
+ client_dir=_CLIENT_DIR,
3912
+ pixabay_api_key=pixabay_key,
3913
+ llm_profile=smx.current_profile("coder"),
3914
+ )
3915
+
3916
+ page_content_html = result["html"]
3917
+ layout_plan = result["plan"]
3918
+
3919
+ # Persist page content
3920
+ if final_slug not in smx.pages:
3921
+ db.add_page(final_slug, page_content_html)
3922
+ smx.pages = db.get_pages()
3923
+ else:
3924
+ db.update_page(final_slug, final_slug, page_content_html)
3925
+ smx.pages = db.get_pages()
3926
+
3927
+ # If you have page_layouts support, store the plan for the builder
3928
+ try:
3929
+ if hasattr(db, "upsert_page_layout"):
3930
+ db.upsert_page_layout(final_slug, json.dumps(layout_plan), is_detached=False)
3931
+ except Exception as e:
3932
+ smx.warning(f"upsert_page_layout failed for '{final_slug}': {e}")
3933
+
3934
+ # Nav label default
3935
+ if not nav_label:
3936
+ nav_label = final_slug.capitalize()
3937
+
3938
+ # Compute default nav order
3939
+ nav_order = None
3940
+ try:
3941
+ nav_meta_all = db.get_page_nav_map()
3942
+ existing_orders = [
3943
+ meta.get("nav_order")
3944
+ for meta in nav_meta_all.values()
3945
+ if meta.get("nav_order") is not None
3946
+ ]
3947
+ nav_order = (max(existing_orders) + 1) if existing_orders else 1
3948
+ except Exception as e:
3949
+ smx.warning(f"Could not compute nav order for '{final_slug}': {e}")
3950
+ nav_order = None
3951
+
3952
+ try:
3953
+ db.set_page_nav(
3954
+ final_slug,
3955
+ show_in_nav=show_in_nav,
3956
+ nav_label=nav_label,
3957
+ nav_order=nav_order,
3958
+ )
3959
+ except Exception as e:
3960
+ smx.warning(f"set_page_nav failed for '{final_slug}': {e}")
3961
+
3962
+ # Show banner only on builder/edit page after generation
3963
+ session["published_as"] = final_slug
3964
+ return redirect(url_for("edit_page", page_name=final_slug, published_as=final_slug))
3965
+
3966
+ elif action == "update_page_nav":
3967
+ # Update nav visibility / label / order for an existing page
3968
+ page_name = (request.form.get("page_name") or "").strip().lower()
3969
+ show_raw = request.form.get("show_in_nav")
3970
+ show_in_nav = bool(show_raw)
3971
+ nav_label = (request.form.get("nav_label") or "").strip()
3972
+ nav_order_raw = (request.form.get("nav_order") or "").strip()
3973
+
3974
+ nav_order = None
3975
+ if nav_order_raw:
3976
+ try:
3977
+ nav_order = int(nav_order_raw)
3978
+ except ValueError:
3979
+ nav_order = None
3980
+
3981
+ if page_name:
3982
+ if not nav_label:
3983
+ nav_label = page_name.capitalize()
3984
+ try:
3985
+ db.set_page_nav(
3986
+ page_name,
3987
+ show_in_nav=show_in_nav,
3988
+ nav_label=nav_label,
3989
+ nav_order=nav_order,
3990
+ )
3991
+ except Exception as e:
3992
+ smx.warning(f"update_page_nav failed for '{page_name}': {e}")
3993
+
3994
+ return redirect(url_for("admin_panel"))
3995
+
3996
+ elif action == "reorder_pages":
3997
+ """
3998
+ Persist a new navigation order for pages.
3999
+ Expects a comma-separated list of page names in `page_order_csv`.
4000
+ """
4001
+ order_csv = (request.form.get("page_order_csv") or "").strip()
4002
+ if order_csv:
4003
+ # normalise and dedupe while preserving order
4004
+ raw_names = [n.strip() for n in order_csv.split(",") if n.strip()]
4005
+ seen = set()
4006
+ ordered_names = []
4007
+ for nm in raw_names:
4008
+ if nm in seen:
4009
+ continue
4010
+ seen.add(nm)
4011
+ ordered_names.append(nm)
4012
+
4013
+ try:
4014
+ nav_meta = db.get_page_nav_map()
4015
+ except Exception as e:
4016
+ smx.warning(f"admin_panel: get_page_nav_map failed while reordering pages: {e}")
4017
+ nav_meta = {}
4018
+
4019
+ order_idx = 1
4020
+ for name in ordered_names:
4021
+ # Try to find any existing meta for this page
4022
+ meta = (
4023
+ nav_meta.get(name)
4024
+ or nav_meta.get(name.lower())
4025
+ or {}
4026
+ )
4027
+ show_in_nav = meta.get("show_in_nav", True)
4028
+ nav_label = meta.get("nav_label") or name.capitalize()
4029
+
4030
+ try:
4031
+ db.set_page_nav(
4032
+ name,
4033
+ show_in_nav=show_in_nav,
4034
+ nav_label=nav_label,
4035
+ nav_order=order_idx,
4036
+ )
4037
+ except Exception as e:
4038
+ smx.warning(f"admin_panel: set_page_nav failed for {name}: {e}")
4039
+ order_idx += 1
4040
+
4041
+ # Always bounce back to the admin panel (avoid re-POST)
4042
+ return redirect(url_for("admin_panel"))
3678
4043
 
3679
4044
 
3680
4045
  elif action == "save_llm":
@@ -3700,7 +4065,7 @@ def setup_routes(smx):
3700
4065
  prov = request.form["provider"]
3701
4066
  model = request.form["model"]
3702
4067
  tag = request.form["purpose"]
3703
- desc = request.form["desc"]
4068
+ desc = request.form["desc"]
3704
4069
 
3705
4070
  if not any(r for r in catalog if r["provider"] == prov and r["model"] == model):
3706
4071
  flash("Provider/model not in catalog", "error")
@@ -3711,7 +4076,7 @@ def setup_routes(smx):
3711
4076
  provider = request.form.get("provider", "").strip(),
3712
4077
  model = request.form.get("model", "").strip(),
3713
4078
  api_key = request.form.get("api_key", "").strip(),
3714
- purpose = request.form.get("purpose", "").strip() or "general",
4079
+ purpose = request.form.get("purpose", "").strip(),
3715
4080
  desc = request.form.get("desc", "").strip(),
3716
4081
  )
3717
4082
  _prof.refresh_profiles_cache()
@@ -3781,7 +4146,7 @@ def setup_routes(smx):
3781
4146
  elif action == "create_user":
3782
4147
  viewer_role = (session.get("role") or "").lower()
3783
4148
  if viewer_role not in ("admin", "superadmin"):
3784
- flash("You don't have permission to create users.", "error")
4149
+ flash("You are not authorised to create user.", "error")
3785
4150
  else:
3786
4151
  email = (request.form.get("email") or "").strip()
3787
4152
  username = (request.form.get("username") or "").strip()
@@ -3832,7 +4197,7 @@ def setup_routes(smx):
3832
4197
 
3833
4198
  elif action == "confirm_delete_user":
3834
4199
  if (session.get("role") or "").lower() != "superadmin":
3835
- flash("Only superadmin can delete accounts.", "error")
4200
+ flash("You are not authorised to delete accounts.", "error")
3836
4201
  else:
3837
4202
  session["pending_delete_user_id"] = int(request.form.get("user_id") or 0)
3838
4203
  flash("Confirm deletion below.", "warning")
@@ -3842,7 +4207,7 @@ def setup_routes(smx):
3842
4207
 
3843
4208
  elif action == "delete_user":
3844
4209
  if (session.get("role") or "").lower() != "superadmin":
3845
- flash("You don't have permission to delete account.", "error")
4210
+ flash("You are not authorised to delete account.", "error")
3846
4211
  else:
3847
4212
  target_id = session.get("pending_delete_user_id")
3848
4213
  if target_id:
@@ -3874,7 +4239,7 @@ def setup_routes(smx):
3874
4239
  # ────────────────────────────────────────────────────────────────────────────────
3875
4240
  embedding_model = _llms.load_embed_model()
3876
4241
  embeddings_setup_card = f"""
3877
- <div class="card span-4">
4242
+ <div class="card span-3">
3878
4243
  <h4>Setup Embedding Model</h4>
3879
4244
  <form method="post" style="display:inline-block; margin-right:8px;">
3880
4245
  <input type="hidden" name="action" value="save_llm">
@@ -3935,7 +4300,7 @@ def setup_routes(smx):
3935
4300
  # LLMs
3936
4301
  # ────────────────────────────────────────────────────────────────────────────────
3937
4302
  Add_model_catalog_card = f"""
3938
- <div class="card span-4">
4303
+ <div class="card span-3">
3939
4304
  <h3>Add Model To Catalogue</h3>
3940
4305
  <form method="post" style="margin-bottom:0.5rem;">
3941
4306
  <label for="catalog_prov">Provider</label>
@@ -4066,7 +4431,7 @@ def setup_routes(smx):
4066
4431
  """
4067
4432
 
4068
4433
  models_catalog_list_card = f"""
4069
- <div class="card span-4">
4434
+ <div class="card span-6">
4070
4435
  <h4>Models Catalogue</h4>
4071
4436
  <ul class="catalog-list">
4072
4437
  {cat_items or "<li class='li-row'>No models yet.</li>"}
@@ -4104,7 +4469,7 @@ def setup_routes(smx):
4104
4469
 
4105
4470
  <input type='hidden' id='purpose-field' name='purpose'>
4106
4471
  <input type='hidden' id='desc-field' name='desc'>
4107
-
4472
+ <br>
4108
4473
  <button class='btn btn-primary' type='submit' name='action' value='add_profile'>Add / Update</button>
4109
4474
  </form>
4110
4475
  </div>
@@ -4143,7 +4508,7 @@ def setup_routes(smx):
4143
4508
  # SYSTEM FILES
4144
4509
  # ────────────────────────────────────────────────────────────────────────────────
4145
4510
  sys_files_card = f"""
4146
- <div class="card span-6">
4511
+ <div class="card span-4">
4147
4512
  <h4>Upload System Files (PDFs only)</h4>
4148
4513
  <form id="form-upload" method="post" enctype="multipart/form-data" style="display:inline-block;">
4149
4514
  <input type="file" name="upload_files" accept=".pdf" multiple>
@@ -4175,7 +4540,7 @@ def setup_routes(smx):
4175
4540
  """
4176
4541
 
4177
4542
  manage_sys_files_card = f"""
4178
- <div class='card span-6'>
4543
+ <div class='card span-4'>
4179
4544
  <h4>Manage Company Files</h4>
4180
4545
  <ul class="catalog-list" style="list-style:none; padding-left:0; margin:0;">
4181
4546
  {sys_files_html or "<li>No company file has been uploaded yet.</li>"}
@@ -4190,48 +4555,105 @@ def setup_routes(smx):
4190
4555
  upload_msg = session.pop("upload_msg", "")
4191
4556
  alert_script = f"<script>alert('{upload_msg}');</script>" if upload_msg else ""
4192
4557
 
4558
+ # Load nav metadata (show_in_nav / nav_label) for existing pages
4559
+ try:
4560
+ nav_meta = db.get_page_nav_map()
4561
+ except Exception as e:
4562
+ smx.warning(f"get_page_nav_map failed in admin_panel: {e}")
4563
+ nav_meta = {}
4564
+
4193
4565
  pages_html = ""
4194
4566
  for p in smx.pages:
4567
+ meta = nav_meta.get(p.lower(), {})
4568
+ show_flag = meta.get("show_in_nav", True)
4569
+ label = meta.get("nav_label") or p.capitalize()
4570
+ nav_order_val = meta.get("nav_order")
4571
+ safe_label = html.escape(label, quote=True)
4572
+ order_display = "" if nav_order_val is None else html.escape(str(nav_order_val), quote=True)
4573
+ checked = "checked" if show_flag else ""
4574
+
4195
4575
  pages_html += f"""
4196
4576
  <li class="li-row" data-row-id="{p}">
4197
- <span>{p}</span>
4198
- <span style="float:right;">
4199
- <a class="edit-btn" href="/admin/edit/{p}" title="Edit {p}">🖊️</a>
4200
- <a href="#"
4201
- class="del-btn" title="Delete {p}"
4202
- data-action="open-delete-modal"
4203
- data-delete-url="/admin/delete.json"
4204
- data-delete-field="page_name"
4205
- data-delete-id="{p}"
4206
- data-delete-label="page {p}"
4207
- data-delete-extra='{{"resource":"page"}}'
4208
- data-delete-remove="[data-row-id='{p}']">🗑️</a>
4209
- </span>
4577
+ <form method="post" style="display:flex; align-items:center; gap:0.4rem; justify-content:space-between; width:100%;">
4578
+ <input type="hidden" name="action" value="update_page_nav">
4579
+ <input type="hidden" name="page_name" value="{p}">
4580
+ <span style="flex:0 0 auto;">{p}</span>
4581
+ <span style="flex:1 1 auto; text-align:right; font-size:0.75rem;">
4582
+ <label style="display:inline-flex; align-items:center; gap:0.25rem; margin-right:0.4rem;">
4583
+ <input type="checkbox" name="show_in_nav" value="1" {checked} style="margin:0; width:auto;">
4584
+ <span>Show</span>
4585
+ </label>
4586
+ <input
4587
+ type="number"
4588
+ name="nav_order"
4589
+ value="{order_display}"
4590
+ placeholder="#"
4591
+ min="0"
4592
+ style="width:3rem; font-size:0.75rem; padding:2px 4px; border-radius:4px; border:1px solid #ccc; text-align:right; margin-right:0.25rem;"
4593
+ >
4594
+ <input
4595
+ type="text"
4596
+ name="nav_label"
4597
+ value="{safe_label}"
4598
+ placeholder="Nav label"
4599
+ style="max-width:8.5rem; font-size:0.75rem; padding:2px 4px; border-radius:4px; border:1px solid #ccc;"
4600
+ >
4601
+ <button type="submit" style="font-size:0.7rem; padding:2px 6px; margin-left:0.25rem;">
4602
+ Save
4603
+ </button>
4604
+ </span>
4605
+ <span style="flex:0 0 auto; margin-left:0.4rem;">
4606
+ <a class="edit-btn" href="/admin/edit/{p}" title="Edit {p}">🖊️</a>
4607
+ <a href="#"
4608
+ class="del-btn" title="Delete {p}"
4609
+ data-action="open-delete-modal"
4610
+ data-delete-url="/admin/delete.json"
4611
+ data-delete-field="page_name"
4612
+ data-delete-id="{p}"
4613
+ data-delete-label="page {p}"
4614
+ data-delete-extra='{{"resource":"page"}}'
4615
+ data-delete-remove="[data-row-id='{p}']">🗑️</a>
4616
+ </span>
4617
+ </form>
4210
4618
  </li>
4211
4619
  """
4212
4620
 
4213
4621
  add_new_page_card = f"""
4214
- <div class="card span-9">
4215
- <h4>Add New Page</h4>
4216
- <form id="form-add-page" method="post">
4217
- <input type="text" name="page_name" placeholder="Page Name" required>
4218
- <textarea name="site_desc" placeholder="Website description"></textarea>
4219
- <div style="text-align:right;">
4220
- <button type="submit" name="action" value="add_page">Add Page</button>
4221
- </div>
4222
- </form>
4223
- </div>
4224
- """
4622
+ <div class="card span-12">
4623
+ <h4>Generate New Page</h4>
4624
+ <form id="add-page-form" method="post">
4625
+ <input type="hidden" name="action" value="add_page">
4626
+ <input type="text" name="page_name" placeholder="Page Name" required>
4627
+ <textarea name="site_desc" placeholder="Website description"></textarea>
4628
+ <div style="display:flex; align-items:center; justify-content:space-between; margin-top:0.35rem;">
4629
+ <label style="display:inline-flex; align-items:center; gap:0.4rem; font-size:0.8rem;">
4630
+ <input type="checkbox" name="show_in_nav" checked style="margin:0; width:auto;">
4631
+ <span>Show in nav</span>
4632
+ </label>
4633
+ <input
4634
+ type="text"
4635
+ name="nav_label"
4636
+ placeholder="Navigation label (optional)"
4637
+ style="font-size:0.8rem; padding:3px 6px; max-width:11rem;"
4638
+ >
4639
+ </div>
4640
+ <div style="text-align:right; margin-top:0.4rem;">
4641
+ <button id="add-page-btn" type="submit">Generate</button>
4642
+ </div>
4643
+ </form>
4644
+ </div>
4645
+ """
4225
4646
 
4226
4647
  manage_page_card = f"""
4227
- <div class="card span-3">
4648
+ <div class="card span-12">
4228
4649
  <h4>Manage Pages</h4>
4229
- <ul class="catalog-list">
4650
+ <ul id="pages-list" class="catalog-list">
4230
4651
  {pages_html or "<li>No page has been added yet.</li>"}
4231
4652
  </ul>
4232
4653
  </div>
4233
4654
  """
4234
4655
 
4656
+
4235
4657
  # ────────────────────────────────────────────────────────────────────────────────
4236
4658
  # USERS & ROLES
4237
4659
  # ────────────────────────────────────────────────────────────────────────────────
@@ -4529,10 +4951,33 @@ def setup_routes(smx):
4529
4951
  </section>
4530
4952
  """
4531
4953
 
4954
+ existing_secret_names = []
4955
+ try:
4956
+ existing_secret_names = db.list_secret_names()
4957
+ except Exception:
4958
+ existing_secret_names = []
4959
+
4960
+ pixabay_saved = False
4961
+ try:
4962
+ pixabay_saved = bool(db.get_secret("PIXABAY_API_KEY") or os.environ.get("PIXABAY_API_KEY"))
4963
+ except Exception:
4964
+ pixabay_saved = bool(os.environ.get("PIXABAY_API_KEY"))
4965
+
4966
+ secretes_link_card = f"""
4967
+ <div class="card span-4">
4968
+ <h4>Integrations (Secrets)</h4>
4969
+ <div style="font-size:.72rem;color:#555;margin-top:-6px;margin-bottom:10px;line-height:1.35;">
4970
+ Store secrete credentials.
4971
+ </div>
4972
+ <a href="{url_for('admin_secretes')}" class="btn">Manage secretes</a>
4973
+ </div>
4974
+ """
4975
+
4532
4976
  system_section = f"""
4533
4977
  <section id="system" class="section">
4534
4978
  <h2>System</h2>
4535
4979
  <div class="admin-grid">
4980
+ {secretes_link_card}
4536
4981
  {sys_files_card}
4537
4982
  {manage_sys_files_card}
4538
4983
  </div>
@@ -4561,15 +5006,47 @@ def setup_routes(smx):
4561
5006
 
4562
5007
  admin_shell = f"""{admin_layout_css}
4563
5008
  <div class="admin-shell">
4564
- {side_nav}
4565
- <div class="admin-main">
4566
- {models_section}
4567
- {pages_section}
4568
- {system_section}
4569
- {users_section}
4570
- {audits_section}
5009
+ <div id="adminSidebarScrim" class="admin-scrim" aria-hidden="true"></div>
5010
+ {side_nav}
5011
+ <div class="admin-main">
5012
+ <button id="adminSidebarToggle"
5013
+ class="admin-sidebar-toggle"
5014
+ aria-label="Open admin menu"></button>
5015
+ {models_section}
5016
+ {pages_section}
5017
+ {system_section}
5018
+ {users_section}
5019
+ {audits_section}
5020
+ </div>
4571
5021
  </div>
4572
- </div>
5022
+ <script>
5023
+ document.addEventListener('DOMContentLoaded', function () {{
5024
+ const sidebar = document.querySelector('.admin-sidenav');
5025
+ const toggle = document.getElementById('adminSidebarToggle');
5026
+ const scrim = document.getElementById('adminSidebarScrim');
5027
+
5028
+ function setOpen(open) {{
5029
+ if (!sidebar || !toggle) return;
5030
+ sidebar.classList.toggle('open', open);
5031
+ toggle.classList.toggle('is-open', open);
5032
+ toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
5033
+ document.body.classList.toggle('no-scroll', open);
5034
+ if (scrim) scrim.classList.toggle('show', open);
5035
+ }}
5036
+
5037
+ if (toggle) {{
5038
+ toggle.addEventListener('click', function () {{
5039
+ setOpen(!sidebar.classList.contains('open'));
5040
+ }});
5041
+ }}
5042
+
5043
+ if (scrim) {{
5044
+ scrim.addEventListener('click', function () {{
5045
+ setOpen(false);
5046
+ }});
5047
+ }}
5048
+ }});
5049
+ </script>
4573
5050
  """
4574
5051
 
4575
5052
  # ─────────────────────────────────────────────────────────
@@ -4605,16 +5082,19 @@ def setup_routes(smx):
4605
5082
  {delete_modal_block}
4606
5083
 
4607
5084
  <!-- Profiles helper scripts -->
4608
- <script>
4609
- /* Name suggestions popover */
4610
- const nameExamples = {{
4611
- 'Administration': 'Admin',
4612
- 'Chatting': 'Chat',
4613
- 'Coding': 'Coding',
4614
- 'Vision-to-text': 'Vision2Text',
4615
- 'Summarisation': 'Summarisation',
4616
- 'Classification': 'Classification',
4617
- }};
5085
+ <script>
5086
+ /* Name suggestions popover */
5087
+ const purpose_tags = {PURPOSE_TAGS}
5088
+ const nameExamples = {{}};
5089
+ const capitalize = (s) =>
5090
+ s.charAt(0).toUpperCase() + s.slice(1);
5091
+ for (let i = 0; i < purpose_tags.length; i++) {{
5092
+ purpose_tags[i] = capitalize(purpose_tags[i]);
5093
+ const tag = purpose_tags[i]
5094
+ const key = tag;
5095
+ nameExamples[key] = tag;
5096
+ }}
5097
+
4618
5098
  const txt = document.getElementById('profile_name');
4619
5099
  const infoBtn = document.getElementById('name-help');
4620
5100
  const popover = document.getElementById('name-suggestions');
@@ -4740,7 +5220,7 @@ def setup_routes(smx):
4740
5220
 
4741
5221
  if (form) {{
4742
5222
  form.addEventListener('submit', function () {{
4743
- if (btn) {{ btn.disabled = true; btn.textContent = 'Adding…'; }}
5223
+ if (btn) {{ btn.disabled = true; btn.textContent = 'Generating…'; }}
4744
5224
  if (overlay) overlay.style.display = 'flex';
4745
5225
  }});
4746
5226
  }}
@@ -4750,7 +5230,7 @@ def setup_routes(smx):
4750
5230
  const o = document.getElementById('loader-overlay');
4751
5231
  if (o) o.style.display = 'none';
4752
5232
  const b = document.getElementById('add-page-btn');
4753
- if (b) {{ b.disabled = false; b.textContent = 'Add Page'; }}
5233
+ if (b) {{ b.disabled = false; b.textContent = 'Generate'; }}
4754
5234
  }});
4755
5235
  }});
4756
5236
  </script>
@@ -4892,7 +5372,99 @@ def setup_routes(smx):
4892
5372
  if(e.target === backdrop) closeModal();
4893
5373
  }});
4894
5374
  }})();
4895
- </script>
5375
+ </script>
5376
+
5377
+ <script>
5378
+ // Drag & drop reordering for the "Manage Pages" list
5379
+ document.addEventListener('DOMContentLoaded', function () {{
5380
+ const list = document.querySelector('#pages .catalog-list');
5381
+ if (!list) return;
5382
+
5383
+ let draggingEl = null;
5384
+
5385
+ function getPageName(li) {{
5386
+ if (!li) return '';
5387
+ if (li.dataset.pageName) return li.dataset.pageName;
5388
+
5389
+ // Prefer an explicit hidden input if present
5390
+ const hidden = li.querySelector('input[name="page_name"]');
5391
+ if (hidden && hidden.value) return hidden.value.trim();
5392
+
5393
+ // Fallback: first span's text
5394
+ const span = li.querySelector('span');
5395
+ if (span && span.textContent) return span.textContent.trim();
5396
+
5397
+ return '';
5398
+ }}
5399
+
5400
+ // Set up draggable behaviour
5401
+ list.querySelectorAll('li.li-row').forEach(function (li) {{
5402
+ const name = getPageName(li);
5403
+ if (!name) return;
5404
+
5405
+ li.dataset.pageName = name;
5406
+ li.setAttribute('draggable', 'true');
5407
+
5408
+ li.addEventListener('dragstart', function (e) {{
5409
+ draggingEl = li;
5410
+ li.classList.add('dragging');
5411
+ if (e.dataTransfer) {{
5412
+ e.dataTransfer.effectAllowed = 'move';
5413
+ e.dataTransfer.setData('text/plain', name);
5414
+ }}
5415
+ }});
5416
+
5417
+ li.addEventListener('dragend', function () {{
5418
+ li.classList.remove('dragging');
5419
+ draggingEl = null;
5420
+
5421
+ // After drop, collect new order and POST it
5422
+ const items = Array.from(list.querySelectorAll('li.li-row'));
5423
+ const names = items
5424
+ .map(function (node) {{ return getPageName(node); }})
5425
+ .filter(Boolean);
5426
+
5427
+ if (!names.length) return;
5428
+
5429
+ const fd = new FormData();
5430
+ fd.append('action', 'reorder_pages');
5431
+ fd.append('page_order_csv', names.join(','));
5432
+
5433
+ fetch('/admin', {{
5434
+ method: 'POST',
5435
+ body: fd,
5436
+ credentials: 'same-origin'
5437
+ }})
5438
+ .then(function (res) {{
5439
+ if (!res.ok) {{
5440
+ console.error('Failed to save page order', res.status);
5441
+ }}
5442
+ // Reload so navbar + list reflect the new order
5443
+ window.location.reload();
5444
+ }})
5445
+ .catch(function (err) {{
5446
+ console.error('Error saving page order', err);
5447
+ }});
5448
+ }});
5449
+
5450
+ li.addEventListener('dragover', function (e) {{
5451
+ if (!draggingEl || draggingEl === li) return;
5452
+ e.preventDefault();
5453
+
5454
+ const rect = li.getBoundingClientRect();
5455
+ const offsetY = e.clientY - rect.top;
5456
+ const before = offsetY < (rect.height / 2);
5457
+
5458
+ if (before) {{
5459
+ list.insertBefore(draggingEl, li);
5460
+ }}else {{
5461
+ list.insertBefore(draggingEl, li.nextSibling);
5462
+ }}
5463
+ }});
5464
+ }});
5465
+ }});
5466
+ </script>
5467
+
4896
5468
  </body>
4897
5469
  </html>
4898
5470
  """,
@@ -4902,6 +5474,50 @@ def setup_routes(smx):
4902
5474
  profiles=profiles
4903
5475
  )
4904
5476
 
5477
+
5478
+ @smx.app.route("/admin/secretes", methods=["GET", "POST"])
5479
+ def admin_secretes():
5480
+ role = (session.get("role") or "").lower()
5481
+ if role not in ("admin", "superadmin"):
5482
+ return jsonify({"error": "forbidden"}), 403
5483
+
5484
+ if request.method == "POST":
5485
+ action = (request.form.get("action") or "").strip()
5486
+
5487
+ if action == "save_secret":
5488
+ name = (request.form.get("secret_name") or "").strip()
5489
+ value = (request.form.get("secret_value") or "").strip()
5490
+
5491
+ if not name:
5492
+ flash("Secret name is required.")
5493
+ return redirect(url_for("admin_secretes"))
5494
+
5495
+ # We don’t allow saving blank values accidentally.
5496
+ if not value:
5497
+ flash("Secret value is required.")
5498
+ return redirect(url_for("admin_secretes"))
5499
+
5500
+ db.set_secret(name, value)
5501
+ flash(f"Saved: {name.upper()} ✓")
5502
+ return redirect(url_for("admin_secretes"))
5503
+
5504
+ if action == "delete_secret":
5505
+ name = (request.form.get("secret_name") or "").strip()
5506
+ if name:
5507
+ db.delete_secret(name)
5508
+ flash(f"Deleted: {name.upper()}")
5509
+ return redirect(url_for("admin_secretes"))
5510
+
5511
+ # GET
5512
+ names = []
5513
+ try:
5514
+ names = db.list_secret_names()
5515
+ except Exception:
5516
+ names = []
5517
+
5518
+ return render_template("admin_secretes.html", secret_names=names)
5519
+
5520
+
4905
5521
  @smx.app.route("/admin/delete.json", methods=["POST"])
4906
5522
  def admin_delete_universal():
4907
5523
 
@@ -5083,47 +5699,56 @@ def setup_routes(smx):
5083
5699
  smx.warning(f"/admin/delete.json error: {e}")
5084
5700
  return jsonify(ok=False, error=str(e)), 500
5085
5701
 
5086
- # Override the generic page renderer to inject a gallery on the "service" page
5702
+
5087
5703
  @smx.app.route('/page/<page_name>')
5088
5704
  def view_page(page_name):
5089
- smx.page = page_name.lower()
5090
- nav_html = _generate_nav()
5091
- content = smx.pages.get(page_name, f"No content found for page '{page_name}'.")
5092
-
5093
- # only on the service page, build a gallery
5094
- media_html = ''
5095
- if page_name.lower() == 'service':
5096
- media_folder = os.path.join(_CLIENT_DIR, 'uploads', 'media')
5097
- if os.path.isdir(media_folder):
5098
- files = sorted(os.listdir(media_folder))
5099
- # wrap each file in an <img> tag (you can special‑case videos if you like)
5100
- thumbs = []
5101
- for fn in files:
5102
- src = url_for('serve_media', filename=fn)
5103
- thumbs.append(f'<img src="{src}" alt="{fn}" style="max-width:150px; margin:5px;"/>')
5104
- if thumbs:
5105
- media_html = f'''
5106
- <section id="media-gallery" style="margin-top:20px;">
5107
- <h3>Media Gallery</h3>
5108
- <div style="display:flex; flex-wrap:wrap; gap:10px;">
5109
- {''.join(thumbs)}
5110
- </div>
5111
- </section>
5112
- '''
5705
+ hero_fix_css = """
5706
+ <style>
5707
+ div[id^="smx-page-"] .hero-overlay{
5708
+ background:linear-gradient(90deg,
5709
+ rgba(2,6,23,.62) 0%,
5710
+ rgba(2,6,23,.40) 42%,
5711
+ rgba(2,6,23,.14) 72%,
5712
+ rgba(2,6,23,.02) 100%
5713
+ ) !important;
5714
+ }
5715
+ @media (max-width: 860px){
5716
+ div[id^="smx-page-"] .hero-overlay{
5717
+ background:linear-gradient(180deg,
5718
+ rgba(2,6,23,.16) 0%,
5719
+ rgba(2,6,23,.55) 70%,
5720
+ rgba(2,6,23,.70) 100%
5721
+ ) !important;
5722
+ }
5723
+ }
5724
+ div[id^="smx-page-"] .hero-panel{
5725
+ background:rgba(2,6,23,.24) !important;
5726
+ backdrop-filter: blur(4px) !important;
5727
+ -webkit-backdrop-filter: blur(4px) !important;
5728
+ }
5729
+ </style>
5730
+ """
5113
5731
 
5114
- view_page_html = f"""
5115
- {head_html()}
5116
- {nav_html}
5117
- <div style=" width:100%; box-sizing:border-box; padding-top:5px;">
5118
- <div style="text-align:center; border:1px solid #ccc;
5119
- border-radius:8px; background-color:#f9f9f9;">
5120
- <div>{content}</div>
5121
- {media_html}
5122
- </div>
5123
- </div>
5124
- {footer_html()}
5125
- """
5126
- return Response(view_page_html, mimetype="text/html")
5732
+ smx.page = page_name.lower()
5733
+ nav_html = _generate_nav()
5734
+ # Always fetch the latest HTML from disk/DB (prevents stale cache across workers)
5735
+ content = db.get_page_html(page_name)
5736
+ if content is None:
5737
+ content = smx.pages.get(page_name, f"No content found for page '{page_name}'.")
5738
+
5739
+ view_page_html = f"""
5740
+ {head_html()}
5741
+ {nav_html}
5742
+ <main style="padding-top:calc(52px + env(safe-area-inset-top)); width:100%; box-sizing:border-box;">
5743
+ {content}
5744
+ </main>
5745
+ {hero_fix_css}
5746
+ {footer_html()}
5747
+ """
5748
+ resp = Response(view_page_html, mimetype="text/html")
5749
+ # Prevent the browser/proxies from keeping an old copy during active editing/publishing
5750
+ resp.headers["Cache-Control"] = "no-store"
5751
+ return resp
5127
5752
 
5128
5753
 
5129
5754
  @smx.app.route('/docs')
@@ -5183,7 +5808,6 @@ def setup_routes(smx):
5183
5808
  html += "</table>"
5184
5809
  return html
5185
5810
 
5186
-
5187
5811
  @smx.app.route("/admin/chunks/edit/<int:chunk_id>", methods=["GET", "POST"])
5188
5812
  def edit_chunk(chunk_id):
5189
5813
  if request.method == "POST":
@@ -5218,78 +5842,212 @@ def setup_routes(smx):
5218
5842
  def edit_page(page_name):
5219
5843
  if request.method == "POST":
5220
5844
  new_page_name = request.form.get("page_name", "").strip()
5221
- new_content = request.form.get("page_content", "").strip()
5845
+ # Keep page_content formatting exactly as typed
5846
+ new_content = request.form.get("page_content", "")
5847
+
5222
5848
  if page_name in smx.pages and new_page_name:
5223
5849
  db.update_page(page_name, new_page_name, new_content)
5850
+ smx.pages = db.get_pages()
5224
5851
  return redirect(url_for("admin_panel"))
5225
- # Load the full content for the page to be edited.
5226
- content = smx.pages.get(page_name, "")
5227
- return render_template_string("""
5228
- <!DOCTYPE html>
5229
- <html>
5230
- <head>
5231
- <meta charset="UTF-8">
5232
- <title>Edit Page - {{ page_name }}</title>
5233
- <style>
5234
- body {
5235
- background: #f4f7f9;
5236
- padding: 20px;
5237
- }
5238
- .editor {
5239
- max-width: 800px;
5240
- margin: 0 auto;
5241
- background: #fff;
5242
- padding: 20px;
5243
- border-radius: 8px;
5244
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
5245
- }
5246
- input, textarea {
5247
- width: 100%;
5248
- margin: 10px 0;
5249
- padding: 10px;
5250
- border: 1px solid #ccc;
5251
- border-radius: 4px;
5252
- }
5253
- button {
5254
- padding: 10px 20px;
5255
- background: #007acc;
5256
- border: none;
5257
- color: #fff;
5258
- border-radius: 4px;
5259
- cursor: pointer;
5260
- }
5261
- button:hover {
5262
- background: #005fa3;
5263
- }
5264
- a.button {
5265
- padding: 10px 20px;
5266
- background: #aaa;
5267
- border: none;
5268
- color: #fff;
5269
- border-radius: 4px;
5270
- text-decoration: none;
5271
- }
5272
- a.button:hover {
5273
- background: #888;
5274
- }
5275
- </style>
5276
- </head>
5277
- <body>
5278
- <div class="editor">
5279
- <h1>Edit Page - {{ page_name }}</h1>
5280
- <form method="post">
5281
- <input type="text" name="page_name" value="{{ page_name }}" required>
5282
- <textarea name="page_content" rows="20">{{ content }}</textarea>
5283
- <div style="margin-top:15px;">
5284
- <button type="submit">Update Page</button>
5285
- <a class="button" href="{{ url_for('admin_panel') }}">Cancel</a>
5286
- </div>
5287
- </form>
5288
- </div>
5289
- </body>
5290
- </html>
5291
- """, page_name=page_name, content=content)
5852
+
5853
+ content = db.get_page_html(page_name) or ""
5854
+
5855
+ # NEW: builder layout json (stored separately)
5856
+ layout_row = getattr(db, "get_page_layout", None)
5857
+ layout_json = None
5858
+ if callable(layout_row):
5859
+ try:
5860
+ row = db.get_page_layout(page_name)
5861
+ layout_json = (row or {}).get("layout_json")
5862
+ except Exception:
5863
+ layout_json = None
5864
+ published_as = request.args.get("published_as")
5865
+ return render_template(
5866
+ "edit_page.html",
5867
+ page_name=page_name,
5868
+ content=content,
5869
+ layout_json=layout_json,
5870
+ published_as=published_as,
5871
+ )
5292
5872
 
5873
+
5874
+ # ────────────────────────────────────────────────────
5875
+ # PIXABAY
5876
+ # ────────────────────────────────────────────────────
5877
+ @smx.app.route("/admin/pixabay/search.json", methods=["GET"])
5878
+ def admin_pixabay_search():
5879
+ role = (session.get("role") or "").lower()
5880
+ if role not in ("admin", "superadmin"):
5881
+ return jsonify({"error": "forbidden"}), 403
5882
+
5883
+ q = (request.args.get("q") or "").strip()
5884
+ orientation = (request.args.get("orientation") or "horizontal").strip().lower()
5885
+ image_type = (request.args.get("image_type") or "photo").strip().lower()
5886
+
5887
+ api_key = None
5888
+ try:
5889
+ api_key = db.get_secret("PIXABAY_API_KEY")
5890
+ except Exception:
5891
+ api_key = None
5892
+
5893
+ if not api_key:
5894
+ return jsonify({"error": "Missing PIXABAY_API_KEY. Add it in Admin → Manage secretes."}), 400
5895
+
5896
+ try:
5897
+ from syntaxmatrix.media.media_pixabay import pixabay_search
5898
+ hits = pixabay_search(
5899
+ api_key=api_key,
5900
+ query=q,
5901
+ image_type=image_type,
5902
+ orientation=orientation,
5903
+ per_page=24,
5904
+ safesearch=True,
5905
+ editors_choice=False,
5906
+ min_width=960,
5907
+ )
5908
+ payload = []
5909
+ for h in hits:
5910
+ payload.append({
5911
+ "id": h.id,
5912
+ "page_url": h.page_url,
5913
+ "preview_url": h.preview_url,
5914
+ "large_image_url": h.large_image_url,
5915
+ "webformat_url": h.webformat_url,
5916
+ "width": h.width,
5917
+ "height": h.height,
5918
+ "tags": h.tags,
5919
+ "user": h.user,
5920
+ "type": h.image_type
5921
+ })
5922
+ return jsonify({"items": payload}), 200
5923
+
5924
+ except Exception as e:
5925
+ return jsonify({"error": str(e)}), 500
5926
+
5927
+ @smx.app.route("/admin/pixabay/import.json", methods=["POST"])
5928
+ def admin_pixabay_import():
5929
+ role = (session.get("role") or "").lower()
5930
+ if role not in ("admin", "superadmin"):
5931
+ return jsonify({"error": "forbidden"}), 403
5932
+
5933
+ api_key = None
5934
+ try:
5935
+ api_key = db.get_secret("PIXABAY_API_KEY")
5936
+ except Exception:
5937
+ api_key = None
5938
+
5939
+ if not api_key:
5940
+ return jsonify({"error": "Missing PIXABAY_API_KEY. Add it in Admin → Manage secretes."}), 400
5941
+
5942
+ payload = request.get_json(force=True) or {}
5943
+ pixabay_id = payload.get("id")
5944
+ if not pixabay_id:
5945
+ return jsonify({"error": "Missing id"}), 400
5946
+
5947
+ min_width = int(payload.get("min_width") or 0)
5948
+ min_width = max(0, min(3000, min_width))
5949
+
5950
+ try:
5951
+ import requests
5952
+ from syntaxmatrix.media.media_pixabay import PixabayHit, import_pixabay_hit
5953
+
5954
+ # Look up the hit by ID from Pixabay API (prevents client tampering)
5955
+ r = requests.get(
5956
+ "https://pixabay.com/api/",
5957
+ params={"key": api_key, "id": str(pixabay_id)},
5958
+ timeout=15
5959
+ )
5960
+ r.raise_for_status()
5961
+ data = r.json() or {}
5962
+ hits = data.get("hits") or []
5963
+ if not hits:
5964
+ return jsonify({"error": "Pixabay image not found"}), 404
5965
+
5966
+ h = hits[0]
5967
+ hit = PixabayHit(
5968
+ id=int(h.get("id")),
5969
+ page_url=str(h.get("pageURL") or ""),
5970
+ tags=str(h.get("tags") or ""),
5971
+ user=str(h.get("user") or ""),
5972
+ preview_url=str(h.get("previewURL") or ""),
5973
+ webformat_url=str(h.get("webformatURL") or ""),
5974
+ large_image_url=str(h.get("largeImageURL") or ""),
5975
+ width=int(h.get("imageWidth") or 0),
5976
+ height=int(h.get("imageHeight") or 0),
5977
+ image_type=str(h.get("type") or "photo"),
5978
+ )
5979
+
5980
+ # Paths
5981
+ media_dir = os.path.join(_CLIENT_DIR, "uploads", "media")
5982
+ imported_dir = os.path.join(media_dir, "images", "imported")
5983
+ thumbs_dir = os.path.join(media_dir, "images", "thumbs")
5984
+ os.makedirs(imported_dir, exist_ok=True)
5985
+ os.makedirs(thumbs_dir, exist_ok=True)
5986
+
5987
+ # Download-once guard: if already imported, reuse local file
5988
+ existing_jpg = os.path.join(imported_dir, f"pixabay-{hit.id}.jpg")
5989
+ existing_png = os.path.join(imported_dir, f"pixabay-{hit.id}.png")
5990
+
5991
+ if os.path.exists(existing_jpg) or os.path.exists(existing_png):
5992
+ existing_abs = existing_png if os.path.exists(existing_png) else existing_jpg
5993
+ rel_path = os.path.relpath(existing_abs, media_dir).replace("\\", "/")
5994
+ return jsonify({
5995
+ "rel_path": rel_path,
5996
+ "url": url_for("serve_media", filename=rel_path),
5997
+ "thumb_url": None,
5998
+ "source_url": hit.page_url,
5999
+ "author": hit.user,
6000
+ "tags": hit.tags,
6001
+ }), 200
6002
+
6003
+ meta = import_pixabay_hit(
6004
+ hit,
6005
+ media_images_dir=imported_dir,
6006
+ thumbs_dir=thumbs_dir,
6007
+ max_width=1920,
6008
+ thumb_width=800,
6009
+ min_width=min_width
6010
+ )
6011
+
6012
+ # Convert absolute paths to rel paths + URLs
6013
+ rel_path = os.path.relpath(meta["file_path"], media_dir).replace("\\", "/")
6014
+ thumb_rel = None
6015
+ if meta.get("thumb_path"):
6016
+ thumb_rel = os.path.relpath(meta["thumb_path"], media_dir).replace("\\", "/")
6017
+
6018
+ # Register in DB (for local-first & Media sources)
6019
+ try:
6020
+ db.upsert_media_asset(
6021
+ rel_path=rel_path,
6022
+ kind="image",
6023
+ thumb_path=thumb_rel,
6024
+ sha256=meta.get("sha256"),
6025
+ dhash=meta.get("dhash"),
6026
+ width=int(meta.get("width") or 0),
6027
+ height=int(meta.get("height") or 0),
6028
+ mime=meta.get("mime"),
6029
+ source="pixabay",
6030
+ source_url=meta.get("source_url"),
6031
+ author=meta.get("author"),
6032
+ licence="Pixabay Content Licence",
6033
+ tags=meta.get("tags"),
6034
+ )
6035
+ except Exception:
6036
+ pass
6037
+
6038
+ return jsonify({
6039
+ "rel_path": rel_path,
6040
+ "url": url_for("serve_media", filename=rel_path),
6041
+ "thumb_url": url_for("serve_media", filename=thumb_rel) if thumb_rel else None,
6042
+ "source_url": meta.get("source_url"),
6043
+ "author": meta.get("author"),
6044
+ "tags": meta.get("tags"),
6045
+ }), 200
6046
+
6047
+ except Exception as e:
6048
+ return jsonify({"error": str(e)}), 500
6049
+
6050
+
5293
6051
  # ────────────────────────────────────────────────────
5294
6052
  # ACCOUNTS
5295
6053
  # ────────────────────────────────────────────────────
@@ -5429,20 +6187,227 @@ def setup_routes(smx):
5429
6187
  return any(r in ("admin", "superadmin") for r in roles if r)
5430
6188
  return dict(can_see_admin=can_see_admin)
5431
6189
 
6190
+
6191
+ def _is_admin_request() -> bool:
6192
+ r = (session.get("role") or "").lower()
6193
+ if r in ("admin", "superadmin"):
6194
+ return True
6195
+
6196
+ # Fallback to Flask-Login user roles (matches your inject_role_helpers logic)
6197
+ if not getattr(current_user, "is_authenticated", False):
6198
+ return False
6199
+
6200
+ roles = getattr(current_user, "roles", None)
6201
+ if roles is None:
6202
+ rr = getattr(current_user, "role", None)
6203
+ roles = [rr] if rr else []
6204
+
6205
+ return any((str(x or "")).lower() in ("admin", "superadmin") for x in roles)
6206
+
6207
+
6208
+ @smx.app.route("/admin/page_layouts/<page_name>", methods=["GET", "POST"])
6209
+ def page_layouts_api(page_name):
6210
+ if not _is_admin_request():
6211
+ return jsonify({"error": "forbidden"}), 403
6212
+
6213
+ if request.method == "GET":
6214
+ try:
6215
+ row = db.get_page_layout(page_name) or {}
6216
+ return jsonify(row), 200
6217
+ except Exception as e:
6218
+ return jsonify({"error": str(e)}), 500
6219
+
6220
+ # POST: save layout json
6221
+ from syntaxmatrix.page_layout_contract import normalise_layout, validate_layout
6222
+ payload = request.get_json(force=True) or {}
6223
+ payload = normalise_layout(payload, mode="draft")
6224
+
6225
+ issues = validate_layout(payload)
6226
+ errors = [i.to_dict() for i in issues if i.level == "error"]
6227
+ warnings = [i.to_dict() for i in issues if i.level == "warning"]
6228
+
6229
+ if errors:
6230
+ return jsonify({"error": "invalid layout", "issues": errors, "warnings": warnings}), 400
6231
+
6232
+ layout_json = json.dumps(payload, ensure_ascii=False)
6233
+ db.upsert_page_layout(page_name, layout_json)
6234
+ return jsonify({"ok": True, "warnings": warnings}), 200
6235
+
6236
+
6237
+ @smx.app.route("/admin/page_layouts/<page_name>/publish", methods=["POST"])
6238
+ def publish_layout_patch_only(page_name):
6239
+
6240
+ role = (session.get("role") or "").lower()
6241
+ if role not in ("admin", "superadmin"):
6242
+ return jsonify({"error": "forbidden"}), 403
6243
+
6244
+ try:
6245
+ # Load layout (prefer request body; fallback to DB)
6246
+ payload = request.get_json(silent=True) or {}
6247
+ if not (isinstance(payload, dict) and isinstance(payload.get("sections"), list)):
6248
+ row = db.get_page_layout(page_name) or {}
6249
+ raw = (row or {}).get("layout_json") or ""
6250
+ payload = json.loads(raw) if raw else {}
6251
+
6252
+ if not (isinstance(payload, dict) and isinstance(payload.get("sections"), list)):
6253
+ return jsonify({"error": "no layout to publish"}), 400
6254
+
6255
+ # Always patch the latest HTML on disk/DB (avoids stale smx.pages in other workers)
6256
+ existing_html = db.get_page_html(page_name) or ""
6257
+ if not existing_html:
6258
+ # Fallback only (older behaviour)
6259
+ if not isinstance(smx.pages, dict):
6260
+ smx.pages = db.get_pages()
6261
+ page_key = (page_name or "").strip()
6262
+ existing_html = smx.pages.get(page_key) or smx.pages.get(page_key.lower()) or ""
6263
+
6264
+ # Keep a copy of what was originally stored so we can correctly detect changes
6265
+ original_html = existing_html
6266
+
6267
+ # NEW: ensure any newly-added layout sections exist in the stored HTML
6268
+ # so validate_compiled_html won't reject the publish.
6269
+ existing_html, inserted_sections = ensure_sections_exist(
6270
+ existing_html,
6271
+ payload,
6272
+ page_slug=page_name
6273
+ )
6274
+
6275
+ if not existing_html:
6276
+ return jsonify({"error": "page html not found"}), 404
6277
+
6278
+ payload = normalise_layout(payload, mode="prod")
6279
+
6280
+ issues = validate_layout(payload)
6281
+ errors = [i.to_dict() for i in issues if i.level == "error"]
6282
+ warnings = [i.to_dict() for i in issues if i.level == "warning"]
6283
+
6284
+ if errors:
6285
+ return jsonify({"error": "invalid layout", "issues": errors, "warnings": warnings}), 400
6286
+
6287
+ # Optional but very useful: validate current HTML has the anchors we need
6288
+ html_issues = validate_compiled_html(existing_html, payload)
6289
+ html_errors = [i.to_dict() for i in html_issues if i.level == "error"]
6290
+ html_warnings = [i.to_dict() for i in html_issues if i.level == "warning"]
6291
+
6292
+ if html_errors:
6293
+ return jsonify({"error": "html not compatible with patching", "issues": html_errors, "warnings": html_warnings}), 400
6294
+
6295
+ updated_html, stats = patch_page_publish(existing_html, payload, page_slug=page_name)
6296
+
6297
+ # If nothing changed, still return ok
6298
+ if updated_html == original_html:
6299
+ return jsonify({"ok": True, "mode": "noop", "stats": stats}), 200
6300
+
6301
+ # Persist patched HTML
6302
+ db.update_page(page_name, page_name, updated_html)
6303
+ smx.pages = db.get_pages()
6304
+
6305
+ return jsonify({"ok": True, "mode": "patched", "stats": stats}), 200
6306
+
6307
+ except Exception as e:
6308
+ smx.warning(f"publish_layout_patch_only error: {e}")
6309
+ return jsonify({"error": str(e)}), 500
6310
+
6311
+
6312
+ @smx.app.route("/admin/page_layouts/<page_name>/compile", methods=["POST"])
6313
+ def compile_page_layout(page_name):
6314
+ role = (session.get("role") or "").lower()
6315
+ if role not in ("admin", "superadmin"):
6316
+ return jsonify({"error": "forbidden"}), 403
6317
+
6318
+ try:
6319
+ payload = request.get_json(force=True) or {}
6320
+ html_doc = compile_layout_to_html(payload, page_slug=page_name)
6321
+ return jsonify({"html": html_doc}), 200
6322
+ except Exception as e:
6323
+ return jsonify({"error": str(e)}), 500
6324
+
6325
+
6326
+ @smx.app.route("/admin/media/list.json", methods=["GET"])
6327
+ def list_media_json():
6328
+ role = (session.get("role") or "").lower()
6329
+ if role not in ("admin", "superadmin"):
6330
+ return jsonify({"error": "forbidden"}), 403
6331
+
6332
+ media_dir = os.path.join(_CLIENT_DIR, "uploads", "media")
6333
+ items = []
6334
+ img_ext = {".png", ".jpg", ".jpeg", ".webp", ".gif"}
6335
+ vid_ext = {".mp4", ".webm", ".mov", ".m4v"}
6336
+
6337
+ for root, _, files in os.walk(media_dir):
6338
+ for fn in files:
6339
+ abs_path = os.path.join(root, fn)
6340
+ rel = os.path.relpath(abs_path, media_dir).replace("\\", "/")
6341
+ ext = os.path.splitext(fn.lower())[1]
6342
+ kind = "other"
6343
+ if ext in img_ext:
6344
+ kind = "image"
6345
+ elif ext in vid_ext:
6346
+ kind = "video"
6347
+ items.append({
6348
+ "name": fn,
6349
+ "path": rel,
6350
+ "url": url_for("serve_media", filename=rel),
6351
+ "kind": kind
6352
+ })
6353
+
6354
+ items.sort(key=lambda x: x["path"])
6355
+ return jsonify({"items": items}), 200
6356
+
6357
+ # # Example usage in your existing routes
6358
+ # @smx.app.route("/admin/generate_image", methods=["POST"])
6359
+ # def generate_image_route():
6360
+ # prompt = request.json.get("prompt", "").strip()
6361
+ # kind = request.json.get("kind", "image")
6362
+ # count = int(request.json.get("count", 1))
6363
+ # out_dir = os.path.join(MEDIA_IMAGES_GENERATED_ICONS if kind == "icon" else MEDIA_IMAGES_GENERATED)
6364
+
6365
+ # if not prompt:
6366
+ # return jsonify({"error": "Missing prompt"}), 400
6367
+
6368
+ # vision_profile = smx.get_image_generator_profile()
6369
+
6370
+ # # Call the agent's generate_image function
6371
+ # try:
6372
+ # result = image_generator_agent(prompt, vision_profile, out_dir, count)
6373
+ # return jsonify({"items": result}), 200
6374
+ # except Exception as e:
6375
+ # return jsonify({"error": f"Image generation failed: {str(e)}"}), 500
6376
+
6377
+
5432
6378
  # --- UPLOAD MEDIA --------------------------------------
5433
6379
  @smx.app.route("/admin/upload_media", methods=["POST"])
5434
- def upload_media():
5435
- # Retrieve uploaded media files (images, videos, etc.).
6380
+ def upload_media():
5436
6381
  uploaded_files = request.files.getlist("media_files")
5437
6382
  file_paths = []
6383
+
5438
6384
  for file in uploaded_files:
5439
- if file.filename:
5440
- filepath = os.path.join(MEDIA_FOLDER, file.filename)
6385
+ if not file or not file.filename:
6386
+ continue
6387
+
6388
+ fn = file.filename
6389
+ ext = os.path.splitext(fn.lower())[1]
6390
+ img_ext = {".png", ".jpg", ".jpeg", ".webp", ".gif"}
6391
+ vid_ext = {".mp4", ".webm", ".mov", ".m4v"}
6392
+
6393
+ if ext in img_ext:
6394
+ filepath = os.path.join(MEDIA_IMAGES_UPLOADED, fn)
5441
6395
  file.save(filepath)
5442
- # This path can be copied by the developer. Adjust if you have a web server serving these files.
5443
- file_paths.append(f"/uploads/media/{file.filename}")
6396
+ rel = os.path.relpath(filepath, MEDIA_FOLDER).replace("\\", "/")
6397
+ file_paths.append(f"/uploads/media/{rel}")
6398
+ elif ext in vid_ext:
6399
+ filepath = os.path.join(MEDIA_VIDEOS_UPLOADED, fn)
6400
+ file.save(filepath)
6401
+ rel = os.path.relpath(filepath, MEDIA_FOLDER).replace("\\", "/")
6402
+ file_paths.append(f"/uploads/media/{rel}")
6403
+ else:
6404
+ filepath = os.path.join(MEDIA_FOLDER, fn)
6405
+ file.save(filepath)
6406
+ file_paths.append(f"/uploads/media/{fn}")
6407
+
5444
6408
  return jsonify({"file_paths": file_paths})
5445
6409
 
6410
+
5446
6411
  # Serve the raw media files
5447
6412
  @smx.app.route('/uploads/media/<path:filename>')
5448
6413
  def serve_media(filename):
@@ -5460,104 +6425,7 @@ def setup_routes(smx):
5460
6425
 
5461
6426
  max_rows = 5000
5462
6427
  max_cols = 80
5463
-
5464
- def _smx_repair_python_cell(py_code: str) -> str:
5465
-
5466
- _CELL_REPAIR_RULES = """
5467
- You are an experienced Python code reviewer
5468
- Fix the Python cell to satisfy:
5469
- - Single valid cell; imports at the top.
5470
- - Do not import or invoke or use 'python-dotenv' or 'dotenv' because it's not needed.
5471
- - No top-level statements between if/elif/else branches.
5472
- - Regression must use either sklearn with train_test_split (then X_test exists) and R^2/MAE/RMSE,
5473
- or statsmodels OLS. No accuracy_score in regression.
5474
- - Keep all plotting + savefig + BytesIO + display inside the branch that created the figure.
5475
- - Return ONLY the corrected cell.
5476
- """
5477
- code = textwrap.dedent(py_code or "").strip()
5478
- needs_fix = False
5479
- if re.search(r"\baccuracy_score\b", code) and re.search(r"\bLinearRegression\b|\bOLS\b", code):
5480
- needs_fix = True
5481
- if re.search(r"\bX_test\b", code) and not re.search(r"\bX_test\s*=", code):
5482
- needs_fix = True
5483
- try:
5484
- ast.parse(code)
5485
- except SyntaxError:
5486
- needs_fix = True
5487
- if not needs_fix:
5488
- return code
5489
-
5490
- _prompt = f"```python\n{code}\n```"
5491
-
5492
- prof = _prof.get_profile("classification") or _prof.get_profile("admin")
5493
- if not prof:
5494
- return code
5495
-
5496
- prof["client"] = _prof.get_client(prof)
5497
- _client = prof["client"]
5498
- _model = prof["model"]
5499
- _provider = prof["provider"].lower()
5500
-
5501
- #1 Google
5502
- if _provider == "google":
5503
- from google.genai import types
5504
-
5505
- fixed = _client.models.generate_content(
5506
- model=_model,
5507
- contents=_prompt,
5508
- config=types.GenerateContentConfig(
5509
- system_instruction=_CELL_REPAIR_RULES,
5510
- temperature=0.8,
5511
- max_output_tokens=1024,
5512
- ),
5513
- )
5514
-
5515
- #2 Openai
5516
- elif _provider == "openai" and _model in GPT_MODELS_LATEST:
5517
-
5518
- args = set_args(
5519
- model=_model,
5520
- instructions=_CELL_REPAIR_RULES,
5521
- input=[{"role": "user", "content": _prompt}],
5522
- previous_id=None,
5523
- store=False,
5524
- reasoning_effort="medium",
5525
- verbosity="medium",
5526
- )
5527
- fixed = _out(_client.responses.create(**args))
5528
-
5529
- # Anthropic
5530
- elif _provider == "anthropic":
5531
-
5532
- fixed = _client.messages.create(
5533
- model=_model,
5534
- max_tokens=1024,
5535
- system=_CELL_REPAIR_RULES,
5536
- messages=[{"role": "user", "content":_prompt}],
5537
- stream=False,
5538
- )
5539
-
5540
- # OpenAI SDK
5541
- else:
5542
- fixed = _client.chat.completions.create(
5543
- model=_model,
5544
- messages=[
5545
- {"role": "system", "content":_CELL_REPAIR_RULES},
5546
- {"role": "user", "content":_prompt},
5547
- ],
5548
- max_tokens=1024,
5549
- )
5550
-
5551
- fixed_txt = clean_llm_code(fixed)
5552
-
5553
- try:
5554
- # Only accept the repaired cell if it's valid Python
5555
- ast.parse(fixed_txt)
5556
- return fixed_txt
5557
- except Exception:
5558
- # If the repaired version is still broken, fall back to the original code
5559
- return code
5560
-
6428
+
5561
6429
  section = request.args.get("section", "explore")
5562
6430
  datasets = [f for f in os.listdir(DATA_FOLDER) if f.lower().endswith(".csv")]
5563
6431
  selected_dataset = request.form.get("dataset") or request.args.get("dataset")
@@ -5594,6 +6462,8 @@ def setup_routes(smx):
5594
6462
  eda_df = df
5595
6463
  llm_usage = None
5596
6464
 
6465
+ TOKENS = {}
6466
+
5597
6467
  if request.method == "POST" and "askai_question" in request.form:
5598
6468
  askai_question = request.form["askai_question"].strip()
5599
6469
  if df is not None:
@@ -5609,10 +6479,60 @@ def setup_routes(smx):
5609
6479
  columns_summary = ", ".join(df.columns.tolist())
5610
6480
  dataset_context = f"columns: {columns_summary}"
5611
6481
  dataset_profile = f"modality: tabular; columns: {columns_summary}"
5612
-
5613
- refined_question = refine_question_agent(askai_question, dataset_context)
5614
- tags = []
5615
- if refined_question.lower() == "incompatible" or refined_question.lower() == "mismatch":
6482
+
6483
+ # ai_code = smx.ai_generate_code(refined_question, tags, df)
6484
+ # llm_usage = smx.get_last_llm_usage()
6485
+ # ai_code = auto_inject_template(ai_code, tags, df)
6486
+
6487
+ # # --- 1) Strip dotenv ASAP (kill imports, %magics, !pip) ---
6488
+ # ctx = {
6489
+ # "question": refined_question,
6490
+ # "df_columns": list(df.columns),
6491
+ # }
6492
+ # ai_code = ToolRunner(EARLY_SANITIZERS).run(ai_code, ctx) # dotenv first
6493
+
6494
+ # # --- 2) Domain/Plotting patches ---
6495
+ # ai_code = fix_scatter_and_summary(ai_code)
6496
+ # ai_code = fix_importance_groupby(ai_code)
6497
+ # ai_code = inject_auto_preprocessing(ai_code)
6498
+ # ai_code = patch_plot_code(ai_code, df, refined_question)
6499
+ # ai_code = ensure_matplotlib_title(ai_code)
6500
+ # ai_code = patch_pie_chart(ai_code, df, refined_question)
6501
+ # ai_code = patch_pairplot(ai_code, df)
6502
+ # ai_code = fix_seaborn_boxplot_nameerror(ai_code)
6503
+ # ai_code = fix_seaborn_barplot_nameerror(ai_code)
6504
+ # ai_code = get_plotting_imports(ai_code)
6505
+ # ai_code = patch_prefix_seaborn_calls(ai_code)
6506
+ # ai_code = patch_fix_sentinel_plot_calls(ai_code)
6507
+ # ai_code = patch_ensure_seaborn_import(ai_code)
6508
+ # ai_code = patch_rmse_calls(ai_code)
6509
+ # ai_code = patch_fix_seaborn_palette_calls(ai_code)
6510
+ # ai_code = patch_quiet_specific_warnings(ai_code)
6511
+ # ai_code = clean_llm_code(ai_code)
6512
+ # ai_code = ensure_image_output(ai_code)
6513
+ # ai_code = ensure_accuracy_block(ai_code)
6514
+ # ai_code = ensure_output(ai_code)
6515
+ # ai_code = fix_plain_prints(ai_code)
6516
+ # ai_code = fix_print_html(ai_code)
6517
+ # ai_code = fix_to_datetime_errors(ai_code)
6518
+
6519
+ # # --- 3-4) Global syntax/data fixers (must run AFTER patches, BEFORE final repair) ---
6520
+ # ai_code = ToolRunner(SYNTAX_AND_REPAIR).run(ai_code, ctx)
6521
+
6522
+ # # # --- 4) Final catch-all repair (run LAST) ---
6523
+ # ai_code = smx.repair_python_cell(ai_code)
6524
+ # ai_code = harden_ai_code(ai_code)
6525
+ # ai_code = drop_bad_classification_metrics(ai_code, df)
6526
+ # ai_code = patch_fix_sentinel_plot_calls(ai_code)
6527
+
6528
+ from syntaxmatrix.agentic import agents_orchestrer
6529
+ orch = agents_orchestrer.OrchestrateMLSystem(askai_question, cleaned_path)
6530
+ result = orch.operator_agent()
6531
+
6532
+ refined_question = result["specs_cot"]
6533
+
6534
+ compatibility = context_compatibility(askai_question, dataset_context)
6535
+ if compatibility.lower() == "incompatible" or compatibility.lower() == "mismatch":
5616
6536
  return ("""
5617
6537
  <div style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center;">
5618
6538
  <h1 style="margin: 0 0 10px 0;">Oops: Context mismatch</h1>
@@ -5624,51 +6544,30 @@ def setup_routes(smx):
5624
6544
  else:
5625
6545
  tags = classify_ml_job_agent(refined_question, dataset_profile)
5626
6546
 
5627
- ai_code = smx.ai_generate_code(refined_question, tags, df)
5628
- llm_usage = smx.get_last_llm_usage()
5629
- ai_code = auto_inject_template(ai_code, tags, df)
5630
-
5631
- # --- 1) Strip dotenv ASAP (kill imports, %magics, !pip) ---
5632
- ctx = {
5633
- "question": refined_question,
5634
- "df_columns": list(df.columns),
5635
- }
5636
- ai_code = ToolRunner(EARLY_SANITIZERS).run(ai_code, ctx) # dotenv first
5637
-
5638
- # --- 2) Domain/Plotting patches ---
5639
- ai_code = fix_scatter_and_summary(ai_code)
5640
- ai_code = fix_importance_groupby(ai_code)
5641
- ai_code = inject_auto_preprocessing(ai_code)
5642
- ai_code = patch_plot_code(ai_code, df, refined_question)
5643
- ai_code = ensure_matplotlib_title(ai_code)
5644
- ai_code = patch_pie_chart(ai_code, df, refined_question)
5645
- ai_code = patch_pairplot(ai_code, df)
5646
- ai_code = fix_seaborn_boxplot_nameerror(ai_code)
5647
- ai_code = fix_seaborn_barplot_nameerror(ai_code)
5648
- ai_code = get_plotting_imports(ai_code)
5649
- ai_code = patch_prefix_seaborn_calls(ai_code)
5650
- ai_code = patch_fix_sentinel_plot_calls(ai_code)
5651
- ai_code = patch_ensure_seaborn_import(ai_code)
5652
- ai_code = patch_rmse_calls(ai_code)
5653
- ai_code = patch_fix_seaborn_palette_calls(ai_code)
5654
- ai_code = patch_quiet_specific_warnings(ai_code)
5655
- ai_code = clean_llm_code(ai_code)
5656
- ai_code = ensure_image_output(ai_code)
5657
- ai_code = ensure_accuracy_block(ai_code)
5658
- ai_code = ensure_output(ai_code)
5659
- ai_code = fix_plain_prints(ai_code)
5660
- ai_code = fix_print_html(ai_code)
5661
- ai_code = fix_to_datetime_errors(ai_code)
6547
+ TOKENS["Refiner"] = [
6548
+ result['token_usage'].get('Refiner')['usage'].get('provider'),
6549
+ result['token_usage'].get('Refiner')['usage'].get('model'),
6550
+ result['token_usage'].get('Refiner')['usage'].get('input_tokens'),
6551
+ result['token_usage'].get('Refiner')['usage'].get('output_tokens'),
6552
+ result['token_usage'].get('Refiner')['usage'].get('total_tokens'),
6553
+ ]
6554
+ TOKENS["Coder"] = [
6555
+ result['token_usage'].get('Coder')['usage'].get('provider'),
6556
+ result['token_usage'].get('Coder')['usage'].get('model'),
6557
+ result['token_usage'].get('Coder')['usage'].get('input_tokens'),
6558
+ result['token_usage'].get('Coder')['usage'].get('output_tokens'),
6559
+ result['token_usage'].get('Coder')['usage'].get('total_tokens'),
6560
+ ]
5662
6561
 
5663
- # --- 3-4) Global syntax/data fixers (must run AFTER patches, BEFORE final repair) ---
5664
- ai_code = ToolRunner(SYNTAX_AND_REPAIR).run(ai_code, ctx)
5665
-
5666
- # # --- 4) Final catch-all repair (run LAST) ---
5667
- ai_code = _smx_repair_python_cell(ai_code)
5668
- ai_code = harden_ai_code(ai_code)
5669
- ai_code = drop_bad_classification_metrics(ai_code, df)
5670
- ai_code = patch_fix_sentinel_plot_calls(ai_code)
5671
-
6562
+ ai_code = result.get("python_code", "")
6563
+ # ai_code = patch_quiet_specific_warnings(ai_code)
6564
+ # ai_code = fix_print_html(ai_code)
6565
+ # ai_code = fix_plain_prints(ai_code)
6566
+ # ai_code = harden_ai_code(ai_code)
6567
+ # ai_code = ensure_image_output(ai_code)
6568
+ # ai_code = ensure_accuracy_block(ai_code)
6569
+ # ai_code = ensure_output(ai_code)
6570
+
5672
6571
  # Always make sure 'df' is in the kernel before running user code
5673
6572
  df_init_code = (
5674
6573
  f"import pandas as pd\n"
@@ -5904,14 +6803,34 @@ def setup_routes(smx):
5904
6803
 
5905
6804
  # 3) Data Preview
5906
6805
  preview_cols = df.columns
5907
- preview_df = _coerce_intlike_for_display(df[preview_cols].head(8))
6806
+
6807
+ head_df = _coerce_intlike_for_display(df[preview_cols].head(8))
5908
6808
  data_cells.append({
5909
- "title": "Data Preview",
5910
- "output": Markup(datatable_box(preview_df)),
6809
+ "title": "Dataset Head",
6810
+ "output": Markup(datatable_box(head_df)),
5911
6811
  "code": f"df[{list(preview_cols)}].head(8)",
5912
6812
  "span": "eda-col-6"
5913
6813
  })
5914
6814
 
6815
+ # Calculate the start index for the middle 8 rows
6816
+ n_rows = len(df)
6817
+ start_index = max(0, floor(n_rows / 2) - 4)
6818
+ middle_df = df.iloc[start_index : start_index + 8]
6819
+ data_cells.append({
6820
+ "title": "Dataset Middle (8 Rows)",
6821
+ "output": Markup(datatable_box(middle_df[list(preview_cols)])),
6822
+ "code": f"n = len(df)\nstart_index = max(0, floor(n / 2) - 4)\ndf.iloc[start_index : start_index + 8][{list(preview_cols)}]",
6823
+ "span": "eda-col-6"
6824
+ })
6825
+
6826
+ tail_df = _coerce_intlike_for_display(df[preview_cols].tail(8))
6827
+ data_cells.append({
6828
+ "title": "Dataset Tail",
6829
+ "output": Markup(datatable_box(tail_df)),
6830
+ "code": f"df[{list(preview_cols)}].tail(8)",
6831
+ "span": "eda-col-6"
6832
+ })
6833
+
5915
6834
  # 4) Summary Statistics
5916
6835
  summary_cols = df.columns
5917
6836
  summary_df = _coerce_intlike_for_display(df[summary_cols].describe())
@@ -6265,7 +7184,7 @@ def setup_routes(smx):
6265
7184
  "})\\n"
6266
7185
  "missing_df[missing_df['Missing Values'] > 0]"
6267
7186
  ),
6268
- "span":"eda-col-4"
7187
+ "span":"eda-col-3"
6269
7188
  })
6270
7189
 
6271
7190
  # 9) Missingness (Top 20) – Plotly bar chart
@@ -6504,7 +7423,7 @@ def setup_routes(smx):
6504
7423
  "vc = s.value_counts(dropna=False)\n"
6505
7424
  "top_k = 8 # Top-8 + Other (+ Missing)\n"
6506
7425
  ),
6507
- "span": "eda-col-4"
7426
+ "span": "eda-col-5"
6508
7427
  })
6509
7428
  else:
6510
7429
  data_cells.append({
@@ -6525,7 +7444,7 @@ def setup_routes(smx):
6525
7444
  cell["highlighted_code"] = Markup(_pygmentize(cell["code"]))
6526
7445
 
6527
7446
  highlighted_ai_code = _pygmentize(ai_code)
6528
- smxAI = "smxAI"
7447
+ smxAI = "smx-Orion"
6529
7448
 
6530
7449
  return render_template(
6531
7450
  "dashboard.html",
@@ -6541,7 +7460,7 @@ def setup_routes(smx):
6541
7460
  smxAI=smxAI,
6542
7461
  data_cells=data_cells,
6543
7462
  session_id=session_id,
6544
- llm_usage=llm_usage
7463
+ TOKENS=TOKENS
6545
7464
  )
6546
7465
 
6547
7466
 
@@ -6868,4 +7787,5 @@ def setup_routes(smx):
6868
7787
  {footer}
6869
7788
  </body>
6870
7789
  </html>
6871
- """, error_message=str(e)), 500
7790
+ """, error_message=str(e)), 500
7791
+