@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/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
+ };