@spark-apps/quickpeek 1.1.1 → 1.1.3
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.d.mts +308 -0
- package/dist/index.mjs +1857 -0
- package/dist/qp.js +636 -364
- package/dist/voices-BtYZey4Y.d.mts +287 -0
- package/dist/web.d.mts +102 -0
- package/dist/web.mjs +1025 -0
- package/package.json +13 -1
package/dist/web.mjs
ADDED
|
@@ -0,0 +1,1025 @@
|
|
|
1
|
+
// src/lib/utils.ts
|
|
2
|
+
var LANGUAGE_NAMES = {
|
|
3
|
+
ar: "Arabic",
|
|
4
|
+
es: "Spanish",
|
|
5
|
+
fr: "French",
|
|
6
|
+
de: "German",
|
|
7
|
+
zh: "Chinese",
|
|
8
|
+
ja: "Japanese",
|
|
9
|
+
ko: "Korean",
|
|
10
|
+
pt: "Portuguese",
|
|
11
|
+
ru: "Russian",
|
|
12
|
+
hi: "Hindi",
|
|
13
|
+
tr: "Turkish",
|
|
14
|
+
it: "Italian"
|
|
15
|
+
};
|
|
16
|
+
function getLanguageName(lang) {
|
|
17
|
+
if (!lang || lang === "en") return null;
|
|
18
|
+
return LANGUAGE_NAMES[lang] || lang.toUpperCase();
|
|
19
|
+
}
|
|
20
|
+
function hexToASS(hex, alpha = 0) {
|
|
21
|
+
const clean = hex.replace("#", "");
|
|
22
|
+
const r = clean.slice(0, 2);
|
|
23
|
+
const g = clean.slice(2, 4);
|
|
24
|
+
const b = clean.slice(4, 6);
|
|
25
|
+
const a = alpha.toString(16).padStart(2, "0").toUpperCase();
|
|
26
|
+
return `&H${a}${b}${g}${r}`.toUpperCase();
|
|
27
|
+
}
|
|
28
|
+
function rgbaToHex(color) {
|
|
29
|
+
if (color.startsWith("#")) return color;
|
|
30
|
+
const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
31
|
+
if (!match) return "#000000";
|
|
32
|
+
const r = parseInt(match[1], 10).toString(16).padStart(2, "0");
|
|
33
|
+
const g = parseInt(match[2], 10).toString(16).padStart(2, "0");
|
|
34
|
+
const b = parseInt(match[3], 10).toString(16).padStart(2, "0");
|
|
35
|
+
return `#${r}${g}${b}`.toUpperCase();
|
|
36
|
+
}
|
|
37
|
+
var LOWERCASE_WORDS = /* @__PURE__ */ new Set([
|
|
38
|
+
"a",
|
|
39
|
+
"an",
|
|
40
|
+
"the",
|
|
41
|
+
"and",
|
|
42
|
+
"but",
|
|
43
|
+
"or",
|
|
44
|
+
"nor",
|
|
45
|
+
"for",
|
|
46
|
+
"yet",
|
|
47
|
+
"so",
|
|
48
|
+
"at",
|
|
49
|
+
"by",
|
|
50
|
+
"in",
|
|
51
|
+
"of",
|
|
52
|
+
"on",
|
|
53
|
+
"to",
|
|
54
|
+
"up",
|
|
55
|
+
"as",
|
|
56
|
+
"if",
|
|
57
|
+
"off",
|
|
58
|
+
"per",
|
|
59
|
+
"via",
|
|
60
|
+
"from",
|
|
61
|
+
"into",
|
|
62
|
+
"onto",
|
|
63
|
+
"upon",
|
|
64
|
+
"with",
|
|
65
|
+
"over",
|
|
66
|
+
"under",
|
|
67
|
+
"than"
|
|
68
|
+
]);
|
|
69
|
+
function windowsPathToWSL(path) {
|
|
70
|
+
const driveMatch = /^([A-Z]):[/\\]/i.exec(path);
|
|
71
|
+
if (!driveMatch) return null;
|
|
72
|
+
const drive = driveMatch[1].toLowerCase();
|
|
73
|
+
return `/mnt/${drive}/${path.slice(3).replace(/\\/g, "/")}`;
|
|
74
|
+
}
|
|
75
|
+
function parsePartial(partial) {
|
|
76
|
+
if (!partial) return {};
|
|
77
|
+
const parts = partial.split("-");
|
|
78
|
+
if (parts.length !== 2) return {};
|
|
79
|
+
const ts = parts[0] ? parseFloat(parts[0]) : NaN;
|
|
80
|
+
const te = parts[1] ? parseFloat(parts[1]) : NaN;
|
|
81
|
+
const result = {};
|
|
82
|
+
if (!Number.isNaN(ts)) result.trimStart = ts;
|
|
83
|
+
if (!Number.isNaN(te)) result.trimEnd = te;
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
function getErrorMessage(err) {
|
|
87
|
+
return err instanceof Error ? err.message : String(err);
|
|
88
|
+
}
|
|
89
|
+
function toTitleCase(text) {
|
|
90
|
+
return text.replace(/\w+/g, (word, index) => {
|
|
91
|
+
if (index === 0) {
|
|
92
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
93
|
+
}
|
|
94
|
+
if (LOWERCASE_WORDS.has(word.toLowerCase())) {
|
|
95
|
+
return word.toLowerCase();
|
|
96
|
+
}
|
|
97
|
+
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/lib/api-client.ts
|
|
102
|
+
var SPARK_AI_BASE = "https://sparkbrain.app";
|
|
103
|
+
var SPARK_AI_CHAT = `${SPARK_AI_BASE}/api/chat`;
|
|
104
|
+
var SPARK_AI_TOKEN = `${SPARK_AI_BASE}/api/models/token`;
|
|
105
|
+
var API_KEY = process.env.SPARK_AI_KEY || "";
|
|
106
|
+
var cachedToken = null;
|
|
107
|
+
async function getOrRefreshToken() {
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
if (cachedToken && cachedToken.expiresAt > now + 3e5) {
|
|
110
|
+
return cachedToken.token;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const response = await fetch(SPARK_AI_TOKEN, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: { "X-Api-Key": API_KEY }
|
|
116
|
+
});
|
|
117
|
+
const data = await response.json();
|
|
118
|
+
if (!response.ok || !data.success || !data.token) {
|
|
119
|
+
throw new Error(data.error || `Token exchange failed: ${response.status}`);
|
|
120
|
+
}
|
|
121
|
+
cachedToken = {
|
|
122
|
+
token: data.token,
|
|
123
|
+
expiresAt: data.expiresAt || now + 36e5
|
|
124
|
+
};
|
|
125
|
+
return data.token;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
throw new Error(`Failed to get Spark AI token: ${getErrorMessage(err)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function callAI(systemPrompt, userPrompt, tier = "free") {
|
|
131
|
+
if (!API_KEY) {
|
|
132
|
+
return {
|
|
133
|
+
success: false,
|
|
134
|
+
error: "AI service is not configured. Please report this issue.",
|
|
135
|
+
isAuthError: true
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const token = await getOrRefreshToken();
|
|
140
|
+
let lastRateLimitError = null;
|
|
141
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
142
|
+
try {
|
|
143
|
+
const response = await fetch(SPARK_AI_CHAT, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: {
|
|
146
|
+
"Content-Type": "application/json",
|
|
147
|
+
Authorization: `Bearer ${token}`
|
|
148
|
+
},
|
|
149
|
+
body: JSON.stringify({
|
|
150
|
+
messages: [
|
|
151
|
+
{ role: "system", content: systemPrompt },
|
|
152
|
+
{ role: "user", content: userPrompt }
|
|
153
|
+
]
|
|
154
|
+
})
|
|
155
|
+
});
|
|
156
|
+
const data = await response.json();
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
const error = data.error || `Spark AI error: ${response.status}`;
|
|
159
|
+
if (response.status === 429 && attempt < 3) {
|
|
160
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
161
|
+
const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 1e3 * Math.pow(2, attempt - 1);
|
|
162
|
+
lastRateLimitError = {
|
|
163
|
+
success: false,
|
|
164
|
+
error,
|
|
165
|
+
isRateLimit: true,
|
|
166
|
+
...retryAfter ? { retryAfterSeconds: parseInt(retryAfter, 10) } : {}
|
|
167
|
+
};
|
|
168
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, 3e4)));
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
success: false,
|
|
173
|
+
error,
|
|
174
|
+
isRateLimit: response.status === 429,
|
|
175
|
+
isAuthError: response.status === 401 || response.status === 403
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (!data.success || !data.response) {
|
|
179
|
+
return {
|
|
180
|
+
success: false,
|
|
181
|
+
error: data.error || "No response from Spark AI"
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
success: true,
|
|
186
|
+
content: data.response,
|
|
187
|
+
limits: {
|
|
188
|
+
used: data.limits?.used ?? 0,
|
|
189
|
+
limit: data.limits?.limit ?? 100,
|
|
190
|
+
tier,
|
|
191
|
+
...data.limits?.resetAt ? { resetAt: data.limits.resetAt } : {}
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
} catch (err) {
|
|
195
|
+
if (attempt === 3) throw err;
|
|
196
|
+
const delayMs = 1e3 * Math.pow(2, attempt - 1);
|
|
197
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return lastRateLimitError || { success: false, error: "Request failed after retries" };
|
|
201
|
+
} catch (err) {
|
|
202
|
+
return { success: false, error: `Spark AI error: ${getErrorMessage(err)}` };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function parseAIPlanResponse(content) {
|
|
206
|
+
try {
|
|
207
|
+
let processedContent = content;
|
|
208
|
+
const codeBlockMatch = processedContent.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
209
|
+
if (codeBlockMatch?.[1]) {
|
|
210
|
+
processedContent = codeBlockMatch[1].trim();
|
|
211
|
+
}
|
|
212
|
+
const jsonMatch = processedContent.match(/\{[\s\S]*\}/);
|
|
213
|
+
if (!jsonMatch) {
|
|
214
|
+
return { success: false, error: "No valid JSON found in AI response" };
|
|
215
|
+
}
|
|
216
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
217
|
+
return { success: true, plan: parsed };
|
|
218
|
+
} catch (err) {
|
|
219
|
+
return {
|
|
220
|
+
success: false,
|
|
221
|
+
error: `Failed to parse AI response: ${err instanceof Error ? err.message : String(err)}`
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function normalizeUrl(input) {
|
|
226
|
+
let url = input.trim();
|
|
227
|
+
url = url.replace(/^(https?:\/\/)?(www\.)?/i, "");
|
|
228
|
+
return `https://${url}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/lib/crawl.ts
|
|
232
|
+
import { chromium } from "playwright";
|
|
233
|
+
|
|
234
|
+
// src/lib/scraping.ts
|
|
235
|
+
var EXCLUDED_LINK_PATTERNS = [
|
|
236
|
+
// Legal & Policy
|
|
237
|
+
"privacy",
|
|
238
|
+
"terms",
|
|
239
|
+
"cookie",
|
|
240
|
+
"legal",
|
|
241
|
+
"policy",
|
|
242
|
+
"disclaimer",
|
|
243
|
+
"copyright",
|
|
244
|
+
"gdpr",
|
|
245
|
+
// Auth & Account
|
|
246
|
+
"login",
|
|
247
|
+
"signin",
|
|
248
|
+
"sign in",
|
|
249
|
+
"signup",
|
|
250
|
+
"sign up",
|
|
251
|
+
"register",
|
|
252
|
+
"logout",
|
|
253
|
+
"sign out",
|
|
254
|
+
"my account",
|
|
255
|
+
"profile",
|
|
256
|
+
"settings",
|
|
257
|
+
// Support
|
|
258
|
+
"contact",
|
|
259
|
+
"about",
|
|
260
|
+
"help",
|
|
261
|
+
"support",
|
|
262
|
+
"faq",
|
|
263
|
+
// Social Media
|
|
264
|
+
"facebook",
|
|
265
|
+
"twitter",
|
|
266
|
+
"linkedin",
|
|
267
|
+
"instagram",
|
|
268
|
+
"youtube",
|
|
269
|
+
"tiktok",
|
|
270
|
+
"github",
|
|
271
|
+
// Company
|
|
272
|
+
"blog",
|
|
273
|
+
"news",
|
|
274
|
+
"press",
|
|
275
|
+
"careers",
|
|
276
|
+
"jobs",
|
|
277
|
+
"team",
|
|
278
|
+
// Developer
|
|
279
|
+
"docs",
|
|
280
|
+
"documentation",
|
|
281
|
+
"api",
|
|
282
|
+
"developers",
|
|
283
|
+
// Commerce
|
|
284
|
+
"pricing",
|
|
285
|
+
"plans",
|
|
286
|
+
"subscribe",
|
|
287
|
+
"newsletter",
|
|
288
|
+
"upgrade",
|
|
289
|
+
"buy",
|
|
290
|
+
"purchase",
|
|
291
|
+
// Misc
|
|
292
|
+
"accessibility",
|
|
293
|
+
"sitemap",
|
|
294
|
+
"rss",
|
|
295
|
+
"feed",
|
|
296
|
+
"home"
|
|
297
|
+
];
|
|
298
|
+
var INTERACTIVE_SELECTORS = [
|
|
299
|
+
"button",
|
|
300
|
+
"input",
|
|
301
|
+
"select",
|
|
302
|
+
"textarea",
|
|
303
|
+
"a[href]",
|
|
304
|
+
'[role="button"]',
|
|
305
|
+
'[role="checkbox"]',
|
|
306
|
+
'[role="radio"]',
|
|
307
|
+
'[role="switch"]',
|
|
308
|
+
'[role="slider"]',
|
|
309
|
+
'[role="combobox"]',
|
|
310
|
+
'[role="menuitem"]',
|
|
311
|
+
'[type="submit"]',
|
|
312
|
+
"[onclick]"
|
|
313
|
+
];
|
|
314
|
+
function isExcludedLink(text, href = "") {
|
|
315
|
+
const lowerText = text.toLowerCase();
|
|
316
|
+
const lowerHref = href.toLowerCase();
|
|
317
|
+
return EXCLUDED_LINK_PATTERNS.some((p) => lowerText.includes(p) || lowerHref.includes(p));
|
|
318
|
+
}
|
|
319
|
+
function shouldSkipLink(text, href, currentPath) {
|
|
320
|
+
if (!href || href === "#") return true;
|
|
321
|
+
try {
|
|
322
|
+
const linkPath = new URL(href).pathname;
|
|
323
|
+
if (linkPath === "/" || linkPath === currentPath) return true;
|
|
324
|
+
} catch {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
return isExcludedLink(text, href);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/lib/crawl.ts
|
|
331
|
+
function getActionForType(type) {
|
|
332
|
+
switch (type) {
|
|
333
|
+
case "button":
|
|
334
|
+
case "link":
|
|
335
|
+
return "click";
|
|
336
|
+
case "file":
|
|
337
|
+
return "upload";
|
|
338
|
+
case "range":
|
|
339
|
+
return "drag";
|
|
340
|
+
case "select":
|
|
341
|
+
return "select";
|
|
342
|
+
case "checkbox":
|
|
343
|
+
return "check";
|
|
344
|
+
default:
|
|
345
|
+
return "type";
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
var noopTask = () => ({ succeed: () => {
|
|
349
|
+
} });
|
|
350
|
+
async function crawlPage(url, task = noopTask) {
|
|
351
|
+
const browserTask = task("Launching browser");
|
|
352
|
+
const browser = await chromium.launch({ headless: true });
|
|
353
|
+
const page = await browser.newPage();
|
|
354
|
+
browserTask.succeed("Browser launched");
|
|
355
|
+
const crawlTask = task("Crawling page");
|
|
356
|
+
await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
|
|
357
|
+
const title = await page.title();
|
|
358
|
+
const rawElements = await page.evaluate(`
|
|
359
|
+
(() => {
|
|
360
|
+
const items = [];
|
|
361
|
+
|
|
362
|
+
// Helper to get parent context
|
|
363
|
+
const getCtx = (el) => {
|
|
364
|
+
let p = el.parentElement;
|
|
365
|
+
while (p && p !== document.body) {
|
|
366
|
+
if (p.id) return '#' + p.id;
|
|
367
|
+
const cls = Array.from(p.classList).find(c =>
|
|
368
|
+
/toolbar|header|footer|sidebar|panel|modal|dialog|nav|menu|controls|actions/i.test(c)
|
|
369
|
+
);
|
|
370
|
+
if (cls) return '.' + cls;
|
|
371
|
+
if (['header', 'footer', 'nav', 'main', 'aside', 'form'].includes(p.tagName.toLowerCase())) {
|
|
372
|
+
return p.tagName.toLowerCase();
|
|
373
|
+
}
|
|
374
|
+
p = p.parentElement;
|
|
375
|
+
}
|
|
376
|
+
return '';
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// Buttons (use textContent to catch hidden elements too)
|
|
380
|
+
document.querySelectorAll('button, [role="button"]').forEach(el => {
|
|
381
|
+
const text = (el.textContent || el.innerText || '').trim().replace(/\\s+/g, ' ').slice(0, 40);
|
|
382
|
+
const testId = el.getAttribute('data-testid');
|
|
383
|
+
const ariaLabel = el.getAttribute('aria-label');
|
|
384
|
+
if (!text && !testId && !ariaLabel) return;
|
|
385
|
+
items.push({ type: 'button', text: text || ariaLabel || testId, testId, ctx: getCtx(el) });
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// Inputs (including file inputs, range sliders, checkboxes, selects)
|
|
389
|
+
document.querySelectorAll('input, textarea, select').forEach(el => {
|
|
390
|
+
if (el.type === 'hidden') return;
|
|
391
|
+
const testId = el.getAttribute('data-testid');
|
|
392
|
+
const inputType = el.type || 'text';
|
|
393
|
+
|
|
394
|
+
// Get label text for any input type
|
|
395
|
+
let labelText = '';
|
|
396
|
+
if (el.labels?.[0]) {
|
|
397
|
+
labelText = el.labels[0].textContent?.trim() || '';
|
|
398
|
+
} else if (el.id) {
|
|
399
|
+
const label = document.querySelector('label[for="' + el.id + '"]');
|
|
400
|
+
if (label) labelText = label.textContent?.trim() || '';
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const text = labelText || el.placeholder || el.name || el.id || '';
|
|
404
|
+
|
|
405
|
+
// Include file, range, and checkbox inputs even without text
|
|
406
|
+
const alwaysInclude = ['file', 'range', 'checkbox'].includes(inputType);
|
|
407
|
+
if (!text && !testId && !alwaysInclude) return;
|
|
408
|
+
|
|
409
|
+
items.push({
|
|
410
|
+
type: inputType,
|
|
411
|
+
text: text || (inputType === 'file' ? 'file upload' : inputType === 'range' ? 'slider' : inputType === 'checkbox' ? 'checkbox' : testId),
|
|
412
|
+
testId,
|
|
413
|
+
id: el.id,
|
|
414
|
+
name: el.name,
|
|
415
|
+
placeholder: el.placeholder,
|
|
416
|
+
isFile: inputType === 'file'
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Links (use textContent to catch hidden elements too)
|
|
421
|
+
document.querySelectorAll('a[href]').forEach(el => {
|
|
422
|
+
const text = (el.textContent || el.innerText || '').trim().replace(/\\s+/g, ' ').slice(0, 40);
|
|
423
|
+
if (text && text.length >= 2) items.push({ type: 'link', text });
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
return items.slice(0, 40);
|
|
427
|
+
})()
|
|
428
|
+
`);
|
|
429
|
+
const filteredElements = rawElements.filter((el) => {
|
|
430
|
+
if (!el.text) return true;
|
|
431
|
+
const text = el.text.toLowerCase().trim();
|
|
432
|
+
return !EXCLUDED_LINK_PATTERNS.some((pattern) => text.includes(pattern));
|
|
433
|
+
});
|
|
434
|
+
const btnTexts = {};
|
|
435
|
+
for (const el of filteredElements) {
|
|
436
|
+
if (el.type === "button" && el.text) {
|
|
437
|
+
btnTexts[el.text] = (btnTexts[el.text] || 0) + 1;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const elements = filteredElements.map((el) => {
|
|
441
|
+
let selector;
|
|
442
|
+
if (el.testId) {
|
|
443
|
+
selector = `[data-testid="${el.testId}"]`;
|
|
444
|
+
} else if (el.isFile || el.type === "file") {
|
|
445
|
+
selector = 'input[type="file"]';
|
|
446
|
+
} else if (el.type === "range") {
|
|
447
|
+
selector = el.id ? `#${el.id}` : 'input[type="range"]';
|
|
448
|
+
} else if (el.type === "checkbox") {
|
|
449
|
+
selector = el.id ? `#${el.id}` : 'input[type="checkbox"]';
|
|
450
|
+
} else if (el.type === "button") {
|
|
451
|
+
if (btnTexts[el.text] === 1) {
|
|
452
|
+
selector = `text="${el.text}"`;
|
|
453
|
+
} else {
|
|
454
|
+
selector = el.ctx ? `${el.ctx} >> text="${el.text}"` : `button:has-text("${el.text}")`;
|
|
455
|
+
}
|
|
456
|
+
} else if (el.type === "link") {
|
|
457
|
+
selector = `text="${el.text}"`;
|
|
458
|
+
} else if (el.id) {
|
|
459
|
+
selector = `#${el.id}`;
|
|
460
|
+
} else if (el.name) {
|
|
461
|
+
selector = `[name="${el.name}"]`;
|
|
462
|
+
} else if (el.placeholder) {
|
|
463
|
+
selector = `[placeholder="${el.placeholder}"]`;
|
|
464
|
+
} else {
|
|
465
|
+
selector = `text="${el.text}"`;
|
|
466
|
+
}
|
|
467
|
+
let normalizedType;
|
|
468
|
+
if (el.isFile || el.type === "file") {
|
|
469
|
+
normalizedType = "file";
|
|
470
|
+
} else if (el.type === "range") {
|
|
471
|
+
normalizedType = "range";
|
|
472
|
+
} else if (el.type === "checkbox") {
|
|
473
|
+
normalizedType = "checkbox";
|
|
474
|
+
} else if (el.type === "select" || el.type === "select-one" || el.type === "select-multiple") {
|
|
475
|
+
normalizedType = "select";
|
|
476
|
+
} else if (el.type === "button" || el.type === "link") {
|
|
477
|
+
normalizedType = el.type;
|
|
478
|
+
} else {
|
|
479
|
+
normalizedType = "input";
|
|
480
|
+
}
|
|
481
|
+
const action = getActionForType(normalizedType);
|
|
482
|
+
const target = normalizedType === "file" ? selector : (el.text || "").toLowerCase();
|
|
483
|
+
return { type: normalizedType, text: el.text, selector, action, target };
|
|
484
|
+
});
|
|
485
|
+
await browser.close();
|
|
486
|
+
crawlTask.succeed(`Found ${elements.length} interactive elements`);
|
|
487
|
+
return { title, elements };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/lib/plan-validation.ts
|
|
491
|
+
var INVALID_TARGETS = [
|
|
492
|
+
/\.png$/i,
|
|
493
|
+
/\.jpg$/i,
|
|
494
|
+
/\.gif$/i,
|
|
495
|
+
/\.webp$/i,
|
|
496
|
+
/\.svg$/i,
|
|
497
|
+
/sprite_sheet/i,
|
|
498
|
+
/downloaded/i
|
|
499
|
+
];
|
|
500
|
+
var ACTIONS_REQUIRING_TARGET = ["click", "type", "select", "check", "drag"];
|
|
501
|
+
function isFilePath(value) {
|
|
502
|
+
if (!value) return false;
|
|
503
|
+
return /\.\w{2,5}$/.test(value) && (/[/\\]/.test(value) || /^[a-zA-Z]:/.test(value));
|
|
504
|
+
}
|
|
505
|
+
function isInvalidTarget(target) {
|
|
506
|
+
return INVALID_TARGETS.some((pattern) => pattern.test(target));
|
|
507
|
+
}
|
|
508
|
+
function validateStep(step) {
|
|
509
|
+
if (!step?.action) {
|
|
510
|
+
return "Missing action";
|
|
511
|
+
}
|
|
512
|
+
if (step.action === "navigate") {
|
|
513
|
+
if (!step.target) return "Navigate requires a URL target";
|
|
514
|
+
if (!step.target.startsWith("http://") && !step.target.startsWith("https://")) {
|
|
515
|
+
return "Navigate target must be a valid URL";
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (step.action === "media") {
|
|
519
|
+
if (!step.mediaFile && !isFilePath(step.value)) {
|
|
520
|
+
return "Media step requires a file (image or video)";
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (step.action === "upload") {
|
|
524
|
+
if (!step.mediaFile && !isFilePath(step.value)) {
|
|
525
|
+
return "Upload step requires a file";
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (ACTIONS_REQUIRING_TARGET.includes(step.action)) {
|
|
529
|
+
if (!step.target) return `${step.action} requires a target`;
|
|
530
|
+
if (isInvalidTarget(step.target)) return `Invalid target pattern: ${step.target}`;
|
|
531
|
+
}
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
function isValidStep(step) {
|
|
535
|
+
return validateStep(step) === null;
|
|
536
|
+
}
|
|
537
|
+
function filterValidSteps(steps) {
|
|
538
|
+
return steps.filter(isValidStep);
|
|
539
|
+
}
|
|
540
|
+
var REFINE_PLAN_SYSTEM_PROMPT = `You refine demo video plans based on user feedback.
|
|
541
|
+
|
|
542
|
+
AVAILABLE ACTIONS (use these exact formats):
|
|
543
|
+
|
|
544
|
+
1. WAIT - Pause for narration (intro/outro/explanations)
|
|
545
|
+
Format: { "action": "wait", "caption": "Your narration text..." }
|
|
546
|
+
Use when: Starting intro, ending outro, explaining something without interaction
|
|
547
|
+
NO target needed
|
|
548
|
+
|
|
549
|
+
2. CLICK - Click buttons, links, or interactive elements
|
|
550
|
+
Format: { "action": "click", "target": "button text", "caption": "Click description..." }
|
|
551
|
+
Target: Just the button/link text (case insensitive, partial match OK)
|
|
552
|
+
Examples: "extract frame", "download", "submit", "next"
|
|
553
|
+
Use when: User says "click", "press", "tap", "select button", "open", "close"
|
|
554
|
+
|
|
555
|
+
3. TYPE - Enter text into input fields
|
|
556
|
+
Format: { "action": "type", "target": "input[name=\\"field\\"]", "value": "text to type", "caption": "..." }
|
|
557
|
+
Selectors: input[type="text"], input[name="..."], textarea, #id
|
|
558
|
+
Use when: User says "type", "enter", "input", "fill in", "write"
|
|
559
|
+
CRITICAL: PRESERVE existing value - never change what's being typed
|
|
560
|
+
|
|
561
|
+
4. UPLOAD - Upload a file to a web form's file input element
|
|
562
|
+
Format: { "action": "upload", "target": "input[type=\\"file\\"]", "value": "path/to/file", "caption": "..." }
|
|
563
|
+
Target: REQUIRED - CSS selector for file input, usually input[type="file"]
|
|
564
|
+
Value: Path to file being uploaded
|
|
565
|
+
Use when: Uploading a file TO a website's form (the page has a file picker)
|
|
566
|
+
CRITICAL: PRESERVE existing value - never change the file path
|
|
567
|
+
|
|
568
|
+
5. SCROLL - Scroll page or zoom camera
|
|
569
|
+
Format: { "action": "scroll", "value": "direction", "caption": "..." }
|
|
570
|
+
Values: "up", "down", "bottom", "80%" (page scroll) OR "-10", "10" (zoom: negative=out, positive=in)
|
|
571
|
+
Use when: User says "scroll", "zoom in", "zoom out", "show more", "go to bottom"
|
|
572
|
+
NO target needed
|
|
573
|
+
|
|
574
|
+
6. DRAG - Drag sliders or rotate 3D objects
|
|
575
|
+
Format: { "action": "drag", "target": "input[type=\\"range\\"]", "value": "50", "caption": "..." }
|
|
576
|
+
Values: "0"-"100" for sliders, "rotate" for 3D rotation (stays at new angle), "around" for full 360\xB0 (returns to start)
|
|
577
|
+
Selectors: input[type="range"], canvas, .slider
|
|
578
|
+
Use when: User says "drag", "slide", "adjust slider", "rotate", "spin", "move"
|
|
579
|
+
NOTE: Use "rotate" (not "around") for showcasing 3D models - it rotates ~270\xB0 and stays at the new position
|
|
580
|
+
|
|
581
|
+
7. SELECT - Choose option from dropdown
|
|
582
|
+
Format: { "action": "select", "target": "select[name=\\"..\\"]", "value": "option text", "caption": "..." }
|
|
583
|
+
Selector: select, select[name="..."], #id
|
|
584
|
+
Use when: User says "select", "choose", "pick from dropdown", "change option"
|
|
585
|
+
|
|
586
|
+
8. CHECK - Toggle checkbox
|
|
587
|
+
Format: { "action": "check", "target": "input[type=\\"checkbox\\"]", "caption": "..." }
|
|
588
|
+
Selector: input[type="checkbox"], [role="checkbox"]
|
|
589
|
+
Use when: User says "check", "uncheck", "toggle", "enable", "disable"
|
|
590
|
+
|
|
591
|
+
9. MEDIA - Display a media file fullscreen (NOT for uploading to a form!)
|
|
592
|
+
Format: { "action": "media", "value": "path/to/file.png", "caption": "..." }
|
|
593
|
+
Target: OPTIONAL - "scale" (fill viewport) or "center" (original size). Defaults to center.
|
|
594
|
+
Value: REQUIRED - File path to display
|
|
595
|
+
Supported: .png, .jpg, .jpeg, .gif, .webp, .mp4, .webm, .mov, .glb
|
|
596
|
+
Use when: Showing a file directly in the demo (screenshot, 3D model, video clip)
|
|
597
|
+
NOT for uploading - use UPLOAD action if page has a file input
|
|
598
|
+
CRITICAL: PRESERVE existing value - never change the file path
|
|
599
|
+
|
|
600
|
+
10. SHOW_DOWNLOAD - Display a download animation/result
|
|
601
|
+
Format: { "action": "showDownload", "caption": "..." }
|
|
602
|
+
Use when: Showing the final result after a download action
|
|
603
|
+
NO target needed
|
|
604
|
+
|
|
605
|
+
IMPORTANT - UPLOAD vs MEDIA:
|
|
606
|
+
\u2022 UPLOAD: File goes INTO a web form (page has <input type="file">). Requires target selector.
|
|
607
|
+
\u2022 MEDIA: File is DISPLAYED fullscreen in demo (no web interaction). No target needed.
|
|
608
|
+
Example: "Show the 3D model" \u2192 use MEDIA action
|
|
609
|
+
Example: "Upload a file to the converter" \u2192 use UPLOAD action
|
|
610
|
+
|
|
611
|
+
TARGET SYNTAX (simplified):
|
|
612
|
+
\u2022 Buttons/Links: Just use the text! "extract frame", "download", "submit" (case insensitive)
|
|
613
|
+
\u2022 Inputs: input[type="text"], input[name="field"], input[type="file"], input[type="range"]
|
|
614
|
+
\u2022 By ID: #elementId (only if text won't work)
|
|
615
|
+
\u2022 By class: .className (only if text won't work)
|
|
616
|
+
|
|
617
|
+
PREFER SIMPLE TEXT for buttons: "download" instead of button:has-text("Download")
|
|
618
|
+
|
|
619
|
+
DETECTING USER INTENT:
|
|
620
|
+
\u2022 "add intro/outro" \u2192 add wait step at start/end
|
|
621
|
+
\u2022 "remove step X" \u2192 delete that step
|
|
622
|
+
\u2022 "change caption" \u2192 update the caption text only
|
|
623
|
+
\u2022 "click X instead" \u2192 change target to X
|
|
624
|
+
\u2022 "add a click on X" \u2192 insert new click step
|
|
625
|
+
\u2022 "show the slider" \u2192 add drag step for input[type="range"]
|
|
626
|
+
\u2022 "zoom in/out" \u2192 add scroll with numeric value
|
|
627
|
+
\u2022 "reorder" \u2192 rearrange step sequence
|
|
628
|
+
|
|
629
|
+
RULES:
|
|
630
|
+
\u2022 Return ONLY valid JSON
|
|
631
|
+
\u2022 Keep step ids sequential (1, 2, 3...)
|
|
632
|
+
\u2022 PRESERVE upload/type values EXACTLY - copy from input unchanged
|
|
633
|
+
\u2022 Only modify steps mentioned in feedback
|
|
634
|
+
\u2022 Use available elements list when provided to find correct selectors
|
|
635
|
+
|
|
636
|
+
Return ONLY the modified JSON plan:
|
|
637
|
+
{
|
|
638
|
+
"steps": [
|
|
639
|
+
{ "id": 1, "action": "wait", "caption": "intro text" },
|
|
640
|
+
{ "id": 2, "action": "click", "target": "start", "caption": "Click to begin" },
|
|
641
|
+
{ "id": 3, "action": "upload", "target": "input[type=\\"file\\"]", "value": "path/to/file.png", "caption": "Upload the file" },
|
|
642
|
+
{ "id": 4, "action": "click", "target": "extract", "caption": "Process the file" },
|
|
643
|
+
{ "id": 5, "action": "media", "value": "path/to/result.png", "caption": "Here's the result" },
|
|
644
|
+
...
|
|
645
|
+
]
|
|
646
|
+
}`;
|
|
647
|
+
var FIX_PLAN_SYSTEM_PROMPT = `You are a web automation expert. Fix demo plan steps that failed validation.
|
|
648
|
+
|
|
649
|
+
RULES:
|
|
650
|
+
\u2022 Return ONLY valid JSON with the complete plan structure
|
|
651
|
+
\u2022 Fix ONLY the failed steps - keep working steps unchanged
|
|
652
|
+
\u2022 Use the available page elements to find correct selectors
|
|
653
|
+
\u2022 For click targets: use simplified button text like "extract frame", "download" (case insensitive)
|
|
654
|
+
\u2022 For elements not found, check if a similar element exists in the page elements list
|
|
655
|
+
\u2022 If an element truly doesn't exist, remove the step or change the action
|
|
656
|
+
\u2022 Preserve file paths in "value" fields
|
|
657
|
+
\u2022 Keep step ids sequential starting from 1
|
|
658
|
+
|
|
659
|
+
TARGET PATTERNS:
|
|
660
|
+
\u2022 Buttons/Links: Just the text! "extract frame", "download", "submit" (case insensitive)
|
|
661
|
+
\u2022 File inputs: input[type="file"]
|
|
662
|
+
\u2022 Sliders: input[type="range"]
|
|
663
|
+
\u2022 Text inputs: input[name="field"], #id, [placeholder="..."]
|
|
664
|
+
|
|
665
|
+
ACTION FORMATS:
|
|
666
|
+
\u2022 click: { "target": "download" } - use simplified button text
|
|
667
|
+
\u2022 scroll: { "value": "up|down" } for page, { "value": "10|-10" } for zoom (positive=in, negative=out)
|
|
668
|
+
\u2022 drag: { "target": "input[type=\\"range\\"]", "value": "10" } for sliders, { "value": "rotate" } for 3D rotation (auto-targets canvas)
|
|
669
|
+
\u2022 media: { "value": "path/to/file.png" } - display file fullscreen (NOT upload!)
|
|
670
|
+
\u2022 upload: { "target": "input[type=\\"file\\"]", "value": "path/to/file" } - upload to form
|
|
671
|
+
|
|
672
|
+
IMPORTANT: Only use elements that actually exist on the page based on the provided list.`;
|
|
673
|
+
function collectPreservedValues(steps) {
|
|
674
|
+
const uploadSteps = [];
|
|
675
|
+
const mediaSteps = [];
|
|
676
|
+
const otherValues = /* @__PURE__ */ new Map();
|
|
677
|
+
for (const step of steps) {
|
|
678
|
+
if (step.action === "upload") {
|
|
679
|
+
uploadSteps.push({
|
|
680
|
+
value: step.value,
|
|
681
|
+
target: step.target,
|
|
682
|
+
mediaFile: step.mediaFile,
|
|
683
|
+
mediaType: step.mediaType
|
|
684
|
+
});
|
|
685
|
+
} else if (step.action === "media") {
|
|
686
|
+
mediaSteps.push({
|
|
687
|
+
mediaFile: step.mediaFile,
|
|
688
|
+
mediaType: step.mediaType,
|
|
689
|
+
target: step.target
|
|
690
|
+
});
|
|
691
|
+
} else if (step.value && step.target) {
|
|
692
|
+
otherValues.set(`${step.action}:${step.target}`, step.value);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return { uploadSteps, mediaSteps, otherValues };
|
|
696
|
+
}
|
|
697
|
+
function restorePreservedValues(step, preserved, indices) {
|
|
698
|
+
let { uploadIdx, mediaIdx } = indices;
|
|
699
|
+
if (step.action === "upload" && uploadIdx < preserved.uploadSteps.length) {
|
|
700
|
+
const original = preserved.uploadSteps[uploadIdx];
|
|
701
|
+
if (original.value !== void 0) step.value = original.value;
|
|
702
|
+
if (original.target !== void 0) step.target = original.target;
|
|
703
|
+
if (original.mediaFile !== void 0) step.mediaFile = original.mediaFile;
|
|
704
|
+
if (original.mediaType !== void 0) step.mediaType = original.mediaType;
|
|
705
|
+
uploadIdx++;
|
|
706
|
+
}
|
|
707
|
+
if (step.action === "media" && mediaIdx < preserved.mediaSteps.length) {
|
|
708
|
+
const original = preserved.mediaSteps[mediaIdx];
|
|
709
|
+
if (original.mediaFile !== void 0) step.mediaFile = original.mediaFile;
|
|
710
|
+
if (original.mediaType !== void 0) step.mediaType = original.mediaType;
|
|
711
|
+
if (!step.target && original.target) step.target = original.target;
|
|
712
|
+
mediaIdx++;
|
|
713
|
+
}
|
|
714
|
+
if (step.action !== "upload" && step.action !== "media" && step.target) {
|
|
715
|
+
const original = preserved.otherValues.get(`${step.action}:${step.target}`);
|
|
716
|
+
if (original) step.value = original;
|
|
717
|
+
}
|
|
718
|
+
return { uploadIdx, mediaIdx };
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// src/lib/prompts.ts
|
|
722
|
+
function buildSystemPrompt(maxSteps) {
|
|
723
|
+
let scope;
|
|
724
|
+
if (maxSteps <= 5) {
|
|
725
|
+
scope = `SCOPE: Minimal demo (${maxSteps} steps max) - ONE complete user journey only`;
|
|
726
|
+
} else if (maxSteps <= 8) {
|
|
727
|
+
scope = `SCOPE: Standard demo (${maxSteps} steps max) - One journey with 1-2 options`;
|
|
728
|
+
} else {
|
|
729
|
+
scope = `SCOPE: Extended demo (${maxSteps} steps max) - Journey plus key settings`;
|
|
730
|
+
}
|
|
731
|
+
return `You create demo video scripts for web apps.
|
|
732
|
+
|
|
733
|
+
${scope}
|
|
734
|
+
|
|
735
|
+
CORE FLOW (always follow this structure):
|
|
736
|
+
1. wait - Intro (what does this app do?)
|
|
737
|
+
2. Actions - Complete ONE user journey (upload\u2192process\u2192result)
|
|
738
|
+
3. wait - Outro (what was accomplished?)
|
|
739
|
+
|
|
740
|
+
TARGET SYNTAX (simplified):
|
|
741
|
+
\u2022 Buttons/Links: Use plain text like "extract frame", "download" (case insensitive, partial match OK)
|
|
742
|
+
\u2022 File inputs: input[type="file"] - ALWAYS use this exact selector
|
|
743
|
+
\u2022 Sliders/Range: input[type="range"] - ALWAYS use this for sliders
|
|
744
|
+
\u2022 Select: select or select[name="..."]
|
|
745
|
+
\u2022 Text inputs: input[type="text"], input[name="..."], or #id
|
|
746
|
+
|
|
747
|
+
SKIP THESE ELEMENTS (never include in demo):
|
|
748
|
+
\u2022 Navigation: Home, About, Contact, Help, Support, FAQ
|
|
749
|
+
\u2022 Auth: Login, Sign In, Sign Up, Register, My Account
|
|
750
|
+
\u2022 Business: Pricing, Plans, Subscribe, Buy, Upgrade
|
|
751
|
+
\u2022 Footer: Privacy, Terms, Blog, Careers, Social media
|
|
752
|
+
|
|
753
|
+
FORBIDDEN:
|
|
754
|
+
\u2022 Repeating same action multiple times (e.g., multiple clicks on same button)
|
|
755
|
+
\u2022 Clicking downloaded files or result images
|
|
756
|
+
\u2022 Vague targets like "slider", "animation", "options" - use CSS selectors instead
|
|
757
|
+
\u2022 More steps than maxSteps allows
|
|
758
|
+
|
|
759
|
+
ACTION FORMATS (STRICT - follow exactly):
|
|
760
|
+
\u2022 click: { "action": "click", "target": "extract frame" } - use simplified button text
|
|
761
|
+
\u2022 type: { "action": "type", "target": "input[name=\\"field\\"]", "value": "text to type" }
|
|
762
|
+
\u2022 upload: { "action": "upload", "target": "input[type=\\"file\\"]", "value": "[file]" }
|
|
763
|
+
\u2022 wait: { "action": "wait" } - for intro/outro captions
|
|
764
|
+
\u2022 scroll: { "action": "scroll", "value": "-80" } - negative=zoom out, positive=zoom in
|
|
765
|
+
\u2022 drag: { "action": "drag", "target": "input[type=\\"range\\"]", "value": "50" }
|
|
766
|
+
\u2022 select: { "action": "select", "target": "select[name=\\"..\\"]", "value": "option" }
|
|
767
|
+
\u2022 check: { "action": "check", "target": "input[type=\\"checkbox\\"]" }
|
|
768
|
+
|
|
769
|
+
EXAMPLES:
|
|
770
|
+
\u2022 Click button: "target": "extract frame" or "target": "download"
|
|
771
|
+
\u2022 File upload: "target": "input[type=\\"file\\"]"
|
|
772
|
+
\u2022 Slider: "target": "input[type=\\"range\\"]"`;
|
|
773
|
+
}
|
|
774
|
+
function buildUserPrompt(url, title, elements, langName, description, maxSteps) {
|
|
775
|
+
const lang = langName ? `
|
|
776
|
+
Language: Write ALL captions in ${langName}.` : "";
|
|
777
|
+
const purpose = description ? `
|
|
778
|
+
Purpose: ${description}` : "";
|
|
779
|
+
const minSteps = Math.max(3, Math.floor(maxSteps * 0.6));
|
|
780
|
+
const clickables = elements.filter((e) => e.action === "click");
|
|
781
|
+
const uploadables = elements.filter((e) => e.action === "upload");
|
|
782
|
+
const typables = elements.filter((e) => e.action === "type");
|
|
783
|
+
const draggables = elements.filter((e) => e.action === "drag");
|
|
784
|
+
const selectables = elements.filter((e) => e.action === "select");
|
|
785
|
+
const checkables = elements.filter((e) => e.action === "check");
|
|
786
|
+
const elementDict = [];
|
|
787
|
+
if (clickables.length > 0) {
|
|
788
|
+
elementDict.push(`CLICK targets (use simplified text):
|
|
789
|
+
${clickables.map((e) => `\u2022 "${e.target || e.text.toLowerCase()}"`).join("\n")}`);
|
|
790
|
+
}
|
|
791
|
+
if (uploadables.length > 0) {
|
|
792
|
+
elementDict.push(`UPLOAD targets (use selector):
|
|
793
|
+
${uploadables.map((e) => `\u2022 ${e.selector}`).join("\n")}`);
|
|
794
|
+
}
|
|
795
|
+
if (typables.length > 0) {
|
|
796
|
+
elementDict.push(`TYPE targets (use selector):
|
|
797
|
+
${typables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`);
|
|
798
|
+
}
|
|
799
|
+
if (draggables.length > 0) {
|
|
800
|
+
elementDict.push(`DRAG targets (use selector):
|
|
801
|
+
${draggables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`);
|
|
802
|
+
}
|
|
803
|
+
if (selectables.length > 0) {
|
|
804
|
+
elementDict.push(`SELECT targets (use selector):
|
|
805
|
+
${selectables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`);
|
|
806
|
+
}
|
|
807
|
+
if (checkables.length > 0) {
|
|
808
|
+
elementDict.push(`CHECK targets (use selector):
|
|
809
|
+
${checkables.map((e) => `\u2022 "${e.text}" \u2192 ${e.selector}`).join("\n")}`);
|
|
810
|
+
}
|
|
811
|
+
const elementSummary = elementDict.length > 0 ? elementDict.join("\n\n") : "(no interactive elements found)";
|
|
812
|
+
return `URL: ${url}
|
|
813
|
+
Title: ${title}${purpose}
|
|
814
|
+
|
|
815
|
+
AVAILABLE ELEMENTS (grouped by action):
|
|
816
|
+
${elementSummary}
|
|
817
|
+
|
|
818
|
+
REQUIREMENTS:
|
|
819
|
+
\u2022 ${minSteps}-${maxSteps} steps total (start with wait intro, end with wait outro)
|
|
820
|
+
\u2022 Complete ONE user journey: input \u2192 process \u2192 output
|
|
821
|
+
\u2022 For CLICK: use simplified text target like "extract frame" or "download"
|
|
822
|
+
\u2022 For UPLOAD/DRAG/TYPE: use the CSS selector shown above
|
|
823
|
+
\u2022 For buttons that appear after actions: use partial text like "extract" or "download"${lang}
|
|
824
|
+
|
|
825
|
+
Return JSON:
|
|
826
|
+
{
|
|
827
|
+
"url": "${url}",
|
|
828
|
+
"title": "Short demo title",
|
|
829
|
+
"description": "What this app does",
|
|
830
|
+
"steps": [
|
|
831
|
+
{ "id": 1, "action": "wait", "caption": "Welcome! This app..." },
|
|
832
|
+
{ "id": 2, "action": "upload", "target": "input[type=\\"file\\"]", "caption": "Upload your file...", "value": "[file]" },
|
|
833
|
+
{ "id": 3, "action": "drag", "target": "input[type=\\"range\\"]", "caption": "Adjust the slider...", "value": "50" },
|
|
834
|
+
{ "id": 4, "action": "click", "target": "process", "caption": "Click to process..." },
|
|
835
|
+
{ "id": 5, "action": "click", "target": "download", "caption": "Download your result..." },
|
|
836
|
+
{ "id": 6, "action": "wait", "caption": "Done! Your file is ready." }
|
|
837
|
+
]
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
TARGET SYNTAX:
|
|
841
|
+
\u2022 Buttons/Links: Plain text like "extract frame", "download" (case insensitive)
|
|
842
|
+
\u2022 File inputs: input[type="file"]
|
|
843
|
+
\u2022 Sliders: input[type="range"]
|
|
844
|
+
\u2022 Text inputs: Use selector from list above`;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/lib/voices.ts
|
|
848
|
+
var LANGUAGE_VOICES = {
|
|
849
|
+
en: "en-GB-SoniaNeural",
|
|
850
|
+
ar: "ar-LB-LaylaNeural",
|
|
851
|
+
es: "es-ES-ElviraNeural",
|
|
852
|
+
fr: "fr-FR-DeniseNeural",
|
|
853
|
+
de: "de-DE-KatjaNeural",
|
|
854
|
+
it: "it-IT-ElsaNeural",
|
|
855
|
+
pt: "pt-BR-FranciscaNeural",
|
|
856
|
+
ru: "ru-RU-SvetlanaNeural",
|
|
857
|
+
zh: "zh-CN-XiaoxiaoNeural",
|
|
858
|
+
ja: "ja-JP-NanamiNeural",
|
|
859
|
+
ko: "ko-KR-SunHiNeural",
|
|
860
|
+
hi: "hi-IN-SwaraNeural",
|
|
861
|
+
tr: "tr-TR-EmelNeural",
|
|
862
|
+
nl: "nl-NL-ColetteNeural",
|
|
863
|
+
pl: "pl-PL-AgnieszkaNeural",
|
|
864
|
+
sv: "sv-SE-SofieNeural",
|
|
865
|
+
he: "he-IL-HilaNeural",
|
|
866
|
+
th: "th-TH-PremwadeeNeural",
|
|
867
|
+
vi: "vi-VN-HoaiMyNeural",
|
|
868
|
+
id: "id-ID-GadisNeural",
|
|
869
|
+
ms: "ms-MY-YasminNeural",
|
|
870
|
+
fil: "fil-PH-BlessicaNeural",
|
|
871
|
+
uk: "uk-UA-PolinaNeural",
|
|
872
|
+
cs: "cs-CZ-VlastaNeural",
|
|
873
|
+
el: "el-GR-AthinaNeural",
|
|
874
|
+
ro: "ro-RO-AlinaNeural",
|
|
875
|
+
hu: "hu-HU-NoemiNeural",
|
|
876
|
+
da: "da-DK-ChristelNeural",
|
|
877
|
+
fi: "fi-FI-NooraNeural",
|
|
878
|
+
no: "nb-NO-PernilleNeural",
|
|
879
|
+
sk: "sk-SK-ViktoriaNeural",
|
|
880
|
+
bg: "bg-BG-KalinaNeural",
|
|
881
|
+
hr: "hr-HR-GabrijelaNeural",
|
|
882
|
+
sl: "sl-SI-PetraNeural",
|
|
883
|
+
et: "et-EE-AnuNeural",
|
|
884
|
+
lv: "lv-LV-EveritaNeural",
|
|
885
|
+
lt: "lt-LT-OnaNeural",
|
|
886
|
+
bn: "bn-IN-TanishaaNeural",
|
|
887
|
+
ta: "ta-IN-PallaviNeural",
|
|
888
|
+
te: "te-IN-ShrutiNeural",
|
|
889
|
+
mr: "mr-IN-AarohiNeural",
|
|
890
|
+
gu: "gu-IN-DhwaniNeural",
|
|
891
|
+
kn: "kn-IN-SapnaNeural",
|
|
892
|
+
ml: "ml-IN-SobhanaNeural",
|
|
893
|
+
ur: "ur-PK-UzmaNeural",
|
|
894
|
+
fa: "fa-IR-DilaraNeural",
|
|
895
|
+
sw: "sw-KE-ZuriNeural",
|
|
896
|
+
af: "af-ZA-AdriNeural",
|
|
897
|
+
ca: "ca-ES-JoanaNeural",
|
|
898
|
+
cy: "cy-GB-NiaNeural",
|
|
899
|
+
ga: "ga-IE-OrlaNeural",
|
|
900
|
+
mt: "mt-MT-GraceNeural",
|
|
901
|
+
is: "is-IS-GudrunNeural",
|
|
902
|
+
mk: "mk-MK-MarijaNeural",
|
|
903
|
+
sr: "sr-RS-SophieNeural",
|
|
904
|
+
bs: "bs-BA-VesnaNeural",
|
|
905
|
+
sq: "sq-AL-AnilaNeural",
|
|
906
|
+
am: "am-ET-MekdesNeural",
|
|
907
|
+
az: "az-AZ-BabekNeural",
|
|
908
|
+
ka: "ka-GE-EkaNeural",
|
|
909
|
+
kk: "kk-KZ-AigulNeural",
|
|
910
|
+
mn: "mn-MN-YesUINeural",
|
|
911
|
+
ne: "ne-NP-HemkalaNeural",
|
|
912
|
+
ps: "ps-AF-LatifaNeural",
|
|
913
|
+
si: "si-LK-ThiliniNeural",
|
|
914
|
+
so: "so-SO-UbaxNeural",
|
|
915
|
+
zu: "zu-ZA-ThandoNeural"
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
// src/lib/SparkStripe-feature-gating.ts
|
|
919
|
+
var APP_ID = process.env.SPARK_APP_ID ?? "quickpeek";
|
|
920
|
+
var BASE = "https://sparkstripe.com";
|
|
921
|
+
var TIERS = {
|
|
922
|
+
"price_1TADnPEf9opkuWid9NtqOYON": "pro",
|
|
923
|
+
"price_1TADnPEf9opkuWidgbvvX9ma": "pro",
|
|
924
|
+
"price_1TADm7Ef9opkuWid3I9tquIB": "pro",
|
|
925
|
+
"price_1TADm8Ef9opkuWid1RSA2wCa": "pro",
|
|
926
|
+
"price_1TCREmEf9opkuWidvhuqzqDM": "pro",
|
|
927
|
+
"price_1TCREmEf9opkuWidacf1tiC7": "pro"
|
|
928
|
+
};
|
|
929
|
+
var RANK = {
|
|
930
|
+
"free": 0,
|
|
931
|
+
"pro": 1
|
|
932
|
+
};
|
|
933
|
+
var FEATURES = { "free": [], "pro": [
|
|
934
|
+
"Everything in Free",
|
|
935
|
+
"No watermark",
|
|
936
|
+
"Full video composition (voiceover + captions + music + transitions)",
|
|
937
|
+
"Unlimited videos",
|
|
938
|
+
"Priority support"
|
|
939
|
+
] };
|
|
940
|
+
async function _fetch(email, init) {
|
|
941
|
+
const r = await fetch(`${BASE}/api/public/subscription/status-public?email=${encodeURIComponent(email)}&app_id=${APP_ID}`, init);
|
|
942
|
+
if (!r.ok) throw new Error(`spark-stripe: ${r.status}`);
|
|
943
|
+
return r.json();
|
|
944
|
+
}
|
|
945
|
+
var getStatus = (email) => _fetch(email);
|
|
946
|
+
var _cache = /* @__PURE__ */ new Map();
|
|
947
|
+
async function getStatusCached(email) {
|
|
948
|
+
const k = email.toLowerCase(), hit = _cache.get(k);
|
|
949
|
+
if (hit && Date.now() < hit.exp) return hit.data;
|
|
950
|
+
const data = await getStatus(email);
|
|
951
|
+
_cache.set(k, { data, exp: Date.now() + 6e4 });
|
|
952
|
+
return data;
|
|
953
|
+
}
|
|
954
|
+
function getTier(d) {
|
|
955
|
+
if (!d.subscription || !d.registered) return "free";
|
|
956
|
+
if (d.subscription.status === "canceled" || d.subscription.status === "past_due") return "free";
|
|
957
|
+
return TIERS[d.subscription.price_id] ?? "free";
|
|
958
|
+
}
|
|
959
|
+
var isPaid = (d) => getTier(d) !== "free";
|
|
960
|
+
var hasAccess = (d, min) => (RANK[getTier(d)] ?? 0) >= (RANK[min] ?? 0);
|
|
961
|
+
var hasTrialRemaining = (d) => isPaid(d) || !!d.trial && !d.trial.is_expired && d.trial.uses_remaining > 0;
|
|
962
|
+
var showPaywall = (d) => !isPaid(d) && !hasTrialRemaining(d);
|
|
963
|
+
var isCanceling = (d) => !!d.subscription && d.subscription.cancel_at_period_end && d.subscription.status !== "canceled";
|
|
964
|
+
async function getTierByEmail(email) {
|
|
965
|
+
try {
|
|
966
|
+
return getTier(await getStatusCached(email));
|
|
967
|
+
} catch {
|
|
968
|
+
return "free";
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
async function isVerified(email) {
|
|
972
|
+
try {
|
|
973
|
+
return (await getStatusCached(email)).verified;
|
|
974
|
+
} catch {
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
function pricingUrl(opts) {
|
|
979
|
+
const u = new URL(`${BASE}/pricing/${APP_ID}`);
|
|
980
|
+
if (opts?.email) u.searchParams.set("email", opts.email);
|
|
981
|
+
if (opts?.returnUrl) u.searchParams.set("return_url", opts.returnUrl);
|
|
982
|
+
return u.toString();
|
|
983
|
+
}
|
|
984
|
+
export {
|
|
985
|
+
ACTIONS_REQUIRING_TARGET,
|
|
986
|
+
EXCLUDED_LINK_PATTERNS,
|
|
987
|
+
FEATURES,
|
|
988
|
+
FIX_PLAN_SYSTEM_PROMPT,
|
|
989
|
+
INTERACTIVE_SELECTORS,
|
|
990
|
+
INVALID_TARGETS,
|
|
991
|
+
LANGUAGE_VOICES,
|
|
992
|
+
REFINE_PLAN_SYSTEM_PROMPT,
|
|
993
|
+
buildSystemPrompt,
|
|
994
|
+
buildUserPrompt,
|
|
995
|
+
callAI,
|
|
996
|
+
collectPreservedValues,
|
|
997
|
+
crawlPage,
|
|
998
|
+
filterValidSteps,
|
|
999
|
+
getErrorMessage,
|
|
1000
|
+
getLanguageName,
|
|
1001
|
+
getStatus,
|
|
1002
|
+
getStatusCached,
|
|
1003
|
+
getTier,
|
|
1004
|
+
getTierByEmail,
|
|
1005
|
+
hasAccess,
|
|
1006
|
+
hasTrialRemaining,
|
|
1007
|
+
hexToASS,
|
|
1008
|
+
isCanceling,
|
|
1009
|
+
isExcludedLink,
|
|
1010
|
+
isInvalidTarget,
|
|
1011
|
+
isPaid,
|
|
1012
|
+
isValidStep,
|
|
1013
|
+
isVerified,
|
|
1014
|
+
normalizeUrl,
|
|
1015
|
+
parseAIPlanResponse,
|
|
1016
|
+
parsePartial,
|
|
1017
|
+
pricingUrl,
|
|
1018
|
+
restorePreservedValues,
|
|
1019
|
+
rgbaToHex,
|
|
1020
|
+
shouldSkipLink,
|
|
1021
|
+
showPaywall,
|
|
1022
|
+
toTitleCase,
|
|
1023
|
+
validateStep,
|
|
1024
|
+
windowsPathToWSL
|
|
1025
|
+
};
|