analyzthis_design 2.5.2 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -7
- package/agents/cards/mood-board.md +7 -8
- package/agents/manifests/mood-board.json +13 -9
- package/agents/router.json +1 -1
- package/dist/README.md +19 -7
- package/dist/agents/cards/mood-board.md +7 -8
- package/dist/agents/manifests/mood-board.json +13 -9
- package/dist/agents/router.json +1 -1
- package/dist/bin/cli.js +177 -28
- package/dist/lib/install.js +1 -0
- package/dist/lib/mcp-server.js +167 -99
- package/dist/lib/moodboard-film.js +119 -0
- package/dist/lib/moodboard-view.js +426 -0
- package/dist/lib/moodboard.js +557 -80
- package/dist/skills/anuj/SKILL.md +2 -0
- package/dist/skills/arjun/SKILL.md +2 -0
- package/dist/skills/getting-started/SKILL.md +2 -2
- package/dist/skills/meera/SKILL.md +2 -0
- package/dist/skills/mood-board/SKILL.md +87 -106
- package/dist/skills/noor/SKILL.md +2 -0
- package/dist/skills/persona-orchestrator/references/lens.md +4 -4
- package/dist/skills/priya/SKILL.md +2 -0
- package/dist/skills/raj/SKILL.md +2 -0
- package/dist/skills/zara/SKILL.md +23 -24
- package/package.json +11 -3
- package/skills/anuj/SKILL.md +2 -0
- package/skills/arjun/SKILL.md +2 -0
- package/skills/getting-started/SKILL.md +2 -2
- package/skills/meera/SKILL.md +2 -0
- package/skills/mood-board/SKILL.md +87 -106
- package/skills/noor/SKILL.md +2 -0
- package/skills/persona-orchestrator/references/lens.md +4 -4
- package/skills/priya/SKILL.md +2 -0
- package/skills/raj/SKILL.md +2 -0
- package/skills/zara/SKILL.md +23 -24
package/dist/lib/mcp-server.js
CHANGED
|
@@ -208,23 +208,55 @@ var TOOLS = [
|
|
|
208
208
|
},
|
|
209
209
|
{
|
|
210
210
|
name: 'analyzthis_mood_board',
|
|
211
|
-
description: '
|
|
211
|
+
description: 'Visual mood board. Zara crops 20-24 real UI elements (cards, tables, type, colour, nav) from 6+ live sites into a sectioned collage with real colours and fonts. Arjun/Noor/Priya/Meera vote as advice. The board opens in the browser and the USER clicks keep / maybe / reject — the user\'s choice overrides every persona, and the locked direction is saved for all personas. kind=mood keeps the older link-and-tag board.',
|
|
212
212
|
inputSchema: {
|
|
213
213
|
type: 'object',
|
|
214
214
|
properties: {
|
|
215
215
|
task: { type: 'string', description: 'Visual direction brief (e.g., "B2B fintech dashboard, trustworthy, high-contrast, data-dense")' },
|
|
216
|
-
kind: { type: 'string', enum: ['
|
|
217
|
-
action: { type: 'string', enum: ['create', '
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
216
|
+
kind: { type: 'string', enum: ['ui', 'mood'], description: 'ui = visual element board (default). mood = older tagged-URL board.' },
|
|
217
|
+
action: { type: 'string', enum: ['create', 'brief', 'capture_script', 'tiles', 'film', 'jury', 'votes', 'show', 'decide', 'rescout', 'lock', 'status', 'frames', 'verdict'], description: 'create (default) → brief → tiles (Zara crops) and film (records urls scrolling as motion tiles; needs playwright-core + Chrome) → jury → votes (advice) → show (user decides in browser) → rescout or lock. decide records a user choice from chat. frames/verdict are older names for tiles/votes.' },
|
|
218
|
+
board: { type: 'string', description: 'Board id' },
|
|
219
|
+
adjectives: { type: 'array', items: { type: 'string' }, description: 'Three feel words for the brief' },
|
|
220
|
+
not_this: { type: 'string', description: 'One thing this board must not look like' },
|
|
221
|
+
sections: { type: 'array', items: { type: 'string', enum: ['first_screen', 'typography', 'color', 'components', 'imagery', 'motion'] }, description: 'Sections this board needs' },
|
|
222
|
+
confirmed_by_user: { type: 'boolean', description: 'User approved the brief (action=brief)' },
|
|
223
|
+
selector: { type: 'string', description: 'CSS selector for action=capture_script' },
|
|
224
|
+
text: { type: 'string', description: 'Visible text that narrows the selector match (action=capture_script)' },
|
|
225
|
+
tiles: {
|
|
226
|
+
type: 'array',
|
|
227
|
+
description: 'Element crops (action=tiles)',
|
|
228
|
+
items: {
|
|
229
|
+
type: 'object',
|
|
230
|
+
properties: {
|
|
231
|
+
page_url: { type: 'string', description: 'Exact page the element came from (required)' },
|
|
232
|
+
image_path: { type: 'string', description: 'Local image file, or the saved Page.captureScreenshot JSON file' },
|
|
233
|
+
image_base64: { type: 'string', description: 'Inline Page.captureScreenshot data when no file was written' },
|
|
234
|
+
image_url: { type: 'string', description: 'Remote image URL if no local crop' },
|
|
235
|
+
video_path: { type: 'string', description: 'Local WebM/MP4 scroll film (motion tiles)' },
|
|
236
|
+
stills: { type: 'array', items: { type: 'string' }, description: 'Up to 4 waypoint still image paths for a film' },
|
|
237
|
+
section: { type: 'string', enum: ['first_screen', 'typography', 'color', 'components', 'imagery', 'motion'] },
|
|
238
|
+
element: { type: 'string', description: 'What the element is, e.g. "pricing card"' },
|
|
239
|
+
tokens: { type: 'object', description: 'From capture_script: colors, fonts, radius, spacing, font_size' },
|
|
240
|
+
steal: { type: 'string' }, refuse: { type: 'string' }, peak: { type: 'string' },
|
|
241
|
+
concept: { type: 'boolean', description: 'True if this is a concept shot, not shipped product' },
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
votes: {
|
|
246
|
+
type: 'array',
|
|
247
|
+
description: 'Persona advice (action=votes): [{tile, persona, vote: keep|skip, because}]',
|
|
248
|
+
items: { type: 'object' },
|
|
249
|
+
},
|
|
250
|
+
verdict: { type: 'string', enum: ['SHIP', 'REVISE', 'BLOCK'], description: 'Team advice verdict (action=votes)' },
|
|
251
|
+
summary: { type: 'string', description: 'Team because-lines (action=votes)' },
|
|
252
|
+
tile: { type: 'string', description: 'Tile number or id (action=decide)' },
|
|
253
|
+
decision: { type: 'string', enum: ['keep', 'maybe', 'reject', 'clear'], description: 'User decision (action=decide)' },
|
|
254
|
+
note: { type: 'string', description: 'User note (decide, rescout, lock)' },
|
|
255
|
+
urls: { type: 'array', items: { type: 'string' }, description: 'Reference URLs (kind=mood), or pages to film (action=film)' },
|
|
256
|
+
element: { type: 'string', description: 'Tile name (action=film), default "Scroll film — <site>"' },
|
|
257
|
+
section: { type: 'string', enum: ['first_screen', 'typography', 'color', 'components', 'imagery', 'motion'], description: 'Section for action=film (default motion)' },
|
|
258
|
+
steal: { type: 'string' }, refuse: { type: 'string' }, peak: { type: 'string' },
|
|
259
|
+
auto: { type: 'boolean', description: 'Auto-collect references (kind=mood only)' },
|
|
228
260
|
},
|
|
229
261
|
},
|
|
230
262
|
},
|
|
@@ -475,114 +507,147 @@ function handleCombinationPass(skillId, args) {
|
|
|
475
507
|
return { prompt: prompt, skill: skillId, task: task, persona: skillId, verdicts: 1 };
|
|
476
508
|
}
|
|
477
509
|
|
|
510
|
+
// One live board server per board id, kept for the life of the MCP process.
|
|
511
|
+
var moodBoardServers = {};
|
|
512
|
+
|
|
513
|
+
function tileCountLine(board) {
|
|
514
|
+
var sites = {};
|
|
515
|
+
board.tiles.forEach(function(t) { if (t.site) sites[t.site] = true; });
|
|
516
|
+
return board.tiles.length + ' tile(s) from ' + Object.keys(sites).length + ' site(s)';
|
|
517
|
+
}
|
|
518
|
+
|
|
478
519
|
function handleMoodBoard(args) {
|
|
479
|
-
var skill = readSkill('mood-board');
|
|
480
520
|
var task = args.task || '';
|
|
481
|
-
var kind = args.kind === '
|
|
521
|
+
var kind = args.kind === 'mood' ? 'mood' : 'ui';
|
|
482
522
|
var action = args.action || 'create';
|
|
483
523
|
var mb = require('./moodboard');
|
|
524
|
+
var view = require('./moodboard-view');
|
|
525
|
+
var ref = { boardId: args.board };
|
|
526
|
+
var zaraNext = function(board) {
|
|
527
|
+
return 'You are Zara on a Scout Pass. Open real pages in the browser. Do not invent URLs.\n\n' + board.scout.brief +
|
|
528
|
+
'\n\nBoard id: ' + board.board_id +
|
|
529
|
+
'\nToken script for Runtime.evaluate: call analyzthis_mood_board action=capture_script selector=<css>.' +
|
|
530
|
+
'\nSave tiles with action=tiles, board=' + board.board_id + ', tiles=[{page_url,image_path,section,element,tokens,steal,refuse,peak}].' +
|
|
531
|
+
'\nThen action=jury for advice, then action=show and open the URL so the user decides.';
|
|
532
|
+
};
|
|
484
533
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
peak: args.peak,
|
|
495
|
-
}],
|
|
534
|
+
try {
|
|
535
|
+
if (action === 'capture_script') {
|
|
536
|
+
return { result: mb.captureScript(args.selector || 'body', args.text || '') };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (action === 'brief') {
|
|
540
|
+
var briefed = mb.setBrief({
|
|
541
|
+
boardId: args.board, adjectives: args.adjectives, not_this: args.not_this,
|
|
542
|
+
sections: args.sections, confirmed_by_user: args.confirmed_by_user,
|
|
496
543
|
});
|
|
544
|
+
return { result: 'Brief saved.', board_id: briefed.board.board_id, brief: briefed.board.brief };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (action === 'tiles' || action === 'frames') {
|
|
548
|
+
var tiles = args.tiles || args.frames || [{
|
|
549
|
+
page_url: args.page_url || args.url, element: args.element || args.title, section: args.section,
|
|
550
|
+
image_path: args.image_path, image_url: args.image_url, tokens: args.tokens,
|
|
551
|
+
video_path: args.video_path, stills: args.stills, steal: args.steal, refuse: args.refuse, peak: args.peak,
|
|
552
|
+
}];
|
|
553
|
+
var added = mb.addTiles({ boardId: args.board, tiles: tiles });
|
|
497
554
|
return {
|
|
498
|
-
result: 'Saved
|
|
555
|
+
result: 'Saved ' + added.added.length + ' tile(s). Board now has ' + tileCountLine(added.board) +
|
|
556
|
+
'. Target ' + mb.TILE_TARGET.min + '-' + mb.TILE_TARGET.max + ' from ' + mb.TILE_TARGET.sites + '+ sites.' +
|
|
557
|
+
(added.skipped.length ? ' Skipped: ' + JSON.stringify(added.skipped) : ''),
|
|
499
558
|
board_id: added.board.board_id,
|
|
500
|
-
|
|
559
|
+
skipped: added.skipped,
|
|
501
560
|
};
|
|
502
|
-
} catch (e) {
|
|
503
|
-
return { error: e.message };
|
|
504
561
|
}
|
|
505
|
-
}
|
|
506
562
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
return {
|
|
512
|
-
error: 'NEED_SCOUT',
|
|
513
|
-
board_id: juryBoard.board_id,
|
|
514
|
-
scout: juryBoard.scout,
|
|
515
|
-
prompt: 'Zara has not scouted yet.\n\n' + ((juryBoard.scout && juryBoard.scout.brief) || ''),
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
var frameLines = juryBoard.frames.map(function(f, idx) {
|
|
519
|
-
return (idx + 1) + '. ' + (f.title || 'Frame') + ' — ' + (f.url || '') +
|
|
520
|
-
'\n Steal: ' + (f.steal || '') +
|
|
521
|
-
'\n Refuse: ' + (f.refuse || '') +
|
|
522
|
-
'\n Peak: ' + (f.peak || '');
|
|
523
|
-
}).join('\n');
|
|
524
|
-
var juryPrompt = 'You are the UI image-board jury: Arjun, Noor, Priya, Meera. Zara does not vote.\n\n' +
|
|
525
|
-
'Job: ' + (juryBoard.task || '') + '\n\n' +
|
|
526
|
-
'Frames:\n' + frameLines + '\n\n' +
|
|
527
|
-
'KEEP or SKIP each frame with one because-sentence. Rank the best 2-3. Verdict SHIP only if the set fits the job. REVISE or BLOCK sends Zara to scout again.\n' +
|
|
528
|
-
'Do not reward cinematic marketing motion on a daily working surface.\n\n' +
|
|
529
|
-
'Then call analyzthis_mood_board with action=verdict, board=' + juryBoard.board_id +
|
|
530
|
-
', verdict=SHIP|REVISE|BLOCK, and summary=the because-lines.';
|
|
531
|
-
return { prompt: juryPrompt, board_id: juryBoard.board_id, scout: juryBoard.scout, frames: juryBoard.frames };
|
|
532
|
-
} catch (e) {
|
|
533
|
-
return { error: e.message };
|
|
563
|
+
if (action === 'jury') {
|
|
564
|
+
var juryBoard = mb.loadUiBoard(ref);
|
|
565
|
+
if (!juryBoard.tiles.length) return { error: 'NEED_SCOUT: Zara has not added tiles yet.' };
|
|
566
|
+
return { prompt: mb.juryPrompt(juryBoard, ''), board_id: juryBoard.board_id };
|
|
534
567
|
}
|
|
535
|
-
}
|
|
536
568
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
boardId: args.board,
|
|
541
|
-
verdict: args.verdict,
|
|
542
|
-
summary: args.summary,
|
|
569
|
+
if (action === 'votes' || action === 'verdict') {
|
|
570
|
+
var voted = mb.recordPersonaVotes({
|
|
571
|
+
boardId: args.board, votes: args.votes || [], verdict: args.verdict, summary: args.summary,
|
|
543
572
|
});
|
|
544
|
-
|
|
545
|
-
|
|
573
|
+
return {
|
|
574
|
+
result: 'Team advice saved' + (voted.ignored.length ? ' (ignored: ' + JSON.stringify(voted.ignored) + ')' : '') +
|
|
575
|
+
'. The user decides next: call action=show and open the URL in the browser.',
|
|
576
|
+
board_id: voted.board.board_id,
|
|
577
|
+
scout: voted.board.scout,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (action === 'film') {
|
|
582
|
+
return require('./moodboard-film').filmTiles({
|
|
583
|
+
boardId: args.board, urls: args.urls || (args.page_url ? [args.page_url] : []),
|
|
584
|
+
section: args.section, element: args.element, steal: args.steal, refuse: args.refuse, peak: args.peak,
|
|
585
|
+
}).then(function(out) {
|
|
586
|
+
var added = out.saved ? out.saved.added : [];
|
|
546
587
|
return {
|
|
547
|
-
result: '
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
'\n\nBoard id: ' + judged.board.board_id + '\nAfter 3-5 new frames, call action=frames, then action=jury.',
|
|
588
|
+
result: 'Filmed ' + added.length + ' site(s) as motion tiles' + (out.saved ? '. Board now has ' + tileCountLine(out.saved.board) : '') + '.' +
|
|
589
|
+
(out.failed.length ? ' Failed: ' + JSON.stringify(out.failed) : '') +
|
|
590
|
+
(out.saved && out.saved.skipped.length ? ' Skipped: ' + JSON.stringify(out.saved.skipped) : ''),
|
|
591
|
+
board_id: args.board,
|
|
552
592
|
};
|
|
553
|
-
}
|
|
554
|
-
|
|
593
|
+
}).catch(function(e) { return { error: e.message }; });
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (action === 'show') {
|
|
597
|
+
var existing = moodBoardServers[args.board];
|
|
598
|
+
if (existing) return { result: 'Board is live at ' + existing.url + ' — open it with browser_navigate.', url: existing.url };
|
|
599
|
+
return view.serveBoard({ boardId: args.board }).then(function(live) {
|
|
600
|
+
moodBoardServers[args.board] = live;
|
|
555
601
|
return {
|
|
556
|
-
result: '
|
|
557
|
-
|
|
558
|
-
scout: scout,
|
|
602
|
+
result: 'Board is live at ' + live.url + ' — open it with browser_navigate. The user clicks keep / maybe / reject; their choice overrides every persona.',
|
|
603
|
+
url: live.url,
|
|
559
604
|
};
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
|
|
605
|
+
}).catch(function(e) { return { error: e.message }; });
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (action === 'decide') {
|
|
609
|
+
var decided = mb.recordUserDecision({ boardId: args.board, tile: args.tile, decision: args.decision, note: args.note });
|
|
610
|
+
return { result: 'User decision saved on tile ' + args.tile + '. It outranks every persona vote.', board_id: decided.board.board_id };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (action === 'rescout') {
|
|
614
|
+
var again = mb.requestRescout({ boardId: args.board, note: args.note });
|
|
615
|
+
return { prompt: zaraNext(again.board), board_id: again.board.board_id, scout: again.board.scout };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (action === 'lock') {
|
|
619
|
+
var locked = mb.lockDirection({ boardId: args.board, note: args.note });
|
|
620
|
+
return { result: 'Direction locked by the user and saved to the session as design_direction. Every persona must follow it.', direction: locked.board.direction };
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (action === 'status') {
|
|
624
|
+
var b = mb.loadUiBoard(ref);
|
|
625
|
+
return { result: JSON.stringify({ board_id: b.board_id, scout: b.scout, brief: b.brief, tiles: tileCountLine(b), direction: mb.computeDirection(b) }, null, 2) };
|
|
568
626
|
}
|
|
627
|
+
} catch (e) {
|
|
628
|
+
return { error: e.message };
|
|
569
629
|
}
|
|
570
630
|
|
|
571
631
|
if (kind === 'ui') {
|
|
572
|
-
if (!task) return { error: 'task is required to open a
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
'
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
}
|
|
632
|
+
if (!task) return { error: 'task is required to open a mood board' };
|
|
633
|
+
return mb.buildMoodBoard({
|
|
634
|
+
task: task, kind: 'ui', discover: false,
|
|
635
|
+
brief: { adjectives: args.adjectives, not_this: args.not_this, sections: args.sections },
|
|
636
|
+
}).then(function(created) {
|
|
637
|
+
return {
|
|
638
|
+
prompt: 'First show the user this brief (job, 3 adjectives, one "not this", sections) and let them correct it with action=brief confirmed_by_user=true.\n\n' + zaraNext(created.board),
|
|
639
|
+
board_id: created.board.board_id,
|
|
640
|
+
brief: created.board.brief,
|
|
641
|
+
};
|
|
642
|
+
}).catch(function(e) { return { error: e.message }; });
|
|
584
643
|
}
|
|
585
644
|
|
|
645
|
+
return handleLinkMoodBoard(args);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function handleLinkMoodBoard(args) {
|
|
649
|
+
var skill = readSkill('mood-board');
|
|
650
|
+
var task = args.task || '';
|
|
586
651
|
var urls = (args.urls || []).join('\n');
|
|
587
652
|
var prompt = 'Build a mood board.\n\n' +
|
|
588
653
|
(skill ? skill.slice(0, 3000) : '') + '\n\n' +
|
|
@@ -971,8 +1036,8 @@ function startStdioServer(catalog) {
|
|
|
971
1036
|
if (method === 'tools/call') {
|
|
972
1037
|
var toolName = params.name;
|
|
973
1038
|
var toolArgs = params.arguments || {};
|
|
974
|
-
|
|
975
|
-
|
|
1039
|
+
// Some tools (mood board create / show) are async.
|
|
1040
|
+
Promise.resolve().then(function() { return handleToolCall(toolName, toolArgs); }).then(function(result) {
|
|
976
1041
|
if (result.error) {
|
|
977
1042
|
send({
|
|
978
1043
|
jsonrpc: '2.0',
|
|
@@ -995,6 +1060,9 @@ function startStdioServer(catalog) {
|
|
|
995
1060
|
},
|
|
996
1061
|
});
|
|
997
1062
|
}
|
|
1063
|
+
}).catch(function(e) {
|
|
1064
|
+
send({ jsonrpc: '2.0', id: id, error: { code: -32603, message: e.message } });
|
|
1065
|
+
});
|
|
998
1066
|
return;
|
|
999
1067
|
}
|
|
1000
1068
|
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mood board scroll films.
|
|
5
|
+
*
|
|
6
|
+
* filmSite — opens a real page in headless Chrome, records it scrolling
|
|
7
|
+
* through four depths with its animations running, and saves a still at
|
|
8
|
+
* each depth. Wheel events drive the scroll so sites with their own scroll
|
|
9
|
+
* engine (Lenis, Locomotive, WebGL) move too.
|
|
10
|
+
* filmTiles — films each URL and puts the result on a UI board as a
|
|
11
|
+
* motion tile (film + poster + waypoint stills), then cleans up.
|
|
12
|
+
*
|
|
13
|
+
* Needs `playwright-core` (optional dependency) and Google Chrome installed.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
var fs = require('fs');
|
|
17
|
+
var os = require('os');
|
|
18
|
+
var path = require('path');
|
|
19
|
+
|
|
20
|
+
var DEFAULTS = { width: 1280, height: 860, stops: 4, settleMs: 3000, holdMs: 1200 };
|
|
21
|
+
|
|
22
|
+
function loadPlaywright() {
|
|
23
|
+
var tries = [function() { return require('playwright-core'); },
|
|
24
|
+
function() { return require(require.resolve('playwright-core', { paths: [process.cwd()] })); }];
|
|
25
|
+
for (var i = 0; i < tries.length; i++) {
|
|
26
|
+
try { return tries[i](); } catch (e) { /* try next location */ }
|
|
27
|
+
}
|
|
28
|
+
throw new Error('Scroll films need playwright-core. Run: npm install playwright-core (Google Chrome must be installed).');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function launchChrome(chromium) {
|
|
32
|
+
return chromium.launch({ channel: 'chrome', headless: true }).catch(function() {
|
|
33
|
+
return chromium.launch({ headless: true });
|
|
34
|
+
}).catch(function() {
|
|
35
|
+
throw new Error('Could not start Chrome. Install Google Chrome, or run: npx playwright-core install chromium');
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function wait(ms) { return new Promise(function(r) { setTimeout(r, ms); }); }
|
|
40
|
+
|
|
41
|
+
// Wheel-scrolls `distance` px in small steps so the film shows real motion.
|
|
42
|
+
function wheelBy(page, distance) {
|
|
43
|
+
var steps = Math.max(1, Math.round(distance / 60));
|
|
44
|
+
var chain = Promise.resolve();
|
|
45
|
+
for (var i = 0; i < steps; i++) {
|
|
46
|
+
chain = chain.then(function() { return page.mouse.wheel(0, 60); }).then(function() { return wait(40); });
|
|
47
|
+
}
|
|
48
|
+
return chain;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function filmSite(opts) {
|
|
52
|
+
var given = opts || {};
|
|
53
|
+
opts = Object.assign({}, DEFAULTS);
|
|
54
|
+
Object.keys(given).forEach(function(k) { if (given[k] != null) opts[k] = given[k]; });
|
|
55
|
+
if (!/^https?:\/\//i.test(String(opts.url || ''))) throw new Error('Film needs an http(s) url');
|
|
56
|
+
var outDir = opts.outDir || fs.mkdtempSync(path.join(os.tmpdir(), 'analyzthis-film-'));
|
|
57
|
+
var size = { width: opts.width, height: opts.height };
|
|
58
|
+
var browser = await launchChrome(loadPlaywright().chromium);
|
|
59
|
+
var context;
|
|
60
|
+
try {
|
|
61
|
+
context = await browser.newContext({ viewport: size, recordVideo: { dir: outDir, size: size } });
|
|
62
|
+
var page = await context.newPage();
|
|
63
|
+
await page.goto(opts.url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
64
|
+
await wait(opts.settleMs);
|
|
65
|
+
await page.mouse.move(size.width / 2, size.height / 2);
|
|
66
|
+
var docH = await page.evaluate(function() { return document.documentElement.scrollHeight; });
|
|
67
|
+
// Virtual-scroll sites report no page height; still travel three screens.
|
|
68
|
+
var travel = Math.max(docH - size.height, size.height * 3);
|
|
69
|
+
var stills = [];
|
|
70
|
+
for (var i = 0; i < opts.stops; i++) {
|
|
71
|
+
if (i > 0) await wheelBy(page, Math.round(travel / opts.stops));
|
|
72
|
+
await wait(opts.holdMs);
|
|
73
|
+
var still = path.join(outDir, 'still-' + (i + 1) + '.png');
|
|
74
|
+
await page.screenshot({ path: still });
|
|
75
|
+
stills.push(still);
|
|
76
|
+
}
|
|
77
|
+
var video = page.video();
|
|
78
|
+
await context.close();
|
|
79
|
+
context = null;
|
|
80
|
+
return { url: opts.url, video: await video.path(), stills: stills, dir: outDir };
|
|
81
|
+
} finally {
|
|
82
|
+
if (context) await context.close().catch(function() {});
|
|
83
|
+
await browser.close().catch(function() {});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function filmTiles(opts) {
|
|
88
|
+
opts = opts || {};
|
|
89
|
+
var urls = (opts.urls || []).filter(Boolean);
|
|
90
|
+
if (!opts.boardId || !urls.length) throw new Error('--board and at least one --url are required');
|
|
91
|
+
var mb = require('./moodboard');
|
|
92
|
+
var films = [];
|
|
93
|
+
var failed = [];
|
|
94
|
+
for (var i = 0; i < urls.length; i++) {
|
|
95
|
+
try { films.push(await filmSite({ url: urls[i], stops: opts.stops })); }
|
|
96
|
+
catch (e) { failed.push({ url: urls[i], reason: e.message }); }
|
|
97
|
+
}
|
|
98
|
+
var saved = null;
|
|
99
|
+
try {
|
|
100
|
+
if (films.length) {
|
|
101
|
+
saved = mb.addTiles({
|
|
102
|
+
project: opts.project, boardId: opts.boardId, workspaceDir: opts.workspaceDir,
|
|
103
|
+
tiles: films.map(function(f) {
|
|
104
|
+
return {
|
|
105
|
+
page_url: f.url, section: opts.section || 'motion',
|
|
106
|
+
element: opts.element || 'Scroll film — ' + mb.siteOf(f.url),
|
|
107
|
+
video_path: f.video, stills: f.stills,
|
|
108
|
+
steal: opts.steal, refuse: opts.refuse, peak: opts.peak,
|
|
109
|
+
};
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
} finally {
|
|
114
|
+
films.forEach(function(f) { fs.rmSync(f.dir, { recursive: true, force: true }); });
|
|
115
|
+
}
|
|
116
|
+
return { saved: saved, failed: failed };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { filmSite: filmSite, filmTiles: filmTiles, loadPlaywright: loadPlaywright };
|