syntaxmatrix 2.5.8.2__py3-none-any.whl → 2.6.1__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.
- syntaxmatrix/agentic/agents.py +1149 -54
- syntaxmatrix/agentic/agents_orchestrer.py +326 -0
- syntaxmatrix/agentic/code_tools_registry.py +27 -32
- syntaxmatrix/commentary.py +16 -16
- syntaxmatrix/core.py +145 -75
- syntaxmatrix/db.py +416 -4
- syntaxmatrix/{display.py → display_html.py} +2 -6
- syntaxmatrix/gpt_models_latest.py +1 -1
- syntaxmatrix/media/__init__.py +0 -0
- syntaxmatrix/media/media_pixabay.py +277 -0
- syntaxmatrix/models.py +1 -1
- syntaxmatrix/page_builder_defaults.py +183 -0
- syntaxmatrix/page_builder_generation.py +1122 -0
- syntaxmatrix/page_layout_contract.py +644 -0
- syntaxmatrix/page_patch_publish.py +1471 -0
- syntaxmatrix/preface.py +128 -8
- syntaxmatrix/profiles.py +26 -13
- syntaxmatrix/routes.py +1475 -429
- syntaxmatrix/selftest_page_templates.py +360 -0
- syntaxmatrix/settings/client_items.py +28 -0
- syntaxmatrix/settings/model_map.py +1022 -208
- syntaxmatrix/settings/prompts.py +328 -130
- syntaxmatrix/static/assets/hero-default.svg +22 -0
- syntaxmatrix/static/icons/bot-icon.png +0 -0
- syntaxmatrix/static/icons/favicon.png +0 -0
- syntaxmatrix/static/icons/logo.png +0 -0
- syntaxmatrix/static/icons/logo3.png +0 -0
- syntaxmatrix/templates/admin_branding.html +104 -0
- syntaxmatrix/templates/admin_secretes.html +108 -0
- syntaxmatrix/templates/dashboard.html +116 -72
- syntaxmatrix/templates/edit_page.html +2535 -0
- syntaxmatrix/utils.py +2365 -2411
- {syntaxmatrix-2.5.8.2.dist-info → syntaxmatrix-2.6.1.dist-info}/METADATA +6 -2
- {syntaxmatrix-2.5.8.2.dist-info → syntaxmatrix-2.6.1.dist-info}/RECORD +37 -24
- syntaxmatrix/generate_page.py +0 -644
- syntaxmatrix/static/icons/hero_bg.jpg +0 -0
- {syntaxmatrix-2.5.8.2.dist-info → syntaxmatrix-2.6.1.dist-info}/WHEEL +0 -0
- {syntaxmatrix-2.5.8.2.dist-info → syntaxmatrix-2.6.1.dist-info}/licenses/LICENSE.txt +0 -0
- {syntaxmatrix-2.5.8.2.dist-info → syntaxmatrix-2.6.1.dist-info}/top_level.txt +0 -0
syntaxmatrix/routes.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import
|
|
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
|
|
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.
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
/*
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
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;
|
|
3366
3442
|
}
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3443
|
+
.admin-scrim.show{
|
|
3444
|
+
opacity: 1;
|
|
3445
|
+
pointer-events: auto;
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
.admin-sidebar-toggle{
|
|
3449
|
+
display: none; /* only visible on mobile */
|
|
3450
|
+
}
|
|
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:
|
|
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{
|
|
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-
|
|
3476
|
-
margin-
|
|
3477
|
-
|
|
3478
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3671
|
-
page_name = page_name.lower()
|
|
3672
|
-
|
|
3673
|
-
|
|
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
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
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()
|
|
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
|
|
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("
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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,8 +4508,8 @@ def setup_routes(smx):
|
|
|
4143
4508
|
# SYSTEM FILES
|
|
4144
4509
|
# ────────────────────────────────────────────────────────────────────────────────
|
|
4145
4510
|
sys_files_card = f"""
|
|
4146
|
-
<div class="card span-
|
|
4147
|
-
<h4>Upload System Files
|
|
4511
|
+
<div class="card span-3">
|
|
4512
|
+
<h4>Upload System Files<br>(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>
|
|
4150
4515
|
<button type="submit" name="action" value="upload_files">Upload</button>
|
|
@@ -4175,7 +4540,7 @@ def setup_routes(smx):
|
|
|
4175
4540
|
"""
|
|
4176
4541
|
|
|
4177
4542
|
manage_sys_files_card = f"""
|
|
4178
|
-
<div class='card span-
|
|
4543
|
+
<div class='card span-3'>
|
|
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
|
-
<
|
|
4198
|
-
|
|
4199
|
-
<
|
|
4200
|
-
<
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
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
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
<
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
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-
|
|
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,16 +4951,50 @@ 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-3">
|
|
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
|
+
|
|
4976
|
+
branding_link_card = f"""
|
|
4977
|
+
<div class="card span-3">
|
|
4978
|
+
<h4>Branding</h4>
|
|
4979
|
+
<div style="font-size:.72rem;color:#555;margin-top:-6px;margin-bottom:10px;line-height:1.35;">
|
|
4980
|
+
Upload your company logo and favicon (PNG/JPG). Defaults are used if nothing is uploaded.
|
|
4981
|
+
</div>
|
|
4982
|
+
<a href="{url_for('admin_branding')}" class="btn">Manage branding</a>
|
|
4983
|
+
</div>
|
|
4984
|
+
"""
|
|
4985
|
+
|
|
4532
4986
|
system_section = f"""
|
|
4533
4987
|
<section id="system" class="section">
|
|
4534
4988
|
<h2>System</h2>
|
|
4535
4989
|
<div class="admin-grid">
|
|
4990
|
+
{secretes_link_card}
|
|
4991
|
+
{branding_link_card}
|
|
4536
4992
|
{sys_files_card}
|
|
4537
4993
|
{manage_sys_files_card}
|
|
4538
4994
|
</div>
|
|
4995
|
+
|
|
4539
4996
|
</section>
|
|
4540
4997
|
"""
|
|
4541
|
-
|
|
4542
4998
|
users_section = f"""
|
|
4543
4999
|
<section id="users" class="section">
|
|
4544
5000
|
<h2>Users</h2>
|
|
@@ -4561,15 +5017,47 @@ def setup_routes(smx):
|
|
|
4561
5017
|
|
|
4562
5018
|
admin_shell = f"""{admin_layout_css}
|
|
4563
5019
|
<div class="admin-shell">
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
5020
|
+
<div id="adminSidebarScrim" class="admin-scrim" aria-hidden="true"></div>
|
|
5021
|
+
{side_nav}
|
|
5022
|
+
<div class="admin-main">
|
|
5023
|
+
<button id="adminSidebarToggle"
|
|
5024
|
+
class="admin-sidebar-toggle"
|
|
5025
|
+
aria-label="Open admin menu"></button>
|
|
5026
|
+
{models_section}
|
|
5027
|
+
{pages_section}
|
|
5028
|
+
{system_section}
|
|
5029
|
+
{users_section}
|
|
5030
|
+
{audits_section}
|
|
5031
|
+
</div>
|
|
4571
5032
|
</div>
|
|
4572
|
-
|
|
5033
|
+
<script>
|
|
5034
|
+
document.addEventListener('DOMContentLoaded', function () {{
|
|
5035
|
+
const sidebar = document.querySelector('.admin-sidenav');
|
|
5036
|
+
const toggle = document.getElementById('adminSidebarToggle');
|
|
5037
|
+
const scrim = document.getElementById('adminSidebarScrim');
|
|
5038
|
+
|
|
5039
|
+
function setOpen(open) {{
|
|
5040
|
+
if (!sidebar || !toggle) return;
|
|
5041
|
+
sidebar.classList.toggle('open', open);
|
|
5042
|
+
toggle.classList.toggle('is-open', open);
|
|
5043
|
+
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
5044
|
+
document.body.classList.toggle('no-scroll', open);
|
|
5045
|
+
if (scrim) scrim.classList.toggle('show', open);
|
|
5046
|
+
}}
|
|
5047
|
+
|
|
5048
|
+
if (toggle) {{
|
|
5049
|
+
toggle.addEventListener('click', function () {{
|
|
5050
|
+
setOpen(!sidebar.classList.contains('open'));
|
|
5051
|
+
}});
|
|
5052
|
+
}}
|
|
5053
|
+
|
|
5054
|
+
if (scrim) {{
|
|
5055
|
+
scrim.addEventListener('click', function () {{
|
|
5056
|
+
setOpen(false);
|
|
5057
|
+
}});
|
|
5058
|
+
}}
|
|
5059
|
+
}});
|
|
5060
|
+
</script>
|
|
4573
5061
|
"""
|
|
4574
5062
|
|
|
4575
5063
|
# ─────────────────────────────────────────────────────────
|
|
@@ -4605,16 +5093,19 @@ def setup_routes(smx):
|
|
|
4605
5093
|
{delete_modal_block}
|
|
4606
5094
|
|
|
4607
5095
|
<!-- Profiles helper scripts -->
|
|
4608
|
-
<script>
|
|
4609
|
-
/* Name suggestions popover
|
|
4610
|
-
const
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
5096
|
+
<script>
|
|
5097
|
+
/* Name suggestions popover */
|
|
5098
|
+
const purpose_tags = {PURPOSE_TAGS}
|
|
5099
|
+
const nameExamples = {{}};
|
|
5100
|
+
const capitalize = (s) =>
|
|
5101
|
+
s.charAt(0).toUpperCase() + s.slice(1);
|
|
5102
|
+
for (let i = 0; i < purpose_tags.length; i++) {{
|
|
5103
|
+
purpose_tags[i] = capitalize(purpose_tags[i]);
|
|
5104
|
+
const tag = purpose_tags[i]
|
|
5105
|
+
const key = tag;
|
|
5106
|
+
nameExamples[key] = tag;
|
|
5107
|
+
}}
|
|
5108
|
+
|
|
4618
5109
|
const txt = document.getElementById('profile_name');
|
|
4619
5110
|
const infoBtn = document.getElementById('name-help');
|
|
4620
5111
|
const popover = document.getElementById('name-suggestions');
|
|
@@ -4740,7 +5231,7 @@ def setup_routes(smx):
|
|
|
4740
5231
|
|
|
4741
5232
|
if (form) {{
|
|
4742
5233
|
form.addEventListener('submit', function () {{
|
|
4743
|
-
if (btn) {{ btn.disabled = true; btn.textContent = '
|
|
5234
|
+
if (btn) {{ btn.disabled = true; btn.textContent = 'Generating…'; }}
|
|
4744
5235
|
if (overlay) overlay.style.display = 'flex';
|
|
4745
5236
|
}});
|
|
4746
5237
|
}}
|
|
@@ -4750,7 +5241,7 @@ def setup_routes(smx):
|
|
|
4750
5241
|
const o = document.getElementById('loader-overlay');
|
|
4751
5242
|
if (o) o.style.display = 'none';
|
|
4752
5243
|
const b = document.getElementById('add-page-btn');
|
|
4753
|
-
if (b) {{ b.disabled = false; b.textContent = '
|
|
5244
|
+
if (b) {{ b.disabled = false; b.textContent = 'Generate'; }}
|
|
4754
5245
|
}});
|
|
4755
5246
|
}});
|
|
4756
5247
|
</script>
|
|
@@ -4892,7 +5383,99 @@ def setup_routes(smx):
|
|
|
4892
5383
|
if(e.target === backdrop) closeModal();
|
|
4893
5384
|
}});
|
|
4894
5385
|
}})();
|
|
4895
|
-
|
|
5386
|
+
</script>
|
|
5387
|
+
|
|
5388
|
+
<script>
|
|
5389
|
+
// Drag & drop reordering for the "Manage Pages" list
|
|
5390
|
+
document.addEventListener('DOMContentLoaded', function () {{
|
|
5391
|
+
const list = document.querySelector('#pages .catalog-list');
|
|
5392
|
+
if (!list) return;
|
|
5393
|
+
|
|
5394
|
+
let draggingEl = null;
|
|
5395
|
+
|
|
5396
|
+
function getPageName(li) {{
|
|
5397
|
+
if (!li) return '';
|
|
5398
|
+
if (li.dataset.pageName) return li.dataset.pageName;
|
|
5399
|
+
|
|
5400
|
+
// Prefer an explicit hidden input if present
|
|
5401
|
+
const hidden = li.querySelector('input[name="page_name"]');
|
|
5402
|
+
if (hidden && hidden.value) return hidden.value.trim();
|
|
5403
|
+
|
|
5404
|
+
// Fallback: first span's text
|
|
5405
|
+
const span = li.querySelector('span');
|
|
5406
|
+
if (span && span.textContent) return span.textContent.trim();
|
|
5407
|
+
|
|
5408
|
+
return '';
|
|
5409
|
+
}}
|
|
5410
|
+
|
|
5411
|
+
// Set up draggable behaviour
|
|
5412
|
+
list.querySelectorAll('li.li-row').forEach(function (li) {{
|
|
5413
|
+
const name = getPageName(li);
|
|
5414
|
+
if (!name) return;
|
|
5415
|
+
|
|
5416
|
+
li.dataset.pageName = name;
|
|
5417
|
+
li.setAttribute('draggable', 'true');
|
|
5418
|
+
|
|
5419
|
+
li.addEventListener('dragstart', function (e) {{
|
|
5420
|
+
draggingEl = li;
|
|
5421
|
+
li.classList.add('dragging');
|
|
5422
|
+
if (e.dataTransfer) {{
|
|
5423
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
5424
|
+
e.dataTransfer.setData('text/plain', name);
|
|
5425
|
+
}}
|
|
5426
|
+
}});
|
|
5427
|
+
|
|
5428
|
+
li.addEventListener('dragend', function () {{
|
|
5429
|
+
li.classList.remove('dragging');
|
|
5430
|
+
draggingEl = null;
|
|
5431
|
+
|
|
5432
|
+
// After drop, collect new order and POST it
|
|
5433
|
+
const items = Array.from(list.querySelectorAll('li.li-row'));
|
|
5434
|
+
const names = items
|
|
5435
|
+
.map(function (node) {{ return getPageName(node); }})
|
|
5436
|
+
.filter(Boolean);
|
|
5437
|
+
|
|
5438
|
+
if (!names.length) return;
|
|
5439
|
+
|
|
5440
|
+
const fd = new FormData();
|
|
5441
|
+
fd.append('action', 'reorder_pages');
|
|
5442
|
+
fd.append('page_order_csv', names.join(','));
|
|
5443
|
+
|
|
5444
|
+
fetch('/admin', {{
|
|
5445
|
+
method: 'POST',
|
|
5446
|
+
body: fd,
|
|
5447
|
+
credentials: 'same-origin'
|
|
5448
|
+
}})
|
|
5449
|
+
.then(function (res) {{
|
|
5450
|
+
if (!res.ok) {{
|
|
5451
|
+
console.error('Failed to save page order', res.status);
|
|
5452
|
+
}}
|
|
5453
|
+
// Reload so navbar + list reflect the new order
|
|
5454
|
+
window.location.reload();
|
|
5455
|
+
}})
|
|
5456
|
+
.catch(function (err) {{
|
|
5457
|
+
console.error('Error saving page order', err);
|
|
5458
|
+
}});
|
|
5459
|
+
}});
|
|
5460
|
+
|
|
5461
|
+
li.addEventListener('dragover', function (e) {{
|
|
5462
|
+
if (!draggingEl || draggingEl === li) return;
|
|
5463
|
+
e.preventDefault();
|
|
5464
|
+
|
|
5465
|
+
const rect = li.getBoundingClientRect();
|
|
5466
|
+
const offsetY = e.clientY - rect.top;
|
|
5467
|
+
const before = offsetY < (rect.height / 2);
|
|
5468
|
+
|
|
5469
|
+
if (before) {{
|
|
5470
|
+
list.insertBefore(draggingEl, li);
|
|
5471
|
+
}}else {{
|
|
5472
|
+
list.insertBefore(draggingEl, li.nextSibling);
|
|
5473
|
+
}}
|
|
5474
|
+
}});
|
|
5475
|
+
}});
|
|
5476
|
+
}});
|
|
5477
|
+
</script>
|
|
5478
|
+
|
|
4896
5479
|
</body>
|
|
4897
5480
|
</html>
|
|
4898
5481
|
""",
|
|
@@ -4902,6 +5485,158 @@ def setup_routes(smx):
|
|
|
4902
5485
|
profiles=profiles
|
|
4903
5486
|
)
|
|
4904
5487
|
|
|
5488
|
+
|
|
5489
|
+
@smx.app.route("/admin/secretes", methods=["GET", "POST"])
|
|
5490
|
+
def admin_secretes():
|
|
5491
|
+
role = (session.get("role") or "").lower()
|
|
5492
|
+
if role not in ("admin", "superadmin"):
|
|
5493
|
+
return jsonify({"error": "forbidden"}), 403
|
|
5494
|
+
|
|
5495
|
+
if request.method == "POST":
|
|
5496
|
+
action = (request.form.get("action") or "").strip()
|
|
5497
|
+
|
|
5498
|
+
if action == "save_secret":
|
|
5499
|
+
name = (request.form.get("secret_name") or "").strip()
|
|
5500
|
+
value = (request.form.get("secret_value") or "").strip()
|
|
5501
|
+
|
|
5502
|
+
if not name:
|
|
5503
|
+
flash("Secret name is required.")
|
|
5504
|
+
return redirect(url_for("admin_secretes"))
|
|
5505
|
+
|
|
5506
|
+
# We don’t allow saving blank values accidentally.
|
|
5507
|
+
if not value:
|
|
5508
|
+
flash("Secret value is required.")
|
|
5509
|
+
return redirect(url_for("admin_secretes"))
|
|
5510
|
+
|
|
5511
|
+
db.set_secret(name, value)
|
|
5512
|
+
flash(f"Saved: {name.upper()} ✓")
|
|
5513
|
+
return redirect(url_for("admin_secretes"))
|
|
5514
|
+
|
|
5515
|
+
if action == "delete_secret":
|
|
5516
|
+
name = (request.form.get("secret_name") or "").strip()
|
|
5517
|
+
if name:
|
|
5518
|
+
db.delete_secret(name)
|
|
5519
|
+
flash(f"Deleted: {name.upper()}")
|
|
5520
|
+
return redirect(url_for("admin_secretes"))
|
|
5521
|
+
|
|
5522
|
+
# GET
|
|
5523
|
+
names = []
|
|
5524
|
+
try:
|
|
5525
|
+
names = db.list_secret_names()
|
|
5526
|
+
except Exception:
|
|
5527
|
+
names = []
|
|
5528
|
+
|
|
5529
|
+
return render_template("admin_secretes.html", secret_names=names)
|
|
5530
|
+
|
|
5531
|
+
|
|
5532
|
+
@smx.app.route("/admin/branding", methods=["GET", "POST"])
|
|
5533
|
+
@admin_required
|
|
5534
|
+
def admin_branding():
|
|
5535
|
+
branding_dir = os.path.join(_CLIENT_DIR, "branding")
|
|
5536
|
+
os.makedirs(branding_dir, exist_ok=True)
|
|
5537
|
+
|
|
5538
|
+
allowed_ext = {".png", ".jpg", ".jpeg"}
|
|
5539
|
+
max_logo_bytes = 5 * 1024 * 1024 # 5 MB
|
|
5540
|
+
max_favicon_bytes = 1 * 1024 * 1024 # 1 MB
|
|
5541
|
+
|
|
5542
|
+
def _find(base: str):
|
|
5543
|
+
for ext in (".png", ".jpg", ".jpeg"):
|
|
5544
|
+
p = os.path.join(branding_dir, f"{base}{ext}")
|
|
5545
|
+
if os.path.exists(p):
|
|
5546
|
+
return f"{base}{ext}"
|
|
5547
|
+
return None
|
|
5548
|
+
|
|
5549
|
+
def _delete_all(base: str):
|
|
5550
|
+
for ext in (".png", ".jpg", ".jpeg"):
|
|
5551
|
+
p = os.path.join(branding_dir, f"{base}{ext}")
|
|
5552
|
+
if os.path.exists(p):
|
|
5553
|
+
try:
|
|
5554
|
+
os.remove(p)
|
|
5555
|
+
except Exception:
|
|
5556
|
+
pass
|
|
5557
|
+
|
|
5558
|
+
def _save_upload(field_name: str, base: str, max_bytes: int):
|
|
5559
|
+
f = request.files.get(field_name)
|
|
5560
|
+
if not f or not f.filename:
|
|
5561
|
+
return False, None
|
|
5562
|
+
|
|
5563
|
+
ext = os.path.splitext(f.filename.lower())[1].strip()
|
|
5564
|
+
if ext not in allowed_ext:
|
|
5565
|
+
return False, f"Invalid file type for {base}. Use PNG or JPG."
|
|
5566
|
+
|
|
5567
|
+
# size check
|
|
5568
|
+
try:
|
|
5569
|
+
f.stream.seek(0, os.SEEK_END)
|
|
5570
|
+
size = f.stream.tell()
|
|
5571
|
+
f.stream.seek(0)
|
|
5572
|
+
except Exception:
|
|
5573
|
+
size = None
|
|
5574
|
+
|
|
5575
|
+
if size is not None and size > max_bytes:
|
|
5576
|
+
return False, f"{base.capitalize()} is too large. Max {max_bytes // (1024*1024)} MB."
|
|
5577
|
+
|
|
5578
|
+
# Replace existing logo.* / favicon.*
|
|
5579
|
+
_delete_all(base)
|
|
5580
|
+
|
|
5581
|
+
out_path = os.path.join(branding_dir, f"{base}{ext}")
|
|
5582
|
+
try:
|
|
5583
|
+
f.save(out_path)
|
|
5584
|
+
except Exception as e:
|
|
5585
|
+
return False, f"Failed to save {base}: {e}"
|
|
5586
|
+
|
|
5587
|
+
return True, None
|
|
5588
|
+
|
|
5589
|
+
# POST actions
|
|
5590
|
+
if request.method == "POST":
|
|
5591
|
+
action = (request.form.get("action") or "upload").strip().lower()
|
|
5592
|
+
|
|
5593
|
+
if action == "reset":
|
|
5594
|
+
_delete_all("logo")
|
|
5595
|
+
_delete_all("favicon")
|
|
5596
|
+
try:
|
|
5597
|
+
smx._apply_branding_from_disk()
|
|
5598
|
+
except Exception:
|
|
5599
|
+
pass
|
|
5600
|
+
flash("Branding reset to defaults ✓")
|
|
5601
|
+
return redirect(url_for("admin_branding"))
|
|
5602
|
+
|
|
5603
|
+
ok1, err1 = _save_upload("logo_file", "logo", max_logo_bytes)
|
|
5604
|
+
ok2, err2 = _save_upload("favicon_file", "favicon", max_favicon_bytes)
|
|
5605
|
+
|
|
5606
|
+
if err1:
|
|
5607
|
+
flash(err1, "error")
|
|
5608
|
+
if err2:
|
|
5609
|
+
flash(err2, "error")
|
|
5610
|
+
|
|
5611
|
+
if ok1 or ok2:
|
|
5612
|
+
try:
|
|
5613
|
+
smx._apply_branding_from_disk()
|
|
5614
|
+
except Exception:
|
|
5615
|
+
pass
|
|
5616
|
+
flash("Branding updated ✓")
|
|
5617
|
+
|
|
5618
|
+
return redirect(url_for("admin_branding"))
|
|
5619
|
+
|
|
5620
|
+
# GET: show current status
|
|
5621
|
+
logo_fn = _find("logo")
|
|
5622
|
+
fav_fn = _find("favicon")
|
|
5623
|
+
|
|
5624
|
+
cache_bust = int(time.time())
|
|
5625
|
+
|
|
5626
|
+
logo_url = f"/branding/{logo_fn}?v={cache_bust}" if logo_fn else None
|
|
5627
|
+
favicon_url = f"/branding/{fav_fn}?v={cache_bust}" if fav_fn else None
|
|
5628
|
+
|
|
5629
|
+
default_logo_html = getattr(smx, "_default_site_logo", smx.site_logo)
|
|
5630
|
+
default_favicon_url = getattr(smx, "_default_favicon", smx.favicon)
|
|
5631
|
+
|
|
5632
|
+
return render_template(
|
|
5633
|
+
"admin_branding.html",
|
|
5634
|
+
logo_url=logo_url,
|
|
5635
|
+
favicon_url=favicon_url,
|
|
5636
|
+
default_logo_html=Markup(default_logo_html),
|
|
5637
|
+
default_favicon_url=default_favicon_url,
|
|
5638
|
+
)
|
|
5639
|
+
|
|
4905
5640
|
@smx.app.route("/admin/delete.json", methods=["POST"])
|
|
4906
5641
|
def admin_delete_universal():
|
|
4907
5642
|
|
|
@@ -5083,47 +5818,56 @@ def setup_routes(smx):
|
|
|
5083
5818
|
smx.warning(f"/admin/delete.json error: {e}")
|
|
5084
5819
|
return jsonify(ok=False, error=str(e)), 500
|
|
5085
5820
|
|
|
5086
|
-
|
|
5821
|
+
|
|
5087
5822
|
@smx.app.route('/page/<page_name>')
|
|
5088
5823
|
def view_page(page_name):
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5824
|
+
hero_fix_css = """
|
|
5825
|
+
<style>
|
|
5826
|
+
div[id^="smx-page-"] .hero-overlay{
|
|
5827
|
+
background:linear-gradient(90deg,
|
|
5828
|
+
rgba(2,6,23,.62) 0%,
|
|
5829
|
+
rgba(2,6,23,.40) 42%,
|
|
5830
|
+
rgba(2,6,23,.14) 72%,
|
|
5831
|
+
rgba(2,6,23,.02) 100%
|
|
5832
|
+
) !important;
|
|
5833
|
+
}
|
|
5834
|
+
@media (max-width: 860px){
|
|
5835
|
+
div[id^="smx-page-"] .hero-overlay{
|
|
5836
|
+
background:linear-gradient(180deg,
|
|
5837
|
+
rgba(2,6,23,.16) 0%,
|
|
5838
|
+
rgba(2,6,23,.55) 70%,
|
|
5839
|
+
rgba(2,6,23,.70) 100%
|
|
5840
|
+
) !important;
|
|
5841
|
+
}
|
|
5842
|
+
}
|
|
5843
|
+
div[id^="smx-page-"] .hero-panel{
|
|
5844
|
+
background:rgba(2,6,23,.24) !important;
|
|
5845
|
+
backdrop-filter: blur(4px) !important;
|
|
5846
|
+
-webkit-backdrop-filter: blur(4px) !important;
|
|
5847
|
+
}
|
|
5848
|
+
</style>
|
|
5849
|
+
"""
|
|
5113
5850
|
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5851
|
+
smx.page = page_name.lower()
|
|
5852
|
+
nav_html = _generate_nav()
|
|
5853
|
+
# Always fetch the latest HTML from disk/DB (prevents stale cache across workers)
|
|
5854
|
+
content = db.get_page_html(page_name)
|
|
5855
|
+
if content is None:
|
|
5856
|
+
content = smx.pages.get(page_name, f"No content found for page '{page_name}'.")
|
|
5857
|
+
|
|
5858
|
+
view_page_html = f"""
|
|
5859
|
+
{head_html()}
|
|
5860
|
+
{nav_html}
|
|
5861
|
+
<main style="padding-top:calc(52px + env(safe-area-inset-top)); width:100%; box-sizing:border-box;">
|
|
5862
|
+
{content}
|
|
5863
|
+
</main>
|
|
5864
|
+
{hero_fix_css}
|
|
5865
|
+
{footer_html()}
|
|
5866
|
+
"""
|
|
5867
|
+
resp = Response(view_page_html, mimetype="text/html")
|
|
5868
|
+
# Prevent the browser/proxies from keeping an old copy during active editing/publishing
|
|
5869
|
+
resp.headers["Cache-Control"] = "no-store"
|
|
5870
|
+
return resp
|
|
5127
5871
|
|
|
5128
5872
|
|
|
5129
5873
|
@smx.app.route('/docs')
|
|
@@ -5183,7 +5927,6 @@ def setup_routes(smx):
|
|
|
5183
5927
|
html += "</table>"
|
|
5184
5928
|
return html
|
|
5185
5929
|
|
|
5186
|
-
|
|
5187
5930
|
@smx.app.route("/admin/chunks/edit/<int:chunk_id>", methods=["GET", "POST"])
|
|
5188
5931
|
def edit_chunk(chunk_id):
|
|
5189
5932
|
if request.method == "POST":
|
|
@@ -5218,78 +5961,212 @@ def setup_routes(smx):
|
|
|
5218
5961
|
def edit_page(page_name):
|
|
5219
5962
|
if request.method == "POST":
|
|
5220
5963
|
new_page_name = request.form.get("page_name", "").strip()
|
|
5221
|
-
|
|
5964
|
+
# Keep page_content formatting exactly as typed
|
|
5965
|
+
new_content = request.form.get("page_content", "")
|
|
5966
|
+
|
|
5222
5967
|
if page_name in smx.pages and new_page_name:
|
|
5223
5968
|
db.update_page(page_name, new_page_name, new_content)
|
|
5969
|
+
smx.pages = db.get_pages()
|
|
5224
5970
|
return redirect(url_for("admin_panel"))
|
|
5225
|
-
|
|
5226
|
-
content =
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
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)
|
|
5971
|
+
|
|
5972
|
+
content = db.get_page_html(page_name) or ""
|
|
5973
|
+
|
|
5974
|
+
# NEW: builder layout json (stored separately)
|
|
5975
|
+
layout_row = getattr(db, "get_page_layout", None)
|
|
5976
|
+
layout_json = None
|
|
5977
|
+
if callable(layout_row):
|
|
5978
|
+
try:
|
|
5979
|
+
row = db.get_page_layout(page_name)
|
|
5980
|
+
layout_json = (row or {}).get("layout_json")
|
|
5981
|
+
except Exception:
|
|
5982
|
+
layout_json = None
|
|
5983
|
+
published_as = request.args.get("published_as")
|
|
5984
|
+
return render_template(
|
|
5985
|
+
"edit_page.html",
|
|
5986
|
+
page_name=page_name,
|
|
5987
|
+
content=content,
|
|
5988
|
+
layout_json=layout_json,
|
|
5989
|
+
published_as=published_as,
|
|
5990
|
+
)
|
|
5292
5991
|
|
|
5992
|
+
|
|
5993
|
+
# ────────────────────────────────────────────────────
|
|
5994
|
+
# PIXABAY
|
|
5995
|
+
# ────────────────────────────────────────────────────
|
|
5996
|
+
@smx.app.route("/admin/pixabay/search.json", methods=["GET"])
|
|
5997
|
+
def admin_pixabay_search():
|
|
5998
|
+
role = (session.get("role") or "").lower()
|
|
5999
|
+
if role not in ("admin", "superadmin"):
|
|
6000
|
+
return jsonify({"error": "forbidden"}), 403
|
|
6001
|
+
|
|
6002
|
+
q = (request.args.get("q") or "").strip()
|
|
6003
|
+
orientation = (request.args.get("orientation") or "horizontal").strip().lower()
|
|
6004
|
+
image_type = (request.args.get("image_type") or "photo").strip().lower()
|
|
6005
|
+
|
|
6006
|
+
api_key = None
|
|
6007
|
+
try:
|
|
6008
|
+
api_key = db.get_secret("PIXABAY_API_KEY")
|
|
6009
|
+
except Exception:
|
|
6010
|
+
api_key = None
|
|
6011
|
+
|
|
6012
|
+
if not api_key:
|
|
6013
|
+
return jsonify({"error": "Missing PIXABAY_API_KEY. Add it in Admin → Manage secretes."}), 400
|
|
6014
|
+
|
|
6015
|
+
try:
|
|
6016
|
+
from syntaxmatrix.media.media_pixabay import pixabay_search
|
|
6017
|
+
hits = pixabay_search(
|
|
6018
|
+
api_key=api_key,
|
|
6019
|
+
query=q,
|
|
6020
|
+
image_type=image_type,
|
|
6021
|
+
orientation=orientation,
|
|
6022
|
+
per_page=24,
|
|
6023
|
+
safesearch=True,
|
|
6024
|
+
editors_choice=False,
|
|
6025
|
+
min_width=960,
|
|
6026
|
+
)
|
|
6027
|
+
payload = []
|
|
6028
|
+
for h in hits:
|
|
6029
|
+
payload.append({
|
|
6030
|
+
"id": h.id,
|
|
6031
|
+
"page_url": h.page_url,
|
|
6032
|
+
"preview_url": h.preview_url,
|
|
6033
|
+
"large_image_url": h.large_image_url,
|
|
6034
|
+
"webformat_url": h.webformat_url,
|
|
6035
|
+
"width": h.width,
|
|
6036
|
+
"height": h.height,
|
|
6037
|
+
"tags": h.tags,
|
|
6038
|
+
"user": h.user,
|
|
6039
|
+
"type": h.image_type
|
|
6040
|
+
})
|
|
6041
|
+
return jsonify({"items": payload}), 200
|
|
6042
|
+
|
|
6043
|
+
except Exception as e:
|
|
6044
|
+
return jsonify({"error": str(e)}), 500
|
|
6045
|
+
|
|
6046
|
+
@smx.app.route("/admin/pixabay/import.json", methods=["POST"])
|
|
6047
|
+
def admin_pixabay_import():
|
|
6048
|
+
role = (session.get("role") or "").lower()
|
|
6049
|
+
if role not in ("admin", "superadmin"):
|
|
6050
|
+
return jsonify({"error": "forbidden"}), 403
|
|
6051
|
+
|
|
6052
|
+
api_key = None
|
|
6053
|
+
try:
|
|
6054
|
+
api_key = db.get_secret("PIXABAY_API_KEY")
|
|
6055
|
+
except Exception:
|
|
6056
|
+
api_key = None
|
|
6057
|
+
|
|
6058
|
+
if not api_key:
|
|
6059
|
+
return jsonify({"error": "Missing PIXABAY_API_KEY. Add it in Admin → Manage secretes."}), 400
|
|
6060
|
+
|
|
6061
|
+
payload = request.get_json(force=True) or {}
|
|
6062
|
+
pixabay_id = payload.get("id")
|
|
6063
|
+
if not pixabay_id:
|
|
6064
|
+
return jsonify({"error": "Missing id"}), 400
|
|
6065
|
+
|
|
6066
|
+
min_width = int(payload.get("min_width") or 0)
|
|
6067
|
+
min_width = max(0, min(3000, min_width))
|
|
6068
|
+
|
|
6069
|
+
try:
|
|
6070
|
+
import requests
|
|
6071
|
+
from syntaxmatrix.media.media_pixabay import PixabayHit, import_pixabay_hit
|
|
6072
|
+
|
|
6073
|
+
# Look up the hit by ID from Pixabay API (prevents client tampering)
|
|
6074
|
+
r = requests.get(
|
|
6075
|
+
"https://pixabay.com/api/",
|
|
6076
|
+
params={"key": api_key, "id": str(pixabay_id)},
|
|
6077
|
+
timeout=15
|
|
6078
|
+
)
|
|
6079
|
+
r.raise_for_status()
|
|
6080
|
+
data = r.json() or {}
|
|
6081
|
+
hits = data.get("hits") or []
|
|
6082
|
+
if not hits:
|
|
6083
|
+
return jsonify({"error": "Pixabay image not found"}), 404
|
|
6084
|
+
|
|
6085
|
+
h = hits[0]
|
|
6086
|
+
hit = PixabayHit(
|
|
6087
|
+
id=int(h.get("id")),
|
|
6088
|
+
page_url=str(h.get("pageURL") or ""),
|
|
6089
|
+
tags=str(h.get("tags") or ""),
|
|
6090
|
+
user=str(h.get("user") or ""),
|
|
6091
|
+
preview_url=str(h.get("previewURL") or ""),
|
|
6092
|
+
webformat_url=str(h.get("webformatURL") or ""),
|
|
6093
|
+
large_image_url=str(h.get("largeImageURL") or ""),
|
|
6094
|
+
width=int(h.get("imageWidth") or 0),
|
|
6095
|
+
height=int(h.get("imageHeight") or 0),
|
|
6096
|
+
image_type=str(h.get("type") or "photo"),
|
|
6097
|
+
)
|
|
6098
|
+
|
|
6099
|
+
# Paths
|
|
6100
|
+
media_dir = os.path.join(_CLIENT_DIR, "uploads", "media")
|
|
6101
|
+
imported_dir = os.path.join(media_dir, "images", "imported")
|
|
6102
|
+
thumbs_dir = os.path.join(media_dir, "images", "thumbs")
|
|
6103
|
+
os.makedirs(imported_dir, exist_ok=True)
|
|
6104
|
+
os.makedirs(thumbs_dir, exist_ok=True)
|
|
6105
|
+
|
|
6106
|
+
# Download-once guard: if already imported, reuse local file
|
|
6107
|
+
existing_jpg = os.path.join(imported_dir, f"pixabay-{hit.id}.jpg")
|
|
6108
|
+
existing_png = os.path.join(imported_dir, f"pixabay-{hit.id}.png")
|
|
6109
|
+
|
|
6110
|
+
if os.path.exists(existing_jpg) or os.path.exists(existing_png):
|
|
6111
|
+
existing_abs = existing_png if os.path.exists(existing_png) else existing_jpg
|
|
6112
|
+
rel_path = os.path.relpath(existing_abs, media_dir).replace("\\", "/")
|
|
6113
|
+
return jsonify({
|
|
6114
|
+
"rel_path": rel_path,
|
|
6115
|
+
"url": url_for("serve_media", filename=rel_path),
|
|
6116
|
+
"thumb_url": None,
|
|
6117
|
+
"source_url": hit.page_url,
|
|
6118
|
+
"author": hit.user,
|
|
6119
|
+
"tags": hit.tags,
|
|
6120
|
+
}), 200
|
|
6121
|
+
|
|
6122
|
+
meta = import_pixabay_hit(
|
|
6123
|
+
hit,
|
|
6124
|
+
media_images_dir=imported_dir,
|
|
6125
|
+
thumbs_dir=thumbs_dir,
|
|
6126
|
+
max_width=1920,
|
|
6127
|
+
thumb_width=800,
|
|
6128
|
+
min_width=min_width
|
|
6129
|
+
)
|
|
6130
|
+
|
|
6131
|
+
# Convert absolute paths to rel paths + URLs
|
|
6132
|
+
rel_path = os.path.relpath(meta["file_path"], media_dir).replace("\\", "/")
|
|
6133
|
+
thumb_rel = None
|
|
6134
|
+
if meta.get("thumb_path"):
|
|
6135
|
+
thumb_rel = os.path.relpath(meta["thumb_path"], media_dir).replace("\\", "/")
|
|
6136
|
+
|
|
6137
|
+
# Register in DB (for local-first & Media sources)
|
|
6138
|
+
try:
|
|
6139
|
+
db.upsert_media_asset(
|
|
6140
|
+
rel_path=rel_path,
|
|
6141
|
+
kind="image",
|
|
6142
|
+
thumb_path=thumb_rel,
|
|
6143
|
+
sha256=meta.get("sha256"),
|
|
6144
|
+
dhash=meta.get("dhash"),
|
|
6145
|
+
width=int(meta.get("width") or 0),
|
|
6146
|
+
height=int(meta.get("height") or 0),
|
|
6147
|
+
mime=meta.get("mime"),
|
|
6148
|
+
source="pixabay",
|
|
6149
|
+
source_url=meta.get("source_url"),
|
|
6150
|
+
author=meta.get("author"),
|
|
6151
|
+
licence="Pixabay Content Licence",
|
|
6152
|
+
tags=meta.get("tags"),
|
|
6153
|
+
)
|
|
6154
|
+
except Exception:
|
|
6155
|
+
pass
|
|
6156
|
+
|
|
6157
|
+
return jsonify({
|
|
6158
|
+
"rel_path": rel_path,
|
|
6159
|
+
"url": url_for("serve_media", filename=rel_path),
|
|
6160
|
+
"thumb_url": url_for("serve_media", filename=thumb_rel) if thumb_rel else None,
|
|
6161
|
+
"source_url": meta.get("source_url"),
|
|
6162
|
+
"author": meta.get("author"),
|
|
6163
|
+
"tags": meta.get("tags"),
|
|
6164
|
+
}), 200
|
|
6165
|
+
|
|
6166
|
+
except Exception as e:
|
|
6167
|
+
return jsonify({"error": str(e)}), 500
|
|
6168
|
+
|
|
6169
|
+
|
|
5293
6170
|
# ────────────────────────────────────────────────────
|
|
5294
6171
|
# ACCOUNTS
|
|
5295
6172
|
# ────────────────────────────────────────────────────
|
|
@@ -5429,26 +6306,240 @@ def setup_routes(smx):
|
|
|
5429
6306
|
return any(r in ("admin", "superadmin") for r in roles if r)
|
|
5430
6307
|
return dict(can_see_admin=can_see_admin)
|
|
5431
6308
|
|
|
6309
|
+
|
|
6310
|
+
def _is_admin_request() -> bool:
|
|
6311
|
+
r = (session.get("role") or "").lower()
|
|
6312
|
+
if r in ("admin", "superadmin"):
|
|
6313
|
+
return True
|
|
6314
|
+
|
|
6315
|
+
# Fallback to Flask-Login user roles (matches your inject_role_helpers logic)
|
|
6316
|
+
if not getattr(current_user, "is_authenticated", False):
|
|
6317
|
+
return False
|
|
6318
|
+
|
|
6319
|
+
roles = getattr(current_user, "roles", None)
|
|
6320
|
+
if roles is None:
|
|
6321
|
+
rr = getattr(current_user, "role", None)
|
|
6322
|
+
roles = [rr] if rr else []
|
|
6323
|
+
|
|
6324
|
+
return any((str(x or "")).lower() in ("admin", "superadmin") for x in roles)
|
|
6325
|
+
|
|
6326
|
+
|
|
6327
|
+
@smx.app.route("/admin/page_layouts/<page_name>", methods=["GET", "POST"])
|
|
6328
|
+
def page_layouts_api(page_name):
|
|
6329
|
+
if not _is_admin_request():
|
|
6330
|
+
return jsonify({"error": "forbidden"}), 403
|
|
6331
|
+
|
|
6332
|
+
if request.method == "GET":
|
|
6333
|
+
try:
|
|
6334
|
+
row = db.get_page_layout(page_name) or {}
|
|
6335
|
+
return jsonify(row), 200
|
|
6336
|
+
except Exception as e:
|
|
6337
|
+
return jsonify({"error": str(e)}), 500
|
|
6338
|
+
|
|
6339
|
+
# POST: save layout json
|
|
6340
|
+
from syntaxmatrix.page_layout_contract import normalise_layout, validate_layout
|
|
6341
|
+
payload = request.get_json(force=True) or {}
|
|
6342
|
+
payload = normalise_layout(payload, mode="draft")
|
|
6343
|
+
|
|
6344
|
+
issues = validate_layout(payload)
|
|
6345
|
+
errors = [i.to_dict() for i in issues if i.level == "error"]
|
|
6346
|
+
warnings = [i.to_dict() for i in issues if i.level == "warning"]
|
|
6347
|
+
|
|
6348
|
+
if errors:
|
|
6349
|
+
return jsonify({"error": "invalid layout", "issues": errors, "warnings": warnings}), 400
|
|
6350
|
+
|
|
6351
|
+
layout_json = json.dumps(payload, ensure_ascii=False)
|
|
6352
|
+
db.upsert_page_layout(page_name, layout_json)
|
|
6353
|
+
return jsonify({"ok": True, "warnings": warnings}), 200
|
|
6354
|
+
|
|
6355
|
+
|
|
6356
|
+
@smx.app.route("/admin/page_layouts/<page_name>/publish", methods=["POST"])
|
|
6357
|
+
def publish_layout_patch_only(page_name):
|
|
6358
|
+
|
|
6359
|
+
role = (session.get("role") or "").lower()
|
|
6360
|
+
if role not in ("admin", "superadmin"):
|
|
6361
|
+
return jsonify({"error": "forbidden"}), 403
|
|
6362
|
+
|
|
6363
|
+
try:
|
|
6364
|
+
# Load layout (prefer request body; fallback to DB)
|
|
6365
|
+
payload = request.get_json(silent=True) or {}
|
|
6366
|
+
if not (isinstance(payload, dict) and isinstance(payload.get("sections"), list)):
|
|
6367
|
+
row = db.get_page_layout(page_name) or {}
|
|
6368
|
+
raw = (row or {}).get("layout_json") or ""
|
|
6369
|
+
payload = json.loads(raw) if raw else {}
|
|
6370
|
+
|
|
6371
|
+
if not (isinstance(payload, dict) and isinstance(payload.get("sections"), list)):
|
|
6372
|
+
return jsonify({"error": "no layout to publish"}), 400
|
|
6373
|
+
|
|
6374
|
+
# Always patch the latest HTML on disk/DB (avoids stale smx.pages in other workers)
|
|
6375
|
+
existing_html = db.get_page_html(page_name) or ""
|
|
6376
|
+
if not existing_html:
|
|
6377
|
+
# Fallback only (older behaviour)
|
|
6378
|
+
if not isinstance(smx.pages, dict):
|
|
6379
|
+
smx.pages = db.get_pages()
|
|
6380
|
+
page_key = (page_name or "").strip()
|
|
6381
|
+
existing_html = smx.pages.get(page_key) or smx.pages.get(page_key.lower()) or ""
|
|
6382
|
+
|
|
6383
|
+
# Keep a copy of what was originally stored so we can correctly detect changes
|
|
6384
|
+
original_html = existing_html
|
|
6385
|
+
|
|
6386
|
+
# NEW: ensure any newly-added layout sections exist in the stored HTML
|
|
6387
|
+
# so validate_compiled_html won't reject the publish.
|
|
6388
|
+
existing_html, inserted_sections = ensure_sections_exist(
|
|
6389
|
+
existing_html,
|
|
6390
|
+
payload,
|
|
6391
|
+
page_slug=page_name
|
|
6392
|
+
)
|
|
6393
|
+
|
|
6394
|
+
if not existing_html:
|
|
6395
|
+
return jsonify({"error": "page html not found"}), 404
|
|
6396
|
+
|
|
6397
|
+
payload = normalise_layout(payload, mode="prod")
|
|
6398
|
+
|
|
6399
|
+
issues = validate_layout(payload)
|
|
6400
|
+
errors = [i.to_dict() for i in issues if i.level == "error"]
|
|
6401
|
+
warnings = [i.to_dict() for i in issues if i.level == "warning"]
|
|
6402
|
+
|
|
6403
|
+
if errors:
|
|
6404
|
+
return jsonify({"error": "invalid layout", "issues": errors, "warnings": warnings}), 400
|
|
6405
|
+
|
|
6406
|
+
# Optional but very useful: validate current HTML has the anchors we need
|
|
6407
|
+
html_issues = validate_compiled_html(existing_html, payload)
|
|
6408
|
+
html_errors = [i.to_dict() for i in html_issues if i.level == "error"]
|
|
6409
|
+
html_warnings = [i.to_dict() for i in html_issues if i.level == "warning"]
|
|
6410
|
+
|
|
6411
|
+
if html_errors:
|
|
6412
|
+
return jsonify({"error": "html not compatible with patching", "issues": html_errors, "warnings": html_warnings}), 400
|
|
6413
|
+
|
|
6414
|
+
updated_html, stats = patch_page_publish(existing_html, payload, page_slug=page_name)
|
|
6415
|
+
|
|
6416
|
+
# If nothing changed, still return ok
|
|
6417
|
+
if updated_html == original_html:
|
|
6418
|
+
return jsonify({"ok": True, "mode": "noop", "stats": stats}), 200
|
|
6419
|
+
|
|
6420
|
+
# Persist patched HTML
|
|
6421
|
+
db.update_page(page_name, page_name, updated_html)
|
|
6422
|
+
smx.pages = db.get_pages()
|
|
6423
|
+
|
|
6424
|
+
return jsonify({"ok": True, "mode": "patched", "stats": stats}), 200
|
|
6425
|
+
|
|
6426
|
+
except Exception as e:
|
|
6427
|
+
smx.warning(f"publish_layout_patch_only error: {e}")
|
|
6428
|
+
return jsonify({"error": str(e)}), 500
|
|
6429
|
+
|
|
6430
|
+
|
|
6431
|
+
@smx.app.route("/admin/page_layouts/<page_name>/compile", methods=["POST"])
|
|
6432
|
+
def compile_page_layout(page_name):
|
|
6433
|
+
role = (session.get("role") or "").lower()
|
|
6434
|
+
if role not in ("admin", "superadmin"):
|
|
6435
|
+
return jsonify({"error": "forbidden"}), 403
|
|
6436
|
+
|
|
6437
|
+
try:
|
|
6438
|
+
payload = request.get_json(force=True) or {}
|
|
6439
|
+
html_doc = compile_layout_to_html(payload, page_slug=page_name)
|
|
6440
|
+
return jsonify({"html": html_doc}), 200
|
|
6441
|
+
except Exception as e:
|
|
6442
|
+
return jsonify({"error": str(e)}), 500
|
|
6443
|
+
|
|
6444
|
+
|
|
6445
|
+
@smx.app.route("/admin/media/list.json", methods=["GET"])
|
|
6446
|
+
def list_media_json():
|
|
6447
|
+
role = (session.get("role") or "").lower()
|
|
6448
|
+
if role not in ("admin", "superadmin"):
|
|
6449
|
+
return jsonify({"error": "forbidden"}), 403
|
|
6450
|
+
|
|
6451
|
+
media_dir = os.path.join(_CLIENT_DIR, "uploads", "media")
|
|
6452
|
+
items = []
|
|
6453
|
+
img_ext = {".png", ".jpg", ".jpeg", ".webp", ".gif"}
|
|
6454
|
+
vid_ext = {".mp4", ".webm", ".mov", ".m4v"}
|
|
6455
|
+
|
|
6456
|
+
for root, _, files in os.walk(media_dir):
|
|
6457
|
+
for fn in files:
|
|
6458
|
+
abs_path = os.path.join(root, fn)
|
|
6459
|
+
rel = os.path.relpath(abs_path, media_dir).replace("\\", "/")
|
|
6460
|
+
ext = os.path.splitext(fn.lower())[1]
|
|
6461
|
+
kind = "other"
|
|
6462
|
+
if ext in img_ext:
|
|
6463
|
+
kind = "image"
|
|
6464
|
+
elif ext in vid_ext:
|
|
6465
|
+
kind = "video"
|
|
6466
|
+
items.append({
|
|
6467
|
+
"name": fn,
|
|
6468
|
+
"path": rel,
|
|
6469
|
+
"url": url_for("serve_media", filename=rel),
|
|
6470
|
+
"kind": kind
|
|
6471
|
+
})
|
|
6472
|
+
|
|
6473
|
+
items.sort(key=lambda x: x["path"])
|
|
6474
|
+
return jsonify({"items": items}), 200
|
|
6475
|
+
|
|
6476
|
+
# # Example usage in your existing routes
|
|
6477
|
+
# @smx.app.route("/admin/generate_image", methods=["POST"])
|
|
6478
|
+
# def generate_image_route():
|
|
6479
|
+
# prompt = request.json.get("prompt", "").strip()
|
|
6480
|
+
# kind = request.json.get("kind", "image")
|
|
6481
|
+
# count = int(request.json.get("count", 1))
|
|
6482
|
+
# out_dir = os.path.join(MEDIA_IMAGES_GENERATED_ICONS if kind == "icon" else MEDIA_IMAGES_GENERATED)
|
|
6483
|
+
|
|
6484
|
+
# if not prompt:
|
|
6485
|
+
# return jsonify({"error": "Missing prompt"}), 400
|
|
6486
|
+
|
|
6487
|
+
# vision_profile = smx.get_image_generator_profile()
|
|
6488
|
+
|
|
6489
|
+
# # Call the agent's generate_image function
|
|
6490
|
+
# try:
|
|
6491
|
+
# result = image_generator_agent(prompt, vision_profile, out_dir, count)
|
|
6492
|
+
# return jsonify({"items": result}), 200
|
|
6493
|
+
# except Exception as e:
|
|
6494
|
+
# return jsonify({"error": f"Image generation failed: {str(e)}"}), 500
|
|
6495
|
+
|
|
6496
|
+
|
|
5432
6497
|
# --- UPLOAD MEDIA --------------------------------------
|
|
5433
6498
|
@smx.app.route("/admin/upload_media", methods=["POST"])
|
|
5434
|
-
def upload_media():
|
|
5435
|
-
# Retrieve uploaded media files (images, videos, etc.).
|
|
6499
|
+
def upload_media():
|
|
5436
6500
|
uploaded_files = request.files.getlist("media_files")
|
|
5437
6501
|
file_paths = []
|
|
6502
|
+
|
|
5438
6503
|
for file in uploaded_files:
|
|
5439
|
-
if file.filename:
|
|
5440
|
-
|
|
6504
|
+
if not file or not file.filename:
|
|
6505
|
+
continue
|
|
6506
|
+
|
|
6507
|
+
fn = file.filename
|
|
6508
|
+
ext = os.path.splitext(fn.lower())[1]
|
|
6509
|
+
img_ext = {".png", ".jpg", ".jpeg", ".webp", ".gif"}
|
|
6510
|
+
vid_ext = {".mp4", ".webm", ".mov", ".m4v"}
|
|
6511
|
+
|
|
6512
|
+
if ext in img_ext:
|
|
6513
|
+
filepath = os.path.join(MEDIA_IMAGES_UPLOADED, fn)
|
|
5441
6514
|
file.save(filepath)
|
|
5442
|
-
|
|
5443
|
-
file_paths.append(f"/uploads/media/{
|
|
6515
|
+
rel = os.path.relpath(filepath, MEDIA_FOLDER).replace("\\", "/")
|
|
6516
|
+
file_paths.append(f"/uploads/media/{rel}")
|
|
6517
|
+
elif ext in vid_ext:
|
|
6518
|
+
filepath = os.path.join(MEDIA_VIDEOS_UPLOADED, fn)
|
|
6519
|
+
file.save(filepath)
|
|
6520
|
+
rel = os.path.relpath(filepath, MEDIA_FOLDER).replace("\\", "/")
|
|
6521
|
+
file_paths.append(f"/uploads/media/{rel}")
|
|
6522
|
+
else:
|
|
6523
|
+
filepath = os.path.join(MEDIA_FOLDER, fn)
|
|
6524
|
+
file.save(filepath)
|
|
6525
|
+
file_paths.append(f"/uploads/media/{fn}")
|
|
6526
|
+
|
|
5444
6527
|
return jsonify({"file_paths": file_paths})
|
|
5445
6528
|
|
|
6529
|
+
|
|
5446
6530
|
# Serve the raw media files
|
|
5447
6531
|
@smx.app.route('/uploads/media/<path:filename>')
|
|
5448
6532
|
def serve_media(filename):
|
|
5449
6533
|
media_dir = os.path.join(_CLIENT_DIR, 'uploads', 'media')
|
|
5450
6534
|
return send_from_directory(media_dir, filename)
|
|
5451
6535
|
|
|
6536
|
+
|
|
6537
|
+
@smx.app.route("/branding/<path:filename>")
|
|
6538
|
+
def serve_branding(filename):
|
|
6539
|
+
branding_dir = os.path.join(_CLIENT_DIR, "branding")
|
|
6540
|
+
return send_from_directory(branding_dir, filename)
|
|
6541
|
+
|
|
6542
|
+
|
|
5452
6543
|
# ────────────────────────────────────────────────────────────────────────────────────────
|
|
5453
6544
|
# DASHBOARD
|
|
5454
6545
|
# ────────────────────────────────────────────────────────────────────────────────────────
|
|
@@ -5460,104 +6551,7 @@ def setup_routes(smx):
|
|
|
5460
6551
|
|
|
5461
6552
|
max_rows = 5000
|
|
5462
6553
|
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
|
-
|
|
6554
|
+
|
|
5561
6555
|
section = request.args.get("section", "explore")
|
|
5562
6556
|
datasets = [f for f in os.listdir(DATA_FOLDER) if f.lower().endswith(".csv")]
|
|
5563
6557
|
selected_dataset = request.form.get("dataset") or request.args.get("dataset")
|
|
@@ -5594,6 +6588,8 @@ def setup_routes(smx):
|
|
|
5594
6588
|
eda_df = df
|
|
5595
6589
|
llm_usage = None
|
|
5596
6590
|
|
|
6591
|
+
TOKENS = {}
|
|
6592
|
+
|
|
5597
6593
|
if request.method == "POST" and "askai_question" in request.form:
|
|
5598
6594
|
askai_question = request.form["askai_question"].strip()
|
|
5599
6595
|
if df is not None:
|
|
@@ -5609,10 +6605,60 @@ def setup_routes(smx):
|
|
|
5609
6605
|
columns_summary = ", ".join(df.columns.tolist())
|
|
5610
6606
|
dataset_context = f"columns: {columns_summary}"
|
|
5611
6607
|
dataset_profile = f"modality: tabular; columns: {columns_summary}"
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
6608
|
+
|
|
6609
|
+
# ai_code = smx.ai_generate_code(refined_question, tags, df)
|
|
6610
|
+
# llm_usage = smx.get_last_llm_usage()
|
|
6611
|
+
# ai_code = auto_inject_template(ai_code, tags, df)
|
|
6612
|
+
|
|
6613
|
+
# # --- 1) Strip dotenv ASAP (kill imports, %magics, !pip) ---
|
|
6614
|
+
# ctx = {
|
|
6615
|
+
# "question": refined_question,
|
|
6616
|
+
# "df_columns": list(df.columns),
|
|
6617
|
+
# }
|
|
6618
|
+
# ai_code = ToolRunner(EARLY_SANITIZERS).run(ai_code, ctx) # dotenv first
|
|
6619
|
+
|
|
6620
|
+
# # --- 2) Domain/Plotting patches ---
|
|
6621
|
+
# ai_code = fix_scatter_and_summary(ai_code)
|
|
6622
|
+
# ai_code = fix_importance_groupby(ai_code)
|
|
6623
|
+
# ai_code = inject_auto_preprocessing(ai_code)
|
|
6624
|
+
# ai_code = patch_plot_code(ai_code, df, refined_question)
|
|
6625
|
+
# ai_code = ensure_matplotlib_title(ai_code)
|
|
6626
|
+
# ai_code = patch_pie_chart(ai_code, df, refined_question)
|
|
6627
|
+
# ai_code = patch_pairplot(ai_code, df)
|
|
6628
|
+
# ai_code = fix_seaborn_boxplot_nameerror(ai_code)
|
|
6629
|
+
# ai_code = fix_seaborn_barplot_nameerror(ai_code)
|
|
6630
|
+
# ai_code = get_plotting_imports(ai_code)
|
|
6631
|
+
# ai_code = patch_prefix_seaborn_calls(ai_code)
|
|
6632
|
+
# ai_code = patch_fix_sentinel_plot_calls(ai_code)
|
|
6633
|
+
# ai_code = patch_ensure_seaborn_import(ai_code)
|
|
6634
|
+
# ai_code = patch_rmse_calls(ai_code)
|
|
6635
|
+
# ai_code = patch_fix_seaborn_palette_calls(ai_code)
|
|
6636
|
+
# ai_code = patch_quiet_specific_warnings(ai_code)
|
|
6637
|
+
# ai_code = clean_llm_code(ai_code)
|
|
6638
|
+
# ai_code = ensure_image_output(ai_code)
|
|
6639
|
+
# ai_code = ensure_accuracy_block(ai_code)
|
|
6640
|
+
# ai_code = ensure_output(ai_code)
|
|
6641
|
+
# ai_code = fix_plain_prints(ai_code)
|
|
6642
|
+
# ai_code = fix_print_html(ai_code)
|
|
6643
|
+
# ai_code = fix_to_datetime_errors(ai_code)
|
|
6644
|
+
|
|
6645
|
+
# # --- 3-4) Global syntax/data fixers (must run AFTER patches, BEFORE final repair) ---
|
|
6646
|
+
# ai_code = ToolRunner(SYNTAX_AND_REPAIR).run(ai_code, ctx)
|
|
6647
|
+
|
|
6648
|
+
# # # --- 4) Final catch-all repair (run LAST) ---
|
|
6649
|
+
# ai_code = smx.repair_python_cell(ai_code)
|
|
6650
|
+
# ai_code = harden_ai_code(ai_code)
|
|
6651
|
+
# ai_code = drop_bad_classification_metrics(ai_code, df)
|
|
6652
|
+
# ai_code = patch_fix_sentinel_plot_calls(ai_code)
|
|
6653
|
+
|
|
6654
|
+
from syntaxmatrix.agentic import agents_orchestrer
|
|
6655
|
+
orch = agents_orchestrer.OrchestrateMLSystem(askai_question, cleaned_path)
|
|
6656
|
+
result = orch.operator_agent()
|
|
6657
|
+
|
|
6658
|
+
refined_question = result["specs_cot"]
|
|
6659
|
+
|
|
6660
|
+
compatibility = context_compatibility(askai_question, dataset_context)
|
|
6661
|
+
if compatibility.lower() == "incompatible" or compatibility.lower() == "mismatch":
|
|
5616
6662
|
return ("""
|
|
5617
6663
|
<div style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center;">
|
|
5618
6664
|
<h1 style="margin: 0 0 10px 0;">Oops: Context mismatch</h1>
|
|
@@ -5624,51 +6670,30 @@ def setup_routes(smx):
|
|
|
5624
6670
|
else:
|
|
5625
6671
|
tags = classify_ml_job_agent(refined_question, dataset_profile)
|
|
5626
6672
|
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
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)
|
|
6673
|
+
TOKENS["Refiner"] = [
|
|
6674
|
+
result['token_usage'].get('Refiner')['usage'].get('provider'),
|
|
6675
|
+
result['token_usage'].get('Refiner')['usage'].get('model'),
|
|
6676
|
+
result['token_usage'].get('Refiner')['usage'].get('input_tokens'),
|
|
6677
|
+
result['token_usage'].get('Refiner')['usage'].get('output_tokens'),
|
|
6678
|
+
result['token_usage'].get('Refiner')['usage'].get('total_tokens'),
|
|
6679
|
+
]
|
|
6680
|
+
TOKENS["Coder"] = [
|
|
6681
|
+
result['token_usage'].get('Coder')['usage'].get('provider'),
|
|
6682
|
+
result['token_usage'].get('Coder')['usage'].get('model'),
|
|
6683
|
+
result['token_usage'].get('Coder')['usage'].get('input_tokens'),
|
|
6684
|
+
result['token_usage'].get('Coder')['usage'].get('output_tokens'),
|
|
6685
|
+
result['token_usage'].get('Coder')['usage'].get('total_tokens'),
|
|
6686
|
+
]
|
|
5662
6687
|
|
|
5663
|
-
|
|
5664
|
-
ai_code =
|
|
5665
|
-
|
|
5666
|
-
#
|
|
5667
|
-
ai_code =
|
|
5668
|
-
ai_code =
|
|
5669
|
-
ai_code =
|
|
5670
|
-
ai_code =
|
|
5671
|
-
|
|
6688
|
+
ai_code = result.get("python_code", "")
|
|
6689
|
+
# ai_code = patch_quiet_specific_warnings(ai_code)
|
|
6690
|
+
# ai_code = fix_print_html(ai_code)
|
|
6691
|
+
# ai_code = fix_plain_prints(ai_code)
|
|
6692
|
+
# ai_code = harden_ai_code(ai_code)
|
|
6693
|
+
# ai_code = ensure_image_output(ai_code)
|
|
6694
|
+
# ai_code = ensure_accuracy_block(ai_code)
|
|
6695
|
+
# ai_code = ensure_output(ai_code)
|
|
6696
|
+
|
|
5672
6697
|
# Always make sure 'df' is in the kernel before running user code
|
|
5673
6698
|
df_init_code = (
|
|
5674
6699
|
f"import pandas as pd\n"
|
|
@@ -5904,14 +6929,34 @@ def setup_routes(smx):
|
|
|
5904
6929
|
|
|
5905
6930
|
# 3) Data Preview
|
|
5906
6931
|
preview_cols = df.columns
|
|
5907
|
-
|
|
6932
|
+
|
|
6933
|
+
head_df = _coerce_intlike_for_display(df[preview_cols].head(8))
|
|
5908
6934
|
data_cells.append({
|
|
5909
|
-
"title": "
|
|
5910
|
-
"output": Markup(datatable_box(
|
|
6935
|
+
"title": "Dataset Head",
|
|
6936
|
+
"output": Markup(datatable_box(head_df)),
|
|
5911
6937
|
"code": f"df[{list(preview_cols)}].head(8)",
|
|
5912
6938
|
"span": "eda-col-6"
|
|
5913
6939
|
})
|
|
5914
6940
|
|
|
6941
|
+
# Calculate the start index for the middle 8 rows
|
|
6942
|
+
n_rows = len(df)
|
|
6943
|
+
start_index = max(0, floor(n_rows / 2) - 4)
|
|
6944
|
+
middle_df = df.iloc[start_index : start_index + 8]
|
|
6945
|
+
data_cells.append({
|
|
6946
|
+
"title": "Dataset Middle (8 Rows)",
|
|
6947
|
+
"output": Markup(datatable_box(middle_df[list(preview_cols)])),
|
|
6948
|
+
"code": f"n = len(df)\nstart_index = max(0, floor(n / 2) - 4)\ndf.iloc[start_index : start_index + 8][{list(preview_cols)}]",
|
|
6949
|
+
"span": "eda-col-6"
|
|
6950
|
+
})
|
|
6951
|
+
|
|
6952
|
+
tail_df = _coerce_intlike_for_display(df[preview_cols].tail(8))
|
|
6953
|
+
data_cells.append({
|
|
6954
|
+
"title": "Dataset Tail",
|
|
6955
|
+
"output": Markup(datatable_box(tail_df)),
|
|
6956
|
+
"code": f"df[{list(preview_cols)}].tail(8)",
|
|
6957
|
+
"span": "eda-col-6"
|
|
6958
|
+
})
|
|
6959
|
+
|
|
5915
6960
|
# 4) Summary Statistics
|
|
5916
6961
|
summary_cols = df.columns
|
|
5917
6962
|
summary_df = _coerce_intlike_for_display(df[summary_cols].describe())
|
|
@@ -6265,7 +7310,7 @@ def setup_routes(smx):
|
|
|
6265
7310
|
"})\\n"
|
|
6266
7311
|
"missing_df[missing_df['Missing Values'] > 0]"
|
|
6267
7312
|
),
|
|
6268
|
-
"span":"eda-col-
|
|
7313
|
+
"span":"eda-col-3"
|
|
6269
7314
|
})
|
|
6270
7315
|
|
|
6271
7316
|
# 9) Missingness (Top 20) – Plotly bar chart
|
|
@@ -6504,7 +7549,7 @@ def setup_routes(smx):
|
|
|
6504
7549
|
"vc = s.value_counts(dropna=False)\n"
|
|
6505
7550
|
"top_k = 8 # Top-8 + Other (+ Missing)\n"
|
|
6506
7551
|
),
|
|
6507
|
-
"span": "eda-col-
|
|
7552
|
+
"span": "eda-col-5"
|
|
6508
7553
|
})
|
|
6509
7554
|
else:
|
|
6510
7555
|
data_cells.append({
|
|
@@ -6525,7 +7570,7 @@ def setup_routes(smx):
|
|
|
6525
7570
|
cell["highlighted_code"] = Markup(_pygmentize(cell["code"]))
|
|
6526
7571
|
|
|
6527
7572
|
highlighted_ai_code = _pygmentize(ai_code)
|
|
6528
|
-
smxAI = "
|
|
7573
|
+
smxAI = "smx-Orion"
|
|
6529
7574
|
|
|
6530
7575
|
return render_template(
|
|
6531
7576
|
"dashboard.html",
|
|
@@ -6541,7 +7586,7 @@ def setup_routes(smx):
|
|
|
6541
7586
|
smxAI=smxAI,
|
|
6542
7587
|
data_cells=data_cells,
|
|
6543
7588
|
session_id=session_id,
|
|
6544
|
-
|
|
7589
|
+
TOKENS=TOKENS
|
|
6545
7590
|
)
|
|
6546
7591
|
|
|
6547
7592
|
|
|
@@ -6868,4 +7913,5 @@ def setup_routes(smx):
|
|
|
6868
7913
|
{footer}
|
|
6869
7914
|
</body>
|
|
6870
7915
|
</html>
|
|
6871
|
-
""", error_message=str(e)), 500
|
|
7916
|
+
""", error_message=str(e)), 500
|
|
7917
|
+
|