synthos 0.6.0 → 0.7.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 +33 -1
- package/default-pages/app_builder.html +40 -0
- package/default-pages/app_builder.json +1 -0
- package/default-pages/json_tools.html +89 -159
- package/default-pages/json_tools.json +1 -0
- package/default-pages/my_notes.html +33 -0
- package/default-pages/my_notes.json +12 -0
- package/default-pages/neon_asteroids.html +77 -0
- package/default-pages/neon_asteroids.json +12 -0
- package/default-pages/sidebar_builder.html +49 -0
- package/default-pages/sidebar_builder.json +1 -0
- package/default-pages/solar_explorer.html +1956 -0
- package/default-pages/solar_explorer.json +12 -0
- package/default-pages/solar_tutorial.html +476 -0
- package/default-pages/solar_tutorial.json +1 -0
- package/default-pages/two-panel_builder.html +66 -0
- package/default-pages/two-panel_builder.json +1 -0
- package/dist/connectors/index.d.ts +3 -0
- package/dist/connectors/index.d.ts.map +1 -0
- package/dist/connectors/index.js +6 -0
- package/dist/connectors/index.js.map +1 -0
- package/dist/connectors/registry.d.ts +3 -0
- package/dist/connectors/registry.d.ts.map +1 -0
- package/dist/connectors/registry.js +100 -0
- package/dist/connectors/registry.js.map +1 -0
- package/dist/connectors/types.d.ts +61 -0
- package/dist/connectors/types.d.ts.map +1 -0
- package/dist/connectors/types.js +3 -0
- package/dist/connectors/types.js.map +1 -0
- package/dist/files.d.ts +2 -0
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +12 -1
- package/dist/files.js.map +1 -1
- package/dist/init.d.ts +8 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +155 -3
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts +11 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +281 -0
- package/dist/migrations.js.map +1 -0
- package/dist/models/index.d.ts +3 -0
- package/dist/models/index.d.ts.map +1 -0
- package/dist/models/index.js +10 -0
- package/dist/models/index.js.map +1 -0
- package/dist/models/providers.d.ts +7 -0
- package/dist/models/providers.d.ts.map +1 -0
- package/dist/models/providers.js +33 -0
- package/dist/models/providers.js.map +1 -0
- package/dist/models/types.d.ts +21 -0
- package/dist/models/types.d.ts.map +1 -0
- package/dist/models/types.js +3 -0
- package/dist/models/types.js.map +1 -0
- package/dist/pages.d.ts +21 -2
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +202 -23
- package/dist/pages.js.map +1 -1
- package/dist/scripts.js +2 -2
- package/dist/scripts.js.map +1 -1
- package/dist/service/createCompletePrompt.d.ts +3 -2
- package/dist/service/createCompletePrompt.d.ts.map +1 -1
- package/dist/service/createCompletePrompt.js +11 -16
- package/dist/service/createCompletePrompt.js.map +1 -1
- package/dist/service/debugLog.d.ts +11 -0
- package/dist/service/debugLog.d.ts.map +1 -0
- package/dist/service/debugLog.js +26 -0
- package/dist/service/debugLog.js.map +1 -0
- package/dist/service/modelInstructions.d.ts +7 -0
- package/dist/service/modelInstructions.d.ts.map +1 -0
- package/dist/service/modelInstructions.js +16 -0
- package/dist/service/modelInstructions.js.map +1 -0
- package/dist/service/requiresSettings.d.ts +2 -2
- package/dist/service/requiresSettings.d.ts.map +1 -1
- package/dist/service/requiresSettings.js.map +1 -1
- package/dist/service/server.d.ts.map +1 -1
- package/dist/service/server.js +15 -0
- package/dist/service/server.js.map +1 -1
- package/dist/service/transformPage.d.ts +81 -2
- package/dist/service/transformPage.d.ts.map +1 -1
- package/dist/service/transformPage.js +672 -82
- package/dist/service/transformPage.js.map +1 -1
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +579 -13
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.d.ts +4 -0
- package/dist/service/useConnectorRoutes.d.ts.map +1 -0
- package/dist/service/useConnectorRoutes.js +389 -0
- package/dist/service/useConnectorRoutes.js.map +1 -0
- package/dist/service/useDataRoutes.d.ts.map +1 -1
- package/dist/service/useDataRoutes.js +83 -70
- package/dist/service/useDataRoutes.js.map +1 -1
- package/dist/service/usePageRoutes.d.ts.map +1 -1
- package/dist/service/usePageRoutes.js +243 -38
- package/dist/service/usePageRoutes.js.map +1 -1
- package/dist/settings.d.ts +33 -4
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +108 -15
- package/dist/settings.js.map +1 -1
- package/dist/synthos-cli.d.ts.map +1 -1
- package/dist/synthos-cli.js +11 -1
- package/dist/synthos-cli.js.map +1 -1
- package/dist/themes.d.ts +9 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js +64 -0
- package/dist/themes.js.map +1 -0
- package/package.json +5 -3
- package/required-pages/builder.html +74 -0
- package/required-pages/builder.json +1 -0
- package/required-pages/pages.html +169 -126
- package/required-pages/pages.json +1 -0
- package/required-pages/settings.html +812 -156
- package/required-pages/settings.json +1 -0
- package/required-pages/synthos_apis.html +272 -0
- package/required-pages/synthos_apis.json +1 -0
- package/required-pages/synthos_scripts.html +87 -0
- package/required-pages/synthos_scripts.json +1 -0
- package/src/connectors/index.ts +12 -0
- package/src/connectors/registry.ts +98 -0
- package/src/connectors/types.ts +68 -0
- package/src/files.ts +11 -0
- package/src/init.ts +151 -5
- package/src/migrations.ts +266 -0
- package/src/models/index.ts +2 -0
- package/src/models/providers.ts +33 -0
- package/src/models/types.ts +23 -0
- package/src/pages.ts +234 -26
- package/src/scripts.ts +2 -2
- package/src/service/createCompletePrompt.ts +14 -18
- package/src/service/debugLog.ts +17 -0
- package/src/service/modelInstructions.ts +14 -0
- package/src/service/requiresSettings.ts +3 -3
- package/src/service/server.ts +19 -2
- package/src/service/transformPage.ts +709 -88
- package/src/service/useApiRoutes.ts +632 -16
- package/src/service/useConnectorRoutes.ts +427 -0
- package/src/service/useDataRoutes.ts +87 -71
- package/src/service/usePageRoutes.ts +237 -44
- package/src/settings.ts +143 -20
- package/src/synthos-cli.ts +11 -1
- package/src/themes.ts +71 -0
- package/default-pages/[application].html +0 -95
- package/default-pages/[markdown].html +0 -271
- package/default-pages/[sidebar].html +0 -114
- package/default-pages/[split-application].html +0 -118
- package/default-pages/solar_system.html +0 -432
- package/default-pages/space_invaders.html +0 -617
- package/required-pages/apis.html +0 -362
- package/required-pages/home.html +0 -126
- package/required-pages/scripts.html +0 -350
package/src/init.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
1
2
|
import path from "path";
|
|
2
|
-
import { checkIfExists, copyFile, copyFiles, ensureFolderExists, saveFile } from "./files";
|
|
3
|
+
import { checkIfExists, copyFile, copyFiles, deleteFile, ensureFolderExists, listFiles, saveFile } from "./files";
|
|
4
|
+
import { PAGE_VERSION } from "./pages";
|
|
3
5
|
import { DefaultSettings } from "./settings";
|
|
4
6
|
|
|
5
7
|
export interface SynthOSConfig {
|
|
@@ -7,20 +9,29 @@ export interface SynthOSConfig {
|
|
|
7
9
|
requiredPagesFolder: string;
|
|
8
10
|
defaultPagesFolder: string;
|
|
9
11
|
defaultScriptsFolder: string;
|
|
12
|
+
defaultThemesFolder: string;
|
|
13
|
+
pageScriptsFolder: string;
|
|
14
|
+
debug: boolean;
|
|
15
|
+
debugPageUpdates: boolean;
|
|
10
16
|
}
|
|
11
17
|
|
|
12
|
-
export function createConfig(pagesFolder = '.synthos'): SynthOSConfig {
|
|
18
|
+
export function createConfig(pagesFolder = '.synthos', options?: { debug?: boolean; debugPageUpdates?: boolean }): SynthOSConfig {
|
|
13
19
|
return {
|
|
14
20
|
pagesFolder: path.join(process.cwd(), pagesFolder),
|
|
15
21
|
requiredPagesFolder: path.join(__dirname, '../required-pages'),
|
|
16
22
|
defaultPagesFolder: path.join(__dirname, '../default-pages'),
|
|
17
|
-
defaultScriptsFolder: path.join(__dirname, '../default-scripts')
|
|
23
|
+
defaultScriptsFolder: path.join(__dirname, '../default-scripts'),
|
|
24
|
+
defaultThemesFolder: path.join(__dirname, '../default-themes'),
|
|
25
|
+
pageScriptsFolder: path.join(__dirname, '../page-scripts'),
|
|
26
|
+
debug: options?.debug ?? false,
|
|
27
|
+
debugPageUpdates: options?.debugPageUpdates ?? false
|
|
18
28
|
};
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
export async function init(config: SynthOSConfig, includeDefaultPages: boolean = true): Promise<boolean> {
|
|
22
32
|
// Check for existing folder
|
|
23
33
|
if (await checkIfExists(config.pagesFolder)) {
|
|
34
|
+
await repairMissingFolders(config);
|
|
24
35
|
return false;
|
|
25
36
|
}
|
|
26
37
|
|
|
@@ -55,12 +66,147 @@ export async function init(config: SynthOSConfig, includeDefaultPages: boolean =
|
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
await saveFile(path.join(scriptsFolder, 'example.sh'), '#!/bin/bash\n\n# This is an example script\n\n# You can run this script using the following command:\n# sh .synthos/scripts/example.sh\n\n# This script will print "Hello, World!" to the console\n\necho "Hello, World!"\n');
|
|
69
|
+
|
|
70
|
+
// Setup default themes
|
|
71
|
+
console.log(`Copying default themes to .synthos folder...`);
|
|
72
|
+
const themesFolder = path.join(config.pagesFolder, 'themes');
|
|
73
|
+
await ensureFolderExists(themesFolder);
|
|
74
|
+
await copyFiles(config.defaultThemesFolder, themesFolder);
|
|
75
|
+
|
|
58
76
|
// Copy pages
|
|
59
77
|
if (includeDefaultPages) {
|
|
60
78
|
console.log(`Copying default pages to .synthos folder...`);
|
|
61
|
-
await
|
|
79
|
+
await copyDefaultPages(config.defaultPagesFolder, config.pagesFolder);
|
|
62
80
|
}
|
|
63
81
|
|
|
64
82
|
return true;
|
|
65
83
|
}
|
|
66
|
-
|
|
84
|
+
|
|
85
|
+
async function repairMissingFolders(config: SynthOSConfig): Promise<void> {
|
|
86
|
+
// Rebuild scripts folder from defaults if missing
|
|
87
|
+
const scriptsFolder = path.join(config.pagesFolder, 'scripts');
|
|
88
|
+
if (!await checkIfExists(scriptsFolder)) {
|
|
89
|
+
console.log(`Restoring default scripts to .synthos folder...`);
|
|
90
|
+
await ensureFolderExists(scriptsFolder);
|
|
91
|
+
switch (process.platform) {
|
|
92
|
+
case 'win32':
|
|
93
|
+
await copyFile(path.join(config.defaultScriptsFolder, 'windows-terminal.json'), scriptsFolder);
|
|
94
|
+
break;
|
|
95
|
+
case 'darwin':
|
|
96
|
+
await copyFile(path.join(config.defaultScriptsFolder, 'mac-terminal.json'), scriptsFolder);
|
|
97
|
+
break;
|
|
98
|
+
case 'android':
|
|
99
|
+
await copyFile(path.join(config.defaultScriptsFolder, 'android-terminal.json'), scriptsFolder);
|
|
100
|
+
break;
|
|
101
|
+
case 'linux':
|
|
102
|
+
default:
|
|
103
|
+
await copyFile(path.join(config.defaultScriptsFolder, 'linux-terminal.json'), scriptsFolder);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
await saveFile(path.join(scriptsFolder, 'example.sh'), '#!/bin/bash\n\n# This is an example script\n\n# You can run this script using the following command:\n# sh .synthos/scripts/example.sh\n\n# This script will print "Hello, World!" to the console\n\necho "Hello, World!"\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Rebuild themes folder from defaults if missing
|
|
110
|
+
const themesFolder = path.join(config.pagesFolder, 'themes');
|
|
111
|
+
if (!await checkIfExists(themesFolder)) {
|
|
112
|
+
console.log(`Restoring default themes to .synthos folder...`);
|
|
113
|
+
await ensureFolderExists(themesFolder);
|
|
114
|
+
await copyFiles(config.defaultThemesFolder, themesFolder);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Ensure pages/ subfolder exists
|
|
118
|
+
const pagesSubdir = path.join(config.pagesFolder, 'pages');
|
|
119
|
+
if (!await checkIfExists(pagesSubdir)) {
|
|
120
|
+
// No pages folder and no flat files — rebuild from defaults
|
|
121
|
+
const htmlFiles = (await listFiles(config.pagesFolder)).filter(f => f.endsWith('.html'));
|
|
122
|
+
if (htmlFiles.length === 0) {
|
|
123
|
+
console.log(`Restoring default pages to .synthos/pages/ folder...`);
|
|
124
|
+
await copyDefaultPages(config.defaultPagesFolder, config.pagesFolder);
|
|
125
|
+
} else {
|
|
126
|
+
await ensureFolderExists(pagesSubdir);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Migrate any stray flat .html files from root into pages/<name>/
|
|
131
|
+
await migrateFlatPages(config.pagesFolder);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function toTitleCase(name: string): string {
|
|
135
|
+
// Strip brackets, replace underscores/hyphens with spaces, then Title Case each word
|
|
136
|
+
return name
|
|
137
|
+
.replace(/[\[\]]/g, '')
|
|
138
|
+
.replace(/[_-]/g, ' ')
|
|
139
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function migrateFlatPages(pagesFolder: string): Promise<void> {
|
|
143
|
+
const pagesSubdir = path.join(pagesFolder, 'pages');
|
|
144
|
+
const htmlFiles = (await listFiles(pagesFolder)).filter(f => f.endsWith('.html'));
|
|
145
|
+
if (htmlFiles.length === 0) return;
|
|
146
|
+
|
|
147
|
+
console.log(`Migrating ${htmlFiles.length} page(s) to .synthos/pages/ folder...`);
|
|
148
|
+
await ensureFolderExists(pagesSubdir);
|
|
149
|
+
const now = new Date().toISOString();
|
|
150
|
+
|
|
151
|
+
for (const file of htmlFiles) {
|
|
152
|
+
const pageName = file.replace(/\.html$/, '');
|
|
153
|
+
const category = pageName.startsWith('[') ? 'Builder' : 'Pages';
|
|
154
|
+
const title = toTitleCase(pageName);
|
|
155
|
+
const pageFolder = path.join(pagesSubdir, pageName);
|
|
156
|
+
await ensureFolderExists(pageFolder);
|
|
157
|
+
await fs.copyFile(
|
|
158
|
+
path.join(pagesFolder, file),
|
|
159
|
+
path.join(pageFolder, 'page.html')
|
|
160
|
+
);
|
|
161
|
+
await saveFile(
|
|
162
|
+
path.join(pageFolder, 'page.json'),
|
|
163
|
+
JSON.stringify({
|
|
164
|
+
title,
|
|
165
|
+
categories: [category],
|
|
166
|
+
pinned: false,
|
|
167
|
+
createdDate: now,
|
|
168
|
+
lastModified: now,
|
|
169
|
+
pageVersion: 1,
|
|
170
|
+
mode: 'unlocked',
|
|
171
|
+
}, null, 4)
|
|
172
|
+
);
|
|
173
|
+
await deleteFile(path.join(pagesFolder, file));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function copyDefaultPages(srcFolder: string, destFolder: string): Promise<void> {
|
|
178
|
+
const pagesDir = path.join(destFolder, 'pages');
|
|
179
|
+
await ensureFolderExists(pagesDir);
|
|
180
|
+
const files = await fs.readdir(srcFolder);
|
|
181
|
+
const now = new Date().toISOString();
|
|
182
|
+
for (const file of files) {
|
|
183
|
+
if (!file.endsWith('.html')) continue;
|
|
184
|
+
const pageName = file.replace(/\.html$/, '');
|
|
185
|
+
const pageFolder = path.join(pagesDir, pageName);
|
|
186
|
+
await ensureFolderExists(pageFolder);
|
|
187
|
+
await fs.copyFile(path.join(srcFolder, file), path.join(pageFolder, 'page.html'));
|
|
188
|
+
|
|
189
|
+
// Read companion .json metadata from source folder, fall back to defaults
|
|
190
|
+
let metadata: Record<string, unknown> = {};
|
|
191
|
+
const jsonPath = path.join(srcFolder, `${pageName}.json`);
|
|
192
|
+
if (await checkIfExists(jsonPath)) {
|
|
193
|
+
try {
|
|
194
|
+
const raw = await fs.readFile(jsonPath, 'utf-8');
|
|
195
|
+
metadata = JSON.parse(raw);
|
|
196
|
+
} catch {
|
|
197
|
+
// use defaults
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const fullMetadata = {
|
|
201
|
+
title: typeof metadata.title === 'string' ? metadata.title : '',
|
|
202
|
+
categories: Array.isArray(metadata.categories) ? metadata.categories : [],
|
|
203
|
+
pinned: typeof metadata.pinned === 'boolean' ? metadata.pinned : false,
|
|
204
|
+
createdDate: now,
|
|
205
|
+
lastModified: now,
|
|
206
|
+
pageVersion: typeof metadata.pageVersion === 'number' ? metadata.pageVersion
|
|
207
|
+
: typeof metadata.uxVersion === 'number' ? metadata.uxVersion : PAGE_VERSION,
|
|
208
|
+
mode: metadata.mode === 'locked' ? 'locked' : 'unlocked',
|
|
209
|
+
};
|
|
210
|
+
await saveFile(path.join(pageFolder, 'page.json'), JSON.stringify(fullMetadata, null, 4));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import * as cheerio from 'cheerio';
|
|
2
|
+
import { completePrompt } from 'agentm-core';
|
|
3
|
+
import { deduplicateInlineScripts } from './service/transformPage';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Registry of migration functions: fromVersion -> transform.
|
|
7
|
+
* Each function transforms page HTML from version N to version N+1.
|
|
8
|
+
*/
|
|
9
|
+
const migrations: Record<number, (html: string, completePrompt: completePrompt) => Promise<string>> = {
|
|
10
|
+
1: migrateV1toV2,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Migrate a page's HTML from one version to another by applying
|
|
15
|
+
* sequential migration steps.
|
|
16
|
+
*/
|
|
17
|
+
export async function migratePage(html: string, fromVersion: number, toVersion: number, completePrompt: completePrompt): Promise<string> {
|
|
18
|
+
let current = html;
|
|
19
|
+
for (let v = fromVersion; v < toVersion; v++) {
|
|
20
|
+
const migrate = migrations[v];
|
|
21
|
+
if (!migrate) {
|
|
22
|
+
throw new Error(`No migration defined for version ${v} -> ${v + 1}`);
|
|
23
|
+
}
|
|
24
|
+
current = await migrate(current, completePrompt);
|
|
25
|
+
}
|
|
26
|
+
return current;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** CSS classes that belong to the shared theme and must NOT appear in page-specific <style> blocks. */
|
|
30
|
+
const SHARED_CSS_SELECTORS = [
|
|
31
|
+
'*',
|
|
32
|
+
'body',
|
|
33
|
+
'.chat-panel',
|
|
34
|
+
'.chat-header',
|
|
35
|
+
'.chat-messages',
|
|
36
|
+
'.chat-message',
|
|
37
|
+
'.chat-message p',
|
|
38
|
+
'.chat-message strong',
|
|
39
|
+
'.chat-message pre',
|
|
40
|
+
'.chat-message code',
|
|
41
|
+
'.chat-message a',
|
|
42
|
+
'.link-group',
|
|
43
|
+
'.link-group a',
|
|
44
|
+
'form',
|
|
45
|
+
'.chat-input',
|
|
46
|
+
'.chat-submit',
|
|
47
|
+
'.loading-overlay',
|
|
48
|
+
'.spinner',
|
|
49
|
+
'.viewer-panel',
|
|
50
|
+
'#loadingOverlay',
|
|
51
|
+
'.chat-submit:disabled',
|
|
52
|
+
'.chat-input:disabled',
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const V1_TO_V2_SYSTEM_PROMPT = `You are a code migration tool. You convert SynthOS v1 pages to v2 format.
|
|
56
|
+
|
|
57
|
+
## Rules — What to REMOVE
|
|
58
|
+
|
|
59
|
+
1. **Shared CSS rules** — Remove CSS rules that match these selectors **exactly** (they are now in theme.css):
|
|
60
|
+
\`*\`, \`body\`, \`.chat-panel\`, \`.chat-header\`, \`.chat-messages\`, \`.chat-message\` (and descendants like \`.chat-message p\`, \`.chat-message strong\`, \`.chat-message pre\`, \`.chat-message code\`, \`.chat-message a\`), \`.link-group\`, \`.link-group a\`, \`form\`, \`.chat-input\`, \`.chat-submit\`, \`.loading-overlay\`, \`.spinner\`, \`.viewer-panel\`, \`#loadingOverlay\`, \`.chat-submit:disabled\`, \`.chat-input:disabled\`
|
|
61
|
+
Also remove any \`@keyframes spin\` and scrollbar pseudo-element rules (\`::-webkit-scrollbar\`, \`::-webkit-scrollbar-track\`, \`::-webkit-scrollbar-thumb\`).
|
|
62
|
+
**Important:** Only remove rules matching these selectors exactly. Do NOT remove pseudo-element variants (\`.viewer-panel::before\`, \`.viewer-panel::after\`), pseudo-class variants (\`.chat-input:focus\`, \`.chat-submit:hover\`), or any other compound selectors that extend the shared selectors — those are page-specific styles and must be preserved. Also preserve any \`@keyframes\` referenced by page-specific rules (e.g. if a page has \`.viewer-panel::before\` using a custom animation, keep that \`@keyframes\`).
|
|
63
|
+
|
|
64
|
+
2. **Shared inline JS** — Remove these specific code blocks from \`<script>\` tags:
|
|
65
|
+
- \`document.getElementById('chatInput').focus()\` line
|
|
66
|
+
- \`chatForm\` submit event listener (the one with setTimeout and loading overlay)
|
|
67
|
+
- \`saveLink\` click handler
|
|
68
|
+
- \`resetLink\` click handler
|
|
69
|
+
- \`window.onload\` that ONLY scrolls chatMessages (keep other onload logic!)
|
|
70
|
+
- Chat panel toggle IIFE (references \`synthos-chat-collapsed\`)
|
|
71
|
+
- Focus management IIFE (references \`stopImmediatePropagation\`)
|
|
72
|
+
- \`// Basic chat functionality\` comment
|
|
73
|
+
|
|
74
|
+
3. **Empty \`<script>\` tags** — If a script block becomes empty after stripping, remove it entirely.
|
|
75
|
+
|
|
76
|
+
## Rules — What to ADD
|
|
77
|
+
|
|
78
|
+
1. In \`<head>\`, add these two lines right after the \`<title>\` tag (if not already present):
|
|
79
|
+
\`\`\`
|
|
80
|
+
<script src="/api/theme-info.js"></script>
|
|
81
|
+
<link rel="stylesheet" href="/api/theme.css">
|
|
82
|
+
\`\`\`
|
|
83
|
+
|
|
84
|
+
## Rules — What to TRANSFORM
|
|
85
|
+
|
|
86
|
+
### Data/Table API migration
|
|
87
|
+
The data API changed from global tables to **page-scoped** tables:
|
|
88
|
+
- Old: \`/api/data/:table\` → New: \`/api/data/:page/:table\`
|
|
89
|
+
- Old: \`/api/data/:table/:id\` → New: \`/api/data/:page/:table/:id\`
|
|
90
|
+
- Tables are now stored as sub-folders of each page's folder.
|
|
91
|
+
- **Raw fetch() calls** must be updated: e.g. \`fetch('/api/data/notes')\` → \`fetch('/api/data/' + pageName + '/notes')\`
|
|
92
|
+
- **synthos.data.\* helpers** handle this automatically — they read the current page name from \`window.pageInfo.name\`. Prefer converting raw fetch data calls to use the helpers instead:
|
|
93
|
+
- \`fetch('/api/data/notes')\` → \`synthos.data.list('notes')\`
|
|
94
|
+
- \`fetch('/api/data/notes/' + id)\` → \`synthos.data.get('notes', id)\`
|
|
95
|
+
- \`fetch('/api/data/notes', { method: 'POST', ... })\` → \`synthos.data.save('notes', row)\`
|
|
96
|
+
- \`fetch('/api/data/notes/' + id, { method: 'DELETE' })\` → \`synthos.data.remove('notes', id)\`
|
|
97
|
+
|
|
98
|
+
### Color variables
|
|
99
|
+
Replace hardcoded Nebula Dusk colors with CSS variables in **page-specific** CSS only:
|
|
100
|
+
| Hardcoded | CSS Variable |
|
|
101
|
+
|-----------|-------------|
|
|
102
|
+
| \`#667eea\` | \`var(--accent-primary)\` |
|
|
103
|
+
| \`#764ba2\` | \`var(--accent-secondary)\` |
|
|
104
|
+
| \`#f093fb\` | \`var(--accent-tertiary)\` |
|
|
105
|
+
| \`#b794f6\` | \`var(--text-secondary)\` |
|
|
106
|
+
| \`#e0e0e0\` | \`var(--text-primary)\` |
|
|
107
|
+
| \`rgba(138, 43, 226, 0.3)\` or similar purple rgba | \`var(--border-color)\` or \`var(--accent-glow)\` (use border-color for borders, accent-glow for shadows) |
|
|
108
|
+
| \`#1a1a2e\` | \`var(--bg-primary)\` |
|
|
109
|
+
| \`#16213e\` | \`var(--bg-secondary)\` |
|
|
110
|
+
| \`#0f0f23\` | \`var(--bg-tertiary)\` |
|
|
111
|
+
|
|
112
|
+
For gradients using these colors (e.g. \`linear-gradient(135deg, #667eea, #764ba2)\`), replace with \`linear-gradient(135deg, var(--accent-primary), var(--accent-secondary))\`.
|
|
113
|
+
|
|
114
|
+
## Rules — What to PRESERVE (do NOT modify)
|
|
115
|
+
|
|
116
|
+
- ALL viewer-panel HTML content (games, presentations, tools, etc.)
|
|
117
|
+
- ALL page-specific JavaScript (game logic, presentation logic, keyboard handlers, etc.)
|
|
118
|
+
- ALL page-specific CSS (game styles, presentation styles, layout rules for non-shared classes)
|
|
119
|
+
- Chat message history in \`.chat-messages\`
|
|
120
|
+
- \`<div id="thoughts">\` content
|
|
121
|
+
- External CDN script tags (\`<script src="...">\`)
|
|
122
|
+
- The two-panel layout structure (chat-panel + viewer-panel)
|
|
123
|
+
- The \`<div id="loadingOverlay" class="loading-overlay"><div class="spinner"></div></div>\` element
|
|
124
|
+
- The chat form, link-group, chat-header elements (structure only, their CSS is handled by theme)
|
|
125
|
+
|
|
126
|
+
## V2 page structure example
|
|
127
|
+
|
|
128
|
+
\`\`\`html
|
|
129
|
+
<!DOCTYPE html>
|
|
130
|
+
<html lang="en">
|
|
131
|
+
<head>
|
|
132
|
+
<meta charset="UTF-8">
|
|
133
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
134
|
+
<title>SynthOS - Page Title</title>
|
|
135
|
+
<script src="/api/theme-info.js"></script>
|
|
136
|
+
<link rel="stylesheet" href="/api/theme.css">
|
|
137
|
+
<style>
|
|
138
|
+
/* Only page-specific styles here — no shared chat/layout CSS */
|
|
139
|
+
.my-custom-element {
|
|
140
|
+
color: var(--text-primary);
|
|
141
|
+
background: var(--bg-secondary);
|
|
142
|
+
}
|
|
143
|
+
</style>
|
|
144
|
+
<!-- external CDN scripts if needed -->
|
|
145
|
+
</head>
|
|
146
|
+
<body>
|
|
147
|
+
<div class="chat-panel">
|
|
148
|
+
<div class="chat-header">SynthOS</div>
|
|
149
|
+
<div class="chat-messages" id="chatMessages">
|
|
150
|
+
<!-- chat messages preserved -->
|
|
151
|
+
</div>
|
|
152
|
+
<div class="link-group">
|
|
153
|
+
<a href="#" id="saveLink">Save</a>
|
|
154
|
+
<a href="/pages" id="pagesLink">Pages</a>
|
|
155
|
+
<a href="#" id="resetLink">Reset</a>
|
|
156
|
+
</div>
|
|
157
|
+
<form action="/" method="POST" id="chatForm">
|
|
158
|
+
<input type="text" class="chat-input" id="chatInput" name="message" placeholder="Type a message...">
|
|
159
|
+
<button type="submit" class="chat-submit">Send</button>
|
|
160
|
+
</form>
|
|
161
|
+
</div>
|
|
162
|
+
<div class="viewer-panel" id="viewerPanel">
|
|
163
|
+
<!-- page content preserved -->
|
|
164
|
+
<div id="loadingOverlay" class="loading-overlay"><div class="spinner"></div></div>
|
|
165
|
+
</div>
|
|
166
|
+
<div id="thoughts" style="display: none;">...</div>
|
|
167
|
+
<script>
|
|
168
|
+
// Only page-specific JS here — no shared chat handlers
|
|
169
|
+
</script>
|
|
170
|
+
</body>
|
|
171
|
+
</html>
|
|
172
|
+
\`\`\`
|
|
173
|
+
|
|
174
|
+
## Output format
|
|
175
|
+
|
|
176
|
+
Return ONLY the complete migrated HTML. No markdown fences, no explanation, no commentary. Just the raw HTML starting with \`<!DOCTYPE html>\`.`;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* v1 -> v2: LLM-based migration that strips shared code and adds theme support.
|
|
180
|
+
* Post-processes with cheerio to verify critical elements are present.
|
|
181
|
+
*/
|
|
182
|
+
async function migrateV1toV2(html: string, completePrompt: completePrompt): Promise<string> {
|
|
183
|
+
const system = { role: 'system' as const, content: V1_TO_V2_SYSTEM_PROMPT };
|
|
184
|
+
const prompt = { role: 'user' as const, content: `Convert this v1 page to v2 format:\n\n${html}` };
|
|
185
|
+
|
|
186
|
+
const result = await completePrompt({ prompt, system, maxTokens: 16000 });
|
|
187
|
+
if (!result.completed || !result.value) {
|
|
188
|
+
throw new Error('LLM migration failed: ' + (result.error?.message ?? 'no response'));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Strip markdown fencing if present
|
|
192
|
+
let migrated = result.value.trim();
|
|
193
|
+
if (migrated.startsWith('```')) {
|
|
194
|
+
migrated = migrated.replace(/^```(?:html)?\s*\n?/, '').replace(/\n?```\s*$/, '');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Post-process with cheerio to verify and fix critical elements
|
|
198
|
+
migrated = postProcessV2(migrated);
|
|
199
|
+
|
|
200
|
+
return migrated;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Cheerio-based post-processing to verify the LLM output meets v2 requirements.
|
|
205
|
+
*/
|
|
206
|
+
export function postProcessV2(html: string): string {
|
|
207
|
+
const $ = cheerio.load(html, { decodeEntities: false });
|
|
208
|
+
|
|
209
|
+
// Ensure theme-info.js is in <head>
|
|
210
|
+
if ($('script[src="/api/theme-info.js"]').length === 0) {
|
|
211
|
+
const titleEl = $('title');
|
|
212
|
+
if (titleEl.length > 0) {
|
|
213
|
+
titleEl.after('\n<script src="/api/theme-info.js"></script>');
|
|
214
|
+
} else {
|
|
215
|
+
$('head').prepend('<script src="/api/theme-info.js"></script>\n');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Ensure theme.css is in <head>
|
|
220
|
+
if ($('link[href="/api/theme.css"]').length === 0) {
|
|
221
|
+
const themeScript = $('script[src="/api/theme-info.js"]');
|
|
222
|
+
if (themeScript.length > 0) {
|
|
223
|
+
themeScript.after('\n<link rel="stylesheet" href="/api/theme.css">');
|
|
224
|
+
} else {
|
|
225
|
+
$('head').append('<link rel="stylesheet" href="/api/theme.css">\n');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Remove leftover shared CSS selectors from <style> blocks
|
|
230
|
+
$('style').each(function (_, el) {
|
|
231
|
+
let css = $(el).html() ?? '';
|
|
232
|
+
for (const selector of SHARED_CSS_SELECTORS) {
|
|
233
|
+
// Escape special regex chars in selector
|
|
234
|
+
const escaped = selector.replace(/[.*+?^${}()|[\]\\#]/g, '\\$&');
|
|
235
|
+
// Match the full rule block: selector { ... }
|
|
236
|
+
const pattern = new RegExp(`(?:^|\\n)\\s*${escaped}\\s*\\{[^}]*\\}`, 'g');
|
|
237
|
+
css = css.replace(pattern, '');
|
|
238
|
+
}
|
|
239
|
+
// Remove @keyframes spin
|
|
240
|
+
css = css.replace(/@keyframes\s+spin\s*\{[^}]*(?:\{[^}]*\}[^}]*)*\}/g, '');
|
|
241
|
+
// Remove scrollbar pseudo-element rules
|
|
242
|
+
css = css.replace(/(?:^|\n)\s*(?:\*|body|)::-webkit-scrollbar(?:-(?:track|thumb))?\s*\{[^}]*\}/g, '');
|
|
243
|
+
$(el).html(css);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Remove empty <style> blocks
|
|
247
|
+
$('style').each(function (_, el) {
|
|
248
|
+
const content = ($(el).html() ?? '').trim();
|
|
249
|
+
if (content === '') {
|
|
250
|
+
$(el).remove();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Remove empty <script> blocks (no src)
|
|
255
|
+
$('script').each(function (_, el) {
|
|
256
|
+
const src = $(el).attr('src');
|
|
257
|
+
if (src) return;
|
|
258
|
+
const code = ($(el).html() ?? '').trim();
|
|
259
|
+
if (code === '') {
|
|
260
|
+
$(el).remove();
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Remove duplicate inline scripts that share overlapping declarations
|
|
265
|
+
return deduplicateInlineScripts($.html());
|
|
266
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Provider, ProviderName } from './types';
|
|
2
|
+
|
|
3
|
+
export const AnthropicProvider: Provider = {
|
|
4
|
+
name: 'Anthropic',
|
|
5
|
+
builderModels: ['claude-opus-4-6', 'claude-sonnet-4-5'],
|
|
6
|
+
chatModels: ['claude-haiku-4-5', 'claude-sonnet-4-5'],
|
|
7
|
+
detectModel(model: string): boolean {
|
|
8
|
+
return model.startsWith('claude-');
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const OpenAIProvider: Provider = {
|
|
13
|
+
name: 'OpenAI',
|
|
14
|
+
builderModels: ['gpt-5.2', 'gpt-5.2-codex'],
|
|
15
|
+
chatModels: ['gpt-5-nano', 'gpt-5-mini', 'gpt-4.1'],
|
|
16
|
+
detectModel(model: string): boolean {
|
|
17
|
+
return model.startsWith('gpt-') || model.startsWith('GPT-');
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const PROVIDERS: Provider[] = [AnthropicProvider, OpenAIProvider];
|
|
22
|
+
|
|
23
|
+
export function getProvider(name: ProviderName): Provider {
|
|
24
|
+
const provider = PROVIDERS.find(p => p.name === name);
|
|
25
|
+
if (!provider) {
|
|
26
|
+
throw new Error(`Unknown provider: ${name}`);
|
|
27
|
+
}
|
|
28
|
+
return provider;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function detectProvider(model: string): Provider | undefined {
|
|
32
|
+
return PROVIDERS.find(p => p.detectModel(model));
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type ProviderName = 'Anthropic' | 'OpenAI';
|
|
2
|
+
|
|
3
|
+
export interface ProviderConfig {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
model: string;
|
|
6
|
+
maxTokens: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ModelEntry {
|
|
10
|
+
use: 'builder' | 'chat';
|
|
11
|
+
provider: ProviderName;
|
|
12
|
+
configuration: ProviderConfig;
|
|
13
|
+
imageQuality: 'standard' | 'hd';
|
|
14
|
+
instructions?: string;
|
|
15
|
+
logCompletions?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Provider {
|
|
19
|
+
name: ProviderName;
|
|
20
|
+
builderModels: string[];
|
|
21
|
+
chatModels: string[];
|
|
22
|
+
detectModel(model: string): boolean;
|
|
23
|
+
}
|