vibespot 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/dist/index.js +684 -123
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/ui/chat.js +152 -18
- package/ui/dashboard.js +189 -0
- package/ui/index.html +16 -9
- package/ui/settings.js +10 -7
- package/ui/setup.js +101 -11
- package/ui/styles.css +111 -24
package/dist/index.js
CHANGED
|
@@ -1643,7 +1643,7 @@ import { join as join7, basename as basename3 } from "path";
|
|
|
1643
1643
|
import { readdirSync as readdirSync5 } from "fs";
|
|
1644
1644
|
var ClaudeAPIEngine = class {
|
|
1645
1645
|
client;
|
|
1646
|
-
model = "claude-sonnet-4-
|
|
1646
|
+
model = "claude-sonnet-4-6";
|
|
1647
1647
|
constructor(apiKey) {
|
|
1648
1648
|
this.client = new Anthropic({
|
|
1649
1649
|
apiKey: apiKey || process.env.ANTHROPIC_API_KEY
|
|
@@ -3085,15 +3085,15 @@ import chalk2 from "chalk";
|
|
|
3085
3085
|
|
|
3086
3086
|
// src/server/server.ts
|
|
3087
3087
|
import { createServer } from "http";
|
|
3088
|
-
import { readFileSync as readFileSync5, existsSync as existsSync5, readdirSync as readdirSync11, appendFileSync, rmSync as rmSync5, renameSync as
|
|
3088
|
+
import { readFileSync as readFileSync5, existsSync as existsSync5, readdirSync as readdirSync11, appendFileSync, rmSync as rmSync5, renameSync as renameSync3 } from "fs";
|
|
3089
3089
|
import { join as join15, extname as extname2, basename as basename7 } from "path";
|
|
3090
3090
|
import { homedir as homedir4 } from "os";
|
|
3091
3091
|
import { execSync as execSync4 } from "child_process";
|
|
3092
3092
|
import { WebSocketServer } from "ws";
|
|
3093
3093
|
|
|
3094
3094
|
// src/server/session.ts
|
|
3095
|
-
import { readFileSync as readFileSync4, readdirSync as readdirSync10, existsSync as existsSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, rmSync as rmSync4 } from "fs";
|
|
3096
|
-
import { join as join14 } from "path";
|
|
3095
|
+
import { readFileSync as readFileSync4, readdirSync as readdirSync10, existsSync as existsSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, rmSync as rmSync4, renameSync as renameSync2 } from "fs";
|
|
3096
|
+
import { join as join14, dirname as dirname2 } from "path";
|
|
3097
3097
|
import { homedir as homedir3 } from "os";
|
|
3098
3098
|
|
|
3099
3099
|
// src/server/project-git.ts
|
|
@@ -3147,6 +3147,29 @@ function commitThemeState(themePath, message) {
|
|
|
3147
3147
|
const hashResult = run("git rev-parse --short HEAD", { cwd: themePath });
|
|
3148
3148
|
return hashResult.success ? hashResult.stdout : null;
|
|
3149
3149
|
}
|
|
3150
|
+
function commitTemplateState(themePath, templateId, message, filePaths) {
|
|
3151
|
+
if (!isGitAvailable()) return null;
|
|
3152
|
+
if (!existsSync3(join13(themePath, ".git"))) return null;
|
|
3153
|
+
for (const fp of filePaths) {
|
|
3154
|
+
const fullPath = join13(themePath, fp);
|
|
3155
|
+
if (existsSync3(fullPath)) {
|
|
3156
|
+
run(`git add "${fp}"`, { cwd: themePath });
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
const diff = run("git diff --cached --quiet", { cwd: themePath });
|
|
3160
|
+
if (diff.success) return null;
|
|
3161
|
+
const prefix = `[${templateId}] `;
|
|
3162
|
+
const maxMsg = 72 - prefix.length;
|
|
3163
|
+
const truncated = message.length > maxMsg ? message.slice(0, maxMsg - 3) + "..." : message;
|
|
3164
|
+
const fullMessage = prefix + truncated;
|
|
3165
|
+
const commitResult = run(`git commit -m "${fullMessage.replace(/"/g, '\\"')}"`, { cwd: themePath });
|
|
3166
|
+
if (!commitResult.success) {
|
|
3167
|
+
console.warn(`[project-git] template commit failed: ${commitResult.stderr}`);
|
|
3168
|
+
return null;
|
|
3169
|
+
}
|
|
3170
|
+
const hashResult = run("git rev-parse --short HEAD", { cwd: themePath });
|
|
3171
|
+
return hashResult.success ? hashResult.stdout : null;
|
|
3172
|
+
}
|
|
3150
3173
|
function getHistory(themePath, limit = 50) {
|
|
3151
3174
|
if (!isGitAvailable()) return [];
|
|
3152
3175
|
if (!existsSync3(join13(themePath, ".git"))) return [];
|
|
@@ -3170,6 +3193,30 @@ function getHistory(themePath, limit = 50) {
|
|
|
3170
3193
|
}
|
|
3171
3194
|
return commits;
|
|
3172
3195
|
}
|
|
3196
|
+
function getTemplateHistory(themePath, templateId, limit = 50) {
|
|
3197
|
+
if (!isGitAvailable()) return [];
|
|
3198
|
+
if (!existsSync3(join13(themePath, ".git"))) return [];
|
|
3199
|
+
const escapedId = templateId.replace(/[[\]\\]/g, "\\$&");
|
|
3200
|
+
const result = run(
|
|
3201
|
+
`git log --grep="\\[${escapedId}\\]" --pretty=format:"%h|%H|%s|%at" -n ${limit}`,
|
|
3202
|
+
{ cwd: themePath }
|
|
3203
|
+
);
|
|
3204
|
+
if (!result.success || !result.stdout.trim()) return [];
|
|
3205
|
+
const commits = [];
|
|
3206
|
+
for (const line of result.stdout.split("\n")) {
|
|
3207
|
+
const parts = line.split("|");
|
|
3208
|
+
if (parts.length < 4) continue;
|
|
3209
|
+
const timestamp = parseInt(parts[3], 10) * 1e3;
|
|
3210
|
+
commits.push({
|
|
3211
|
+
hash: parts[0],
|
|
3212
|
+
fullHash: parts[1],
|
|
3213
|
+
message: parts[2],
|
|
3214
|
+
timestamp,
|
|
3215
|
+
date: new Date(timestamp).toISOString()
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3218
|
+
return commits;
|
|
3219
|
+
}
|
|
3173
3220
|
function rollbackToCommit(themePath, commitHash) {
|
|
3174
3221
|
if (!isGitAvailable()) return { success: false, error: "Git not available" };
|
|
3175
3222
|
if (!existsSync3(join13(themePath, ".git"))) return { success: false, error: "Not a git repo" };
|
|
@@ -3187,6 +3234,29 @@ function rollbackToCommit(themePath, commitHash) {
|
|
|
3187
3234
|
run(`git commit -m "${rollbackMsg.replace(/"/g, '\\"')}"`, { cwd: themePath });
|
|
3188
3235
|
return { success: true };
|
|
3189
3236
|
}
|
|
3237
|
+
function rollbackTemplateToCommit(themePath, templateId, commitHash, filePaths) {
|
|
3238
|
+
if (!isGitAvailable()) return { success: false, error: "Git not available" };
|
|
3239
|
+
if (!existsSync3(join13(themePath, ".git"))) return { success: false, error: "Not a git repo" };
|
|
3240
|
+
const verify = run(`git cat-file -t ${commitHash}`, { cwd: themePath });
|
|
3241
|
+
if (!verify.success || verify.stdout.trim() !== "commit") {
|
|
3242
|
+
return { success: false, error: `Commit ${commitHash} not found` };
|
|
3243
|
+
}
|
|
3244
|
+
const msgResult = run(`git log --format="%s" -1 ${commitHash}`, { cwd: themePath });
|
|
3245
|
+
const origMessage = msgResult.success ? msgResult.stdout : commitHash;
|
|
3246
|
+
let restored = 0;
|
|
3247
|
+
for (const fp of filePaths) {
|
|
3248
|
+
const checkout = run(`git checkout ${commitHash} -- "${fp}"`, { cwd: themePath });
|
|
3249
|
+
if (checkout.success) restored++;
|
|
3250
|
+
}
|
|
3251
|
+
if (restored === 0) {
|
|
3252
|
+
return { success: false, error: "No files could be restored from that commit" };
|
|
3253
|
+
}
|
|
3254
|
+
run("git add -A", { cwd: themePath });
|
|
3255
|
+
const prefix = `[${templateId}] `;
|
|
3256
|
+
const rollbackMsg = `${prefix}Rollback to: ${origMessage}`.slice(0, 72);
|
|
3257
|
+
run(`git commit -m "${rollbackMsg.replace(/"/g, '\\"')}"`, { cwd: themePath });
|
|
3258
|
+
return { success: true };
|
|
3259
|
+
}
|
|
3190
3260
|
|
|
3191
3261
|
// src/server/session.ts
|
|
3192
3262
|
var SESSIONS_DIR = join14(homedir3(), ".vibespot", "sessions");
|
|
@@ -3330,6 +3400,14 @@ function addTemplate(pageType, label) {
|
|
|
3330
3400
|
activeSession.updatedAt = Date.now();
|
|
3331
3401
|
return entry;
|
|
3332
3402
|
}
|
|
3403
|
+
function renameTemplate(templateId, newLabel) {
|
|
3404
|
+
if (!activeSession) return false;
|
|
3405
|
+
const tpl = activeSession.templates.find((t) => t.id === templateId);
|
|
3406
|
+
if (!tpl) return false;
|
|
3407
|
+
tpl.label = newLabel;
|
|
3408
|
+
activeSession.updatedAt = Date.now();
|
|
3409
|
+
return true;
|
|
3410
|
+
}
|
|
3333
3411
|
function removeTemplate(templateId) {
|
|
3334
3412
|
if (!activeSession) return false;
|
|
3335
3413
|
const idx = activeSession.templates.findIndex((t) => t.id === templateId);
|
|
@@ -3591,6 +3669,71 @@ function deleteSession(sessionId, deleteFiles = false) {
|
|
|
3591
3669
|
activeSession = null;
|
|
3592
3670
|
}
|
|
3593
3671
|
}
|
|
3672
|
+
function renameSession(sessionId, newName) {
|
|
3673
|
+
const filePath = join14(SESSIONS_DIR, sessionId + ".json");
|
|
3674
|
+
if (!existsSync4(filePath)) return { ok: false, error: "Session not found" };
|
|
3675
|
+
let session;
|
|
3676
|
+
try {
|
|
3677
|
+
session = JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
3678
|
+
} catch {
|
|
3679
|
+
return { ok: false, error: "Failed to read session" };
|
|
3680
|
+
}
|
|
3681
|
+
const oldName = session.themeName;
|
|
3682
|
+
if (oldName === newName) return { ok: true };
|
|
3683
|
+
const oldPath = session.themePath;
|
|
3684
|
+
const newPath = join14(dirname2(oldPath), newName);
|
|
3685
|
+
if (existsSync4(oldPath)) {
|
|
3686
|
+
if (existsSync4(newPath)) return { ok: false, error: "A project with that name already exists" };
|
|
3687
|
+
try {
|
|
3688
|
+
renameSync2(oldPath, newPath);
|
|
3689
|
+
} catch (err) {
|
|
3690
|
+
return { ok: false, error: `Failed to rename folder: ${err instanceof Error ? err.message : String(err)}` };
|
|
3691
|
+
}
|
|
3692
|
+
const cssOld = join14(newPath, "css", `${oldName}-theme.css`);
|
|
3693
|
+
const cssNew = join14(newPath, "css", `${newName}-theme.css`);
|
|
3694
|
+
if (existsSync4(cssOld)) try {
|
|
3695
|
+
renameSync2(cssOld, cssNew);
|
|
3696
|
+
} catch {
|
|
3697
|
+
}
|
|
3698
|
+
const jsOld = join14(newPath, "js", `${oldName}-animations.js`);
|
|
3699
|
+
const jsNew = join14(newPath, "js", `${newName}-animations.js`);
|
|
3700
|
+
if (existsSync4(jsOld)) try {
|
|
3701
|
+
renameSync2(jsOld, jsNew);
|
|
3702
|
+
} catch {
|
|
3703
|
+
}
|
|
3704
|
+
const themeJsonPath = join14(newPath, "theme.json");
|
|
3705
|
+
if (existsSync4(themeJsonPath)) {
|
|
3706
|
+
try {
|
|
3707
|
+
const themeData = JSON.parse(readFileSync4(themeJsonPath, "utf-8"));
|
|
3708
|
+
themeData.label = newName;
|
|
3709
|
+
themeData.name = newName;
|
|
3710
|
+
writeFileSync4(themeJsonPath, JSON.stringify(themeData, null, 2), "utf-8");
|
|
3711
|
+
} catch {
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
if (existsSync4(SESSIONS_DIR)) {
|
|
3716
|
+
for (const f of readdirSync10(SESSIONS_DIR).filter((f2) => f2.endsWith(".json") && f2 !== "_index.json")) {
|
|
3717
|
+
try {
|
|
3718
|
+
const data = JSON.parse(readFileSync4(join14(SESSIONS_DIR, f), "utf-8"));
|
|
3719
|
+
if (data.themeName === oldName) {
|
|
3720
|
+
data.themeName = newName;
|
|
3721
|
+
data.themePath = newPath;
|
|
3722
|
+
data.updatedAt = Date.now();
|
|
3723
|
+
writeFileSync4(join14(SESSIONS_DIR, f), JSON.stringify(data, null, 2), "utf-8");
|
|
3724
|
+
}
|
|
3725
|
+
} catch {
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
if (activeSession && activeSession.themeName === oldName) {
|
|
3730
|
+
activeSession.themeName = newName;
|
|
3731
|
+
activeSession.themePath = newPath;
|
|
3732
|
+
activeSession.updatedAt = Date.now();
|
|
3733
|
+
}
|
|
3734
|
+
rebuildIndex();
|
|
3735
|
+
return { ok: true };
|
|
3736
|
+
}
|
|
3594
3737
|
function writeModulesToDisk() {
|
|
3595
3738
|
if (!activeSession) return;
|
|
3596
3739
|
const themePath = activeSession.themePath;
|
|
@@ -3701,6 +3844,37 @@ function reloadModulesFromDisk() {
|
|
|
3701
3844
|
activeSession.updatedAt = Date.now();
|
|
3702
3845
|
syncFlatFieldsToTemplate();
|
|
3703
3846
|
}
|
|
3847
|
+
function reloadActiveTemplateFromDisk() {
|
|
3848
|
+
if (!activeSession) return;
|
|
3849
|
+
const tpl = getActiveTemplate();
|
|
3850
|
+
if (!tpl) return;
|
|
3851
|
+
const themePath = activeSession.themePath;
|
|
3852
|
+
const modulesDir = join14(themePath, "modules");
|
|
3853
|
+
tpl.modules = [];
|
|
3854
|
+
for (const name of tpl.moduleOrder) {
|
|
3855
|
+
const modDir = join14(modulesDir, `${name}.module`);
|
|
3856
|
+
if (!existsSync4(modDir)) continue;
|
|
3857
|
+
const mod = {
|
|
3858
|
+
moduleName: name,
|
|
3859
|
+
fieldsJson: safeRead(join14(modDir, "fields.json")),
|
|
3860
|
+
metaJson: safeRead(join14(modDir, "meta.json")),
|
|
3861
|
+
moduleHtml: safeRead(join14(modDir, "module.html")),
|
|
3862
|
+
moduleCss: safeRead(join14(modDir, "module.css")),
|
|
3863
|
+
moduleJs: safeRead(join14(modDir, "module.js")) || void 0
|
|
3864
|
+
};
|
|
3865
|
+
if (mod.fieldsJson && mod.moduleHtml) {
|
|
3866
|
+
tpl.modules.push(mod);
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
if (tpl.templateFile) {
|
|
3870
|
+
const tplPath = join14(themePath, tpl.templateFile);
|
|
3871
|
+
if (existsSync4(tplPath)) {
|
|
3872
|
+
tpl.template = safeRead(tplPath);
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
syncFlatFieldsFromTemplate(tpl);
|
|
3876
|
+
activeSession.updatedAt = Date.now();
|
|
3877
|
+
}
|
|
3704
3878
|
function patchBaseTemplate() {
|
|
3705
3879
|
if (!activeSession) return;
|
|
3706
3880
|
const basePath = join14(activeSession.themePath, "templates", "layouts", "base.html");
|
|
@@ -4495,51 +4669,60 @@ ${conversionGuide}`;
|
|
|
4495
4669
|
async function handleGenerateStream(userMessage, onChunk, onStatus) {
|
|
4496
4670
|
const session = getSession();
|
|
4497
4671
|
if (!session) throw new Error("No active session");
|
|
4498
|
-
const
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
apiKey
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
apiKey,
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4672
|
+
const capturedSessionId = session.id;
|
|
4673
|
+
generatingSessionId = capturedSessionId;
|
|
4674
|
+
try {
|
|
4675
|
+
const config = loadConfig();
|
|
4676
|
+
const engine = config.aiEngine || detectDefaultEngine();
|
|
4677
|
+
switch (engine) {
|
|
4678
|
+
case "anthropic-api":
|
|
4679
|
+
case "api": {
|
|
4680
|
+
const apiKey = getApiKeyForEngine("anthropic-api", config);
|
|
4681
|
+
if (!apiKey) throw new Error("Anthropic API key not configured. Open Settings to add one.");
|
|
4682
|
+
await streamWithAnthropicAPI(
|
|
4683
|
+
userMessage,
|
|
4684
|
+
apiKey,
|
|
4685
|
+
session.themeName,
|
|
4686
|
+
config.anthropicApiModel || "claude-sonnet-4-6",
|
|
4687
|
+
onChunk,
|
|
4688
|
+
onStatus
|
|
4689
|
+
);
|
|
4690
|
+
break;
|
|
4691
|
+
}
|
|
4692
|
+
case "openai-api": {
|
|
4693
|
+
const apiKey = getApiKeyForEngine("openai-api", config);
|
|
4694
|
+
if (!apiKey) throw new Error("OpenAI API key not configured. Open Settings to add one.");
|
|
4695
|
+
await streamWithOpenAIAPI(
|
|
4696
|
+
userMessage,
|
|
4697
|
+
apiKey,
|
|
4698
|
+
session.themeName,
|
|
4699
|
+
config.openaiApiModel || "gpt-4o",
|
|
4700
|
+
onChunk,
|
|
4701
|
+
onStatus
|
|
4702
|
+
);
|
|
4703
|
+
break;
|
|
4704
|
+
}
|
|
4705
|
+
case "gemini-api": {
|
|
4706
|
+
const apiKey = getApiKeyForEngine("gemini-api", config);
|
|
4707
|
+
if (!apiKey) throw new Error("Gemini API key not configured. Open Settings to add one.");
|
|
4708
|
+
await streamWithGeminiAPI(userMessage, apiKey, session.themeName, onChunk, onStatus);
|
|
4709
|
+
break;
|
|
4710
|
+
}
|
|
4711
|
+
case "claude-code":
|
|
4712
|
+
await generateWithClaudeCode(userMessage, session.themeName, onChunk, onStatus);
|
|
4713
|
+
break;
|
|
4714
|
+
case "gemini-cli":
|
|
4715
|
+
await generateWithCLI("gemini", userMessage, session.themeName, onChunk, onStatus);
|
|
4716
|
+
break;
|
|
4717
|
+
case "codex-cli":
|
|
4718
|
+
await generateWithCLI("codex", userMessage, session.themeName, onChunk, onStatus);
|
|
4719
|
+
break;
|
|
4720
|
+
default:
|
|
4721
|
+
throw new Error(`Unknown AI engine: ${engine}. Open Settings to configure one.`);
|
|
4531
4722
|
}
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
case "gemini-cli":
|
|
4536
|
-
await generateWithCLI("gemini", userMessage, session.themeName, onChunk, onStatus);
|
|
4537
|
-
break;
|
|
4538
|
-
case "codex-cli":
|
|
4539
|
-
await generateWithCLI("codex", userMessage, session.themeName, onChunk, onStatus);
|
|
4540
|
-
break;
|
|
4541
|
-
default:
|
|
4542
|
-
throw new Error(`Unknown AI engine: ${engine}. Open Settings to configure one.`);
|
|
4723
|
+
} finally {
|
|
4724
|
+
generatingSessionId = null;
|
|
4725
|
+
parseWarningCallback = null;
|
|
4543
4726
|
}
|
|
4544
4727
|
}
|
|
4545
4728
|
function detectDefaultEngine() {
|
|
@@ -4645,33 +4828,73 @@ function setParseWarningCallback(cb) {
|
|
|
4645
4828
|
parseWarningCallback = cb;
|
|
4646
4829
|
}
|
|
4647
4830
|
function finishResponse(fullResponse) {
|
|
4831
|
+
if (generatingSessionId) {
|
|
4832
|
+
const current = getSession();
|
|
4833
|
+
if (!current || current.id !== generatingSessionId) {
|
|
4834
|
+
console.warn("[ai-handler] Session changed during generation \u2014 discarding AI output");
|
|
4835
|
+
return;
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4648
4838
|
addMessage("assistant", fullResponse);
|
|
4649
4839
|
parseAndApplyModules(fullResponse);
|
|
4650
4840
|
saveSession();
|
|
4651
4841
|
}
|
|
4652
|
-
|
|
4842
|
+
var generatingSessionId = null;
|
|
4843
|
+
function isGenerating() {
|
|
4844
|
+
return generatingSessionId !== null;
|
|
4845
|
+
}
|
|
4846
|
+
var RATE_LIMIT_DELAYS = [10, 20, 40, 60, 120];
|
|
4847
|
+
async function streamWithAnthropicAPI(userMessage, apiKey, themeName, model, onChunk, onStatus) {
|
|
4653
4848
|
const client = new Anthropic2({ apiKey });
|
|
4654
4849
|
const conversionGuide = getConversionGuide();
|
|
4655
4850
|
const session = getSession();
|
|
4656
4851
|
const editMode = session.modules.length > 0;
|
|
4657
4852
|
const messages = buildMessagesWithContext(userMessage);
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4853
|
+
const systemPrompt = buildVibeSystemPrompt(conversionGuide, themeName, editMode, getPromptContext().pageType, getPromptContext().brandAssets);
|
|
4854
|
+
for (let attempt = 0; ; attempt++) {
|
|
4855
|
+
try {
|
|
4856
|
+
let fullResponse = "";
|
|
4857
|
+
let statusIndex = 0;
|
|
4858
|
+
const sendStatus = onStatus || (() => {
|
|
4859
|
+
});
|
|
4860
|
+
sendStatus(CLI_STATUS_MESSAGES[0]);
|
|
4861
|
+
const heartbeat = setInterval(() => {
|
|
4862
|
+
statusIndex++;
|
|
4863
|
+
sendStatus(CLI_STATUS_MESSAGES[Math.min(statusIndex, CLI_STATUS_MESSAGES.length - 1)]);
|
|
4864
|
+
}, 6e3);
|
|
4865
|
+
try {
|
|
4866
|
+
const stream = client.messages.stream({
|
|
4867
|
+
model,
|
|
4868
|
+
max_tokens: 48e3,
|
|
4869
|
+
system: systemPrompt,
|
|
4870
|
+
messages
|
|
4871
|
+
});
|
|
4872
|
+
for await (const event of stream) {
|
|
4873
|
+
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
|
|
4874
|
+
const text3 = event.delta.text;
|
|
4875
|
+
fullResponse += text3;
|
|
4876
|
+
onChunk(text3);
|
|
4877
|
+
}
|
|
4878
|
+
}
|
|
4879
|
+
} finally {
|
|
4880
|
+
clearInterval(heartbeat);
|
|
4881
|
+
}
|
|
4882
|
+
finishResponse(fullResponse);
|
|
4883
|
+
return;
|
|
4884
|
+
} catch (err) {
|
|
4885
|
+
const status = err.status;
|
|
4886
|
+
const errType = err.error?.type;
|
|
4887
|
+
const is429 = status === 429 || errType === "rate_limit_error" || err instanceof Error && err.message.includes("429");
|
|
4888
|
+
if (!is429 || attempt >= RATE_LIMIT_DELAYS.length) throw err;
|
|
4889
|
+
const wait = RATE_LIMIT_DELAYS[attempt];
|
|
4890
|
+
console.warn(`[ai-handler] Rate limited (429), attempt ${attempt + 1}/${RATE_LIMIT_DELAYS.length} \u2014 waiting ${wait}s`);
|
|
4891
|
+
if (onStatus) onStatus(`Rate limited by Anthropic API \u2014 retrying in ${wait}s...`);
|
|
4892
|
+
await new Promise((r) => setTimeout(r, wait * 1e3));
|
|
4893
|
+
if (onStatus) onStatus("Retrying...");
|
|
4670
4894
|
}
|
|
4671
4895
|
}
|
|
4672
|
-
finishResponse(fullResponse);
|
|
4673
4896
|
}
|
|
4674
|
-
async function streamWithOpenAIAPI(userMessage, apiKey, themeName, model, onChunk) {
|
|
4897
|
+
async function streamWithOpenAIAPI(userMessage, apiKey, themeName, model, onChunk, onStatus) {
|
|
4675
4898
|
const conversionGuide = getConversionGuide();
|
|
4676
4899
|
const editMode = getSession().modules.length > 0;
|
|
4677
4900
|
const messages = buildMessagesWithContext(userMessage);
|
|
@@ -4683,7 +4906,7 @@ async function streamWithOpenAIAPI(userMessage, apiKey, themeName, model, onChun
|
|
|
4683
4906
|
},
|
|
4684
4907
|
body: JSON.stringify({
|
|
4685
4908
|
model,
|
|
4686
|
-
max_tokens:
|
|
4909
|
+
max_tokens: 48e3,
|
|
4687
4910
|
stream: true,
|
|
4688
4911
|
messages: [
|
|
4689
4912
|
{ role: "system", content: buildVibeSystemPrompt(conversionGuide, themeName, editMode, getPromptContext().pageType, getPromptContext().brandAssets) },
|
|
@@ -4695,34 +4918,46 @@ async function streamWithOpenAIAPI(userMessage, apiKey, themeName, model, onChun
|
|
|
4695
4918
|
const err = await response.text();
|
|
4696
4919
|
throw new Error(`OpenAI API error (${response.status}): ${err}`);
|
|
4697
4920
|
}
|
|
4921
|
+
let statusIndex = 0;
|
|
4922
|
+
const sendStatus = onStatus || (() => {
|
|
4923
|
+
});
|
|
4924
|
+
sendStatus(CLI_STATUS_MESSAGES[0]);
|
|
4925
|
+
const heartbeat = setInterval(() => {
|
|
4926
|
+
statusIndex++;
|
|
4927
|
+
sendStatus(CLI_STATUS_MESSAGES[Math.min(statusIndex, CLI_STATUS_MESSAGES.length - 1)]);
|
|
4928
|
+
}, 6e3);
|
|
4698
4929
|
let fullResponse = "";
|
|
4699
4930
|
const reader = response.body.getReader();
|
|
4700
4931
|
const decoder = new TextDecoder();
|
|
4701
4932
|
let buffer = "";
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4933
|
+
try {
|
|
4934
|
+
while (true) {
|
|
4935
|
+
const { done, value } = await reader.read();
|
|
4936
|
+
if (done) break;
|
|
4937
|
+
buffer += decoder.decode(value, { stream: true });
|
|
4938
|
+
const lines = buffer.split("\n");
|
|
4939
|
+
buffer = lines.pop() || "";
|
|
4940
|
+
for (const line of lines) {
|
|
4941
|
+
if (!line.startsWith("data: ")) continue;
|
|
4942
|
+
const data = line.slice(6).trim();
|
|
4943
|
+
if (data === "[DONE]") break;
|
|
4944
|
+
try {
|
|
4945
|
+
const parsed = JSON.parse(data);
|
|
4946
|
+
const delta = parsed.choices?.[0]?.delta?.content;
|
|
4947
|
+
if (delta) {
|
|
4948
|
+
fullResponse += delta;
|
|
4949
|
+
onChunk(delta);
|
|
4950
|
+
}
|
|
4951
|
+
} catch {
|
|
4718
4952
|
}
|
|
4719
|
-
} catch {
|
|
4720
4953
|
}
|
|
4721
4954
|
}
|
|
4955
|
+
} finally {
|
|
4956
|
+
clearInterval(heartbeat);
|
|
4722
4957
|
}
|
|
4723
4958
|
finishResponse(fullResponse);
|
|
4724
4959
|
}
|
|
4725
|
-
async function streamWithGeminiAPI(userMessage, apiKey, themeName, onChunk) {
|
|
4960
|
+
async function streamWithGeminiAPI(userMessage, apiKey, themeName, onChunk, onStatus) {
|
|
4726
4961
|
const conversionGuide = getConversionGuide();
|
|
4727
4962
|
const session = getSession();
|
|
4728
4963
|
const editMode = session.modules.length > 0;
|
|
@@ -4739,7 +4974,7 @@ async function streamWithGeminiAPI(userMessage, apiKey, themeName, onChunk) {
|
|
|
4739
4974
|
---
|
|
4740
4975
|
${stateContext}` : userMessage;
|
|
4741
4976
|
contents.push({ role: "user", parts: [{ text: userContent }] });
|
|
4742
|
-
const model = "gemini-2.
|
|
4977
|
+
const model = "gemini-2.5-flash";
|
|
4743
4978
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?alt=sse&key=${apiKey}`;
|
|
4744
4979
|
const response = await fetch(url, {
|
|
4745
4980
|
method: "POST",
|
|
@@ -4747,36 +4982,48 @@ ${stateContext}` : userMessage;
|
|
|
4747
4982
|
body: JSON.stringify({
|
|
4748
4983
|
systemInstruction: { parts: [{ text: buildVibeSystemPrompt(conversionGuide, themeName, editMode, getPromptContext().pageType, getPromptContext().brandAssets) }] },
|
|
4749
4984
|
contents,
|
|
4750
|
-
generationConfig: { maxOutputTokens:
|
|
4985
|
+
generationConfig: { maxOutputTokens: 48e3 }
|
|
4751
4986
|
})
|
|
4752
4987
|
});
|
|
4753
4988
|
if (!response.ok) {
|
|
4754
4989
|
const err = await response.text();
|
|
4755
4990
|
throw new Error(`Gemini API error (${response.status}): ${err}`);
|
|
4756
4991
|
}
|
|
4992
|
+
let statusIndex = 0;
|
|
4993
|
+
const sendStatus = onStatus || (() => {
|
|
4994
|
+
});
|
|
4995
|
+
sendStatus(CLI_STATUS_MESSAGES[0]);
|
|
4996
|
+
const heartbeat = setInterval(() => {
|
|
4997
|
+
statusIndex++;
|
|
4998
|
+
sendStatus(CLI_STATUS_MESSAGES[Math.min(statusIndex, CLI_STATUS_MESSAGES.length - 1)]);
|
|
4999
|
+
}, 6e3);
|
|
4757
5000
|
let fullResponse = "";
|
|
4758
5001
|
const reader = response.body.getReader();
|
|
4759
5002
|
const decoder = new TextDecoder();
|
|
4760
5003
|
let buffer = "";
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
5004
|
+
try {
|
|
5005
|
+
while (true) {
|
|
5006
|
+
const { done, value } = await reader.read();
|
|
5007
|
+
if (done) break;
|
|
5008
|
+
buffer += decoder.decode(value, { stream: true });
|
|
5009
|
+
const lines = buffer.split("\n");
|
|
5010
|
+
buffer = lines.pop() || "";
|
|
5011
|
+
for (const line of lines) {
|
|
5012
|
+
if (!line.startsWith("data: ")) continue;
|
|
5013
|
+
const data = line.slice(6).trim();
|
|
5014
|
+
try {
|
|
5015
|
+
const parsed = JSON.parse(data);
|
|
5016
|
+
const text3 = parsed.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
5017
|
+
if (text3) {
|
|
5018
|
+
fullResponse += text3;
|
|
5019
|
+
onChunk(text3);
|
|
5020
|
+
}
|
|
5021
|
+
} catch {
|
|
4776
5022
|
}
|
|
4777
|
-
} catch {
|
|
4778
5023
|
}
|
|
4779
5024
|
}
|
|
5025
|
+
} finally {
|
|
5026
|
+
clearInterval(heartbeat);
|
|
4780
5027
|
}
|
|
4781
5028
|
finishResponse(fullResponse);
|
|
4782
5029
|
}
|
|
@@ -4921,14 +5168,58 @@ function tryParseJSON(raw) {
|
|
|
4921
5168
|
}
|
|
4922
5169
|
return null;
|
|
4923
5170
|
}
|
|
5171
|
+
function tryRepairTruncatedJSON(raw) {
|
|
5172
|
+
const modulesIdx = raw.indexOf('"modules"');
|
|
5173
|
+
if (modulesIdx === -1) return null;
|
|
5174
|
+
const arrayStart = raw.indexOf("[", modulesIdx);
|
|
5175
|
+
if (arrayStart === -1) return null;
|
|
5176
|
+
let lastCompleteModule = -1;
|
|
5177
|
+
let braceDepth = 0;
|
|
5178
|
+
let inString = false;
|
|
5179
|
+
let escaped = false;
|
|
5180
|
+
for (let i = arrayStart + 1; i < raw.length; i++) {
|
|
5181
|
+
const ch = raw[i];
|
|
5182
|
+
if (escaped) {
|
|
5183
|
+
escaped = false;
|
|
5184
|
+
continue;
|
|
5185
|
+
}
|
|
5186
|
+
if (ch === "\\") {
|
|
5187
|
+
escaped = true;
|
|
5188
|
+
continue;
|
|
5189
|
+
}
|
|
5190
|
+
if (ch === '"') {
|
|
5191
|
+
inString = !inString;
|
|
5192
|
+
continue;
|
|
5193
|
+
}
|
|
5194
|
+
if (inString) continue;
|
|
5195
|
+
if (ch === "{") braceDepth++;
|
|
5196
|
+
if (ch === "}") {
|
|
5197
|
+
braceDepth--;
|
|
5198
|
+
if (braceDepth === 0) {
|
|
5199
|
+
lastCompleteModule = i;
|
|
5200
|
+
}
|
|
5201
|
+
}
|
|
5202
|
+
}
|
|
5203
|
+
if (lastCompleteModule === -1) return null;
|
|
5204
|
+
const upToLastModule = raw.slice(0, lastCompleteModule + 1);
|
|
5205
|
+
const repaired = upToLastModule + "]}";
|
|
5206
|
+
const jsonStr = repaired.trimStart().startsWith("{") ? repaired : "{" + repaired;
|
|
5207
|
+
return tryParseJSON(jsonStr);
|
|
5208
|
+
}
|
|
4924
5209
|
function parseAndApplyModules(response) {
|
|
4925
5210
|
let modulesApplied = false;
|
|
4926
|
-
const blockPattern = /```vibespot-modules\s*\n([\s\S]*?)```/g;
|
|
5211
|
+
const blockPattern = /```vibespot-modules\s*\n?([\s\S]*?)```/g;
|
|
4927
5212
|
let match;
|
|
4928
5213
|
while ((match = blockPattern.exec(response)) !== null) {
|
|
4929
5214
|
try {
|
|
5215
|
+
console.log("[parse] Found vibespot-modules block, length:", match[1].length);
|
|
5216
|
+
console.log("[parse] Block start:", JSON.stringify(match[1].slice(0, 100)));
|
|
5217
|
+
console.log("[parse] Block end:", JSON.stringify(match[1].slice(-100)));
|
|
4930
5218
|
const data = tryParseJSON(match[1]);
|
|
4931
|
-
if (!data || typeof data !== "object")
|
|
5219
|
+
if (!data || typeof data !== "object") {
|
|
5220
|
+
console.warn("[parse] tryParseJSON returned:", data);
|
|
5221
|
+
throw new Error("Invalid JSON after repair");
|
|
5222
|
+
}
|
|
4932
5223
|
const obj = data;
|
|
4933
5224
|
if (obj.modules && Array.isArray(obj.modules)) {
|
|
4934
5225
|
const modules = obj.modules.map((m) => ({
|
|
@@ -4952,8 +5243,9 @@ function parseAndApplyModules(response) {
|
|
|
4952
5243
|
}
|
|
4953
5244
|
}
|
|
4954
5245
|
if (!modulesApplied) {
|
|
4955
|
-
const jsonPattern = /```(?:json)?\s*\n(
|
|
5246
|
+
const jsonPattern = /```(?:json)?\s*\n([\s\S]*?)```/g;
|
|
4956
5247
|
while ((match = jsonPattern.exec(response)) !== null) {
|
|
5248
|
+
if (!match[1].includes('"modules"')) continue;
|
|
4957
5249
|
try {
|
|
4958
5250
|
const data = tryParseJSON(match[1]);
|
|
4959
5251
|
if (!data || typeof data !== "object") throw new Error("Invalid JSON after repair");
|
|
@@ -4980,6 +5272,44 @@ function parseAndApplyModules(response) {
|
|
|
4980
5272
|
}
|
|
4981
5273
|
}
|
|
4982
5274
|
if (!modulesApplied) {
|
|
5275
|
+
const fenceCount = (response.match(/```/g) || []).length;
|
|
5276
|
+
if (fenceCount % 2 !== 0 && response.includes('"modules"')) {
|
|
5277
|
+
console.log("[parse] Detected truncated response (odd fence count), attempting salvage...");
|
|
5278
|
+
const lastFenceIdx = response.lastIndexOf("```");
|
|
5279
|
+
let truncated = response.slice(lastFenceIdx + 3);
|
|
5280
|
+
truncated = truncated.replace(/^[\w-]*\s*\n?/, "");
|
|
5281
|
+
const salvaged = tryRepairTruncatedJSON(truncated);
|
|
5282
|
+
if (salvaged) {
|
|
5283
|
+
const obj = salvaged;
|
|
5284
|
+
if (obj.modules && Array.isArray(obj.modules) && obj.modules.length > 0) {
|
|
5285
|
+
console.log("[parse] Salvaged", obj.modules.length, "modules from truncated response");
|
|
5286
|
+
const modules = obj.modules.map((m) => ({
|
|
5287
|
+
moduleName: String(m.moduleName || ""),
|
|
5288
|
+
fieldsJson: typeof m.fieldsJson === "string" ? m.fieldsJson : JSON.stringify(m.fieldsJson, null, 2),
|
|
5289
|
+
metaJson: typeof m.metaJson === "string" ? m.metaJson : JSON.stringify(m.metaJson, null, 2),
|
|
5290
|
+
moduleHtml: String(m.moduleHtml || ""),
|
|
5291
|
+
moduleCss: String(m.moduleCss || ""),
|
|
5292
|
+
moduleJs: m.moduleJs ? String(m.moduleJs) : void 0
|
|
5293
|
+
}));
|
|
5294
|
+
updateModules({
|
|
5295
|
+
modules,
|
|
5296
|
+
sharedCss: obj.sharedCss !== void 0 ? String(obj.sharedCss) : void 0,
|
|
5297
|
+
sharedJs: obj.sharedJs !== void 0 ? String(obj.sharedJs) : void 0
|
|
5298
|
+
});
|
|
5299
|
+
modulesApplied = true;
|
|
5300
|
+
if (parseWarningCallback) {
|
|
5301
|
+
parseWarningCallback("Response was truncated \u2014 some modules may be incomplete. Try sending your request again for the full set.");
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
}
|
|
5307
|
+
if (!modulesApplied) {
|
|
5308
|
+
console.log("[parse] No modules applied. Response length:", response.length);
|
|
5309
|
+
console.log("[parse] Contains 'vibespot-modules':", response.includes("vibespot-modules"));
|
|
5310
|
+
console.log(`[parse] Contains '"modules"':`, response.includes('"modules"'));
|
|
5311
|
+
console.log("[parse] Fence count:", (response.match(/```/g) || []).length);
|
|
5312
|
+
console.log("[parse] Response preview:", response.slice(0, 500));
|
|
4983
5313
|
const hasModuleRef = response.includes("vibespot-modules") || response.includes('"modules"');
|
|
4984
5314
|
const describesProse = /\bmodule|modul/i.test(response) && (/\bcreated?\b|\berstellt\b|\bgenerat/i.test(response) || /\|.*\|.*\|/m.test(response));
|
|
4985
5315
|
if (hasModuleRef || describesProse) {
|
|
@@ -5293,8 +5623,12 @@ function handleApiRoute(method, path, req, res) {
|
|
|
5293
5623
|
if (method === "POST") handleDeleteLocalThemeRoute(req, res);
|
|
5294
5624
|
else jsonResponse(res, 405, { error: "Method not allowed" });
|
|
5295
5625
|
break;
|
|
5626
|
+
case "/api/themes/rename":
|
|
5627
|
+
if (method === "POST") handleRenameThemeRoute(req, res);
|
|
5628
|
+
else jsonResponse(res, 405, { error: "Method not allowed" });
|
|
5629
|
+
break;
|
|
5296
5630
|
case "/api/history":
|
|
5297
|
-
if (method === "GET") handleHistoryRoute(res);
|
|
5631
|
+
if (method === "GET") handleHistoryRoute(req, res);
|
|
5298
5632
|
else jsonResponse(res, 405, { error: "Method not allowed" });
|
|
5299
5633
|
break;
|
|
5300
5634
|
case "/api/rollback":
|
|
@@ -5313,6 +5647,10 @@ function handleApiRoute(method, path, req, res) {
|
|
|
5313
5647
|
if (method === "POST") handleTemplateActivateRoute(req, res);
|
|
5314
5648
|
else jsonResponse(res, 405, { error: "Method not allowed" });
|
|
5315
5649
|
break;
|
|
5650
|
+
case "/api/templates/rename":
|
|
5651
|
+
if (method === "POST") handleTemplateRenameRoute(req, res);
|
|
5652
|
+
else jsonResponse(res, 405, { error: "Method not allowed" });
|
|
5653
|
+
break;
|
|
5316
5654
|
case "/api/module-library":
|
|
5317
5655
|
if (method === "GET") handleModuleLibraryRoute(res);
|
|
5318
5656
|
else jsonResponse(res, 405, { error: "Method not allowed" });
|
|
@@ -5320,6 +5658,10 @@ function handleApiRoute(method, path, req, res) {
|
|
|
5320
5658
|
case "/api/brand-assets":
|
|
5321
5659
|
handleBrandAssetsRoute(method, req, res);
|
|
5322
5660
|
break;
|
|
5661
|
+
case "/api/download-zip":
|
|
5662
|
+
if (method === "GET") handleDownloadZipRoute(res);
|
|
5663
|
+
else jsonResponse(res, 405, { error: "Method not allowed" });
|
|
5664
|
+
break;
|
|
5323
5665
|
default:
|
|
5324
5666
|
if (path.startsWith("/api/settings/job/") && method === "GET") {
|
|
5325
5667
|
handleSettingsJobRoute(path, res);
|
|
@@ -5520,6 +5862,10 @@ function handleSetupInfoRoute(res) {
|
|
|
5520
5862
|
function handleSetupCreateRoute(req, res) {
|
|
5521
5863
|
readBody(req, (body) => {
|
|
5522
5864
|
try {
|
|
5865
|
+
if (isGenerating()) {
|
|
5866
|
+
jsonResponse(res, 409, { error: "Cannot switch projects while AI is generating.", generating: true });
|
|
5867
|
+
return;
|
|
5868
|
+
}
|
|
5523
5869
|
const { name } = JSON.parse(body);
|
|
5524
5870
|
if (!name || typeof name !== "string") {
|
|
5525
5871
|
jsonResponse(res, 400, { error: "Theme name is required" });
|
|
@@ -5543,7 +5889,7 @@ function handleSetupCreateRoute(req, res) {
|
|
|
5543
5889
|
if (newDir) createdAt = join15(process.cwd(), newDir);
|
|
5544
5890
|
}
|
|
5545
5891
|
if (createdAt !== themePath && existsSync5(createdAt)) {
|
|
5546
|
-
|
|
5892
|
+
renameSync3(createdAt, themePath);
|
|
5547
5893
|
}
|
|
5548
5894
|
const tplDir = join15(themePath, "templates");
|
|
5549
5895
|
if (existsSync5(tplDir)) {
|
|
@@ -5566,6 +5912,10 @@ function handleSetupCreateRoute(req, res) {
|
|
|
5566
5912
|
function handleSetupFetchRoute(req, res) {
|
|
5567
5913
|
readBody(req, (body) => {
|
|
5568
5914
|
try {
|
|
5915
|
+
if (isGenerating()) {
|
|
5916
|
+
jsonResponse(res, 409, { error: "Cannot switch projects while AI is generating.", generating: true });
|
|
5917
|
+
return;
|
|
5918
|
+
}
|
|
5569
5919
|
const { name } = JSON.parse(body);
|
|
5570
5920
|
if (!name || typeof name !== "string") {
|
|
5571
5921
|
jsonResponse(res, 400, { error: "Theme name is required" });
|
|
@@ -5596,6 +5946,10 @@ function handleSetupFetchRoute(req, res) {
|
|
|
5596
5946
|
function handleSetupOpenRoute(req, res) {
|
|
5597
5947
|
readBody(req, (body) => {
|
|
5598
5948
|
try {
|
|
5949
|
+
if (isGenerating()) {
|
|
5950
|
+
jsonResponse(res, 409, { error: "Cannot switch projects while AI is generating.", generating: true });
|
|
5951
|
+
return;
|
|
5952
|
+
}
|
|
5599
5953
|
const { path: themePath } = JSON.parse(body);
|
|
5600
5954
|
if (!themePath || typeof themePath !== "string") {
|
|
5601
5955
|
jsonResponse(res, 400, { error: "Theme path is required" });
|
|
@@ -5627,6 +5981,10 @@ function handleSetupOpenRoute(req, res) {
|
|
|
5627
5981
|
function handleSetupResumeRoute(req, res) {
|
|
5628
5982
|
readBody(req, (body) => {
|
|
5629
5983
|
try {
|
|
5984
|
+
if (isGenerating()) {
|
|
5985
|
+
jsonResponse(res, 409, { error: "Cannot switch projects while AI is generating.", generating: true });
|
|
5986
|
+
return;
|
|
5987
|
+
}
|
|
5630
5988
|
const { sessionId } = JSON.parse(body);
|
|
5631
5989
|
if (!sessionId || typeof sessionId !== "string") {
|
|
5632
5990
|
jsonResponse(res, 400, { error: "Session ID is required" });
|
|
@@ -5664,17 +6022,112 @@ function handleSetupApiKeyRoute(req, res) {
|
|
|
5664
6022
|
}
|
|
5665
6023
|
});
|
|
5666
6024
|
}
|
|
6025
|
+
var modelCache = { data: {}, ts: 0 };
|
|
6026
|
+
var MODEL_CACHE_TTL = 10 * 60 * 1e3;
|
|
6027
|
+
var STATIC_MODELS = {
|
|
6028
|
+
"claude-code": [
|
|
6029
|
+
{ id: "sonnet", label: "Claude Sonnet (default)" },
|
|
6030
|
+
{ id: "opus", label: "Claude Opus" },
|
|
6031
|
+
{ id: "haiku", label: "Claude Haiku" }
|
|
6032
|
+
],
|
|
6033
|
+
"codex-cli": [
|
|
6034
|
+
{ id: "o4-mini", label: "o4 Mini (default)" },
|
|
6035
|
+
{ id: "o3", label: "o3" },
|
|
6036
|
+
{ id: "gpt-4o", label: "GPT-4o" }
|
|
6037
|
+
]
|
|
6038
|
+
};
|
|
6039
|
+
async function fetchAnthropicModels(apiKey) {
|
|
6040
|
+
const resp = await fetch("https://api.anthropic.com/v1/models", {
|
|
6041
|
+
headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" }
|
|
6042
|
+
});
|
|
6043
|
+
if (!resp.ok) return [];
|
|
6044
|
+
const data = await resp.json();
|
|
6045
|
+
return data.data.filter((m) => !m.id.startsWith("claude-3-") && !m.id.startsWith("claude-2")).map((m) => ({ id: m.id, label: m.display_name }));
|
|
6046
|
+
}
|
|
6047
|
+
async function fetchOpenAIModels(apiKey) {
|
|
6048
|
+
const resp = await fetch("https://api.openai.com/v1/models", {
|
|
6049
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
6050
|
+
});
|
|
6051
|
+
if (!resp.ok) return [];
|
|
6052
|
+
const data = await resp.json();
|
|
6053
|
+
const keep = /^(gpt-4o|gpt-4o-mini|o[1-4](-mini)?|o[1-4]-pro)$/;
|
|
6054
|
+
return data.data.filter((m) => keep.test(m.id)).sort((a, b) => a.id.localeCompare(b.id)).map((m) => ({ id: m.id, label: m.id }));
|
|
6055
|
+
}
|
|
6056
|
+
async function fetchGeminiModels(apiKey) {
|
|
6057
|
+
const resp = await fetch(
|
|
6058
|
+
`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`
|
|
6059
|
+
);
|
|
6060
|
+
if (!resp.ok) return [];
|
|
6061
|
+
const data = await resp.json();
|
|
6062
|
+
return data.models.filter((m) => m.name.includes("gemini-2")).map((m) => ({ id: m.name.replace("models/", ""), label: m.displayName }));
|
|
6063
|
+
}
|
|
6064
|
+
async function getModelCatalog() {
|
|
6065
|
+
if (Date.now() - modelCache.ts < MODEL_CACHE_TTL && Object.keys(modelCache.data).length > 0) {
|
|
6066
|
+
return modelCache.data;
|
|
6067
|
+
}
|
|
6068
|
+
const config = loadConfig();
|
|
6069
|
+
const catalog = { ...STATIC_MODELS };
|
|
6070
|
+
const jobs2 = [];
|
|
6071
|
+
const anthropicKey = getApiKeyForEngine("anthropic-api", config);
|
|
6072
|
+
if (anthropicKey) {
|
|
6073
|
+
jobs2.push(
|
|
6074
|
+
fetchAnthropicModels(anthropicKey).then((models) => {
|
|
6075
|
+
if (models.length) catalog["anthropic-api"] = models;
|
|
6076
|
+
}).catch(() => {
|
|
6077
|
+
})
|
|
6078
|
+
);
|
|
6079
|
+
}
|
|
6080
|
+
const openaiKey = getApiKeyForEngine("openai-api", config);
|
|
6081
|
+
if (openaiKey) {
|
|
6082
|
+
jobs2.push(
|
|
6083
|
+
fetchOpenAIModels(openaiKey).then((models) => {
|
|
6084
|
+
if (models.length) catalog["openai-api"] = models;
|
|
6085
|
+
}).catch(() => {
|
|
6086
|
+
})
|
|
6087
|
+
);
|
|
6088
|
+
}
|
|
6089
|
+
const geminiKey = getApiKeyForEngine("gemini-api", config);
|
|
6090
|
+
if (geminiKey) {
|
|
6091
|
+
jobs2.push(
|
|
6092
|
+
fetchGeminiModels(geminiKey).then((models) => {
|
|
6093
|
+
if (models.length) {
|
|
6094
|
+
catalog["gemini-api"] = models;
|
|
6095
|
+
catalog["gemini-cli"] = models;
|
|
6096
|
+
}
|
|
6097
|
+
}).catch(() => {
|
|
6098
|
+
})
|
|
6099
|
+
);
|
|
6100
|
+
}
|
|
6101
|
+
await Promise.all(jobs2);
|
|
6102
|
+
modelCache.data = catalog;
|
|
6103
|
+
modelCache.ts = Date.now();
|
|
6104
|
+
return catalog;
|
|
6105
|
+
}
|
|
5667
6106
|
function handleSettingsStatusRoute(res) {
|
|
5668
6107
|
const env = detectEnvironment();
|
|
5669
6108
|
const config = loadConfig();
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
6109
|
+
getModelCatalog().then((models) => {
|
|
6110
|
+
jsonResponse(res, 200, {
|
|
6111
|
+
environment: env,
|
|
6112
|
+
config: {
|
|
6113
|
+
aiEngine: config.aiEngine || null,
|
|
6114
|
+
claudeCodeModel: config.claudeCodeModel || null,
|
|
6115
|
+
anthropicApiModel: config.anthropicApiModel || null,
|
|
6116
|
+
openaiApiModel: config.openaiApiModel || null
|
|
6117
|
+
},
|
|
6118
|
+
models
|
|
6119
|
+
});
|
|
6120
|
+
}).catch(() => {
|
|
6121
|
+
jsonResponse(res, 200, {
|
|
6122
|
+
environment: env,
|
|
6123
|
+
config: {
|
|
6124
|
+
aiEngine: config.aiEngine || null,
|
|
6125
|
+
claudeCodeModel: config.claudeCodeModel || null,
|
|
6126
|
+
anthropicApiModel: config.anthropicApiModel || null,
|
|
6127
|
+
openaiApiModel: config.openaiApiModel || null
|
|
6128
|
+
},
|
|
6129
|
+
models: STATIC_MODELS
|
|
6130
|
+
});
|
|
5678
6131
|
});
|
|
5679
6132
|
}
|
|
5680
6133
|
function handleSettingsEngineRoute(req, res) {
|
|
@@ -6069,7 +6522,31 @@ function handleDeleteLocalThemeRoute(req, res) {
|
|
|
6069
6522
|
}
|
|
6070
6523
|
});
|
|
6071
6524
|
}
|
|
6072
|
-
function
|
|
6525
|
+
function handleRenameThemeRoute(req, res) {
|
|
6526
|
+
readBody(req, (body) => {
|
|
6527
|
+
try {
|
|
6528
|
+
const { sessionId, newName } = JSON.parse(body);
|
|
6529
|
+
if (!sessionId || !newName || typeof newName !== "string") {
|
|
6530
|
+
jsonResponse(res, 400, { error: "sessionId and newName are required" });
|
|
6531
|
+
return;
|
|
6532
|
+
}
|
|
6533
|
+
const sanitized = newName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-|-$/g, "").replace(/-{2,}/g, "-");
|
|
6534
|
+
if (!sanitized) {
|
|
6535
|
+
jsonResponse(res, 400, { error: "Invalid name" });
|
|
6536
|
+
return;
|
|
6537
|
+
}
|
|
6538
|
+
const result = renameSession(sessionId, sanitized);
|
|
6539
|
+
if (result.ok) {
|
|
6540
|
+
jsonResponse(res, 200, { ok: true, newName: sanitized });
|
|
6541
|
+
} else {
|
|
6542
|
+
jsonResponse(res, 400, { error: result.error });
|
|
6543
|
+
}
|
|
6544
|
+
} catch (err) {
|
|
6545
|
+
jsonResponse(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
6546
|
+
}
|
|
6547
|
+
});
|
|
6548
|
+
}
|
|
6549
|
+
function handleHistoryRoute(req, res) {
|
|
6073
6550
|
const session = getSession();
|
|
6074
6551
|
if (!session) {
|
|
6075
6552
|
jsonResponse(res, 404, { error: "No active session" });
|
|
@@ -6079,8 +6556,10 @@ function handleHistoryRoute(res) {
|
|
|
6079
6556
|
jsonResponse(res, 200, { available: false, commits: [] });
|
|
6080
6557
|
return;
|
|
6081
6558
|
}
|
|
6082
|
-
const
|
|
6083
|
-
|
|
6559
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
6560
|
+
const templateId = url.searchParams.get("templateId");
|
|
6561
|
+
const commits = templateId ? getTemplateHistory(session.themePath, templateId, 50) : getHistory(session.themePath, 50);
|
|
6562
|
+
jsonResponse(res, 200, { available: true, commits, filtered: !!templateId });
|
|
6084
6563
|
}
|
|
6085
6564
|
function handleRollbackRoute(req, res) {
|
|
6086
6565
|
readBody(req, (body) => {
|
|
@@ -6090,18 +6569,34 @@ function handleRollbackRoute(req, res) {
|
|
|
6090
6569
|
jsonResponse(res, 404, { error: "No active session" });
|
|
6091
6570
|
return;
|
|
6092
6571
|
}
|
|
6093
|
-
const { hash } = JSON.parse(body);
|
|
6572
|
+
const { hash, templateId } = JSON.parse(body);
|
|
6094
6573
|
if (!hash || typeof hash !== "string") {
|
|
6095
6574
|
jsonResponse(res, 400, { error: "Commit hash is required" });
|
|
6096
6575
|
return;
|
|
6097
6576
|
}
|
|
6098
6577
|
addMessage("assistant", `Rolled back to version ${hash.slice(0, 7)}.`);
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6578
|
+
if (templateId) {
|
|
6579
|
+
const tpl = session.templates.find((t) => t.id === templateId);
|
|
6580
|
+
if (!tpl) {
|
|
6581
|
+
jsonResponse(res, 404, { error: "Template not found" });
|
|
6582
|
+
return;
|
|
6583
|
+
}
|
|
6584
|
+
const filePaths = tpl.moduleOrder.map((n) => `modules/${n}.module`);
|
|
6585
|
+
if (tpl.templateFile) filePaths.push(tpl.templateFile);
|
|
6586
|
+
const result = rollbackTemplateToCommit(session.themePath, templateId, hash, filePaths);
|
|
6587
|
+
if (!result.success) {
|
|
6588
|
+
jsonResponse(res, 500, { error: result.error || "Rollback failed" });
|
|
6589
|
+
return;
|
|
6590
|
+
}
|
|
6591
|
+
reloadActiveTemplateFromDisk();
|
|
6592
|
+
} else {
|
|
6593
|
+
const result = rollbackToCommit(session.themePath, hash);
|
|
6594
|
+
if (!result.success) {
|
|
6595
|
+
jsonResponse(res, 500, { error: result.error || "Rollback failed" });
|
|
6596
|
+
return;
|
|
6597
|
+
}
|
|
6598
|
+
reloadModulesFromDisk();
|
|
6103
6599
|
}
|
|
6104
|
-
reloadModulesFromDisk();
|
|
6105
6600
|
saveSession();
|
|
6106
6601
|
jsonResponse(res, 200, {
|
|
6107
6602
|
ok: true,
|
|
@@ -6141,6 +6636,41 @@ function handleDashboardRoute(res) {
|
|
|
6141
6636
|
}
|
|
6142
6637
|
});
|
|
6143
6638
|
}
|
|
6639
|
+
function handleDownloadZipRoute(res) {
|
|
6640
|
+
const session = getSession();
|
|
6641
|
+
if (!session) {
|
|
6642
|
+
jsonResponse(res, 404, { error: "No active session" });
|
|
6643
|
+
return;
|
|
6644
|
+
}
|
|
6645
|
+
const themePath = session.themePath;
|
|
6646
|
+
if (!existsSync5(themePath)) {
|
|
6647
|
+
jsonResponse(res, 404, { error: "Theme directory not found" });
|
|
6648
|
+
return;
|
|
6649
|
+
}
|
|
6650
|
+
const themeName = session.themeName || "theme";
|
|
6651
|
+
const parentDir = join15(themePath, "..");
|
|
6652
|
+
const folderName = basename7(themePath);
|
|
6653
|
+
try {
|
|
6654
|
+
const zipFileName = `${themeName}.zip`;
|
|
6655
|
+
const tmpZip = join15(parentDir, zipFileName);
|
|
6656
|
+
if (existsSync5(tmpZip)) rmSync5(tmpZip);
|
|
6657
|
+
execSync4(
|
|
6658
|
+
`zip -r "${zipFileName}" "${folderName}" -x "${folderName}/.git/*" "${folderName}/.vibespot/*" "${folderName}/node_modules/*"`,
|
|
6659
|
+
{ cwd: parentDir, timeout: 3e4 }
|
|
6660
|
+
);
|
|
6661
|
+
const zipData = readFileSync5(tmpZip);
|
|
6662
|
+
rmSync5(tmpZip);
|
|
6663
|
+
res.writeHead(200, {
|
|
6664
|
+
"Content-Type": "application/zip",
|
|
6665
|
+
"Content-Disposition": `attachment; filename="${zipFileName}"`,
|
|
6666
|
+
"Content-Length": zipData.length
|
|
6667
|
+
});
|
|
6668
|
+
res.end(zipData);
|
|
6669
|
+
} catch (err) {
|
|
6670
|
+
console.warn("[download-zip] Failed:", err.message);
|
|
6671
|
+
jsonResponse(res, 500, { error: "Failed to create zip archive" });
|
|
6672
|
+
}
|
|
6673
|
+
}
|
|
6144
6674
|
function handleTemplatesRoute(method, req, res) {
|
|
6145
6675
|
const session = getSession();
|
|
6146
6676
|
if (!session) {
|
|
@@ -6236,6 +6766,26 @@ function handleTemplateActivateRoute(req, res) {
|
|
|
6236
6766
|
}
|
|
6237
6767
|
});
|
|
6238
6768
|
}
|
|
6769
|
+
function handleTemplateRenameRoute(req, res) {
|
|
6770
|
+
readBody(req, (body) => {
|
|
6771
|
+
try {
|
|
6772
|
+
const { templateId, newLabel } = JSON.parse(body);
|
|
6773
|
+
if (!templateId || !newLabel || typeof newLabel !== "string") {
|
|
6774
|
+
jsonResponse(res, 400, { error: "templateId and newLabel are required" });
|
|
6775
|
+
return;
|
|
6776
|
+
}
|
|
6777
|
+
const success = renameTemplate(templateId, newLabel.trim());
|
|
6778
|
+
if (!success) {
|
|
6779
|
+
jsonResponse(res, 404, { error: "Template not found" });
|
|
6780
|
+
return;
|
|
6781
|
+
}
|
|
6782
|
+
saveSession();
|
|
6783
|
+
jsonResponse(res, 200, { ok: true, newLabel: newLabel.trim() });
|
|
6784
|
+
} catch (err) {
|
|
6785
|
+
jsonResponse(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
6786
|
+
}
|
|
6787
|
+
});
|
|
6788
|
+
}
|
|
6239
6789
|
function handleModuleLibraryRoute(res) {
|
|
6240
6790
|
const library = getModuleLibrary();
|
|
6241
6791
|
jsonResponse(res, 200, {
|
|
@@ -6384,7 +6934,17 @@ function handleWsConnection(ws) {
|
|
|
6384
6934
|
const currentSession = getSession();
|
|
6385
6935
|
if (currentSession) {
|
|
6386
6936
|
writeModulesToDisk();
|
|
6387
|
-
const
|
|
6937
|
+
const activeTpl = getActiveTemplate();
|
|
6938
|
+
let commitHash = null;
|
|
6939
|
+
if (activeTpl) {
|
|
6940
|
+
const filePaths = activeTpl.moduleOrder.map((n) => `modules/${n}.module`);
|
|
6941
|
+
if (activeTpl.templateFile) filePaths.push(activeTpl.templateFile);
|
|
6942
|
+
if (activeTpl.sharedCss) filePaths.push(`css/${currentSession.themeName}-theme.css`);
|
|
6943
|
+
if (activeTpl.sharedJs) filePaths.push(`js/${currentSession.themeName}-animations.js`);
|
|
6944
|
+
commitHash = commitTemplateState(currentSession.themePath, activeTpl.id, userMessage, filePaths);
|
|
6945
|
+
} else {
|
|
6946
|
+
commitHash = commitThemeState(currentSession.themePath, userMessage);
|
|
6947
|
+
}
|
|
6388
6948
|
if (commitHash) {
|
|
6389
6949
|
ws.send(JSON.stringify({ type: "version_created", hash: commitHash }));
|
|
6390
6950
|
}
|
|
@@ -6529,6 +7089,7 @@ ${errorContext}`;
|
|
|
6529
7089
|
const activeTpl = getActiveTemplate();
|
|
6530
7090
|
ws.send(JSON.stringify({
|
|
6531
7091
|
type: "init",
|
|
7092
|
+
sessionId: session.id,
|
|
6532
7093
|
themeName: session.themeName,
|
|
6533
7094
|
modules: getOrderedModules().map((m) => m.moduleName),
|
|
6534
7095
|
messageCount: session.messages.length,
|