synthos 0.10.0 → 0.11.0
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.
- package/README.md +5 -5
- package/default-pages/elevenlabs_effects_studio/chat-history.json +1 -0
- package/default-pages/elevenlabs_effects_studio/page.html +1345 -1363
- package/default-pages/elevenlabs_effects_studio/page.json +13 -11
- package/default-pages/elevenlabs_voice_studio/chat-history.json +1 -0
- package/default-pages/elevenlabs_voice_studio/page.html +782 -801
- package/default-pages/elevenlabs_voice_studio/page.json +13 -11
- package/default-pages/json_tools/chat-history.json +1 -0
- package/default-pages/json_tools/page.html +70 -90
- package/default-pages/json_tools/page.json +12 -10
- package/default-pages/my_notes/chat-history.json +1 -0
- package/default-pages/my_notes/page.html +115 -131
- package/default-pages/my_notes/page.json +14 -12
- package/default-pages/neon_asteroids/chat-history.json +1 -0
- package/default-pages/neon_asteroids/page.html +1777 -1803
- package/default-pages/neon_asteroids/page.json +14 -12
- package/default-pages/oregon_trail/chat-history.json +1 -0
- package/default-pages/oregon_trail/page.html +290 -307
- package/default-pages/oregon_trail/page.json +14 -12
- package/default-pages/solar_explorer/chat-history.json +1 -0
- package/default-pages/solar_explorer/page.html +1929 -1951
- package/default-pages/solar_explorer/page.json +14 -12
- package/default-pages/solar_tutorial/chat-history.json +1 -0
- package/default-pages/solar_tutorial/page.html +464 -478
- package/default-pages/solar_tutorial/page.json +12 -10
- package/default-pages/us_map/chat-history.json +1 -0
- package/default-pages/us_map/page.html +170 -193
- package/default-pages/us_map/page.json +14 -12
- package/default-pages/us_map/page.light.png +0 -0
- package/default-pages/us_map_1850/chat-history.json +1 -0
- package/default-pages/us_map_1850/page.html +302 -326
- package/default-pages/us_map_1850/page.json +14 -12
- package/default-pages/western_cities_1850/chat-history.json +1 -0
- package/default-pages/western_cities_1850/page.html +503 -527
- package/default-pages/western_cities_1850/page.json +14 -12
- package/default-themes/aurora-dawn.v3.css +15 -14
- package/default-themes/aurora-dusk.v3.css +26 -26
- package/default-themes/cosmos-dawn.v3.css +15 -14
- package/default-themes/cosmos-dusk.v3.css +26 -26
- package/default-themes/elemental-dawn.v3.css +200 -0
- package/default-themes/nebula-dawn.v3.css +15 -14
- package/default-themes/nebula-dusk.v3.css +24 -24
- package/default-themes/solar-flare-dawn.v3.css +15 -14
- package/default-themes/solar-flare-dusk.v3.css +26 -26
- package/dist/builders/anthropic.d.ts +26 -2
- package/dist/builders/anthropic.d.ts.map +1 -1
- package/dist/builders/anthropic.js +132 -31
- package/dist/builders/anthropic.js.map +1 -1
- package/dist/builders/claudecode.d.ts +13 -0
- package/dist/builders/claudecode.d.ts.map +1 -0
- package/dist/builders/claudecode.js +253 -0
- package/dist/builders/claudecode.js.map +1 -0
- package/dist/builders/index.d.ts +2 -1
- package/dist/builders/index.d.ts.map +1 -1
- package/dist/builders/index.js +8 -1
- package/dist/builders/index.js.map +1 -1
- package/dist/builders/openai.js +2 -1
- package/dist/builders/openai.js.map +1 -1
- package/dist/builders/types.d.ts +31 -7
- package/dist/builders/types.d.ts.map +1 -1
- package/dist/builders/types.js +60 -28
- package/dist/builders/types.js.map +1 -1
- package/dist/connectors/types.d.ts +8 -0
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +13 -6
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +161 -14
- package/dist/migrations.js.map +1 -1
- package/dist/models/anthropic.d.ts +1 -0
- package/dist/models/anthropic.d.ts.map +1 -1
- package/dist/models/anthropic.js +129 -29
- package/dist/models/anthropic.js.map +1 -1
- package/dist/models/chainOfThought.d.ts.map +1 -1
- package/dist/models/chainOfThought.js +32 -19
- package/dist/models/chainOfThought.js.map +1 -1
- package/dist/models/index.d.ts +2 -2
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +2 -1
- package/dist/models/index.js.map +1 -1
- package/dist/models/providers.d.ts +1 -0
- package/dist/models/providers.d.ts.map +1 -1
- package/dist/models/providers.js +12 -4
- package/dist/models/providers.js.map +1 -1
- package/dist/models/types.d.ts +15 -1
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js.map +1 -1
- package/dist/pages.d.ts +57 -8
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +258 -45
- package/dist/pages.js.map +1 -1
- package/dist/service/createCompletePrompt.d.ts.map +1 -1
- package/dist/service/createCompletePrompt.js +5 -0
- package/dist/service/createCompletePrompt.js.map +1 -1
- package/dist/service/mediaCache.d.ts +36 -0
- package/dist/service/mediaCache.d.ts.map +1 -0
- package/dist/service/mediaCache.js +182 -0
- package/dist/service/mediaCache.js.map +1 -0
- package/dist/service/pageValidator.d.ts +25 -0
- package/dist/service/pageValidator.d.ts.map +1 -0
- package/dist/service/pageValidator.js +315 -0
- package/dist/service/pageValidator.js.map +1 -0
- package/dist/service/server.d.ts.map +1 -1
- package/dist/service/server.js +4 -0
- package/dist/service/server.js.map +1 -1
- package/dist/service/sharedTableSchema.d.ts +73 -0
- package/dist/service/sharedTableSchema.d.ts.map +1 -0
- package/dist/service/sharedTableSchema.js +206 -0
- package/dist/service/sharedTableSchema.js.map +1 -0
- package/dist/service/transformPage.d.ts +49 -11
- package/dist/service/transformPage.d.ts.map +1 -1
- package/dist/service/transformPage.js +354 -241
- package/dist/service/transformPage.js.map +1 -1
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +288 -34
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.d.ts.map +1 -1
- package/dist/service/useConnectorRoutes.js +170 -32
- package/dist/service/useConnectorRoutes.js.map +1 -1
- package/dist/service/useDataRoutes.d.ts.map +1 -1
- package/dist/service/useDataRoutes.js +59 -2
- package/dist/service/useDataRoutes.js.map +1 -1
- package/dist/service/useExtractRoutes.d.ts +4 -0
- package/dist/service/useExtractRoutes.d.ts.map +1 -0
- package/dist/service/useExtractRoutes.js +304 -0
- package/dist/service/useExtractRoutes.js.map +1 -0
- package/dist/service/usePageRoutes.d.ts +17 -0
- package/dist/service/usePageRoutes.d.ts.map +1 -1
- package/dist/service/usePageRoutes.js +1385 -483
- package/dist/service/usePageRoutes.js.map +1 -1
- package/dist/service/useSharedDataRoutes.d.ts.map +1 -1
- package/dist/service/useSharedDataRoutes.js +54 -2
- package/dist/service/useSharedDataRoutes.js.map +1 -1
- package/dist/settings.d.ts +27 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +40 -1
- package/dist/settings.js.map +1 -1
- package/dist/themes.d.ts +0 -5
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +3 -95
- package/dist/themes.js.map +1 -1
- package/migration-rules/v2-to-v3.md +277 -119
- package/package.json +5 -1
- package/{default-pages/application → required-pages/_shell}/page.html +56 -42
- package/required-pages/_shell/page.json +14 -0
- package/required-pages/_starters/page.html +534 -0
- package/required-pages/_starters/page.json +12 -0
- package/required-pages/builder/page.html +353 -43
- package/required-pages/builder/page.json +12 -10
- package/required-pages/pages/page.html +697 -924
- package/required-pages/pages/page.json +12 -10
- package/required-pages/settings/page.html +1879 -1753
- package/required-pages/settings/page.json +12 -10
- package/required-pages/synthos_apis/page.html +834 -845
- package/required-pages/synthos_apis/page.json +12 -10
- package/required-pages/synthos_scripts/page.html +74 -88
- package/required-pages/synthos_scripts/page.json +12 -10
- package/scripts/append-instructions.py +90 -0
- package/scripts/audit-instructions.py +76 -0
- package/scripts/cleanup-shell-markup.mjs +112 -0
- package/service-connectors/buffer/connector.json +46 -0
- package/service-connectors/canva/connector.json +67 -0
- package/service-connectors/elevenlabs/connector.json +1 -1
- package/src/builders/anthropic.ts +155 -30
- package/src/builders/claudecode.ts +310 -0
- package/src/builders/index.ts +7 -1
- package/src/builders/openai.ts +2 -1
- package/src/builders/types.ts +93 -32
- package/src/connectors/types.ts +8 -0
- package/src/init.ts +13 -7
- package/src/migrations.ts +187 -16
- package/src/models/anthropic.ts +140 -30
- package/src/models/chainOfThought.ts +33 -18
- package/src/models/index.ts +2 -2
- package/src/models/providers.ts +12 -3
- package/src/models/types.ts +21 -1
- package/src/pages.ts +271 -35
- package/src/service/createCompletePrompt.ts +6 -0
- package/src/service/mediaCache.ts +206 -0
- package/src/service/pageValidator.ts +337 -0
- package/src/service/server.ts +4 -0
- package/src/service/sharedTableSchema.ts +236 -0
- package/src/service/transformPage.ts +370 -260
- package/src/service/useApiRoutes.ts +282 -32
- package/src/service/useConnectorRoutes.ts +189 -34
- package/src/service/useDataRoutes.ts +198 -116
- package/src/service/useExtractRoutes.ts +331 -0
- package/src/service/usePageRoutes.ts +1411 -394
- package/src/service/useSharedDataRoutes.ts +184 -109
- package/src/settings.ts +65 -0
- package/src/themes.ts +78 -180
- package/starters/blank_starter/chat-history.json +1 -0
- package/starters/blank_starter/page.dark.png +0 -0
- package/starters/blank_starter/page.html +47 -0
- package/starters/blank_starter/page.json +13 -0
- package/starters/blank_starter/page.light.png +0 -0
- package/starters/calculator_starter/chat-history.json +1 -0
- package/starters/calculator_starter/page.dark.png +0 -0
- package/starters/calculator_starter/page.html +232 -0
- package/starters/calculator_starter/page.json +13 -0
- package/starters/calculator_starter/page.light.png +0 -0
- package/starters/calendar_starter/chat-history.json +1 -0
- package/starters/calendar_starter/page.dark.png +0 -0
- package/starters/calendar_starter/page.html +495 -0
- package/starters/calendar_starter/page.json +13 -0
- package/starters/calendar_starter/page.light.png +0 -0
- package/starters/chat_starter/chat-history.json +1 -0
- package/starters/chat_starter/page.dark.png +0 -0
- package/starters/chat_starter/page.html +351 -0
- package/starters/chat_starter/page.json +13 -0
- package/starters/chat_starter/page.light.png +0 -0
- package/starters/checklist_starter/chat-history.json +1 -0
- package/starters/checklist_starter/page.dark.png +0 -0
- package/starters/checklist_starter/page.html +437 -0
- package/starters/checklist_starter/page.json +13 -0
- package/starters/checklist_starter/page.light.png +0 -0
- package/starters/dashboard_starter/chat-history.json +1 -0
- package/starters/dashboard_starter/page.dark.png +0 -0
- package/starters/dashboard_starter/page.html +195 -0
- package/starters/dashboard_starter/page.json +13 -0
- package/starters/dashboard_starter/page.light.png +0 -0
- package/starters/form_starter/chat-history.json +1 -0
- package/starters/form_starter/page.dark.png +0 -0
- package/starters/form_starter/page.html +313 -0
- package/starters/form_starter/page.json +13 -0
- package/starters/form_starter/page.light.png +0 -0
- package/starters/gallery_starter/chat-history.json +1 -0
- package/starters/gallery_starter/page.dark.png +0 -0
- package/starters/gallery_starter/page.html +418 -0
- package/starters/gallery_starter/page.json +13 -0
- package/starters/gallery_starter/page.light.png +0 -0
- package/starters/generator_starter/chat-history.json +1 -0
- package/starters/generator_starter/page.dark.png +0 -0
- package/starters/generator_starter/page.html +261 -0
- package/starters/generator_starter/page.json +13 -0
- package/starters/generator_starter/page.light.png +0 -0
- package/starters/index.html +538 -0
- package/starters/kanban_starter/chat-history.json +1 -0
- package/starters/kanban_starter/page.dark.png +0 -0
- package/starters/kanban_starter/page.html +432 -0
- package/starters/kanban_starter/page.json +13 -0
- package/starters/kanban_starter/page.light.png +0 -0
- package/starters/presentation_builder/chat-history.json +1 -0
- package/starters/presentation_builder/page.dark.png +0 -0
- package/starters/presentation_builder/page.html +970 -0
- package/starters/presentation_builder/page.json +15 -0
- package/starters/presentation_builder/page.light.png +0 -0
- package/starters/presentation_builder/presentation_voice/voice_config.json +9 -0
- package/starters/pulse_starter/chat-history.json +1 -0
- package/starters/pulse_starter/page.dark.png +0 -0
- package/starters/pulse_starter/page.html +698 -0
- package/starters/pulse_starter/page.json +13 -0
- package/starters/pulse_starter/page.light.png +0 -0
- package/starters/quiz_starter/chat-history.json +1 -0
- package/starters/quiz_starter/page.dark.png +0 -0
- package/starters/quiz_starter/page.html +292 -0
- package/starters/quiz_starter/page.json +13 -0
- package/starters/quiz_starter/page.light.png +0 -0
- package/starters/reference_starter/chat-history.json +1 -0
- package/starters/reference_starter/page.dark.png +0 -0
- package/starters/reference_starter/page.html +250 -0
- package/starters/reference_starter/page.json +13 -0
- package/starters/reference_starter/page.light.png +0 -0
- package/starters/retro_game_starter/chat-history.json +1 -0
- package/starters/retro_game_starter/page.dark.png +0 -0
- package/{default-pages → starters}/retro_game_starter/page.html +1281 -1308
- package/starters/retro_game_starter/page.json +15 -0
- package/starters/retro_game_starter/page.light.png +0 -0
- package/starters/roster_starter/chat-history.json +1 -0
- package/starters/roster_starter/page.dark.png +0 -0
- package/starters/roster_starter/page.html +600 -0
- package/starters/roster_starter/page.json +13 -0
- package/starters/roster_starter/page.light.png +0 -0
- package/starters/server.js +182 -0
- package/starters/start.cmd +1 -0
- package/starters/timeline_starter/chat-history.json +1 -0
- package/starters/timeline_starter/page.dark.png +0 -0
- package/starters/timeline_starter/page.html +446 -0
- package/starters/timeline_starter/page.json +13 -0
- package/starters/timeline_starter/page.light.png +0 -0
- package/starters/tutorial_starter/chat-history.json +1 -0
- package/starters/tutorial_starter/page.dark.png +0 -0
- package/starters/tutorial_starter/page.html +283 -0
- package/starters/tutorial_starter/page.json +13 -0
- package/starters/tutorial_starter/page.light.png +0 -0
- package/static-files/agent.v3.js +122 -0
- package/static-files/connector.v3.js +48 -0
- package/static-files/extract.v3.js +188 -0
- package/static-files/helpers.v3.js +50 -6
- package/static-files/page-bridge.js +114 -0
- package/static-files/page.v3.js +1292 -1290
- package/static-files/script.v3.js +32 -0
- package/static-files/server.v3.js +89 -0
- package/static-files/shell-bridge.v3.js +174 -0
- package/static-files/shell-modals.v3.js +521 -0
- package/static-files/{shell.css → shell.v3.css} +271 -22
- package/static-files/shell.v3.js +1865 -0
- package/static-files/storage.v3.js +176 -0
- package/tests/anthropic.spec.ts +42 -7
- package/tests/builders.spec.ts +72 -4
- package/tests/pageValidator.spec.ts +548 -0
- package/tests/profiles.spec.ts +122 -0
- package/tests/providers.spec.ts +1 -1
- package/tests/sharedTableSchema.spec.ts +242 -0
- package/tests/transformPage.spec.ts +62 -81
- package/default-pages/application/page.json +0 -10
- package/default-pages/retro_game_starter/page.json +0 -12
- package/default-pages/sidebar_page/page.html +0 -51
- package/default-pages/sidebar_page/page.json +0 -10
- package/default-pages/two-panel_page/page.html +0 -68
- package/default-pages/two-panel_page/page.json +0 -10
|
@@ -26,7 +26,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
26
26
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
27
|
};
|
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
-
exports.loadPageWithFallback = exports.usePageRoutes = void 0;
|
|
29
|
+
exports.loadPageWithFallback = exports.usePageRoutes = exports.applySectionModeOverrides = exports.assembleSections = void 0;
|
|
30
30
|
const pages_1 = require("../pages");
|
|
31
31
|
const settings_1 = require("../settings");
|
|
32
32
|
const transformPage_1 = require("./transformPage");
|
|
@@ -34,11 +34,15 @@ const createCompletePrompt_1 = require("./createCompletePrompt");
|
|
|
34
34
|
const debugLog_1 = require("./debugLog");
|
|
35
35
|
const themes_1 = require("../themes");
|
|
36
36
|
const builders_1 = require("../builders");
|
|
37
|
+
const anthropic_1 = require("../builders/anthropic");
|
|
37
38
|
const connectors_1 = require("../connectors");
|
|
38
39
|
const scripts_1 = require("../scripts");
|
|
39
40
|
const path_1 = __importDefault(require("path"));
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const crypto = __importStar(require("crypto"));
|
|
40
43
|
const files_1 = require("../files");
|
|
41
44
|
const cheerio = __importStar(require("cheerio"));
|
|
45
|
+
const sharedTableSchema_1 = require("./sharedTableSchema");
|
|
42
46
|
/**
|
|
43
47
|
* Required CDN imports that must be present on every v2 page.
|
|
44
48
|
* Each entry maps a detection selector to the script tag src to inject.
|
|
@@ -47,23 +51,113 @@ const REQUIRED_IMPORTS = [
|
|
|
47
51
|
{ selector: 'script[src*="marked"]', src: 'https://cdnjs.cloudflare.com/ajax/libs/marked/14.1.1/marked.min.js' },
|
|
48
52
|
{ selector: 'script[src*="html2canvas"]', src: 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js' },
|
|
49
53
|
];
|
|
54
|
+
// ensureRequiredImports is now ensureRequiredImports$() — operates on shared $ instance
|
|
55
|
+
const HOME_PAGE_ROUTE = '/builder';
|
|
56
|
+
const PAGE_NOT_FOUND = 'Page not found';
|
|
57
|
+
const SHELL_PAGE_NAME = '_shell';
|
|
58
|
+
const NO_PERSIST_CATEGORIES = ['_Starters', 'System'];
|
|
59
|
+
// Pages that are served as top-level documents (peers to _shell) — never wrapped
|
|
60
|
+
// in the shell iframe chrome. The page itself owns the full viewport.
|
|
61
|
+
const STANDALONE_PAGE_NAMES = ['_starters'];
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Cache-busting for /static assets
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Versions are computed lazily from file mtime + size, so any edit/deploy
|
|
66
|
+
// produces a new ?v=<hash> suffix and clients fetch fresh content.
|
|
67
|
+
let _staticFolders = [];
|
|
68
|
+
const _assetVersions = new Map();
|
|
69
|
+
function initStaticAssetVersioning(folders) {
|
|
70
|
+
_staticFolders = folders;
|
|
71
|
+
_assetVersions.clear();
|
|
72
|
+
}
|
|
73
|
+
function getAssetVersion(filename) {
|
|
74
|
+
const cached = _assetVersions.get(filename);
|
|
75
|
+
if (cached)
|
|
76
|
+
return cached;
|
|
77
|
+
for (const folder of _staticFolders) {
|
|
78
|
+
try {
|
|
79
|
+
const stat = fs.statSync(path_1.default.join(folder, filename));
|
|
80
|
+
const version = crypto.createHash('sha1')
|
|
81
|
+
.update(`${stat.mtimeMs}:${stat.size}`)
|
|
82
|
+
.digest('hex').slice(0, 8);
|
|
83
|
+
_assetVersions.set(filename, version);
|
|
84
|
+
return version;
|
|
85
|
+
}
|
|
86
|
+
catch { /* try next folder */ }
|
|
87
|
+
}
|
|
88
|
+
_assetVersions.set(filename, 'dev');
|
|
89
|
+
return 'dev';
|
|
90
|
+
}
|
|
91
|
+
function assetUrl(filename) {
|
|
92
|
+
return `/static/${filename}?v=${getAssetVersion(filename)}`;
|
|
93
|
+
}
|
|
50
94
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
95
|
+
* Rewrite /static/<file> URLs in HTML to append cache-busting ?v=<hash>.
|
|
96
|
+
* Matches href= and src= attributes only; leaves URLs with existing query strings alone.
|
|
53
97
|
*/
|
|
54
|
-
function
|
|
55
|
-
|
|
98
|
+
function applyAssetVersions(html) {
|
|
99
|
+
return html.replace(/(href|src)="\/static\/([^"?]+)"/g, (_m, attr, file) => `${attr}="${assetUrl(file)}"`);
|
|
100
|
+
}
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Per-page concurrency lock
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Serializes concurrent transform/patch requests for the same page to prevent
|
|
105
|
+
// version clobbering (two requests reading the same currentVersion, both
|
|
106
|
+
// writing to page.v{N+1}.html). Different pages run in parallel.
|
|
107
|
+
const pageLocks = new Map();
|
|
108
|
+
function withPageLock(page, fn) {
|
|
109
|
+
const prev = pageLocks.get(page) ?? Promise.resolve();
|
|
110
|
+
let releaseLock;
|
|
111
|
+
const next = new Promise(resolve => { releaseLock = resolve; });
|
|
112
|
+
pageLocks.set(page, next);
|
|
113
|
+
return prev.then(fn).finally(() => {
|
|
114
|
+
releaseLock();
|
|
115
|
+
// Clean up if no new work was queued behind us
|
|
116
|
+
if (pageLocks.get(page) === next)
|
|
117
|
+
pageLocks.delete(page);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/** Attributes safe to perform product-name replacement in (user-visible text). */
|
|
121
|
+
const SAFE_REPLACE_ATTRS = ['alt', 'placeholder', 'aria-label', 'title', 'content'];
|
|
122
|
+
/**
|
|
123
|
+
* Replace "SynthOS" (case-insensitive) with the custom product name in
|
|
124
|
+
* user-visible text only. Skips <script>, <style>, and code-bearing
|
|
125
|
+
* attributes (class, id, src, href, onclick, data-*, etc.) to avoid
|
|
126
|
+
* breaking `window.synthos.*` API calls, CSS classes, and JS identifiers.
|
|
127
|
+
*/
|
|
128
|
+
function replaceProductName(html, productName) {
|
|
129
|
+
if (productName === 'SynthOS')
|
|
56
130
|
return html;
|
|
57
131
|
const $ = cheerio.load(html);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
132
|
+
const regex = /synthos/gi;
|
|
133
|
+
// Replace in safe attributes on all elements
|
|
134
|
+
$('*').each((_i, el) => {
|
|
135
|
+
if (el.type !== 'tag')
|
|
136
|
+
return;
|
|
137
|
+
const elem = $(el);
|
|
138
|
+
for (const attr of SAFE_REPLACE_ATTRS) {
|
|
139
|
+
const val = elem.attr(attr);
|
|
140
|
+
if (val && regex.test(val)) {
|
|
141
|
+
elem.attr(attr, val.replace(regex, productName));
|
|
142
|
+
}
|
|
61
143
|
}
|
|
62
|
-
}
|
|
144
|
+
});
|
|
145
|
+
// Replace in text nodes, but skip <script> and <style> contents.
|
|
146
|
+
// Use a simple selector approach: find all text-bearing elements that
|
|
147
|
+
// are NOT script or style, and replace text in their direct text nodes.
|
|
148
|
+
$('*').not('script, style').contents().each((_i, node) => {
|
|
149
|
+
if (node.type === 'text' && node.data) {
|
|
150
|
+
// Skip if parent is script or style (contents() can include nested)
|
|
151
|
+
const parentTag = node.parentNode?.tagName?.toLowerCase?.() ?? node.parentNode?.name?.toLowerCase?.();
|
|
152
|
+
if (parentTag === 'script' || parentTag === 'style')
|
|
153
|
+
return;
|
|
154
|
+
if (regex.test(node.data)) {
|
|
155
|
+
node.data = node.data.replace(regex, productName);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
63
159
|
return $.html();
|
|
64
160
|
}
|
|
65
|
-
const HOME_PAGE_ROUTE = '/builder';
|
|
66
|
-
const PAGE_NOT_FOUND = 'Page not found';
|
|
67
161
|
function injectPageInfoScript(html, pageName) {
|
|
68
162
|
const tag = `<script id="page-info" src="/api/page-info.js?page=${encodeURIComponent(pageName)}"></script>`;
|
|
69
163
|
// Replace any existing page-info script (may have a stale page name from the template)
|
|
@@ -77,43 +171,7 @@ function injectPageInfoScript(html, pageName) {
|
|
|
77
171
|
}
|
|
78
172
|
return tag + '\n' + html;
|
|
79
173
|
}
|
|
80
|
-
|
|
81
|
-
if (pageVersion < 2)
|
|
82
|
-
return html;
|
|
83
|
-
const tag = `<script id="page-helpers" src="/api/page-helpers.js?v=${pageVersion}"></script>`;
|
|
84
|
-
// Remove any existing page-helpers script (may be at wrong position from prior LLM output)
|
|
85
|
-
// so it gets re-injected at the correct position below.
|
|
86
|
-
const existing = html.match(/<script\s+id="page-helpers"[^>]*><\/script>/);
|
|
87
|
-
if (existing) {
|
|
88
|
-
html = html.replace(existing[0], '');
|
|
89
|
-
}
|
|
90
|
-
// Inject into <head> after page-info so helpers are available before inline body scripts
|
|
91
|
-
const pageInfo = html.indexOf('id="page-info"');
|
|
92
|
-
if (pageInfo !== -1) {
|
|
93
|
-
const closeTag = html.indexOf('</script>', pageInfo);
|
|
94
|
-
if (closeTag !== -1) {
|
|
95
|
-
const insertAt = closeTag + '</script>'.length;
|
|
96
|
-
return html.slice(0, insertAt) + '\n' + tag + html.slice(insertAt);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
const idx = html.indexOf('</head>');
|
|
100
|
-
if (idx !== -1) {
|
|
101
|
-
return html.slice(0, idx) + tag + '\n' + html.slice(idx);
|
|
102
|
-
}
|
|
103
|
-
return tag + '\n' + html;
|
|
104
|
-
}
|
|
105
|
-
function injectPageScript(html, pageVersion) {
|
|
106
|
-
if (pageVersion < 2)
|
|
107
|
-
return html;
|
|
108
|
-
if (html.includes('id="page-script"'))
|
|
109
|
-
return html;
|
|
110
|
-
const tag = `<script id="page-script" src="/api/page-script.js?v=${pageVersion}"></script>`;
|
|
111
|
-
const idx = html.indexOf('</body>');
|
|
112
|
-
if (idx !== -1) {
|
|
113
|
-
return html.slice(0, idx) + tag + '\n' + html.slice(idx);
|
|
114
|
-
}
|
|
115
|
-
return html + '\n' + tag;
|
|
116
|
-
}
|
|
174
|
+
// injectPageHelpers is now injectPageHelpers$() — operates on shared $ instance
|
|
117
175
|
/**
|
|
118
176
|
* Wrap each inline <script> body in an IIFE so that top-level const/let
|
|
119
177
|
* declarations become function-scoped. This prevents "Identifier … has
|
|
@@ -138,92 +196,27 @@ function wrapInlineScriptsInIIFE(html) {
|
|
|
138
196
|
return `<script${attrs}>(function(){${body}})();</script>`;
|
|
139
197
|
});
|
|
140
198
|
}
|
|
141
|
-
|
|
142
|
-
* Move any external <script src="..."> tags found in <body> up to the end
|
|
143
|
-
* of <head>, preserving their relative order. Library scripts (d3, Chart.js,
|
|
144
|
-
* marked, etc.) only define globals and don't touch the DOM, so executing
|
|
145
|
-
* them in <head> is safe and guarantees they are available before any inline
|
|
146
|
-
* <body> scripts run. This also eliminates browser "parser-blocking cross
|
|
147
|
-
* site script invoked via document.write" warnings.
|
|
148
|
-
*/
|
|
149
|
-
function hoistExternalScriptsToHead(html) {
|
|
150
|
-
const $ = cheerio.load(html, { decodeEntities: false });
|
|
151
|
-
const hoisted = [];
|
|
152
|
-
$('body script[src]').each((_, el) => {
|
|
153
|
-
const script = $(el);
|
|
154
|
-
// Don't move data-locked system scripts (page-script, page-helpers, etc.)
|
|
155
|
-
if (script.attr('data-locked') !== undefined)
|
|
156
|
-
return;
|
|
157
|
-
hoisted.push($.html(script));
|
|
158
|
-
script.remove();
|
|
159
|
-
});
|
|
160
|
-
if (hoisted.length > 0) {
|
|
161
|
-
$('head').append(hoisted.join('\n') + '\n');
|
|
162
|
-
}
|
|
163
|
-
return $.html();
|
|
164
|
-
}
|
|
199
|
+
// hoistExternalScriptsToHead is now hoistExternalScriptsToHead$() — operates on shared $ instance
|
|
165
200
|
/**
|
|
166
201
|
* Inline error-capture script injected as the first child of <head> so it
|
|
167
202
|
* registers window.onerror / unhandledrejection *before* any page scripts run.
|
|
168
203
|
* Stripped before page transformation so the LLM never sees it.
|
|
169
204
|
*/
|
|
170
|
-
const ERROR_CAPTURE_ID = 'synthos-error-capture';
|
|
171
|
-
const ERROR_CAPTURE_SCRIPT = `<script id="${ERROR_CAPTURE_ID}">
|
|
172
|
-
(function(){
|
|
173
|
-
var E=window.__synthOSErrors=[];
|
|
174
|
-
window.onerror=function(m,s,l,c,e){
|
|
175
|
-
var entry=m+' at '+(s||'?')+':'+(l||'?')+':'+(c||'?');
|
|
176
|
-
if(e&&e.stack)entry+='\\n'+e.stack;
|
|
177
|
-
E.push(entry);showErr();return false;
|
|
178
|
-
};
|
|
179
|
-
window.addEventListener('unhandledrejection',function(ev){
|
|
180
|
-
var r=ev.reason;
|
|
181
|
-
E.push('Unhandled rejection: '+(r&&r.stack?r.stack:String(r)));showErr();
|
|
182
|
-
});
|
|
183
|
-
function showErr(){
|
|
184
|
-
var cm=document.getElementById('chatMessages');if(!cm)return;
|
|
185
|
-
if(showErr._p)return;showErr._p=true;
|
|
186
|
-
setTimeout(function(){
|
|
187
|
-
showErr._p=false;
|
|
188
|
-
var d=document.createElement('div');d.className='chat-message';
|
|
189
|
-
var p=document.createElement('p');
|
|
190
|
-
var pn=(window.pageInfo&&window.pageInfo.productName)||'SynthOS';
|
|
191
|
-
p.innerHTML='<strong>'+pn+':</strong> I noticed a JavaScript error on this page. '+
|
|
192
|
-
'<a href="#" style="color:var(--accent-primary,#a78bfa);text-decoration:underline;cursor:pointer" '+
|
|
193
|
-
'onclick="(function(e){e.preventDefault();var ci=document.getElementById(\\'chatInput\\');'+
|
|
194
|
-
'var f=document.getElementById(\\'chatForm\\');if(!ci||!f)return;'+
|
|
195
|
-
'ci.value=\\'Fix the following JavaScript errors on this page:\\\\n\\\\nCONSOLE_ERRORS:\\\\n\\'+window.__synthOSErrors.join(\\'\\\\n---\\\\n\\');'+
|
|
196
|
-
'window.__synthOSErrors=[];f.requestSubmit?f.requestSubmit():f.submit();})(event)">'+
|
|
197
|
-
'Let me try to fix it</a>';
|
|
198
|
-
d.appendChild(p);cm.appendChild(d);
|
|
199
|
-
cm.scrollTo({top:cm.scrollHeight,behavior:'smooth'});
|
|
200
|
-
},500);
|
|
201
|
-
}
|
|
202
|
-
})();
|
|
203
|
-
</script>`;
|
|
204
|
-
function injectErrorCapture(html, pageVersion) {
|
|
205
|
-
if (pageVersion < 2)
|
|
206
|
-
return html;
|
|
207
|
-
if (html.includes(`id="${ERROR_CAPTURE_ID}"`))
|
|
208
|
-
return html;
|
|
209
|
-
const $ = cheerio.load(html, { decodeEntities: false });
|
|
210
|
-
$('head').prepend(ERROR_CAPTURE_SCRIPT + '\n');
|
|
211
|
-
return $.html();
|
|
212
|
-
}
|
|
213
205
|
/**
|
|
214
|
-
* Inject shell.css
|
|
215
|
-
*
|
|
206
|
+
* Inject shell.v3.css and FluentLM base CSS/JS (always — the shell always emits
|
|
207
|
+
* FluentLM-keyed modals). The theme-name class on <html> stays gated to v3 so
|
|
208
|
+
* pre-v3 themes don't accidentally activate scoped rules.
|
|
216
209
|
*/
|
|
217
210
|
function injectShellAssets(html, themeName, themeVersion, toolbarPosition) {
|
|
218
211
|
const $ = cheerio.load(html, { decodeEntities: false });
|
|
219
212
|
const themeLink = $('link#theme-css');
|
|
220
213
|
// Favicon
|
|
221
214
|
if ($('link#synthos-favicon').length === 0) {
|
|
222
|
-
$('head').prepend(
|
|
215
|
+
$('head').prepend(`<link id="synthos-favicon" rel="icon" type="image/svg+xml" href="${assetUrl('favicon.svg')}">\n`);
|
|
223
216
|
}
|
|
224
|
-
// shell.css — always injected (provides toolbar + layout chrome)
|
|
217
|
+
// shell.v3.css — always injected (provides toolbar + layout chrome)
|
|
225
218
|
if ($('link#shell-css').length === 0) {
|
|
226
|
-
const shellLink =
|
|
219
|
+
const shellLink = `<link id="shell-css" rel="stylesheet" href="${assetUrl('shell.v3.css')}">`;
|
|
227
220
|
if (themeLink.length > 0) {
|
|
228
221
|
themeLink.before(shellLink + '\n');
|
|
229
222
|
}
|
|
@@ -231,36 +224,291 @@ function injectShellAssets(html, themeName, themeVersion, toolbarPosition) {
|
|
|
231
224
|
$('head').append(shellLink + '\n');
|
|
232
225
|
}
|
|
233
226
|
}
|
|
234
|
-
//
|
|
227
|
+
// Theme-name class on <html> — only for v3 themes that ship scoped rules
|
|
235
228
|
if (themeVersion >= 3) {
|
|
236
|
-
// Add theme name class to <html> (e.g. "nebula-dusk")
|
|
237
229
|
$('html').addClass(themeName);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
else if (themeLink.length > 0) {
|
|
246
|
-
themeLink.before(fluentLink + '\n');
|
|
247
|
-
}
|
|
248
|
-
else {
|
|
249
|
-
$('head').append(fluentLink + '\n');
|
|
250
|
-
}
|
|
230
|
+
}
|
|
231
|
+
// FluentLM CSS — always injected; shell modals depend on .flm-dialog-overlay
|
|
232
|
+
if ($('link#fluentlm-css').length === 0) {
|
|
233
|
+
const fluentLink = `<link id="fluentlm-css" rel="stylesheet" href="${assetUrl('fluentlm.min.css')}">`;
|
|
234
|
+
const shellCss = $('link#shell-css');
|
|
235
|
+
if (shellCss.length > 0) {
|
|
236
|
+
shellCss.before(fluentLink + '\n');
|
|
251
237
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
238
|
+
else if (themeLink.length > 0) {
|
|
239
|
+
themeLink.before(fluentLink + '\n');
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
$('head').append(fluentLink + '\n');
|
|
256
243
|
}
|
|
257
244
|
}
|
|
245
|
+
// FluentLM JS — always injected
|
|
246
|
+
if ($('script#fluentlm-js').length === 0) {
|
|
247
|
+
const fluentScript = `<script id="fluentlm-js" src="${assetUrl('fluentlm.min.js')}"></script>`;
|
|
248
|
+
$('body').append(fluentScript + '\n');
|
|
249
|
+
}
|
|
258
250
|
$('html').attr('data-toolbar', toolbarPosition || 'left');
|
|
259
251
|
return $.html();
|
|
260
252
|
}
|
|
261
253
|
// ---------------------------------------------------------------------------
|
|
254
|
+
// iframe architecture helpers
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// Cheerio-based transform steps (operate on shared $ instance)
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
/**
|
|
260
|
+
* Strip shell markup (toolbar, chat panel) from the in-memory DOM.
|
|
261
|
+
*/
|
|
262
|
+
function stripShellMarkup$($) {
|
|
263
|
+
$('.shell-toolbar').remove();
|
|
264
|
+
$('.chat-panel').remove();
|
|
265
|
+
$('#loadingOverlay').remove();
|
|
266
|
+
$('script#page-script').remove();
|
|
267
|
+
$('script#synthos-error-capture').remove();
|
|
268
|
+
// v2-legacy idle animation — the inline <script id="idle-animation"> block
|
|
269
|
+
// declares top-level hideIdleAnimation/showIdleAnimation functions that
|
|
270
|
+
// get wrapped in an IIFE by wrapInlineScriptsInIIFE, so cross-script
|
|
271
|
+
// callers hit ReferenceError. Strip the block and provide no-op globals
|
|
272
|
+
// via the stub script below.
|
|
273
|
+
$('script#idle-animation').remove();
|
|
274
|
+
// Inject hidden stub elements for shell IDs that legacy page scripts may
|
|
275
|
+
// reference (chatForm, chatInput, chatMessages, loadingOverlay) and no-op
|
|
276
|
+
// globals for v2-era helper functions (hideIdleAnimation,
|
|
277
|
+
// showIdleAnimation) that used to live on window via top-level function
|
|
278
|
+
// declarations. Still needed: 17+ pages reference these IDs. Remove once
|
|
279
|
+
// all pages are migrated to use postMessage bridge instead of direct DOM
|
|
280
|
+
// access.
|
|
281
|
+
const stubScript = `<script data-shell-compat="true">(function(){` +
|
|
282
|
+
`var s={chatForm:'form',chatInput:'textarea',chatMessages:'div',loadingOverlay:'div'};` +
|
|
283
|
+
`for(var id in s){if(!document.getElementById(id)){` +
|
|
284
|
+
`var el=document.createElement(s[id]);el.id=id;el.style.display='none';` +
|
|
285
|
+
`document.body.appendChild(el);}}` +
|
|
286
|
+
`if(typeof window.hideIdleAnimation!=='function')window.hideIdleAnimation=function(){};` +
|
|
287
|
+
`if(typeof window.showIdleAnimation!=='function')window.showIdleAnimation=function(){};` +
|
|
288
|
+
`})();</script>`;
|
|
289
|
+
$('body').prepend(stubScript);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Ensure every required CDN import is present in the page's <head>.
|
|
293
|
+
*/
|
|
294
|
+
function ensureRequiredImports$($, pageVersion) {
|
|
295
|
+
if (pageVersion < 2)
|
|
296
|
+
return;
|
|
297
|
+
for (const imp of REQUIRED_IMPORTS) {
|
|
298
|
+
if ($(imp.selector).length === 0) {
|
|
299
|
+
$('head').append(`<script src="${imp.src}"></script>\n`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Inject FluentLM CSS/JS and theme class into page content for iframe rendering.
|
|
305
|
+
*/
|
|
306
|
+
function injectContentAssets$($, themeName, themeVersion) {
|
|
307
|
+
// Inject iframe viewport styles
|
|
308
|
+
if ($('style#iframe-viewport').length === 0) {
|
|
309
|
+
$('head').prepend('<style id="iframe-viewport">html,body,.viewer-panel{height:100%;margin:0}</style>\n');
|
|
310
|
+
}
|
|
311
|
+
// Move non-locked <script> tags from <head> to end of <body>.
|
|
312
|
+
const headScripts = $('head script').not('[data-locked]');
|
|
313
|
+
headScripts.each((_, el) => {
|
|
314
|
+
const $el = $(el);
|
|
315
|
+
$el.remove();
|
|
316
|
+
$('body').append($el);
|
|
317
|
+
$('body').append('\n');
|
|
318
|
+
});
|
|
319
|
+
// Theme-name class on <html> — only for v3 themes that ship scoped rules
|
|
320
|
+
if (themeVersion >= 3) {
|
|
321
|
+
$('html').addClass(themeName);
|
|
322
|
+
}
|
|
323
|
+
// FluentLM CSS + JS — always injected; page-level FluentLM usage should work
|
|
324
|
+
// regardless of theme version.
|
|
325
|
+
if ($('link#fluentlm-css').length === 0) {
|
|
326
|
+
const fluentLink = `<link id="fluentlm-css" rel="stylesheet" href="${assetUrl('fluentlm.min.css')}">`;
|
|
327
|
+
const themeLink = $('link#theme-css');
|
|
328
|
+
if (themeLink.length > 0) {
|
|
329
|
+
themeLink.before(fluentLink + '\n');
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
$('head').append(fluentLink + '\n');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if ($('script#fluentlm-js').length === 0) {
|
|
336
|
+
$('body').append(`<script id="fluentlm-js" src="${assetUrl('fluentlm.min.js')}"></script>\n`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Inject page-info script tag into <head>.
|
|
341
|
+
*/
|
|
342
|
+
function injectPageInfoScript$($, pageName) {
|
|
343
|
+
const tag = `<script id="page-info" src="/api/page-info.js?page=${encodeURIComponent(pageName)}"></script>`;
|
|
344
|
+
const existing = $('script#page-info');
|
|
345
|
+
if (existing.length > 0) {
|
|
346
|
+
existing.replaceWith(tag);
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
$('head').append(tag + '\n');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* V3 helper script IDs and their static file names.
|
|
354
|
+
* shell-bridge.v3.js loads first (creates window.synthos); the rest attach namespaces.
|
|
355
|
+
*/
|
|
356
|
+
const V3_HELPER_SCRIPTS = [
|
|
357
|
+
{ id: 'shell-bridge-v3', file: 'shell-bridge.v3.js' },
|
|
358
|
+
{ id: 'server-v3', file: 'server.v3.js' },
|
|
359
|
+
{ id: 'storage-v3', file: 'storage.v3.js' },
|
|
360
|
+
{ id: 'script-v3', file: 'script.v3.js' },
|
|
361
|
+
{ id: 'connector-v3', file: 'connector.v3.js' },
|
|
362
|
+
{ id: 'agent-v3', file: 'agent.v3.js' },
|
|
363
|
+
{ id: 'extract-v3', file: 'extract.v3.js' },
|
|
364
|
+
];
|
|
365
|
+
/**
|
|
366
|
+
* Inject v3 helper scripts into the page.
|
|
367
|
+
* shell-bridge.v3.js goes at end of <body> (bridge must be last to initialize after DOM).
|
|
368
|
+
* All other v3 scripts go into <head> after page-info.
|
|
369
|
+
*/
|
|
370
|
+
function injectPageHelpers$($, pageVersion) {
|
|
371
|
+
if (pageVersion < 2)
|
|
372
|
+
return;
|
|
373
|
+
// Remove legacy monolithic helpers if present
|
|
374
|
+
$('script#page-helpers').remove();
|
|
375
|
+
$('script#page-bridge').remove();
|
|
376
|
+
// Remove old bridge id in case pages were rendered with an older version
|
|
377
|
+
$('script#shell-v3').remove();
|
|
378
|
+
// Inject v3 scripts
|
|
379
|
+
for (const script of V3_HELPER_SCRIPTS) {
|
|
380
|
+
if ($(`script#${script.id}`).length > 0)
|
|
381
|
+
continue;
|
|
382
|
+
const tag = `<script id="${script.id}" src="${assetUrl(script.file)}"></script>`;
|
|
383
|
+
if (script.id === 'shell-bridge-v3') {
|
|
384
|
+
// Bridge goes at end of body (needs to run after page DOM)
|
|
385
|
+
$('body').append(tag + '\n');
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
// API scripts go into head after page-info
|
|
389
|
+
const pageInfo = $('script#page-info');
|
|
390
|
+
if (pageInfo.length > 0) {
|
|
391
|
+
pageInfo.after('\n' + tag);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
$('head').append(tag + '\n');
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* @deprecated No longer needed — shell-bridge.v3.js replaces page-bridge.js.
|
|
401
|
+
* Kept as a no-op for any call sites not yet updated.
|
|
402
|
+
*/
|
|
403
|
+
function injectPageBridge$($, pageVersion) {
|
|
404
|
+
// No-op: shell-bridge.v3.js is now injected by injectPageHelpers$
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Move external <script src="..."> from <body> to end of <head>.
|
|
408
|
+
*/
|
|
409
|
+
function hoistExternalScriptsToHead$($) {
|
|
410
|
+
const hoisted = [];
|
|
411
|
+
$('body script[src]').each((_, el) => {
|
|
412
|
+
const script = $(el);
|
|
413
|
+
if (script.attr('data-locked') !== undefined)
|
|
414
|
+
return;
|
|
415
|
+
hoisted.push($.html(script));
|
|
416
|
+
script.remove();
|
|
417
|
+
});
|
|
418
|
+
if (hoisted.length > 0) {
|
|
419
|
+
$('head').append(hoisted.join('\n') + '\n');
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Full iframe content preparation pipeline. Parses HTML once, applies all
|
|
424
|
+
* cheerio-based transforms on the in-memory DOM, serializes once, then runs
|
|
425
|
+
* string-based transforms (IIFE wrapping, product-name replacement).
|
|
426
|
+
*
|
|
427
|
+
* Centralised here so every endpoint that serves page HTML into the iframe
|
|
428
|
+
* goes through the same steps.
|
|
429
|
+
*/
|
|
430
|
+
function prepareForIframe(html, page, pageVersion, themeName, themeVersion, productName) {
|
|
431
|
+
// Single cheerio parse
|
|
432
|
+
const $ = cheerio.load(html, { decodeEntities: false });
|
|
433
|
+
// All DOM transforms on the shared $ instance
|
|
434
|
+
stripShellMarkup$($);
|
|
435
|
+
ensureRequiredImports$($, pageVersion);
|
|
436
|
+
injectContentAssets$($, themeName, themeVersion);
|
|
437
|
+
injectPageInfoScript$($, page);
|
|
438
|
+
injectPageHelpers$($, pageVersion);
|
|
439
|
+
injectPageBridge$($, pageVersion);
|
|
440
|
+
hoistExternalScriptsToHead$($);
|
|
441
|
+
// Single serialize, then string-based transforms
|
|
442
|
+
let out = $.html();
|
|
443
|
+
out = wrapInlineScriptsInIIFE(out);
|
|
444
|
+
out = replaceProductName(out, productName);
|
|
445
|
+
return out;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Inject __shellInit script into the shell page with page-specific data.
|
|
449
|
+
*/
|
|
450
|
+
function injectShellInit(shellHtml, page, messages, metadata, version, productName, builderStarter) {
|
|
451
|
+
const isBuilder = metadata.categories.some(c => c === 'Builders' || c === 'System' || c === '_Starters');
|
|
452
|
+
const noPersistHistory = metadata.categories.some(c => NO_PERSIST_CATEGORIES.includes(c));
|
|
453
|
+
const isStarter = metadata.categories.includes('_Starters');
|
|
454
|
+
const initData = JSON.stringify({
|
|
455
|
+
page,
|
|
456
|
+
messages,
|
|
457
|
+
version,
|
|
458
|
+
productName,
|
|
459
|
+
pageTitle: metadata.title,
|
|
460
|
+
pageCategories: metadata.categories,
|
|
461
|
+
isLocked: metadata.mode === 'locked',
|
|
462
|
+
isRequiredPage: metadata.isRequiredPage,
|
|
463
|
+
isBuilder,
|
|
464
|
+
noPersistHistory,
|
|
465
|
+
isStarter,
|
|
466
|
+
greeting: metadata.greeting,
|
|
467
|
+
firstRunGreeting: metadata.firstRunGreeting,
|
|
468
|
+
builderStarter: builderStarter ?? '',
|
|
469
|
+
});
|
|
470
|
+
const script = `<script id="shell-init">window.__shellInit=${initData};</script>`;
|
|
471
|
+
// Inject before shell-modals.v3.js — it's the earliest consumer of window.__shellInit
|
|
472
|
+
// and captures values at module-load time, so __shellInit MUST be set before it loads.
|
|
473
|
+
// Match href with or without cache-bust query.
|
|
474
|
+
const shellModalsIdx = shellHtml.search(/<script src="\/static\/shell-modals\.v3\.js(\?[^"]*)?">/);
|
|
475
|
+
if (shellModalsIdx !== -1) {
|
|
476
|
+
return shellHtml.slice(0, shellModalsIdx) + script + '\n' + shellHtml.slice(shellModalsIdx);
|
|
477
|
+
}
|
|
478
|
+
// Fallback: before shell.v3.js
|
|
479
|
+
const shellJsIdx = shellHtml.search(/<script src="\/static\/shell\.v3\.js(\?[^"]*)?">/);
|
|
480
|
+
if (shellJsIdx !== -1) {
|
|
481
|
+
return shellHtml.slice(0, shellJsIdx) + script + '\n' + shellHtml.slice(shellJsIdx);
|
|
482
|
+
}
|
|
483
|
+
// Fallback: inject before </body>
|
|
484
|
+
const bodyIdx = shellHtml.indexOf('</body>');
|
|
485
|
+
if (bodyIdx !== -1) {
|
|
486
|
+
return shellHtml.slice(0, bodyIdx) + script + '\n' + shellHtml.slice(bodyIdx);
|
|
487
|
+
}
|
|
488
|
+
return shellHtml + '\n' + script;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Set the iframe src attribute in the shell page HTML.
|
|
492
|
+
*/
|
|
493
|
+
function setIframeSrc(shellHtml, page, starter) {
|
|
494
|
+
const starterQuery = starter ? `&starter=${encodeURIComponent(starter)}` : '';
|
|
495
|
+
return shellHtml.replace('id="viewerFrame"', `id="viewerFrame" src="/${page}?frame=1${starterQuery}"`);
|
|
496
|
+
}
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
262
498
|
// Context section builders — assemble ContextSections from enabled features
|
|
263
499
|
// ---------------------------------------------------------------------------
|
|
500
|
+
/**
|
|
501
|
+
* Extract the first non-empty line of each blank-separated block. Used to
|
|
502
|
+
* generate a sketch for sections whose full content is a list of method
|
|
503
|
+
* signatures + descriptions (e.g. SERVER_APIS, SERVER_SCRIPTS).
|
|
504
|
+
*/
|
|
505
|
+
function firstLineOfEachBlock(s) {
|
|
506
|
+
return s
|
|
507
|
+
.split(/\n\s*\n/)
|
|
508
|
+
.map(b => b.trimStart().split('\n')[0].trim())
|
|
509
|
+
.filter(Boolean)
|
|
510
|
+
.join('\n');
|
|
511
|
+
}
|
|
264
512
|
function buildContextSection() {
|
|
265
513
|
const now = new Date();
|
|
266
514
|
const dateTime = now.toLocaleString('en-US', {
|
|
@@ -270,32 +518,47 @@ function buildContextSection() {
|
|
|
270
518
|
return {
|
|
271
519
|
title: '<CONTEXT>',
|
|
272
520
|
content: `Current date and time: ${dateTime}`,
|
|
521
|
+
sketch: null,
|
|
522
|
+
mode: 'always-full',
|
|
273
523
|
instructions: '',
|
|
274
524
|
};
|
|
275
525
|
}
|
|
276
526
|
function buildServerApisSection(customizer) {
|
|
277
|
-
const
|
|
527
|
+
const full = (customizer ? (0, transformPage_1.buildRouteHints)(customizer) : transformPage_1.serverAPIs)
|
|
528
|
+
.replace(/^<SERVER_APIS>\n?/, '');
|
|
278
529
|
return {
|
|
279
530
|
title: '<SERVER_APIS>',
|
|
280
|
-
content:
|
|
531
|
+
content: full,
|
|
532
|
+
sketch: firstLineOfEachBlock(full),
|
|
533
|
+
mode: 'classifier-decides',
|
|
534
|
+
forceFullOnInitial: true,
|
|
281
535
|
instructions: 'provides a list of available server APIs and helper functions you can call from injected scripts. Use synthos.* helpers instead of raw fetch().',
|
|
282
536
|
};
|
|
283
537
|
}
|
|
284
538
|
async function buildServerScriptsSection(pagesFolder) {
|
|
285
|
-
const scripts = await (0, scripts_1.listScripts)(pagesFolder);
|
|
539
|
+
const scripts = (await (0, scripts_1.listScripts)(pagesFolder)) || '';
|
|
286
540
|
return {
|
|
287
541
|
title: '<SERVER_SCRIPTS>',
|
|
288
|
-
content: scripts
|
|
289
|
-
|
|
542
|
+
content: scripts,
|
|
543
|
+
sketch: scripts ? firstLineOfEachBlock(scripts) : null,
|
|
544
|
+
mode: 'always-omit-when-empty',
|
|
545
|
+
forceFullOnInitial: true,
|
|
546
|
+
instructions: 'provides a list of available scripts callable via synthos.script.run(id, variables).',
|
|
290
547
|
};
|
|
291
548
|
}
|
|
292
549
|
function buildConnectorsSection(configuredConnectors) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
550
|
+
const entries = configuredConnectors
|
|
551
|
+
? Object.entries(configuredConnectors).filter(([, cfg]) => cfg.enabled && cfg.apiKey)
|
|
552
|
+
: [];
|
|
553
|
+
if (entries.length === 0) {
|
|
554
|
+
return {
|
|
555
|
+
title: '<CONFIGURED_CONNECTORS>',
|
|
556
|
+
content: '',
|
|
557
|
+
sketch: null,
|
|
558
|
+
mode: 'always-omit-when-empty',
|
|
559
|
+
instructions: '',
|
|
560
|
+
};
|
|
561
|
+
}
|
|
299
562
|
const blocks = entries.map(([id, cfg]) => {
|
|
300
563
|
const def = (0, connectors_1.getConnectorRegistry)().find(d => d.id === id);
|
|
301
564
|
if (!def)
|
|
@@ -317,17 +580,30 @@ function buildConnectorsSection(configuredConnectors) {
|
|
|
317
580
|
}
|
|
318
581
|
return block;
|
|
319
582
|
});
|
|
320
|
-
const content = `The user has configured and enabled these connectors:\n${blocks.join('\n\n')}\n\nYou may use synthos.
|
|
583
|
+
const content = `The user has configured and enabled these connectors:\n${blocks.join('\n\n')}\n\nYou may use synthos.connector.call(connector, method, path, opts) to call them.\nIMPORTANT: Before making any connector call, ALWAYS check that the connector is configured first using synthos.connector.list(). If the connector is not configured, show the user a friendly message with a link to the Settings > Connectors page (/settings?tab=connectors) so they can set it up.\nDo NOT hardcode API keys. The connector proxy attaches authentication automatically.`;
|
|
584
|
+
const sketchNames = entries.map(([id]) => {
|
|
585
|
+
const def = (0, connectors_1.getConnectorRegistry)().find(d => d.id === id);
|
|
586
|
+
return def?.name ?? id;
|
|
587
|
+
});
|
|
321
588
|
return {
|
|
322
589
|
title: '<CONFIGURED_CONNECTORS>',
|
|
323
590
|
content,
|
|
591
|
+
sketch: `Configured connectors: ${sketchNames.join(', ')}`,
|
|
592
|
+
mode: 'always-omit-when-empty',
|
|
324
593
|
instructions: '',
|
|
325
594
|
};
|
|
326
595
|
}
|
|
327
596
|
function buildAgentsSection(configuredAgents) {
|
|
328
597
|
const enabledAgents = (configuredAgents ?? []).filter(a => a.enabled);
|
|
329
|
-
if (enabledAgents.length === 0)
|
|
330
|
-
return
|
|
598
|
+
if (enabledAgents.length === 0) {
|
|
599
|
+
return {
|
|
600
|
+
title: '<CONFIGURED_AGENTS>',
|
|
601
|
+
content: '',
|
|
602
|
+
sketch: null,
|
|
603
|
+
mode: 'always-omit-when-empty',
|
|
604
|
+
instructions: '',
|
|
605
|
+
};
|
|
606
|
+
}
|
|
331
607
|
const agentBlocks = enabledAgents.map(a => {
|
|
332
608
|
let block = `- ${a.name} (id: "${a.id}", provider: ${a.provider})`;
|
|
333
609
|
block += `\n Description: ${a.description}`;
|
|
@@ -340,9 +616,12 @@ function buildAgentsSection(configuredAgents) {
|
|
|
340
616
|
}
|
|
341
617
|
return block;
|
|
342
618
|
});
|
|
619
|
+
const sketchLines = enabledAgents.map(a => `- ${a.name}: ${a.description}`);
|
|
343
620
|
return {
|
|
344
621
|
title: '<CONFIGURED_AGENTS>',
|
|
345
622
|
content: `The user has configured these agents:\n\n${agentBlocks.join('\n\n')}\n\n${transformPage_1.AGENT_API_REFERENCE}`,
|
|
623
|
+
sketch: `Configured agents:\n${sketchLines.join('\n')}`,
|
|
624
|
+
mode: 'always-omit-when-empty',
|
|
346
625
|
instructions: '',
|
|
347
626
|
};
|
|
348
627
|
}
|
|
@@ -353,12 +632,42 @@ function buildThemeSection(themeInfo) {
|
|
|
353
632
|
const colorList = Object.entries(colors)
|
|
354
633
|
.map(([name, value]) => ` --${name}: ${value}`)
|
|
355
634
|
.join('\n');
|
|
356
|
-
content = `Mode: ${mode}
|
|
635
|
+
content = `Mode: ${mode}
|
|
636
|
+
CSS custom properties (use instead of hardcoded values):
|
|
637
|
+
${colorList}
|
|
638
|
+
|
|
639
|
+
Page rendering context: Your page runs inside an iframe. The shell (toolbar, chat panel, modals) is in the parent frame and is NOT part of your page. You own the full <body> — there is no .shell-toolbar, .chat-panel, #chatForm, or #loadingOverlay in your page.
|
|
640
|
+
|
|
641
|
+
Modals and popups: ALWAYS use FluentLM dialog components for any modal or popup. Do NOT create custom overlay classes with position:fixed and z-index. Structure:
|
|
642
|
+
<div class="flm-dialog-overlay" id="myModal" data-light-dismiss>
|
|
643
|
+
<div class="flm-dialog">
|
|
644
|
+
<div class="flm-dialog-header">
|
|
645
|
+
<h2 class="flm-dialog-title">Title</h2>
|
|
646
|
+
<button class="flm-dialog-close" data-icon="Cancel" aria-label="Close"></button>
|
|
647
|
+
</div>
|
|
648
|
+
<div class="flm-dialog-body">Content</div>
|
|
649
|
+
<div class="flm-dialog-footer">
|
|
650
|
+
<button class="flm-button" data-dialog-close>Cancel</button>
|
|
651
|
+
<button class="flm-button flm-button--primary" data-dialog-close>OK</button>
|
|
652
|
+
</div>
|
|
653
|
+
</div>
|
|
654
|
+
</div>
|
|
655
|
+
Show/hide via FluentLM JS: FluentLM auto-wires open/close on elements with data-dialog-open="#id" and data-dialog-close attributes. For programmatic control use: FluentLMDialogComponent.open('overlayId') and FluentLMDialogComponent.close('overlayId'). NEVER use classList.add('is-open') — the correct class is 'flm-dialog-overlay--open' but always prefer the JS API.
|
|
656
|
+
|
|
657
|
+
Navigation: To navigate to another page from within the iframe, use window.__synthOSNavigateTo(url). This routes through the parent shell which handles unsaved-changes checks.
|
|
658
|
+
|
|
659
|
+
Do NOT:
|
|
660
|
+
- Create shell chrome elements (.shell-toolbar, .chat-panel, #chatForm, #loadingOverlay, .chat-messages) — these belong to the parent frame
|
|
661
|
+
- INSERT new <script> blocks that duplicate existing ones — when fixing JavaScript, UPDATE or REPLACE the existing script's nodeId instead. Always give inline scripts a unique id attribute.
|
|
662
|
+
|
|
663
|
+
The <html> element has class "${mode}-mode". Always add .light-mode CSS overrides for any page-specific styles so the page works in both light and dark themes, unless the user has explicitly requested a very specific color scheme.`;
|
|
357
664
|
}
|
|
358
665
|
return {
|
|
359
666
|
title: '<THEME>',
|
|
360
667
|
content,
|
|
361
|
-
|
|
668
|
+
sketch: null,
|
|
669
|
+
mode: 'always-full',
|
|
670
|
+
instructions: 'provides details on the current theme\'s color scheme to help you generate theme-aware pages.',
|
|
362
671
|
};
|
|
363
672
|
}
|
|
364
673
|
const LLMD_READING_GUIDE = `Content below is LLMD v0.2 — a compressed, token-optimized format. Read it as follows:
|
|
@@ -404,58 +713,326 @@ function buildLlmdReadingGuideSection() {
|
|
|
404
713
|
return {
|
|
405
714
|
title: '<LLMD_READING_GUIDE>',
|
|
406
715
|
content: LLMD_READING_GUIDE,
|
|
716
|
+
sketch: 'Reading guide for LLMD v0.2 — a token-compressed format used by some context sections (defines @scope, :k=v attrs, -lists, →relations, ::lang literal blocks, and meta keys).',
|
|
717
|
+
mode: 'classifier-decides',
|
|
718
|
+
forceFullOnInitial: true,
|
|
407
719
|
instructions: '',
|
|
408
720
|
};
|
|
409
721
|
}
|
|
722
|
+
const FLUENTLM_INSTRUCTIONS = `<FLUENTLM_COMPONENTS> is the component library available on every page. You MUST use FluentLM components instead of writing custom HTML/CSS for standard UI elements.
|
|
723
|
+
REQUIRED: Use flm-button for buttons, flm-textfield for inputs, flm-dropdown for selects, flm-dialog/flm-panel/flm-modal for overlays, flm-pivot for tabs, flm-nav for navigation, flm-toggle for switches, flm-card for cards, flm-callout for tooltips, flm-messagebar for alerts, and all other components listed in <FLUENTLM_COMPONENTS>.
|
|
724
|
+
FORBIDDEN: Do NOT create custom CSS classes for buttons (e.g. .my-btn, .okr-btn), inputs, modals, cards, tabs, dropdowns, or any UI element that has a FluentLM equivalent. Do NOT use raw <button>, <input>, or <select> elements without FluentLM classes.
|
|
725
|
+
Apply FluentLM utility classes (flm-text--secondary, flm-stack, etc.) for layout and typography instead of custom CSS where possible.`;
|
|
410
726
|
async function buildFluentLMSection(config) {
|
|
411
727
|
const filePath = await (0, files_1.findFileInFolders)(config.staticFilesFolders, 'fluentlm-instructions.md');
|
|
412
|
-
if (!filePath)
|
|
413
|
-
return
|
|
728
|
+
if (!filePath) {
|
|
729
|
+
return {
|
|
730
|
+
title: '<FLUENTLM_COMPONENTS>',
|
|
731
|
+
content: '',
|
|
732
|
+
sketch: null,
|
|
733
|
+
mode: 'always-omit-when-empty',
|
|
734
|
+
instructions: '',
|
|
735
|
+
};
|
|
736
|
+
}
|
|
414
737
|
try {
|
|
415
738
|
const content = await (0, files_1.loadFile)(filePath);
|
|
739
|
+
const componentNames = (content.match(/^## (.+)$/gm) ?? [])
|
|
740
|
+
.map(h => h.replace(/^## /, '').trim())
|
|
741
|
+
.filter(name => !/^(Global Classes|Global Implementation Notes|CSS Custom Properties)$/i.test(name));
|
|
742
|
+
const sketch = componentNames.length > 0
|
|
743
|
+
? `Available FluentLM components (call full reference for usage): ${componentNames.join(', ')}`
|
|
744
|
+
: 'FluentLM UI component library (full reference available on expand).';
|
|
416
745
|
return {
|
|
417
746
|
title: '<FLUENTLM_COMPONENTS>',
|
|
418
747
|
content,
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
Apply FluentLM utility classes (flm-text--secondary, flm-stack, etc.) for layout and typography instead of custom CSS where possible.`,
|
|
748
|
+
sketch,
|
|
749
|
+
mode: 'always-omit-when-empty',
|
|
750
|
+
instructions: FLUENTLM_INSTRUCTIONS,
|
|
423
751
|
};
|
|
424
752
|
}
|
|
425
753
|
catch {
|
|
426
|
-
return
|
|
754
|
+
return {
|
|
755
|
+
title: '<FLUENTLM_COMPONENTS>',
|
|
756
|
+
content: '',
|
|
757
|
+
sketch: null,
|
|
758
|
+
mode: 'always-omit-when-empty',
|
|
759
|
+
instructions: '',
|
|
760
|
+
};
|
|
427
761
|
}
|
|
428
762
|
}
|
|
429
763
|
async function buildRecommendedFrameworksSection(config) {
|
|
430
764
|
const filePath = await (0, files_1.findFileInFolders)(config.staticFilesFolders, 'recommended-frameworks.llmd');
|
|
431
|
-
if (!filePath)
|
|
432
|
-
return
|
|
765
|
+
if (!filePath) {
|
|
766
|
+
return {
|
|
767
|
+
title: '<RECOMMENDED_FRAMEWORKS>',
|
|
768
|
+
content: '',
|
|
769
|
+
sketch: null,
|
|
770
|
+
mode: 'always-omit-when-empty',
|
|
771
|
+
instructions: '',
|
|
772
|
+
};
|
|
773
|
+
}
|
|
433
774
|
try {
|
|
434
775
|
const content = await (0, files_1.loadFile)(filePath);
|
|
776
|
+
const names = (content.match(/^@(\w+)/gm) ?? [])
|
|
777
|
+
.map(m => m.slice(1))
|
|
778
|
+
.filter(n => n !== 'recommended_frameworks');
|
|
779
|
+
const sketch = names.length > 0
|
|
780
|
+
? `Recommended frameworks (with CDN URLs in full content): ${names.join(', ')}`
|
|
781
|
+
: 'Recommended third-party frameworks with CDN URLs (expand for details).';
|
|
435
782
|
return {
|
|
436
783
|
title: '<RECOMMENDED_FRAMEWORKS>',
|
|
437
784
|
content,
|
|
438
|
-
|
|
785
|
+
sketch,
|
|
786
|
+
mode: 'always-omit-when-empty',
|
|
787
|
+
instructions: 'lists recommended third-party frameworks with CDN URLs. When a page needs a framework from this list, load it via <link> for CSS in <head>, and <script> tags at the end of <body> (before your page scripts). Always use the version shown in the CDN URL (it is the latest approved version).',
|
|
439
788
|
};
|
|
440
789
|
}
|
|
441
790
|
catch {
|
|
791
|
+
return {
|
|
792
|
+
title: '<RECOMMENDED_FRAMEWORKS>',
|
|
793
|
+
content: '',
|
|
794
|
+
sketch: null,
|
|
795
|
+
mode: 'always-omit-when-empty',
|
|
796
|
+
instructions: '',
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
// ---------------------------------------------------------------------------
|
|
801
|
+
// Shared / per-page table sections
|
|
802
|
+
// ---------------------------------------------------------------------------
|
|
803
|
+
// Surfaces table inventory + schemas + one example record per table so the
|
|
804
|
+
// builder generates consumer code against real field names. Both sections
|
|
805
|
+
// ship as `always-omit-when-empty` — workspaces with zero tables pay zero
|
|
806
|
+
// tokens. Spec: docs/specs/shared-tables-schema-sidecar.md.
|
|
807
|
+
const PAGE_TABLES_RESERVED = new Set(['files']);
|
|
808
|
+
/**
|
|
809
|
+
* Load one example record for a table — the first id alphabetically, picked
|
|
810
|
+
* deterministically so prompt content is stable across builds.
|
|
811
|
+
*/
|
|
812
|
+
async function loadFirstRecord(config, parent, table) {
|
|
813
|
+
const sp = config.storageProvider;
|
|
814
|
+
const folder = path_1.default.join(parent, table);
|
|
815
|
+
if (!await sp.checkIfExists(folder))
|
|
442
816
|
return undefined;
|
|
817
|
+
const files = (await sp.listFiles(folder)).filter(f => f.endsWith('.json')).sort();
|
|
818
|
+
if (files.length === 0)
|
|
819
|
+
return undefined;
|
|
820
|
+
try {
|
|
821
|
+
const raw = await sp.loadFile(path_1.default.join(folder, files[0]));
|
|
822
|
+
const parsed = JSON.parse(raw);
|
|
823
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
824
|
+
return parsed;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
catch { /* skip unparsable */ }
|
|
828
|
+
return undefined;
|
|
829
|
+
}
|
|
830
|
+
async function gatherTablesForPrompt(config, parent, reserved) {
|
|
831
|
+
const tables = await (0, sharedTableSchema_1.listTables)(config, parent, reserved);
|
|
832
|
+
const out = [];
|
|
833
|
+
for (const t of tables) {
|
|
834
|
+
const wrapper = t.hasSchema ? await (0, sharedTableSchema_1.loadSchema)(config, parent, t.name) : undefined;
|
|
835
|
+
const example = t.recordCount > 0 ? await loadFirstRecord(config, parent, t.name) : undefined;
|
|
836
|
+
out.push({
|
|
837
|
+
name: t.name,
|
|
838
|
+
recordCount: t.recordCount,
|
|
839
|
+
hasSchema: !!wrapper,
|
|
840
|
+
schema: wrapper?.schema,
|
|
841
|
+
example,
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
return out;
|
|
845
|
+
}
|
|
846
|
+
function escapeXmlAttr(s) {
|
|
847
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
848
|
+
}
|
|
849
|
+
function renderTablesSketch(rootTag, tables) {
|
|
850
|
+
const lines = tables.map(t => ` <table name="${escapeXmlAttr(t.name)}" recordCount="${t.recordCount}" hasSchema="${t.hasSchema}" />`);
|
|
851
|
+
return `<${rootTag}>\n${lines.join('\n')}\n</${rootTag}>`;
|
|
852
|
+
}
|
|
853
|
+
function renderTablesFull(rootTag, tables) {
|
|
854
|
+
const blocks = tables.map(t => {
|
|
855
|
+
const parts = [];
|
|
856
|
+
parts.push(` <table name="${escapeXmlAttr(t.name)}" recordCount="${t.recordCount}">`);
|
|
857
|
+
if (t.schema) {
|
|
858
|
+
parts.push(` <schema>${JSON.stringify(t.schema)}</schema>`);
|
|
859
|
+
}
|
|
860
|
+
if (t.example) {
|
|
861
|
+
parts.push(` <example>${JSON.stringify(t.example)}</example>`);
|
|
862
|
+
}
|
|
863
|
+
parts.push(` </table>`);
|
|
864
|
+
return parts.join('\n');
|
|
865
|
+
});
|
|
866
|
+
return `<${rootTag}>\n${blocks.join('\n')}\n</${rootTag}>`;
|
|
867
|
+
}
|
|
868
|
+
const SHARED_TABLES_INSTRUCTIONS = 'lists shared tables visible across pages, with their schemas (when defined) and one example record. Use these to write code against the user\'s ACTUAL field names — do not invent names. Read/write via synthos.shared.data.* and synthos.shared.data.defineSchema(table, schema) when extending.';
|
|
869
|
+
const PAGE_TABLES_INSTRUCTIONS = 'lists per-page tables scoped to this page, with their schemas (when defined) and one example record. Use these to write code against the user\'s ACTUAL field names. Read/write via synthos.data.* and synthos.data.defineSchema(table, schema) when extending.';
|
|
870
|
+
async function buildSharedTablesSection(config) {
|
|
871
|
+
const parent = path_1.default.join(config.pagesFolder, 'shared');
|
|
872
|
+
const tables = await gatherTablesForPrompt(config, parent, new Set());
|
|
873
|
+
if (tables.length === 0) {
|
|
874
|
+
return {
|
|
875
|
+
title: '<SHARED_TABLES>',
|
|
876
|
+
content: '',
|
|
877
|
+
sketch: null,
|
|
878
|
+
mode: 'always-omit-when-empty',
|
|
879
|
+
instructions: '',
|
|
880
|
+
};
|
|
443
881
|
}
|
|
882
|
+
return {
|
|
883
|
+
title: '<SHARED_TABLES>',
|
|
884
|
+
content: renderTablesFull('SHARED_TABLES', tables),
|
|
885
|
+
sketch: renderTablesSketch('SHARED_TABLES', tables),
|
|
886
|
+
mode: 'always-omit-when-empty',
|
|
887
|
+
instructions: SHARED_TABLES_INSTRUCTIONS,
|
|
888
|
+
};
|
|
444
889
|
}
|
|
445
|
-
function
|
|
890
|
+
async function buildPageTablesSection(config, pageName) {
|
|
891
|
+
const parent = path_1.default.join(config.pagesFolder, 'pages', pageName);
|
|
892
|
+
const tables = await gatherTablesForPrompt(config, parent, PAGE_TABLES_RESERVED);
|
|
893
|
+
if (tables.length === 0) {
|
|
894
|
+
return {
|
|
895
|
+
title: '<PAGE_TABLES>',
|
|
896
|
+
content: '',
|
|
897
|
+
sketch: null,
|
|
898
|
+
mode: 'always-omit-when-empty',
|
|
899
|
+
instructions: '',
|
|
900
|
+
};
|
|
901
|
+
}
|
|
446
902
|
return {
|
|
447
|
-
title: '<
|
|
448
|
-
content:
|
|
449
|
-
|
|
903
|
+
title: '<PAGE_TABLES>',
|
|
904
|
+
content: renderTablesFull('PAGE_TABLES', tables),
|
|
905
|
+
sketch: renderTablesSketch('PAGE_TABLES', tables),
|
|
906
|
+
mode: 'always-omit-when-empty',
|
|
907
|
+
instructions: PAGE_TABLES_INSTRUCTIONS,
|
|
450
908
|
};
|
|
451
909
|
}
|
|
910
|
+
// ---------------------------------------------------------------------------
|
|
911
|
+
// Section assembly — picks full vs sketch per section per the section's mode
|
|
912
|
+
// ---------------------------------------------------------------------------
|
|
913
|
+
/**
|
|
914
|
+
* Resolve each section to either its full content or its sketch (or skip it
|
|
915
|
+
* entirely) based on the section's `mode`, the classifier's `expand` set, and
|
|
916
|
+
* whether this is the initial build (`newBuild`).
|
|
917
|
+
*
|
|
918
|
+
* Per `docs/specs/page-section-sketch-full.md`:
|
|
919
|
+
* - 'always-full' → always render full content.
|
|
920
|
+
* - 'classifier-decides' → render full when title is in `expand`, else sketch.
|
|
921
|
+
* - 'always-omit-when-empty' → skip entirely when sketch is null; otherwise
|
|
922
|
+
* render full when in `expand`, else sketch.
|
|
923
|
+
* - `forceFullOnInitial` on a section forces full content on the first build
|
|
924
|
+
* regardless of `expand`.
|
|
925
|
+
*/
|
|
926
|
+
function assembleSections(sections, expand, newBuild) {
|
|
927
|
+
const out = [];
|
|
928
|
+
for (const s of sections) {
|
|
929
|
+
const expanded = expand.has(s.title) || (newBuild && s.forceFullOnInitial === true);
|
|
930
|
+
switch (s.mode) {
|
|
931
|
+
case 'always-full':
|
|
932
|
+
out.push(s);
|
|
933
|
+
break;
|
|
934
|
+
case 'classifier-decides':
|
|
935
|
+
if (expanded || s.sketch === null) {
|
|
936
|
+
out.push(s);
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
out.push({ ...s, content: s.sketch });
|
|
940
|
+
}
|
|
941
|
+
break;
|
|
942
|
+
case 'always-omit-when-empty':
|
|
943
|
+
if (s.sketch === null) {
|
|
944
|
+
// Empty section — skip entirely.
|
|
945
|
+
break;
|
|
946
|
+
}
|
|
947
|
+
if (expanded) {
|
|
948
|
+
out.push(s);
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
out.push({ ...s, content: s.sketch });
|
|
952
|
+
}
|
|
953
|
+
break;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return out;
|
|
957
|
+
}
|
|
958
|
+
exports.assembleSections = assembleSections;
|
|
959
|
+
/** Apply per-page section-mode overrides from page metadata. */
|
|
960
|
+
function applySectionModeOverrides(sections, overrides) {
|
|
961
|
+
if (!overrides || Object.keys(overrides).length === 0)
|
|
962
|
+
return sections;
|
|
963
|
+
return sections.map(s => {
|
|
964
|
+
const override = overrides[s.title];
|
|
965
|
+
return override ? { ...s, mode: override } : s;
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
exports.applySectionModeOverrides = applySectionModeOverrides;
|
|
452
969
|
function usePageRoutes(config, app, customizer) {
|
|
970
|
+
// Initialize cache-busting for /static/* URLs
|
|
971
|
+
initStaticAssetVersioning(config.staticFilesFolders);
|
|
453
972
|
// Redirect / to /home page
|
|
454
973
|
app.get('/', (req, res) => res.redirect(HOME_PAGE_ROUTE));
|
|
455
|
-
//
|
|
974
|
+
// Builder with starter scaffold. Renders the same shell as /builder, but
|
|
975
|
+
// embeds the starter name in shellInit (so chat POSTs include it) and the
|
|
976
|
+
// iframe src (so iframe content == starter HTML, not the carousel).
|
|
977
|
+
// Must be registered BEFORE /:page — Express matches in declaration order.
|
|
978
|
+
app.get('/builder/:starter', async (req, res) => {
|
|
979
|
+
const { starter } = req.params;
|
|
980
|
+
const isConfigured = await (0, settings_1.hasConfiguredSettings)(config);
|
|
981
|
+
if (!isConfigured) {
|
|
982
|
+
res.redirect('/settings?firstRun=1');
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
// Validate starter — must exist with category "_Starters". 404 otherwise.
|
|
986
|
+
const starterMeta = await (0, pages_1.loadPageMetadata)(config, starter, config.requiredPagesFolders);
|
|
987
|
+
const isValidStarter = !!starterMeta?.categories?.includes('_Starters');
|
|
988
|
+
if (!isValidStarter) {
|
|
989
|
+
res.status(404).send(PAGE_NOT_FOUND);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
// Reuse builder's metadata for the shell wrapping (page name == 'builder').
|
|
993
|
+
const builderMeta = await (0, pages_1.loadPageMetadata)(config, 'builder', config.requiredPagesFolders);
|
|
994
|
+
const settings = await (0, settings_1.loadSettings)(config);
|
|
995
|
+
const themeName = settings.theme ?? 'nebula-dusk';
|
|
996
|
+
const themeVersion = await (0, themes_1.loadThemeVersion)(themeName, config);
|
|
997
|
+
const productName = customizer?.productName ?? 'SynthOS';
|
|
998
|
+
const shellHtml = await loadPageWithFallback(SHELL_PAGE_NAME, config, true);
|
|
999
|
+
if (!shellHtml) {
|
|
1000
|
+
res.status(500).send('Shell page not found');
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
// Builder is noPersistHistory — start fresh: greeting only.
|
|
1004
|
+
const greetingText = builderMeta?.greeting ?? '';
|
|
1005
|
+
const messages = greetingText
|
|
1006
|
+
? [{ role: 'assistant', content: greetingText }]
|
|
1007
|
+
: [];
|
|
1008
|
+
let html = injectShellAssets(shellHtml, themeName, themeVersion, settings.toolbarPosition);
|
|
1009
|
+
html = injectShellInit(html, 'builder', messages, {
|
|
1010
|
+
title: builderMeta?.title ?? '',
|
|
1011
|
+
categories: builderMeta?.categories ?? [],
|
|
1012
|
+
mode: builderMeta?.mode ?? 'unlocked',
|
|
1013
|
+
isRequiredPage: true,
|
|
1014
|
+
greeting: builderMeta?.greeting ?? '',
|
|
1015
|
+
firstRunGreeting: builderMeta?.firstRunGreeting ?? '',
|
|
1016
|
+
}, 0, productName, starter);
|
|
1017
|
+
html = setIframeSrc(html, 'builder', starter);
|
|
1018
|
+
html = injectPageInfoScript(html, 'builder');
|
|
1019
|
+
html = replaceProductName(html, productName);
|
|
1020
|
+
html = applyAssetVersions(html);
|
|
1021
|
+
res.send(html);
|
|
1022
|
+
});
|
|
1023
|
+
// Page retrieval — serves either the shell (parent frame) or page content (iframe)
|
|
456
1024
|
app.get('/:page', async (req, res) => {
|
|
457
|
-
// Redirect if settings not configured
|
|
458
1025
|
const { page } = req.params;
|
|
1026
|
+
const isFrameRequest = req.query.frame === '1';
|
|
1027
|
+
const requestedStarter = isFrameRequest && page === 'builder' && typeof req.query.starter === 'string'
|
|
1028
|
+
? String(req.query.starter)
|
|
1029
|
+
: '';
|
|
1030
|
+
// _shell is an internal page — never served directly
|
|
1031
|
+
if (page === SHELL_PAGE_NAME) {
|
|
1032
|
+
res.redirect(HOME_PAGE_ROUTE);
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
// Redirect if settings not configured
|
|
459
1036
|
const isConfigured = await (0, settings_1.hasConfiguredSettings)(config);
|
|
460
1037
|
if (!isConfigured && page !== 'settings') {
|
|
461
1038
|
res.redirect('/settings?firstRun=1');
|
|
@@ -463,7 +1040,7 @@ function usePageRoutes(config, app, customizer) {
|
|
|
463
1040
|
}
|
|
464
1041
|
// Ensure page exists — force fresh disk read for required pages
|
|
465
1042
|
const isRequiredPage = config.requiredPages.includes(page);
|
|
466
|
-
|
|
1043
|
+
let pageState = await loadPageWithFallback(page, config, isRequiredPage);
|
|
467
1044
|
if (!pageState) {
|
|
468
1045
|
res.status(404).send(PAGE_NOT_FOUND);
|
|
469
1046
|
return;
|
|
@@ -476,30 +1053,73 @@ function usePageRoutes(config, app, customizer) {
|
|
|
476
1053
|
res.redirect(customizer?.tabsListRoute ?? '/pages');
|
|
477
1054
|
return;
|
|
478
1055
|
}
|
|
479
|
-
// Load settings
|
|
1056
|
+
// Load settings
|
|
480
1057
|
const settings = await (0, settings_1.loadSettings)(config);
|
|
481
1058
|
const themeName = settings.theme ?? 'nebula-dusk';
|
|
482
1059
|
const themeVersion = await (0, themes_1.loadThemeVersion)(themeName, config);
|
|
483
|
-
let html = ensureRequiredImports(pageState, pageVersion);
|
|
484
|
-
html = injectErrorCapture(html, pageVersion);
|
|
485
|
-
html = injectShellAssets(html, themeName, themeVersion, settings.toolbarPosition);
|
|
486
|
-
html = injectPageInfoScript(html, page);
|
|
487
|
-
html = injectPageHelpers(html, pageVersion);
|
|
488
|
-
html = injectPageScript(html, pageVersion);
|
|
489
|
-
// Inject version meta tag so undo/try-again links appear on page load
|
|
490
|
-
{
|
|
491
|
-
const latestVersion = await (0, pages_1.getLatestVersion)(config, page);
|
|
492
|
-
if (latestVersion > 0) {
|
|
493
|
-
html = html.replace('</head>', `<meta name="synthos-version" content="${latestVersion}">\n</head>`);
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
html = hoistExternalScriptsToHead(html);
|
|
497
|
-
// Replace branding for white-label forks
|
|
498
1060
|
const productName = customizer?.productName ?? 'SynthOS';
|
|
499
|
-
|
|
500
|
-
|
|
1061
|
+
const isStandalone = STANDALONE_PAGE_NAMES.includes(page);
|
|
1062
|
+
if (isFrameRequest || isStandalone) {
|
|
1063
|
+
// Builder + ?starter=<name>: swap iframe content to the starter's HTML.
|
|
1064
|
+
// page name stays 'builder' (chat POSTs target /builder); the starter HTML
|
|
1065
|
+
// becomes the working scaffold the chat agent edits.
|
|
1066
|
+
if (requestedStarter) {
|
|
1067
|
+
const starterMeta = await (0, pages_1.loadPageMetadata)(config, requestedStarter, config.requiredPagesFolders);
|
|
1068
|
+
if (!starterMeta?.categories?.includes('_Starters')) {
|
|
1069
|
+
res.status(404).send(PAGE_NOT_FOUND);
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
const starterState = await loadPageWithFallback(requestedStarter, config, false);
|
|
1073
|
+
if (!starterState) {
|
|
1074
|
+
res.status(404).send(PAGE_NOT_FOUND);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
pageState = starterState;
|
|
1078
|
+
}
|
|
1079
|
+
// --- iframe content OR standalone top-level page: serve page HTML with
|
|
1080
|
+
// theme + helpers + bridge, no shell wrapping. Standalone pages (peers to
|
|
1081
|
+
// _shell) own the full viewport themselves.
|
|
1082
|
+
const html = prepareForIframe(pageState, page, pageVersion, themeName, themeVersion, productName);
|
|
1083
|
+
res.send(html);
|
|
1084
|
+
}
|
|
1085
|
+
else {
|
|
1086
|
+
// --- Shell request: load _shell page, inject init data, set iframe src ---
|
|
1087
|
+
const shellHtml = await loadPageWithFallback(SHELL_PAGE_NAME, config, true);
|
|
1088
|
+
if (!shellHtml) {
|
|
1089
|
+
res.status(500).send('Shell page not found');
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
// Ensure any legacy v-files / chat-history.json have been migrated
|
|
1093
|
+
// to the c-scheme so we read a single consistent view.
|
|
1094
|
+
await (0, pages_1.migrateLegacyChangeFiles)(config, page);
|
|
1095
|
+
// Build the greeting message from metadata (authoritative source),
|
|
1096
|
+
// followed by the flat concat of all per-change message logs.
|
|
1097
|
+
const greetingText = metadata?.greeting ?? '';
|
|
1098
|
+
const changeMessages = await (0, pages_1.loadAllChangeMessages)(config, page);
|
|
1099
|
+
const messages = [];
|
|
1100
|
+
if (greetingText) {
|
|
1101
|
+
messages.push({ role: 'assistant', content: greetingText });
|
|
1102
|
+
}
|
|
1103
|
+
messages.push(...changeMessages);
|
|
1104
|
+
const latestVersion = await (0, pages_1.getLatestChangeHtmlNumber)(config, page);
|
|
1105
|
+
// Build the shell HTML
|
|
1106
|
+
let html = injectShellAssets(shellHtml, themeName, themeVersion, settings.toolbarPosition);
|
|
1107
|
+
html = injectShellInit(html, page, messages, {
|
|
1108
|
+
title: metadata?.title ?? '',
|
|
1109
|
+
categories: metadata?.categories ?? [],
|
|
1110
|
+
mode: metadata?.mode ?? 'unlocked',
|
|
1111
|
+
isRequiredPage,
|
|
1112
|
+
greeting: metadata?.greeting ?? '',
|
|
1113
|
+
firstRunGreeting: metadata?.firstRunGreeting ?? '',
|
|
1114
|
+
}, latestVersion, productName);
|
|
1115
|
+
html = setIframeSrc(html, page);
|
|
1116
|
+
// Inject page-info script into shell too (for pageInfo access in shell.v3.js)
|
|
1117
|
+
html = injectPageInfoScript(html, page);
|
|
1118
|
+
html = replaceProductName(html, productName);
|
|
1119
|
+
// Append cache-busting ?v=<hash> to all /static/* refs in shell HTML
|
|
1120
|
+
html = applyAssetVersions(html);
|
|
1121
|
+
res.send(html);
|
|
501
1122
|
}
|
|
502
|
-
res.send(html);
|
|
503
1123
|
});
|
|
504
1124
|
// Page save
|
|
505
1125
|
app.post('/:page/save', async (req, res) => {
|
|
@@ -532,41 +1152,33 @@ function usePageRoutes(config, app, customizer) {
|
|
|
532
1152
|
res.status(400).json({ error: '"Builder" is a reserved page name' });
|
|
533
1153
|
return;
|
|
534
1154
|
}
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
if
|
|
1155
|
+
// Save sequence (per the c-scheme spec):
|
|
1156
|
+
// 1. Determine <last> = max N where page.c<N>.html exists on source.
|
|
1157
|
+
// 2. The final HTML is page.c<last>.html (or page.html if none).
|
|
1158
|
+
// 3. Write that HTML to <saveAs>/page.html.
|
|
1159
|
+
// 4. Delete every page.c*.html on source.
|
|
1160
|
+
// 5. Delete every page.c*.json on source.
|
|
1161
|
+
// 6. Update metadata.
|
|
1162
|
+
// 7. Return redirect.
|
|
1163
|
+
// Resolve legacy files before reading latest state
|
|
1164
|
+
await (0, pages_1.migrateLegacyChangeFiles)(config, page);
|
|
1165
|
+
const lastN = await (0, pages_1.getLatestChangeHtmlNumber)(config, page);
|
|
1166
|
+
let finalHtml;
|
|
1167
|
+
if (lastN > 0) {
|
|
1168
|
+
finalHtml = await (0, pages_1.loadChangeHtml)(config, page, lastN);
|
|
1169
|
+
}
|
|
1170
|
+
if (!finalHtml) {
|
|
1171
|
+
// No pending changes — fall back to the current baseline
|
|
1172
|
+
finalHtml = await loadPageWithFallback(page, config, false);
|
|
1173
|
+
}
|
|
1174
|
+
if (!finalHtml) {
|
|
538
1175
|
res.status(404).json({ error: PAGE_NOT_FOUND });
|
|
539
1176
|
return;
|
|
540
1177
|
}
|
|
541
|
-
//
|
|
542
|
-
{
|
|
543
|
-
const $ = cheerio.load(pageState);
|
|
544
|
-
const messages = $('#chatMessages .chat-message');
|
|
545
|
-
messages.slice(1).remove();
|
|
546
|
-
// Remove any undo links
|
|
547
|
-
$('#chatMessages .synthos-undo-link').remove();
|
|
548
|
-
// Update greeting text if provided
|
|
549
|
-
if (greeting && typeof greeting === 'string' && greeting.trim().length > 0) {
|
|
550
|
-
const firstP = messages.first().find('p');
|
|
551
|
-
const strong = firstP.find('strong');
|
|
552
|
-
if (strong.length) {
|
|
553
|
-
firstP.html('<strong>Synthos:</strong> ' + greeting.trim());
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
pageState = $.html();
|
|
557
|
-
}
|
|
558
|
-
// Inject save-line marker at the end of chat messages (skip for locked pages)
|
|
1178
|
+
// Load source page metadata (needed for category checks and greeting fallback)
|
|
559
1179
|
const sourceMetadata = await (0, pages_1.loadPageMetadata)(config, page, config.requiredPagesFolders);
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
// Remove any existing save-line first
|
|
563
|
-
$('#chatMessages .save-line').remove();
|
|
564
|
-
// Append new save-line
|
|
565
|
-
$('#chatMessages').append('<div class="save-line" data-locked="true"><span class="save-line-label">Saved</span></div>');
|
|
566
|
-
pageState = $.html();
|
|
567
|
-
}
|
|
568
|
-
// Save as new page
|
|
569
|
-
await (0, pages_1.savePageState)(config, saveAs, pageState, title, categories);
|
|
1180
|
+
// Write the final HTML as the new baseline for <saveAs>
|
|
1181
|
+
await (0, pages_1.savePageState)(config, saveAs, finalHtml, title, categories);
|
|
570
1182
|
// Copy files (sound effects, etc.) from source page when saving as a different name
|
|
571
1183
|
if (page !== saveAs) {
|
|
572
1184
|
// Check source page's files/ dir in user pages, then fallback folders
|
|
@@ -589,11 +1201,33 @@ function usePageRoutes(config, app, customizer) {
|
|
|
589
1201
|
await config.storageProvider.copyFolderRecursive(sourceFilesDir, targetFilesDir);
|
|
590
1202
|
}
|
|
591
1203
|
}
|
|
592
|
-
//
|
|
593
|
-
|
|
594
|
-
|
|
1204
|
+
// Delete all per-change files (html + json) on source AND destination —
|
|
1205
|
+
// save collapses the journal back to a single baseline.
|
|
1206
|
+
await (0, pages_1.clearChanges)(config, page);
|
|
1207
|
+
if (page !== saveAs) {
|
|
1208
|
+
await (0, pages_1.clearChanges)(config, saveAs);
|
|
1209
|
+
}
|
|
1210
|
+
// Delete data tables from the source starter page (ephemeral data shouldn't persist)
|
|
1211
|
+
if (sourceMetadata?.categories?.some(c => NO_PERSIST_CATEGORIES.includes(c))) {
|
|
1212
|
+
const sourcePageDir = path_1.default.join(config.pagesFolder, 'pages', page);
|
|
1213
|
+
const sp = config.storageProvider;
|
|
1214
|
+
if (await sp.checkIfExists(sourcePageDir)) {
|
|
1215
|
+
const RESERVED_SUBDIRS = new Set(['files']);
|
|
1216
|
+
const entries = await sp.listFolders(sourcePageDir);
|
|
1217
|
+
for (const entry of entries) {
|
|
1218
|
+
if (!RESERVED_SUBDIRS.has(entry)) {
|
|
1219
|
+
await sp.deleteFolder(path_1.default.join(sourcePageDir, entry));
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
// Also update metadata with categories and greeting (in case page.json already existed)
|
|
1225
|
+
const savedGreeting = (greeting && typeof greeting === 'string' && greeting.trim().length > 0)
|
|
1226
|
+
? greeting.trim()
|
|
1227
|
+
: (sourceMetadata?.greeting ?? '');
|
|
595
1228
|
await (0, pages_1.savePageMetadata)(config, saveAs, {
|
|
596
1229
|
title,
|
|
1230
|
+
description: sourceMetadata?.description ?? '',
|
|
597
1231
|
categories,
|
|
598
1232
|
pinned: false,
|
|
599
1233
|
showInAll: true,
|
|
@@ -601,6 +1235,8 @@ function usePageRoutes(config, app, customizer) {
|
|
|
601
1235
|
lastModified: new Date().toISOString(),
|
|
602
1236
|
pageVersion: pages_1.PAGE_VERSION,
|
|
603
1237
|
mode: 'unlocked',
|
|
1238
|
+
greeting: savedGreeting,
|
|
1239
|
+
firstRunGreeting: '',
|
|
604
1240
|
});
|
|
605
1241
|
res.json({ redirect: `/${saveAs}` });
|
|
606
1242
|
}
|
|
@@ -609,60 +1245,126 @@ function usePageRoutes(config, app, customizer) {
|
|
|
609
1245
|
res.status(500).json({ error: err.message });
|
|
610
1246
|
}
|
|
611
1247
|
});
|
|
612
|
-
// Page
|
|
1248
|
+
// Page patch — lightweight DOM patching via cheerio (no LLM)
|
|
1249
|
+
app.post('/:page/patch', async (req, res) => {
|
|
1250
|
+
const { page } = req.params;
|
|
1251
|
+
await withPageLock(page, async () => {
|
|
1252
|
+
try {
|
|
1253
|
+
const { operations, message } = req.body;
|
|
1254
|
+
// Validate operations array
|
|
1255
|
+
if (!operations || !Array.isArray(operations) || operations.length === 0) {
|
|
1256
|
+
res.status(400).json({ error: 'operations is required (non-empty array)' });
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
// Load current page state
|
|
1260
|
+
const pageState = await loadPageWithFallback(page, config, false);
|
|
1261
|
+
if (!pageState) {
|
|
1262
|
+
res.status(404).json({ error: PAGE_NOT_FOUND });
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
// Parse with cheerio
|
|
1266
|
+
const $ = cheerio.load(pageState, { decodeEntities: false });
|
|
1267
|
+
// Apply each operation
|
|
1268
|
+
for (const op of operations) {
|
|
1269
|
+
if (op.op !== 'set-attr') {
|
|
1270
|
+
res.status(400).json({ error: `Unsupported operation: '${op.op}'` });
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
if (!op.selector || typeof op.selector !== 'string') {
|
|
1274
|
+
res.status(400).json({ error: 'Each operation requires a selector string' });
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
if (!op.attr || typeof op.attr !== 'string') {
|
|
1278
|
+
res.status(400).json({ error: 'Each set-attr operation requires an attr string' });
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
if (typeof op.value !== 'string') {
|
|
1282
|
+
res.status(400).json({ error: 'Each set-attr operation requires a value string' });
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
const matches = $(op.selector);
|
|
1286
|
+
if (matches.length !== 1) {
|
|
1287
|
+
res.status(400).json({
|
|
1288
|
+
error: `Selector '${op.selector}' matched ${matches.length} elements (expected exactly 1)`
|
|
1289
|
+
});
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
matches.attr(op.attr, op.value);
|
|
1293
|
+
}
|
|
1294
|
+
const productName = customizer?.productName ?? 'SynthOS';
|
|
1295
|
+
const html = $.html();
|
|
1296
|
+
// Save as next change (c-scheme). Number advances from max(html, json).
|
|
1297
|
+
await (0, pages_1.migrateLegacyChangeFiles)(config, page);
|
|
1298
|
+
const currentVersion = await (0, pages_1.getLatestChangeNumber)(config, page);
|
|
1299
|
+
const nextVersion = currentVersion + 1;
|
|
1300
|
+
await (0, pages_1.saveChangeHtml)(config, page, nextVersion, html);
|
|
1301
|
+
// Update lastModified
|
|
1302
|
+
const metadata = await (0, pages_1.loadPageMetadata)(config, page, config.requiredPagesFolders);
|
|
1303
|
+
if (metadata) {
|
|
1304
|
+
metadata.lastModified = new Date().toISOString();
|
|
1305
|
+
await (0, pages_1.savePageMetadata)(config, page, metadata);
|
|
1306
|
+
}
|
|
1307
|
+
// Persist patch as a self-contained change entry (skip for Starters/System).
|
|
1308
|
+
// Patches are user-less server-side actions, so the journal captures just
|
|
1309
|
+
// the assistant summary.
|
|
1310
|
+
const patchCategories = metadata?.categories ?? [];
|
|
1311
|
+
if (!patchCategories.some(c => NO_PERSIST_CATEGORIES.includes(c))) {
|
|
1312
|
+
const displayMessage = (message && typeof message === 'string') ? message : 'Page patched';
|
|
1313
|
+
await (0, pages_1.saveChangeMessages)(config, page, nextVersion, [
|
|
1314
|
+
{ role: 'assistant', content: displayMessage },
|
|
1315
|
+
]);
|
|
1316
|
+
}
|
|
1317
|
+
// Inject content assets for iframe rendering (no shell chrome)
|
|
1318
|
+
const settings = await (0, settings_1.loadSettings)(config);
|
|
1319
|
+
const pv = metadata?.pageVersion ?? 0;
|
|
1320
|
+
const themeName = settings.theme ?? 'nebula-dusk';
|
|
1321
|
+
const themeVersion = await (0, themes_1.loadThemeVersion)(themeName, config);
|
|
1322
|
+
const out = prepareForIframe(html, page, pv, themeName, themeVersion, productName);
|
|
1323
|
+
res.json({ html: out, changeCount: operations.length, version: nextVersion });
|
|
1324
|
+
}
|
|
1325
|
+
catch (err) {
|
|
1326
|
+
console.error(err);
|
|
1327
|
+
res.status(500).json({ error: err.message });
|
|
1328
|
+
}
|
|
1329
|
+
}); // withPageLock
|
|
1330
|
+
});
|
|
1331
|
+
// Page undo — roll back to the previous change
|
|
613
1332
|
app.post('/:page/undo', async (req, res) => {
|
|
614
1333
|
try {
|
|
615
1334
|
const { page } = req.params;
|
|
616
|
-
|
|
617
|
-
const cv = await (0, pages_1.
|
|
1335
|
+
await (0, pages_1.migrateLegacyChangeFiles)(config, page);
|
|
1336
|
+
const cv = await (0, pages_1.getLatestChangeNumber)(config, page);
|
|
618
1337
|
if (cv <= 0) {
|
|
619
1338
|
res.status(400).send('Nothing to undo');
|
|
620
1339
|
return;
|
|
621
1340
|
}
|
|
622
|
-
//
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
if
|
|
626
|
-
|
|
627
|
-
}
|
|
628
|
-
// Load previous version (v{cv-1}.html, or page.html if rolling back to v0)
|
|
629
|
-
const prevVersion = cv - 1;
|
|
1341
|
+
// Drop the entire change pair (both html + json) for the highest N.
|
|
1342
|
+
// Each user turn owns one pair — rolling it back = removing both.
|
|
1343
|
+
await (0, pages_1.deleteChange)(config, page, cv);
|
|
1344
|
+
// Find the next-lower change with HTML to restore; if none, fall
|
|
1345
|
+
// back to the saved baseline.
|
|
630
1346
|
let previousHtml;
|
|
631
|
-
|
|
632
|
-
|
|
1347
|
+
let prevN = cv - 1;
|
|
1348
|
+
while (prevN > 0 && !previousHtml) {
|
|
1349
|
+
previousHtml = await (0, pages_1.loadChangeHtml)(config, page, prevN);
|
|
1350
|
+
if (!previousHtml)
|
|
1351
|
+
prevN--;
|
|
633
1352
|
}
|
|
634
|
-
|
|
635
|
-
// v0 = the saved page.html baseline (user folder → required folders fallback)
|
|
1353
|
+
if (!previousHtml) {
|
|
636
1354
|
previousHtml = await loadPageWithFallback(page, config, true);
|
|
637
1355
|
}
|
|
638
1356
|
if (!previousHtml) {
|
|
639
1357
|
res.status(500).send('Could not load previous version');
|
|
640
1358
|
return;
|
|
641
1359
|
}
|
|
642
|
-
// Inject
|
|
1360
|
+
// Inject content assets for iframe rendering (no shell chrome)
|
|
643
1361
|
const settings = await (0, settings_1.loadSettings)(config);
|
|
644
1362
|
const metadata = await (0, pages_1.loadPageMetadata)(config, page, config.requiredPagesFolders);
|
|
645
1363
|
const pv = metadata?.pageVersion ?? 0;
|
|
646
1364
|
const themeName = settings.theme ?? 'nebula-dusk';
|
|
647
1365
|
const themeVersion = await (0, themes_1.loadThemeVersion)(themeName, config);
|
|
648
|
-
let out = ensureRequiredImports(previousHtml, pv);
|
|
649
|
-
out = injectErrorCapture(out, pv);
|
|
650
|
-
out = injectShellAssets(out, themeName, themeVersion, settings.toolbarPosition);
|
|
651
|
-
out = injectPageInfoScript(out, page);
|
|
652
|
-
out = injectPageHelpers(out, pv);
|
|
653
|
-
out = injectPageScript(out, pv);
|
|
654
|
-
// Inject version meta tag if still have versions
|
|
655
|
-
if (prevVersion > 0) {
|
|
656
|
-
out = out.replace('</head>', `<meta name="synthos-version" content="${prevVersion}">\n</head>`);
|
|
657
|
-
}
|
|
658
|
-
// Same hardening as POST handler (undo also loads via document.write)
|
|
659
|
-
out = hoistExternalScriptsToHead(out);
|
|
660
|
-
out = wrapInlineScriptsInIIFE(out);
|
|
661
|
-
// Replace branding for white-label forks
|
|
662
1366
|
const productName = customizer?.productName ?? 'SynthOS';
|
|
663
|
-
|
|
664
|
-
out = out.replace(/synthos/gi, productName);
|
|
665
|
-
}
|
|
1367
|
+
const out = prepareForIframe(previousHtml, page, pv, themeName, themeVersion, productName);
|
|
666
1368
|
res.send(out);
|
|
667
1369
|
}
|
|
668
1370
|
catch (err) {
|
|
@@ -670,263 +1372,463 @@ function usePageRoutes(config, app, customizer) {
|
|
|
670
1372
|
res.status(500).send(err.message);
|
|
671
1373
|
}
|
|
672
1374
|
});
|
|
673
|
-
// Page
|
|
674
|
-
app.post('/:page', async (req, res) => {
|
|
1375
|
+
// Page reset — clear change journal, return saved baseline
|
|
1376
|
+
app.post('/:page/reset', async (req, res) => {
|
|
675
1377
|
try {
|
|
676
|
-
// Ensure settings configured
|
|
677
1378
|
const { page } = req.params;
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
1379
|
+
// Clear all change html + json files
|
|
1380
|
+
await (0, pages_1.migrateLegacyChangeFiles)(config, page);
|
|
1381
|
+
await (0, pages_1.clearChanges)(config, page);
|
|
1382
|
+
// Load the fresh baseline (page.html from user folder or required folders)
|
|
1383
|
+
const freshHtml = await loadPageWithFallback(page, config, true);
|
|
1384
|
+
if (!freshHtml) {
|
|
1385
|
+
res.status(404).json({ error: PAGE_NOT_FOUND });
|
|
681
1386
|
return;
|
|
682
1387
|
}
|
|
683
|
-
//
|
|
684
|
-
const
|
|
685
|
-
|
|
686
|
-
|
|
1388
|
+
// Build greeting from metadata (authoritative source)
|
|
1389
|
+
const settings = await (0, settings_1.loadSettings)(config);
|
|
1390
|
+
const metadata = await (0, pages_1.loadPageMetadata)(config, page, config.requiredPagesFolders);
|
|
1391
|
+
const greetingText = metadata?.greeting ?? '';
|
|
1392
|
+
const greeting = greetingText
|
|
1393
|
+
? [{ role: 'assistant', content: greetingText }]
|
|
1394
|
+
: [];
|
|
1395
|
+
const pv = metadata?.pageVersion ?? 0;
|
|
1396
|
+
const themeName = settings.theme ?? 'nebula-dusk';
|
|
1397
|
+
const themeVersion = await (0, themes_1.loadThemeVersion)(themeName, config);
|
|
1398
|
+
const productName = customizer?.productName ?? 'SynthOS';
|
|
1399
|
+
const out = prepareForIframe(freshHtml, page, pv, themeName, themeVersion, productName);
|
|
1400
|
+
res.json({ html: out, greeting: greeting });
|
|
1401
|
+
}
|
|
1402
|
+
catch (err) {
|
|
1403
|
+
console.error(err);
|
|
1404
|
+
res.status(500).json({ error: err.message });
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
// Update page greeting (inline edit from chat panel)
|
|
1408
|
+
app.post('/:page/greeting', async (req, res) => {
|
|
1409
|
+
try {
|
|
1410
|
+
const { page } = req.params;
|
|
1411
|
+
const { greeting } = req.body;
|
|
1412
|
+
if (typeof greeting !== 'string') {
|
|
1413
|
+
res.status(400).json({ error: 'greeting is required (string)' });
|
|
687
1414
|
return;
|
|
688
1415
|
}
|
|
689
|
-
// Reject
|
|
690
|
-
const
|
|
691
|
-
if (
|
|
692
|
-
res.status(
|
|
1416
|
+
// Reject editing on locked pages and the builder page
|
|
1417
|
+
const metadata = await (0, pages_1.loadPageMetadata)(config, page, config.requiredPagesFolders);
|
|
1418
|
+
if (!metadata) {
|
|
1419
|
+
res.status(404).json({ error: PAGE_NOT_FOUND });
|
|
693
1420
|
return;
|
|
694
1421
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
if (typeof message !== 'string') {
|
|
698
|
-
res.status(400).send('Invalid or missing message parameter');
|
|
1422
|
+
if (metadata.mode === 'locked') {
|
|
1423
|
+
res.status(403).json({ error: 'This page is locked' });
|
|
699
1424
|
return;
|
|
700
1425
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
1426
|
+
if (page === 'builder') {
|
|
1427
|
+
res.status(403).json({ error: 'Builder greeting is not editable' });
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
// Update greeting in metadata
|
|
1431
|
+
metadata.greeting = greeting.trim();
|
|
1432
|
+
metadata.lastModified = new Date().toISOString();
|
|
1433
|
+
await (0, pages_1.savePageMetadata)(config, page, metadata);
|
|
1434
|
+
res.json({ ok: true });
|
|
1435
|
+
}
|
|
1436
|
+
catch (err) {
|
|
1437
|
+
console.error(err);
|
|
1438
|
+
res.status(500).json({ error: err.message });
|
|
1439
|
+
}
|
|
1440
|
+
});
|
|
1441
|
+
// Page transformation
|
|
1442
|
+
app.post('/:page', async (req, res) => {
|
|
1443
|
+
const { page } = req.params;
|
|
1444
|
+
await withPageLock(page, async () => {
|
|
1445
|
+
// Hoisted so the catch block can append errors to the pre-committed
|
|
1446
|
+
// page.c<N>.json turn file.
|
|
1447
|
+
let pendingVersion;
|
|
1448
|
+
let shouldPersistTurn = false;
|
|
1449
|
+
let pendingRawMessage;
|
|
1450
|
+
try {
|
|
1451
|
+
// Ensure settings configured
|
|
1452
|
+
const isConfigured = await (0, settings_1.hasConfiguredSettings)(config);
|
|
1453
|
+
if (!isConfigured) {
|
|
1454
|
+
res.status(400).send('Settings not configured');
|
|
1455
|
+
return;
|
|
709
1456
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
1457
|
+
// Ensure page exists
|
|
1458
|
+
const pageState = await loadPageWithFallback(page, config, false);
|
|
1459
|
+
if (!pageState) {
|
|
1460
|
+
res.status(404).send(PAGE_NOT_FOUND);
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
// Reject modifications to locked pages
|
|
1464
|
+
const lockMetadata = await (0, pages_1.loadPageMetadata)(config, page, config.requiredPagesFolders);
|
|
1465
|
+
if (lockMetadata?.mode === 'locked') {
|
|
1466
|
+
res.status(403).send('This page is locked and cannot be modified');
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
// Get required and optional parameters
|
|
1470
|
+
const { message: rawMessage } = req.body;
|
|
1471
|
+
if (typeof rawMessage !== 'string') {
|
|
1472
|
+
res.status(400).send('Invalid or missing message parameter');
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
// If the shell sent captured JS errors, append them as context for the LLM
|
|
1476
|
+
// but keep the raw message clean for chat history persistence.
|
|
1477
|
+
const clientErrors = Array.isArray(req.body.errors) ? req.body.errors : [];
|
|
1478
|
+
const message = clientErrors.length > 0
|
|
1479
|
+
? rawMessage + '\n\nCONSOLE_ERRORS:\n' + clientErrors.join('\n---\n')
|
|
1480
|
+
: rawMessage;
|
|
1481
|
+
// Extract and validate optional attachments
|
|
1482
|
+
let attachments;
|
|
1483
|
+
if (Array.isArray(req.body.attachments)) {
|
|
1484
|
+
attachments = [];
|
|
1485
|
+
for (const att of req.body.attachments) {
|
|
1486
|
+
if (att && typeof att.mediaType === 'string' && att.mediaType.startsWith('image/') && typeof att.data === 'string') {
|
|
1487
|
+
attachments.push({ mediaType: att.mediaType, data: att.data, name: typeof att.name === 'string' ? att.name : undefined });
|
|
1488
|
+
}
|
|
729
1489
|
}
|
|
730
|
-
if (
|
|
731
|
-
|
|
1490
|
+
if (attachments.length === 0)
|
|
1491
|
+
attachments = undefined;
|
|
1492
|
+
}
|
|
1493
|
+
// Create model instance
|
|
1494
|
+
const innerCompletePrompt = await (0, createCompletePrompt_1.createCompletePrompt)(config, 'builder', req.body.model);
|
|
1495
|
+
const debugVerbose = config.debugPageUpdates;
|
|
1496
|
+
let inputChars = 0;
|
|
1497
|
+
let outputChars = 0;
|
|
1498
|
+
const wrapModel = (inner) => {
|
|
1499
|
+
return async (args) => {
|
|
1500
|
+
if (debugVerbose) {
|
|
1501
|
+
console.log((0, debugLog_1.green)((0, debugLog_1.dim)('\n ===== PAGE UPDATE REQUEST =====')));
|
|
1502
|
+
console.log((0, debugLog_1.green)(` SYSTEM:\n${args.system?.content}`));
|
|
1503
|
+
console.log((0, debugLog_1.green)(`\n PROMPT:\n${args.prompt.content}`));
|
|
1504
|
+
}
|
|
1505
|
+
inputChars += (args.system?.content?.length ?? 0) + (args.prompt.content?.length ?? 0);
|
|
1506
|
+
const result = await inner(args);
|
|
732
1507
|
if (result.completed) {
|
|
733
|
-
|
|
1508
|
+
outputChars += result.value?.length ?? 0;
|
|
1509
|
+
}
|
|
1510
|
+
if (debugVerbose) {
|
|
1511
|
+
console.log((0, debugLog_1.green)((0, debugLog_1.dim)('\n ----- PAGE UPDATE RESPONSE -----')));
|
|
1512
|
+
if (result.completed) {
|
|
1513
|
+
console.log((0, debugLog_1.green)(` RESPONSE:\n${result.value}`));
|
|
1514
|
+
}
|
|
1515
|
+
else {
|
|
1516
|
+
console.log((0, debugLog_1.red)(` ERROR: ${result.error?.message}`));
|
|
1517
|
+
}
|
|
1518
|
+
console.log((0, debugLog_1.green)((0, debugLog_1.dim)(' ================================\n')));
|
|
734
1519
|
}
|
|
735
|
-
|
|
736
|
-
|
|
1520
|
+
return result;
|
|
1521
|
+
};
|
|
1522
|
+
};
|
|
1523
|
+
const wrappedCompletePrompt = wrapModel(innerCompletePrompt);
|
|
1524
|
+
// Load settings and build context
|
|
1525
|
+
const pagesFolder = config.pagesFolder;
|
|
1526
|
+
const settings = await (0, settings_1.loadSettings)(config);
|
|
1527
|
+
const entry = (0, settings_1.getModelEntry)(settings, 'builder');
|
|
1528
|
+
const { instructions } = entry;
|
|
1529
|
+
const productName = customizer?.productName ?? 'SynthOS';
|
|
1530
|
+
// Build incoming conversation history from the server-side journal.
|
|
1531
|
+
// Shape: [greeting?, ...flat concat of page.c<n>.json messages]
|
|
1532
|
+
await (0, pages_1.migrateLegacyChangeFiles)(config, page);
|
|
1533
|
+
const pageMeta = await (0, pages_1.loadPageMetadata)(config, page, config.requiredPagesFolders);
|
|
1534
|
+
const incomingHistory = [];
|
|
1535
|
+
if (pageMeta?.greeting) {
|
|
1536
|
+
incomingHistory.push({ role: 'assistant', content: pageMeta.greeting });
|
|
1537
|
+
}
|
|
1538
|
+
incomingHistory.push(...await (0, pages_1.loadAllChangeMessages)(config, page));
|
|
1539
|
+
// Detect first edit (c0 → c1) for saved pages
|
|
1540
|
+
const isRequiredPage = config.requiredPages.includes(page);
|
|
1541
|
+
const pageFolder = path_1.default.join(pagesFolder, 'pages', page);
|
|
1542
|
+
const pageFileExists = await config.storageProvider.checkIfExists(path_1.default.join(pageFolder, 'page.html'));
|
|
1543
|
+
let currentVersion = await (0, pages_1.getLatestChangeNumber)(config, page);
|
|
1544
|
+
const isFirstEdit = !isRequiredPage && pageFileExists && currentVersion === 0;
|
|
1545
|
+
// Try again — drop the last change before re-running the transform
|
|
1546
|
+
const tryAgain = req.body.tryAgain === true;
|
|
1547
|
+
let transformInput = pageState;
|
|
1548
|
+
// New-build reset: when /builder receives its first message in a
|
|
1549
|
+
// fresh conversation (history is just the greeting, or empty), wipe
|
|
1550
|
+
// the change journal and start from a starter scaffold. The AI
|
|
1551
|
+
// reasons about the scaffold's "standard" node layout and emits ops
|
|
1552
|
+
// using those data-nid values; if the live page still holds stale
|
|
1553
|
+
// state from a previous session, those ops silently skip against
|
|
1554
|
+
// the wrong elements and the resulting page looks built but is
|
|
1555
|
+
// missing critical JS.
|
|
1556
|
+
//
|
|
1557
|
+
// Scaffold selection (in order):
|
|
1558
|
+
// 1. req.body.starter if it's a valid `_Starters` page
|
|
1559
|
+
// 2. blank_starter (the silent fallback for /builder with no pick)
|
|
1560
|
+
// 3. builder's own page.html (last-resort)
|
|
1561
|
+
const isNewBuildForBuilder = page === 'builder' && incomingHistory.length <= 1 && !tryAgain;
|
|
1562
|
+
if (isNewBuildForBuilder) {
|
|
1563
|
+
await (0, pages_1.clearChanges)(config, page);
|
|
1564
|
+
let scaffoldName = '';
|
|
1565
|
+
const requestedStarter = typeof req.body.starter === 'string' ? req.body.starter : '';
|
|
1566
|
+
if (requestedStarter) {
|
|
1567
|
+
const sm = await (0, pages_1.loadPageMetadata)(config, requestedStarter, config.requiredPagesFolders);
|
|
1568
|
+
if (sm?.categories?.includes('_Starters')) {
|
|
1569
|
+
scaffoldName = requestedStarter;
|
|
737
1570
|
}
|
|
738
|
-
console.log((0, debugLog_1.green)((0, debugLog_1.dim)(' ================================\n')));
|
|
739
1571
|
}
|
|
740
|
-
|
|
1572
|
+
if (!scaffoldName)
|
|
1573
|
+
scaffoldName = 'blank_starter';
|
|
1574
|
+
let scaffold = await loadPageWithFallback(scaffoldName, config, false);
|
|
1575
|
+
if (!scaffold && scaffoldName !== 'blank_starter') {
|
|
1576
|
+
scaffold = await loadPageWithFallback('blank_starter', config, false);
|
|
1577
|
+
}
|
|
1578
|
+
if (!scaffold) {
|
|
1579
|
+
scaffold = await loadPageWithFallback(page, config, false);
|
|
1580
|
+
}
|
|
1581
|
+
if (scaffold) {
|
|
1582
|
+
transformInput = scaffold;
|
|
1583
|
+
currentVersion = 0;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
if (tryAgain && currentVersion > 0) {
|
|
1587
|
+
// Drop the latest change (html + json) and reload prior state
|
|
1588
|
+
await (0, pages_1.deleteChange)(config, page, currentVersion);
|
|
1589
|
+
currentVersion = await (0, pages_1.getLatestChangeNumber)(config, page);
|
|
1590
|
+
const latestHtml = await (0, pages_1.getLatestChangeHtmlNumber)(config, page);
|
|
1591
|
+
if (latestHtml > 0) {
|
|
1592
|
+
transformInput = await (0, pages_1.loadChangeHtml)(config, page, latestHtml) ?? pageState;
|
|
1593
|
+
}
|
|
1594
|
+
else {
|
|
1595
|
+
transformInput = await loadPageWithFallback(page, config, false) ?? pageState;
|
|
1596
|
+
}
|
|
1597
|
+
// incomingHistory is stale now (it read before deletion); the
|
|
1598
|
+
// builder forces Opus+no-ranged for tryAgain regardless.
|
|
1599
|
+
}
|
|
1600
|
+
// --- Opt-8: NDJSON streaming response ---
|
|
1601
|
+
// Set headers for newline-delimited JSON streaming.
|
|
1602
|
+
// Progress events are emitted before the LLM call; the final result is the last line.
|
|
1603
|
+
res.setHeader('Content-Type', 'application/x-ndjson');
|
|
1604
|
+
res.setHeader('Transfer-Encoding', 'chunked');
|
|
1605
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
1606
|
+
const emitProgress = (status) => {
|
|
1607
|
+
res.write(JSON.stringify({ event: 'progress', status }) + '\n');
|
|
741
1608
|
};
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1609
|
+
// ---- Section assembly: build all sections, classifier picks expand ----
|
|
1610
|
+
// Each section ships with both `full` content and a `sketch`; the
|
|
1611
|
+
// classifier sees sketches and returns `expand: string[]` listing
|
|
1612
|
+
// which sections to render in full. Anything else renders as its
|
|
1613
|
+
// sketch (or skips entirely for empty omit-when-empty sections).
|
|
1614
|
+
// See docs/specs/page-section-sketch-full.md.
|
|
1615
|
+
const isNewBuildRequest = incomingHistory.length <= 1;
|
|
1616
|
+
// Build all candidate sections in deterministic order.
|
|
1617
|
+
const theme = settings.theme;
|
|
1618
|
+
const themeInfo = await (0, themes_1.loadThemeInfo)(theme ?? 'nebula-dusk', config);
|
|
1619
|
+
const allSections = [];
|
|
1620
|
+
allSections.push(buildContextSection());
|
|
1621
|
+
allSections.push(buildLlmdReadingGuideSection());
|
|
1622
|
+
allSections.push(buildServerApisSection(customizer));
|
|
1623
|
+
allSections.push(await buildServerScriptsSection(pagesFolder));
|
|
1624
|
+
allSections.push(buildThemeSection(themeInfo));
|
|
1625
|
+
allSections.push(buildConnectorsSection(settings.connectors));
|
|
1626
|
+
allSections.push(buildAgentsSection(settings.agents));
|
|
1627
|
+
allSections.push(await buildFluentLMSection(config));
|
|
1628
|
+
allSections.push(await buildRecommendedFrameworksSection(config));
|
|
1629
|
+
allSections.push(await buildSharedTablesSection(config));
|
|
1630
|
+
allSections.push(await buildPageTablesSection(config, page));
|
|
1631
|
+
// Custom transform instructions (always-full when present)
|
|
1632
|
+
const customTransformInstructions = customizer ? customizer.getTransformInstructions() : undefined;
|
|
1633
|
+
if (customTransformInstructions && customTransformInstructions.length > 0) {
|
|
1634
|
+
allSections.push({
|
|
1635
|
+
title: '<CUSTOM_INSTRUCTIONS>',
|
|
1636
|
+
content: customTransformInstructions.join('\n'),
|
|
1637
|
+
sketch: null,
|
|
1638
|
+
mode: 'always-full',
|
|
1639
|
+
instructions: '',
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
// User profile (always-full when present — small, identity context).
|
|
1643
|
+
const profileMarkdown = (0, settings_1.renderUserProfile)(settings.profile);
|
|
1644
|
+
if (profileMarkdown) {
|
|
1645
|
+
allSections.push({
|
|
1646
|
+
title: '<USER_PROFILE>',
|
|
1647
|
+
content: profileMarkdown,
|
|
1648
|
+
sketch: null,
|
|
1649
|
+
mode: 'always-full',
|
|
1650
|
+
forceFullOnInitial: true,
|
|
1651
|
+
instructions: 'Details about the current user. Personalize generated pages accordingly when relevant — use the name, address, locations, hours, and other details verbatim when they fit the page being built.',
|
|
1652
|
+
});
|
|
1653
|
+
console.log('user_profile: injected');
|
|
1654
|
+
}
|
|
1655
|
+
// Custom context sections from Customizer (appended last).
|
|
1656
|
+
if (customizer) {
|
|
1657
|
+
allSections.push(...customizer.getContextSections());
|
|
1658
|
+
}
|
|
1659
|
+
// Apply per-page section-mode overrides from page metadata.
|
|
1660
|
+
const sectionsAfterOverride = applySectionModeOverrides(allSections, pageMeta?.sectionModes);
|
|
1661
|
+
// Run the classifier (when an Anthropic API key is available). The
|
|
1662
|
+
// classifier sees sketches for every section that's not always-full
|
|
1663
|
+
// and not empty, and returns `expand: string[]` of titles to render
|
|
1664
|
+
// in full. Without an API key, fall back to expanding everything
|
|
1665
|
+
// expandable (mirrors the previous bypass behavior).
|
|
1666
|
+
let preClassified;
|
|
1667
|
+
let expand = new Set();
|
|
1668
|
+
const classifierSections = sectionsAfterOverride
|
|
1669
|
+
.filter(s => s.mode !== 'always-full' && s.sketch !== null)
|
|
1670
|
+
.map(s => ({ title: s.title, sketch: s.sketch }));
|
|
1671
|
+
if (entry.configuration.apiKey) {
|
|
1672
|
+
emitProgress('classifying');
|
|
1673
|
+
preClassified = await (0, anthropic_1.classifyRequest)(entry.configuration.apiKey, transformInput, rawMessage, classifierSections);
|
|
1674
|
+
console.log(`classifyRequest: "${preClassified.classification}" (expand: ${JSON.stringify(preClassified.expand)})`);
|
|
1675
|
+
expand = new Set(preClassified.expand);
|
|
1676
|
+
}
|
|
1677
|
+
else {
|
|
1678
|
+
// No classifier — expand every non-always-full section that has a sketch.
|
|
1679
|
+
for (const s of classifierSections)
|
|
1680
|
+
expand.add(s.title);
|
|
1681
|
+
}
|
|
1682
|
+
// Resolve each section to either its full content or its sketch
|
|
1683
|
+
// (or skip it entirely for empty omit-when-empty sections).
|
|
1684
|
+
const featureSections = assembleSections(sectionsAfterOverride, expand, isNewBuildRequest);
|
|
1685
|
+
// Create builder. Tool-handler-based section fetching is retired
|
|
1686
|
+
// in v1 — the classifier prefetches everything the model needs.
|
|
1687
|
+
const builder = (0, builders_1.createBuilder)(entry.provider, wrappedCompletePrompt, instructions, productName, {
|
|
1688
|
+
apiKey: entry.configuration.apiKey,
|
|
1689
|
+
model: entry.configuration.model,
|
|
1690
|
+
wrapModel,
|
|
1691
|
+
isFirstEdit,
|
|
1692
|
+
tryAgain,
|
|
1693
|
+
historyLength: incomingHistory.length,
|
|
1694
|
+
preClassified,
|
|
789
1695
|
});
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1696
|
+
// Pre-commit the user message to page.c<N>.json BEFORE the LLM runs
|
|
1697
|
+
// so any thrown error has a concrete file to append to. Skipped for
|
|
1698
|
+
// Starters/System (ephemeral). The final write in the success path
|
|
1699
|
+
// overwrites this with [user, assistant]; the catch block appends
|
|
1700
|
+
// `Error: ...` to this same file if the LLM call throws.
|
|
1701
|
+
const precommitCategories = pageMeta?.categories ?? [];
|
|
1702
|
+
shouldPersistTurn = !precommitCategories.some(c => NO_PERSIST_CATEGORIES.includes(c));
|
|
1703
|
+
pendingVersion = currentVersion + 1;
|
|
1704
|
+
pendingRawMessage = rawMessage;
|
|
1705
|
+
if (shouldPersistTurn) {
|
|
1706
|
+
await (0, pages_1.saveChangeMessages)(config, page, pendingVersion, [
|
|
1707
|
+
{ role: 'user', content: rawMessage },
|
|
1708
|
+
]);
|
|
1709
|
+
}
|
|
1710
|
+
// Transform page
|
|
1711
|
+
emitProgress('transforming');
|
|
1712
|
+
const result = await (0, transformPage_1.transformPage)({
|
|
1713
|
+
pageState: transformInput,
|
|
1714
|
+
message,
|
|
1715
|
+
instructions,
|
|
1716
|
+
builder,
|
|
1717
|
+
additionalSections: featureSections,
|
|
1718
|
+
isBuilder: page === 'builder',
|
|
1719
|
+
productName,
|
|
1720
|
+
attachments,
|
|
1721
|
+
history: incomingHistory,
|
|
1722
|
+
});
|
|
1723
|
+
if (result.completed) {
|
|
1724
|
+
let { html, changeCount, replyText, errorText, skippedOps, skipReasons } = result.value;
|
|
1725
|
+
if (config.debug) {
|
|
1726
|
+
const inTokens = (0, debugLog_1.estimateTokens)(inputChars).toLocaleString();
|
|
1727
|
+
const outTokens = (0, debugLog_1.estimateTokens)(outputChars).toLocaleString();
|
|
1728
|
+
console.log(` page: ${page} | message: ${message.length} chars | changes: ${changeCount} ops | ~${inTokens} in / ~${outTokens} out tokens`);
|
|
1729
|
+
}
|
|
1730
|
+
// Handle 0-ops: model returned no page changes.
|
|
1731
|
+
// Skip HTML snapshot entirely — identical content would pollute undo history.
|
|
1732
|
+
const madeChanges = changeCount > 0;
|
|
1733
|
+
if (!madeChanges) {
|
|
1734
|
+
html = pageState;
|
|
1735
|
+
}
|
|
1736
|
+
const nextVersion = pendingVersion;
|
|
1737
|
+
if (madeChanges) {
|
|
1738
|
+
await (0, pages_1.saveChangeHtml)(config, page, nextVersion, html);
|
|
1739
|
+
}
|
|
1740
|
+
// Update lastModified timestamp only when actual changes were made
|
|
1741
|
+
const metadata = await (0, pages_1.loadPageMetadata)(config, page, config.requiredPagesFolders);
|
|
1742
|
+
if (madeChanges && metadata) {
|
|
1743
|
+
metadata.lastModified = new Date().toISOString();
|
|
1744
|
+
await (0, pages_1.savePageMetadata)(config, page, metadata);
|
|
1745
|
+
}
|
|
1746
|
+
// Finalize the turn's page.c<N>.json — overwrite the user-only
|
|
1747
|
+
// pre-commit with the full [user, assistant] pair.
|
|
1748
|
+
const turnMessages = [{ role: 'user', content: rawMessage }];
|
|
1749
|
+
if (replyText) {
|
|
1750
|
+
turnMessages.push({ role: 'assistant', content: replyText });
|
|
1751
|
+
}
|
|
1752
|
+
else if (errorText) {
|
|
1753
|
+
turnMessages.push({ role: 'assistant', content: `Error: ${errorText}` });
|
|
1754
|
+
}
|
|
1755
|
+
else {
|
|
1756
|
+
turnMessages.push({ role: 'assistant', content: `Made ${changeCount} change(s) to the page.` });
|
|
1757
|
+
}
|
|
1758
|
+
if (shouldPersistTurn) {
|
|
1759
|
+
await (0, pages_1.saveChangeMessages)(config, page, nextVersion, turnMessages);
|
|
1760
|
+
}
|
|
1761
|
+
// Inject content assets for iframe rendering (no shell chrome)
|
|
1762
|
+
const pv = metadata?.pageVersion ?? 0;
|
|
1763
|
+
const themeName = settings.theme ?? 'nebula-dusk';
|
|
1764
|
+
const themeVersion = await (0, themes_1.loadThemeVersion)(themeName, config);
|
|
1765
|
+
const out = prepareForIframe(html, page, pv, themeName, themeVersion, productName);
|
|
1766
|
+
// Surface builder/transform errors to the server log so they're
|
|
1767
|
+
// not lost when only the client sees the NDJSON result event.
|
|
1768
|
+
if (errorText) {
|
|
1769
|
+
console.error((0, debugLog_1.red)(` BUILDER ERROR: ${errorText}`));
|
|
1770
|
+
}
|
|
1771
|
+
// Surface skipped ops to the server log so partial builds are
|
|
1772
|
+
// visible in operator logs, not just the client-side warning.
|
|
1773
|
+
if (skippedOps && skippedOps > 0) {
|
|
1774
|
+
console.warn((0, debugLog_1.red)(` SKIPPED ${skippedOps} op(s) on ${page}: ${skipReasons?.join('; ')}`));
|
|
1775
|
+
}
|
|
1776
|
+
// `version` reflects the latest HTML snapshot, not the journal —
|
|
1777
|
+
// the shell uses it to decide whether the save button lights up.
|
|
1778
|
+
const htmlVersion = madeChanges
|
|
1779
|
+
? nextVersion
|
|
1780
|
+
: await (0, pages_1.getLatestChangeHtmlNumber)(config, page);
|
|
1781
|
+
res.write(JSON.stringify({
|
|
1782
|
+
event: 'result',
|
|
1783
|
+
html: out,
|
|
1784
|
+
replyText: replyText ?? null,
|
|
1785
|
+
errorText: errorText ?? null,
|
|
1786
|
+
changeCount,
|
|
1787
|
+
skippedOps: skippedOps ?? 0,
|
|
1788
|
+
skipReasons: skipReasons ?? [],
|
|
1789
|
+
version: htmlVersion,
|
|
1790
|
+
}) + '\n');
|
|
1791
|
+
res.end();
|
|
814
1792
|
}
|
|
815
1793
|
else {
|
|
816
|
-
|
|
1794
|
+
throw result.error;
|
|
817
1795
|
}
|
|
818
|
-
currentVersion = prevVersion;
|
|
819
1796
|
}
|
|
820
|
-
|
|
821
|
-
const builder = (0, builders_1.createBuilder)(entry.provider, wrappedCompletePrompt, instructions, productName, {
|
|
822
|
-
apiKey: entry.configuration.apiKey,
|
|
823
|
-
model: entry.configuration.model,
|
|
824
|
-
wrapModel,
|
|
825
|
-
isFirstEdit,
|
|
826
|
-
tryAgain,
|
|
827
|
-
});
|
|
828
|
-
// Inject save-line before transform if not already present, so new
|
|
829
|
-
// messages appended by the LLM land after the marker.
|
|
830
|
-
{
|
|
831
|
-
const $pre = cheerio.load(transformInput, { decodeEntities: false });
|
|
832
|
-
if ($pre('#chatMessages').length > 0 && $pre('#chatMessages .save-line').length === 0) {
|
|
833
|
-
$pre('#chatMessages').append('<div class="save-line" data-locked="true"><span class="save-line-label">Saved</span></div>');
|
|
834
|
-
transformInput = $pre.html();
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
// Transform page
|
|
838
|
-
const result = await (0, transformPage_1.transformPage)({
|
|
839
|
-
pageState: transformInput,
|
|
840
|
-
message,
|
|
841
|
-
instructions,
|
|
842
|
-
builder,
|
|
843
|
-
additionalSections: featureSections,
|
|
844
|
-
isBuilder: page === 'builder',
|
|
845
|
-
productName,
|
|
846
|
-
attachments,
|
|
847
|
-
});
|
|
848
|
-
if (result.completed) {
|
|
849
|
-
let { html, changeCount } = result.value;
|
|
1797
|
+
catch (err) {
|
|
850
1798
|
if (config.debug) {
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
// Append the
|
|
857
|
-
//
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
chatMessages.append(`<div class="chat-message"><p><strong>User:</strong> ${escapedMsg}</p></div>`);
|
|
865
|
-
const tryAgainLink = `<a href="#" style="color:var(--themePrimary);text-decoration:underline;cursor:pointer" `
|
|
866
|
-
+ `onclick="(function(e){e.preventDefault();var ci=document.getElementById('chatInput');if(ci){ci.focus();ci.value='';}})(event)">try again</a>`;
|
|
867
|
-
const undoLink = `<a href="#" style="color:var(--themePrimary);text-decoration:underline;cursor:pointer" `
|
|
868
|
-
+ `onclick="(function(e){e.preventDefault();var o=document.getElementById('loadingOverlay');if(o)o.style.display='flex';`
|
|
869
|
-
+ `fetch(window.location.pathname+'/undo',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'})`
|
|
870
|
-
+ `.then(function(r){return r.text()}).then(function(h){document.open();document.write(h);document.close()})`
|
|
871
|
-
+ `.catch(function(){if(o)o.style.display='none'});})(event)">undo</a>`;
|
|
872
|
-
chatMessages.append(`<div class="chat-message"><p><strong>${productName}:</strong> Sorry, I wasn\u2019t able to make changes for that request. Please ${tryAgainLink} or ${undoLink}.</p></div>`);
|
|
1799
|
+
console.log((0, debugLog_1.red)(` ERROR: ${err.message}`));
|
|
1800
|
+
}
|
|
1801
|
+
else {
|
|
1802
|
+
console.error(err);
|
|
1803
|
+
}
|
|
1804
|
+
// Append the error to the pre-committed turn file so the user can
|
|
1805
|
+
// see what went wrong on the next page load.
|
|
1806
|
+
if (pendingVersion != null && shouldPersistTurn && pendingRawMessage != null) {
|
|
1807
|
+
try {
|
|
1808
|
+
await (0, pages_1.saveChangeMessages)(config, page, pendingVersion, [
|
|
1809
|
+
{ role: 'user', content: pendingRawMessage },
|
|
1810
|
+
{ role: 'assistant', content: `Error: ${err.message}` },
|
|
1811
|
+
]);
|
|
873
1812
|
}
|
|
874
|
-
|
|
1813
|
+
catch { /* journal write must never mask the real error */ }
|
|
875
1814
|
}
|
|
876
|
-
//
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
await (0, pages_1.savePageMetadata)(config, page, metadata);
|
|
1815
|
+
// Emit error as NDJSON if headers already sent, otherwise plain error
|
|
1816
|
+
if (res.headersSent) {
|
|
1817
|
+
res.write(JSON.stringify({ event: 'error', error: err.message }) + '\n');
|
|
1818
|
+
res.end();
|
|
1819
|
+
}
|
|
1820
|
+
else {
|
|
1821
|
+
res.status(500).send(err.message);
|
|
884
1822
|
}
|
|
885
|
-
// Inject required imports and page scripts (same as GET)
|
|
886
|
-
const pv = metadata?.pageVersion ?? 0;
|
|
887
|
-
const themeName = settings.theme ?? 'nebula-dusk';
|
|
888
|
-
const themeVersion = await (0, themes_1.loadThemeVersion)(themeName, config);
|
|
889
|
-
let out = ensureRequiredImports(html, pv);
|
|
890
|
-
out = injectErrorCapture(out, pv);
|
|
891
|
-
out = injectShellAssets(out, themeName, themeVersion, settings.toolbarPosition);
|
|
892
|
-
out = injectPageInfoScript(out, page);
|
|
893
|
-
out = injectPageHelpers(out, pv);
|
|
894
|
-
out = injectPageScript(out, pv);
|
|
895
|
-
// Inject version meta tag for client-side undo support
|
|
896
|
-
if (nextVersion > 0) {
|
|
897
|
-
out = out.replace('</head>', `<meta name="synthos-version" content="${nextVersion}">\n</head>`);
|
|
898
|
-
}
|
|
899
|
-
// Hoist external scripts to <head> so libs load before
|
|
900
|
-
// inline body scripts, then wrap inlines in IIFEs to
|
|
901
|
-
// avoid const/let redeclaration via document.write().
|
|
902
|
-
out = hoistExternalScriptsToHead(out);
|
|
903
|
-
out = wrapInlineScriptsInIIFE(out);
|
|
904
|
-
// Replace branding for white-label forks
|
|
905
|
-
if (productName !== 'SynthOS') {
|
|
906
|
-
out = out.replace(/synthos/gi, productName);
|
|
907
|
-
}
|
|
908
|
-
res.send(out);
|
|
909
|
-
}
|
|
910
|
-
else {
|
|
911
|
-
throw result.error;
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
catch (err) {
|
|
915
|
-
if (config.debug) {
|
|
916
|
-
console.log((0, debugLog_1.red)(` ERROR: ${err.message}`));
|
|
917
|
-
}
|
|
918
|
-
else {
|
|
919
|
-
console.error(err);
|
|
920
1823
|
}
|
|
921
|
-
|
|
922
|
-
}
|
|
1824
|
+
}); // withPageLock
|
|
923
1825
|
});
|
|
924
1826
|
}
|
|
925
1827
|
exports.usePageRoutes = usePageRoutes;
|
|
926
1828
|
async function loadPageWithFallback(page, config, reset) {
|
|
927
1829
|
if (reset) {
|
|
928
|
-
// Clear working-state
|
|
929
|
-
await (0, pages_1.
|
|
1830
|
+
// Clear working-state changes so we get the fresh template
|
|
1831
|
+
await (0, pages_1.clearChanges)(config, page);
|
|
930
1832
|
}
|
|
931
1833
|
// Try primary pages folder first
|
|
932
1834
|
const pageState = await (0, pages_1.loadPageState)(config, page);
|