analyzthis_design 2.5.1 → 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/HOW-TO-USE.md +249 -326
- package/README.md +71 -25
- package/agents/calibration-benchmark.json +187 -0
- package/agents/cards/anuj.md +1 -1
- package/agents/cards/arjun.md +1 -1
- package/agents/cards/kavi.md +2 -1
- package/agents/cards/mood-board.md +7 -7
- package/agents/cards/noor.md +1 -1
- package/agents/cards/priya.md +1 -1
- package/agents/cards/zara.md +3 -3
- package/agents/chain.json +1 -1
- package/agents/design-spec-schema.json +18 -1
- package/agents/manifests/mood-board.json +13 -8
- package/agents/model-policy.json +12 -0
- package/agents/router.json +2 -2
- package/agents/session-schema.json +12 -2
- package/dist/HOW-TO-USE.md +249 -326
- package/dist/README.md +71 -25
- package/dist/agents/calibration-benchmark.json +187 -0
- package/dist/agents/cards/anuj.md +1 -1
- package/dist/agents/cards/arjun.md +1 -1
- package/dist/agents/cards/kavi.md +2 -1
- package/dist/agents/cards/mood-board.md +7 -7
- package/dist/agents/cards/noor.md +1 -1
- package/dist/agents/cards/priya.md +1 -1
- package/dist/agents/cards/zara.md +3 -3
- package/dist/agents/chain.json +1 -1
- package/dist/agents/design-spec-schema.json +18 -1
- package/dist/agents/manifests/mood-board.json +13 -8
- package/dist/agents/model-policy.json +12 -0
- package/dist/agents/router.json +2 -2
- package/dist/agents/session-schema.json +12 -2
- package/dist/bin/cli.js +308 -10
- package/dist/lib/calibrate.js +816 -0
- package/dist/lib/chunk-executor.js +41 -1
- package/dist/lib/chunk-models.js +4 -18
- package/dist/lib/chunk-router.js +22 -6
- package/dist/lib/chunk-run.js +55 -1
- package/dist/lib/chunk-synthesis.js +18 -2
- package/dist/lib/chunk-telemetry.js +77 -17
- package/dist/lib/collect.js +15 -1
- package/dist/lib/cost.js +15 -7
- package/dist/lib/design-spec.js +32 -2
- package/dist/lib/evolution-metrics.js +23 -1
- package/dist/lib/feedback-submit.js +50 -1
- package/dist/lib/feedback.js +15 -0
- package/dist/lib/install.js +1 -0
- package/dist/lib/lessons.js +8 -0
- package/dist/lib/mcp-server.js +189 -7
- package/dist/lib/model-policy.js +174 -0
- package/dist/lib/model-registry.js +71 -0
- package/dist/lib/moodboard-film.js +119 -0
- package/dist/lib/moodboard-view.js +426 -0
- package/dist/lib/moodboard.js +638 -27
- package/dist/lib/orchestrator/run.js +67 -8
- package/dist/lib/outcome.js +7 -2
- package/dist/lib/output-quality.js +95 -0
- package/dist/lib/reference-pack.js +9 -1
- package/dist/lib/session.js +48 -4
- package/dist/lib/system-prompt.js +3 -0
- package/dist/lib/ui-libraries.js +232 -0
- package/dist/skills/anuj/SKILL.md +4 -0
- package/dist/skills/arjun/SKILL.md +2 -0
- package/dist/skills/arjun/references/lens.md +2 -0
- package/dist/skills/design-director/SKILL.md +2 -1
- package/dist/skills/design-reference/SKILL.md +6 -0
- package/dist/skills/design-reference/motion.csv +11 -0
- package/dist/skills/design-reference/schema.json +12 -0
- package/dist/skills/design-reference/stacks/shadcn.csv +6 -0
- package/dist/skills/design-reference/ui-libraries.csv +46 -0
- package/dist/skills/design-spec/SKILL.md +1 -1
- package/dist/skills/getting-started/SKILL.md +2 -2
- package/dist/skills/kavi/SKILL.md +3 -2
- package/dist/skills/meera/SKILL.md +2 -0
- package/dist/skills/mood-board/SKILL.md +94 -87
- package/dist/skills/noor/SKILL.md +4 -0
- package/dist/skills/persona-orchestrator/references/lens.md +4 -4
- package/dist/skills/priya/SKILL.md +5 -0
- package/dist/skills/raj/SKILL.md +2 -0
- package/dist/skills/zara/SKILL.md +49 -4
- package/package.json +13 -2
- package/skills/anuj/SKILL.md +4 -0
- package/skills/arjun/SKILL.md +2 -0
- package/skills/arjun/references/lens.md +2 -0
- package/skills/design-director/SKILL.md +2 -1
- package/skills/design-reference/SKILL.md +6 -0
- package/skills/design-reference/motion.csv +11 -0
- package/skills/design-reference/schema.json +12 -0
- package/skills/design-reference/stacks/shadcn.csv +6 -0
- package/skills/design-reference/ui-libraries.csv +46 -0
- package/skills/design-spec/SKILL.md +1 -1
- package/skills/getting-started/SKILL.md +2 -2
- package/skills/kavi/SKILL.md +3 -2
- package/skills/meera/SKILL.md +2 -0
- package/skills/mood-board/SKILL.md +94 -87
- package/skills/noor/SKILL.md +4 -0
- package/skills/persona-orchestrator/references/lens.md +4 -4
- package/skills/priya/SKILL.md +5 -0
- package/skills/raj/SKILL.md +2 -0
- package/skills/zara/SKILL.md +49 -4
package/dist/lib/moodboard.js
CHANGED
|
@@ -66,11 +66,229 @@ function writeJson(filePath, obj) {
|
|
|
66
66
|
fs.writeFileSync(filePath, JSON.stringify(obj, null, 2));
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
function persistBoard(board, workspaceDir) {
|
|
70
|
+
var projectId = board.project_id;
|
|
71
|
+
var bDir = boardDir(projectId, board.board_id);
|
|
72
|
+
board.updated_at = new Date().toISOString();
|
|
73
|
+
if (board.kind === 'ui') normalizeBoard(board);
|
|
74
|
+
writeJson(path.join(bDir, 'board.json'), board);
|
|
75
|
+
var htmlPath = '';
|
|
76
|
+
if (board.kind === 'ui') {
|
|
77
|
+
// Static snapshot opens straight from disk; `moodboard show` serves the
|
|
78
|
+
// clickable version.
|
|
79
|
+
htmlPath = path.join(bDir, 'index.html');
|
|
80
|
+
try { fs.writeFileSync(htmlPath, require('./moodboard-view').renderBoardHtml(board, { interactive: false })); }
|
|
81
|
+
catch (e) { htmlPath = ''; }
|
|
82
|
+
}
|
|
83
|
+
var ws = workspaceDir || defaultWorkspaceDir();
|
|
84
|
+
ensureDir(ws);
|
|
85
|
+
var workspacePath = path.join(ws, board.board_id + '.json');
|
|
86
|
+
try { fs.writeFileSync(workspacePath, JSON.stringify(board, null, 2)); }
|
|
87
|
+
catch (e) { /* best effort */ }
|
|
88
|
+
return { board: board, boardDir: bDir, workspacePath: workspacePath, htmlPath: htmlPath };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Board v3: sections, element tiles, user decisions ──────────────────────
|
|
92
|
+
|
|
93
|
+
// Section targets add up to roughly 20-24 tiles. A board only uses the
|
|
94
|
+
// sections named in its brief, so a dashboard can drop first_screen.
|
|
95
|
+
var SECTIONS = [
|
|
96
|
+
{ id: 'first_screen', label: 'First screen', min: 3, max: 5 },
|
|
97
|
+
{ id: 'typography', label: 'Typography', min: 3, max: 4 },
|
|
98
|
+
{ id: 'color', label: 'Colour and contrast', min: 3, max: 4 },
|
|
99
|
+
{ id: 'components', label: 'Components and density', min: 6, max: 8 },
|
|
100
|
+
{ id: 'imagery', label: 'Imagery and icons', min: 2, max: 4 },
|
|
101
|
+
{ id: 'motion', label: 'Motion and interaction', min: 1, max: 3 },
|
|
102
|
+
];
|
|
103
|
+
var SECTION_IDS = SECTIONS.map(function(s) { return s.id; });
|
|
104
|
+
var TILE_TARGET = { min: 20, max: 24, sites: 6 };
|
|
105
|
+
var USER_DECISIONS = ['keep', 'reject', 'maybe'];
|
|
106
|
+
var JURY = ['arjun', 'meera', 'priya', 'noor'];
|
|
107
|
+
var IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.webp', '.gif'];
|
|
108
|
+
|
|
109
|
+
function clip(value, max) {
|
|
110
|
+
return String(value == null ? '' : value).slice(0, max);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isHttpUrl(value) {
|
|
114
|
+
try {
|
|
115
|
+
var u = new URL(String(value));
|
|
116
|
+
return u.protocol === 'http:' || u.protocol === 'https:';
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function siteOf(url) {
|
|
123
|
+
try { return new URL(url).hostname.replace(/^www\./, ''); }
|
|
124
|
+
catch (e) { return ''; }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// A tile is identified by where it came from plus what element it is, so a
|
|
128
|
+
// rejected "pricing card on linear.app" cannot sneak back in a later round.
|
|
129
|
+
function tileKey(pageUrl, element) {
|
|
130
|
+
var u = String(pageUrl || '').replace(/[?#].*$/, '').replace(/\/+$/, '').toLowerCase();
|
|
131
|
+
return u + '|' + String(element || '').trim().toLowerCase();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function defaultBrief(task, brief) {
|
|
135
|
+
brief = brief || {};
|
|
136
|
+
var sections = (brief.sections || []).filter(function(s) { return SECTION_IDS.indexOf(s) !== -1; });
|
|
137
|
+
return {
|
|
138
|
+
job: clip(brief.job || task, 240),
|
|
139
|
+
adjectives: (brief.adjectives || []).map(function(a) { return clip(a, 40); }).filter(Boolean).slice(0, 3),
|
|
140
|
+
not_this: clip(brief.not_this || '', 200),
|
|
141
|
+
sections: sections.length ? sections : SECTION_IDS.slice(),
|
|
142
|
+
confirmed_by_user: !!brief.confirmed_by_user,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function sanitizeTokens(tokens) {
|
|
147
|
+
tokens = tokens || {};
|
|
148
|
+
var hex = /^#[0-9a-f]{3,8}$/i;
|
|
149
|
+
return {
|
|
150
|
+
colors: (tokens.colors || []).map(function(c) { return String(c).trim(); })
|
|
151
|
+
.filter(function(c) { return hex.test(c) || /^rgba?\([\d\s.,%]+\)$/i.test(c); }).slice(0, 8),
|
|
152
|
+
fonts: (tokens.fonts || []).map(function(f) { return clip(f, 60).trim(); }).filter(Boolean).slice(0, 4),
|
|
153
|
+
radius: clip(tokens.radius || '', 24),
|
|
154
|
+
spacing: clip(tokens.spacing || '', 40),
|
|
155
|
+
font_size: clip(tokens.font_size || '', 24),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function defaultScoutBrief(task, round, constraints) {
|
|
160
|
+
var c = constraints && typeof constraints === 'object' ? constraints : { veto: constraints || '' };
|
|
161
|
+
var lines = [
|
|
162
|
+
'Zara Scout Pass, round ' + (round || 1) + '.',
|
|
163
|
+
'Job: ' + clip(task, 240),
|
|
164
|
+
];
|
|
165
|
+
if (c.adjectives && c.adjectives.length) lines.push('Feel: ' + c.adjectives.join(', ') + (c.not_this ? '. Not this: ' + c.not_this : ''));
|
|
166
|
+
if (c.userNote || (c.kept && c.kept.length) || (c.rejected && c.rejected.length)) {
|
|
167
|
+
lines.push('USER DECISIONS — these outrank every persona, including you:');
|
|
168
|
+
if (c.userNote) lines.push(' User asked: ' + clip(c.userNote, 500));
|
|
169
|
+
if (c.kept && c.kept.length) lines.push(' Kept (build around these): ' + c.kept.join('; '));
|
|
170
|
+
if (c.rejected && c.rejected.length) lines.push(' Rejected (never bring back): ' + c.rejected.join('; '));
|
|
171
|
+
}
|
|
172
|
+
if (c.missing && c.missing.length) lines.push('Sections still thin: ' + c.missing.join(', ') + '. Fill these first.');
|
|
173
|
+
lines.push(
|
|
174
|
+
'Build an element collage: ' + TILE_TARGET.min + '-' + TILE_TARGET.max + ' tiles from at least ' + TILE_TARGET.sites + ' different sites. Crop one element per tile (a card, a table header, a filter bar, a button group, a type pairing). Not whole pages.',
|
|
175
|
+
'Hunt on real pages: live product sites, Mobbin, Dribbble, Pinterest, Awwwards, Google Images. Open each page in the browser. Never invent URLs. Never generate AI comps.',
|
|
176
|
+
'Capture: browser_navigate to the page, browser_take_screenshot with ref=<element selector> and a filename, then run the capture script (moodboard capture-script) with Runtime.evaluate to read real colours, fonts, radius, spacing.',
|
|
177
|
+
'Every tile needs: page_url, image_path, section (' + SECTION_IDS.join(' | ') + '), element, tokens, steal, refuse, peak.',
|
|
178
|
+
'Lens: Peak-End, first 2 seconds. Concept shots are allowed if you say so. Never sell a Dribbble shot as shipped product. Skip AI-purple heroes and cinematic scroll-hijack on daily working screens.',
|
|
179
|
+
'Then stop. Arjun, Noor, Priya and Meera vote as advice. The user decides.'
|
|
180
|
+
);
|
|
181
|
+
if (c.veto) lines.push('Jury notes (advice only): ' + clip(c.veto, 500));
|
|
182
|
+
return lines.join('\n');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Older boards stored Zara's picks as `frames`. Move them into `tiles` once.
|
|
186
|
+
function normalizeBoard(board) {
|
|
187
|
+
if (!board) return board;
|
|
188
|
+
if (!board.brief) board.brief = defaultBrief(board.task, null);
|
|
189
|
+
if (!board.tiles) {
|
|
190
|
+
board.tiles = (board.frames || []).map(function(f, idx) {
|
|
191
|
+
return {
|
|
192
|
+
id: f.id || generateBoardId(),
|
|
193
|
+
n: idx + 1,
|
|
194
|
+
section: SECTION_IDS.indexOf(f.surface) !== -1 ? f.surface : 'components',
|
|
195
|
+
element: f.title || 'Frame',
|
|
196
|
+
site: siteOf(f.url),
|
|
197
|
+
page_url: f.url || '',
|
|
198
|
+
image: '',
|
|
199
|
+
image_url: f.url || '',
|
|
200
|
+
tokens: sanitizeTokens(null),
|
|
201
|
+
steal: f.steal || '', refuse: f.refuse || '', peak: f.peak || '',
|
|
202
|
+
round: f.round || 1,
|
|
203
|
+
source: f.source || 'zara_scout',
|
|
204
|
+
added_at: f.added_at || new Date().toISOString(),
|
|
205
|
+
votes: {},
|
|
206
|
+
user: null,
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
delete board.frames;
|
|
211
|
+
if (!board.decisions) board.decisions = [];
|
|
212
|
+
if (!board.blocked) board.blocked = [];
|
|
213
|
+
board.version = 3;
|
|
214
|
+
return board;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function findTile(board, ref) {
|
|
218
|
+
var asNum = parseInt(ref, 10);
|
|
219
|
+
for (var i = 0; i < board.tiles.length; i++) {
|
|
220
|
+
var t = board.tiles[i];
|
|
221
|
+
if (t.id === ref || (String(asNum) === String(ref) && t.n === asNum)) return t;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// The user's decision is final. Persona votes only produce a suggestion
|
|
227
|
+
// while the user has not decided.
|
|
228
|
+
function tileStatus(tile) {
|
|
229
|
+
if (tile.user && USER_DECISIONS.indexOf(tile.user.decision) !== -1) {
|
|
230
|
+
return { status: tile.user.decision, by: 'user' };
|
|
231
|
+
}
|
|
232
|
+
var keep = 0, skip = 0;
|
|
233
|
+
Object.keys(tile.votes || {}).forEach(function(p) {
|
|
234
|
+
if (tile.votes[p].vote === 'keep') keep++;
|
|
235
|
+
else if (tile.votes[p].vote === 'skip') skip++;
|
|
236
|
+
});
|
|
237
|
+
if (!keep && !skip) return { status: 'undecided', by: 'none' };
|
|
238
|
+
return { status: keep >= skip ? 'suggested_keep' : 'suggested_skip', by: 'personas', keep: keep, skip: skip };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function computeDirection(board) {
|
|
242
|
+
var kept = board.tiles.filter(function(t) { return tileStatus(t).status === 'keep'; });
|
|
243
|
+
var rejected = board.tiles.filter(function(t) { return tileStatus(t).status === 'reject'; });
|
|
244
|
+
var palette = [];
|
|
245
|
+
var fonts = [];
|
|
246
|
+
kept.forEach(function(t) {
|
|
247
|
+
(t.tokens.colors || []).forEach(function(c) { if (palette.indexOf(c.toLowerCase()) === -1) palette.push(c.toLowerCase()); });
|
|
248
|
+
(t.tokens.fonts || []).forEach(function(f) { if (fonts.indexOf(f) === -1) fonts.push(f); });
|
|
249
|
+
});
|
|
250
|
+
var bySection = {};
|
|
251
|
+
board.brief.sections.forEach(function(s) { bySection[s] = 0; });
|
|
252
|
+
kept.forEach(function(t) { if (bySection[t.section] != null) bySection[t.section]++; });
|
|
253
|
+
var overruled = kept.filter(function(t) {
|
|
254
|
+
var skips = Object.keys(t.votes || {}).filter(function(p) { return t.votes[p].vote === 'skip'; });
|
|
255
|
+
return skips.length > 0;
|
|
256
|
+
}).map(function(t) { return t.n; });
|
|
257
|
+
return {
|
|
258
|
+
kept: kept.map(function(t) {
|
|
259
|
+
return { n: t.n, section: t.section, element: t.element, site: t.site, page_url: t.page_url, steal: t.steal, tokens: t.tokens, note: (t.user && t.user.note) || '' };
|
|
260
|
+
}),
|
|
261
|
+
rejected: rejected.map(function(t) {
|
|
262
|
+
return { n: t.n, element: t.element, site: t.site, page_url: t.page_url, note: (t.user && t.user.note) || t.refuse || '' };
|
|
263
|
+
}),
|
|
264
|
+
palette: palette.slice(0, 10),
|
|
265
|
+
fonts: fonts.slice(0, 4),
|
|
266
|
+
by_section: bySection,
|
|
267
|
+
missing_sections: Object.keys(bySection).filter(function(s) { return !bySection[s]; }),
|
|
268
|
+
overruled_personas_on: overruled,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function scoutConstraints(board, userNote) {
|
|
273
|
+
var dir = computeDirection(board);
|
|
274
|
+
return {
|
|
275
|
+
adjectives: board.brief.adjectives,
|
|
276
|
+
not_this: board.brief.not_this,
|
|
277
|
+
userNote: userNote || '',
|
|
278
|
+
kept: dir.kept.map(function(k) { return '#' + k.n + ' ' + k.element + ' (' + k.site + ')' + (k.note ? ' — ' + k.note : ''); }),
|
|
279
|
+
rejected: dir.rejected.map(function(r) { return r.element + ' (' + r.site + ')' + (r.note ? ' — ' + r.note : ''); }),
|
|
280
|
+
missing: dir.missing_sections,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
69
284
|
// ─── Source discovery for mood references ───────────────────────────────────
|
|
70
285
|
|
|
71
286
|
// Known hosts that expose an image or a design article we can fetch as text.
|
|
72
287
|
var MOOD_REFERENCE_HOSTS = [
|
|
73
288
|
'dribbble.com',
|
|
289
|
+
'pinterest.com',
|
|
290
|
+
'www.pinterest.com',
|
|
291
|
+
'pin.it',
|
|
74
292
|
'mobbin.com',
|
|
75
293
|
'behance.net',
|
|
76
294
|
'www.behance.net',
|
|
@@ -362,7 +580,11 @@ async function buildMoodBoard(opts) {
|
|
|
362
580
|
|
|
363
581
|
var refs = [];
|
|
364
582
|
var userRefs = opts.userReferences || [];
|
|
365
|
-
|
|
583
|
+
// The visual element board is the default. `--kind mood` keeps the older
|
|
584
|
+
// link-and-tag board.
|
|
585
|
+
var kind = opts.kind === 'mood' ? 'mood' : 'ui';
|
|
586
|
+
var brief = defaultBrief(task, opts.brief);
|
|
587
|
+
var discover = kind === 'ui' ? !!(opts.urls && opts.urls.length) : opts.discover !== false;
|
|
366
588
|
|
|
367
589
|
if (discover) {
|
|
368
590
|
var plan = discoverReferenceUrls({ task: task, urls: opts.urls || [], config: config });
|
|
@@ -397,6 +619,7 @@ async function buildMoodBoard(opts) {
|
|
|
397
619
|
var board = {
|
|
398
620
|
board_id: boardId,
|
|
399
621
|
project_id: projectId,
|
|
622
|
+
kind: kind,
|
|
400
623
|
task: task,
|
|
401
624
|
created_at: new Date().toISOString(),
|
|
402
625
|
updated_at: new Date().toISOString(),
|
|
@@ -405,20 +628,23 @@ async function buildMoodBoard(opts) {
|
|
|
405
628
|
deliberation: null,
|
|
406
629
|
synthesis: null,
|
|
407
630
|
user_contributions: userRefs.length,
|
|
631
|
+
scout: kind === 'ui' ? {
|
|
632
|
+
persona: 'zara',
|
|
633
|
+
round: 1,
|
|
634
|
+
status: 'awaiting_scout',
|
|
635
|
+
brief: defaultScoutBrief(task, 1, { adjectives: brief.adjectives, not_this: brief.not_this }),
|
|
636
|
+
last_veto: '',
|
|
637
|
+
} : null,
|
|
408
638
|
};
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
try {
|
|
416
|
-
fs.writeFileSync(workspacePath, JSON.stringify(board, null, 2));
|
|
417
|
-
} catch (e) {
|
|
418
|
-
// workspace write is best-effort (e.g. read-only cwd)
|
|
639
|
+
if (kind === 'ui') {
|
|
640
|
+
board.brief = brief;
|
|
641
|
+
board.tiles = [];
|
|
642
|
+
board.decisions = [];
|
|
643
|
+
board.blocked = [];
|
|
644
|
+
board.version = 3;
|
|
419
645
|
}
|
|
420
646
|
|
|
421
|
-
return
|
|
647
|
+
return persistBoard(board, workspaceDir);
|
|
422
648
|
}
|
|
423
649
|
|
|
424
650
|
function detectStack(vaultPath) {
|
|
@@ -443,17 +669,26 @@ async function critiqueMoodBoard(opts) {
|
|
|
443
669
|
var board = loadJson(boardPath);
|
|
444
670
|
if (!board) throw new Error('Board not found: ' + boardId);
|
|
445
671
|
|
|
446
|
-
var
|
|
672
|
+
var isUi = board.kind === 'ui';
|
|
673
|
+
if (isUi) normalizeBoard(board);
|
|
674
|
+
var experts = opts.experts || (isUi ? JURY.slice() : ['arjun', 'meera', 'priya', 'zara', 'noor']);
|
|
447
675
|
var task = opts.task || board.task;
|
|
448
676
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
677
|
+
if (isUi && !board.tiles.length) {
|
|
678
|
+
var need = new Error('Zara has not scouted yet. Add tiles, then critique.');
|
|
679
|
+
need.code = 'NEED_SCOUT';
|
|
680
|
+
throw need;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
var refsSummary = isUi
|
|
684
|
+
? juryTileLines(board)
|
|
685
|
+
: board.references.map(function(r, idx) {
|
|
686
|
+
return '[' + (idx + 1) + '] ' + r.title + ' (' + r.type + ') tags=' + (r.tags || []).join(',') + ' — ' + (r.url || r.description || '').slice(0, 120);
|
|
687
|
+
}).join('\n');
|
|
453
688
|
|
|
454
689
|
var dsText = formatDsPack(board.design_system_pack);
|
|
455
690
|
|
|
456
|
-
var critiqueTask = [
|
|
691
|
+
var critiqueTask = isUi ? juryPrompt(board, dsText) : [
|
|
457
692
|
'Mood-board critique for: ' + task,
|
|
458
693
|
'',
|
|
459
694
|
'References:\n' + refsSummary,
|
|
@@ -463,7 +698,6 @@ async function critiqueMoodBoard(opts) {
|
|
|
463
698
|
'Each persona must use the Honeycomb rigor matrix to evaluate which references are appropriate for the user and why. Output scores, top concerns, and a ranked shortlist of the best 2-3 references. Then participate in adversarial deliberation until consensus.'
|
|
464
699
|
].join('\n');
|
|
465
700
|
|
|
466
|
-
// Use the orchestrator directly, but force the experts to the mood-board set.
|
|
467
701
|
var result = await orchestratorRun({
|
|
468
702
|
task: critiqueTask,
|
|
469
703
|
experts: experts,
|
|
@@ -479,17 +713,330 @@ async function critiqueMoodBoard(opts) {
|
|
|
479
713
|
|
|
480
714
|
board.deliberation = result.deliberation;
|
|
481
715
|
board.synthesis = result.synthesis;
|
|
482
|
-
board.updated_at = new Date().toISOString();
|
|
483
716
|
board.critique_task = critiqueTask;
|
|
717
|
+
if (isUi) applyJuryOutcome(board, result.synthesis);
|
|
484
718
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
719
|
+
var saved = persistBoard(board, opts.workspaceDir || defaultWorkspaceDir());
|
|
720
|
+
return { board: saved.board, boardDir: saved.boardDir, workspacePath: saved.workspacePath, orchestratorResult: result };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// The jury's verdict is advice. It never closes the board or sends Zara back
|
|
724
|
+
// on its own — the board waits for the user.
|
|
725
|
+
function applyJuryOutcome(board, synthesis) {
|
|
726
|
+
var verdict = String((synthesis && synthesis.verdict) || '').toUpperCase();
|
|
727
|
+
var scout = board.scout || {};
|
|
728
|
+
var notes = '';
|
|
729
|
+
if (synthesis && synthesis.summary) notes = synthesis.summary;
|
|
730
|
+
else if (synthesis && synthesis.top3 && synthesis.top3.join) notes = synthesis.top3.join('; ');
|
|
731
|
+
else notes = verdict;
|
|
732
|
+
scout.status = 'awaiting_user';
|
|
733
|
+
scout.jury_verdict = verdict;
|
|
734
|
+
scout.last_veto = clip(notes, 600);
|
|
735
|
+
board.scout = scout;
|
|
736
|
+
}
|
|
491
737
|
|
|
492
|
-
|
|
738
|
+
function juryTileLines(board) {
|
|
739
|
+
return board.tiles.map(function(t) {
|
|
740
|
+
return '#' + t.n + ' [' + t.section + '] ' + t.element + ' — ' + t.site + (t.video ? ' (scroll film)' : '') + ' round=' + t.round +
|
|
741
|
+
'\n steal=' + t.steal + ' | refuse=' + t.refuse + ' | peak=' + t.peak +
|
|
742
|
+
(t.tokens.colors.length ? '\n colours=' + t.tokens.colors.join(' ') : '') +
|
|
743
|
+
(t.tokens.fonts.length ? ' fonts=' + t.tokens.fonts.join(', ') : '') +
|
|
744
|
+
(t.user ? '\n USER ALREADY DECIDED: ' + t.user.decision + (t.user.note ? ' — ' + t.user.note : '') + ' (do not argue with this)' : '');
|
|
745
|
+
}).join('\n');
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function juryPrompt(board, dsText) {
|
|
749
|
+
return [
|
|
750
|
+
'UI element-board jury for: ' + board.task,
|
|
751
|
+
'You are Arjun, Noor, Priya and Meera. Zara scouted these tiles and does not vote. Your votes are advice shown to the user; the user decides.',
|
|
752
|
+
'Vote KEEP or SKIP on every tile with one because-sentence. Do not reward cinematic marketing motion on a daily working surface.',
|
|
753
|
+
'',
|
|
754
|
+
'Tiles:\n' + juryTileLines(board),
|
|
755
|
+
'',
|
|
756
|
+
dsText || '',
|
|
757
|
+
'',
|
|
758
|
+
'Return votes as JSON: {"votes":[{"tile":<n>,"persona":"arjun","vote":"keep|skip","because":"..."}],"verdict":"SHIP|REVISE|BLOCK","summary":"..."}',
|
|
759
|
+
'Then call analyzthis_mood_board with action=votes, board=' + board.board_id + ', and that JSON.'
|
|
760
|
+
].join('\n');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function loadUiBoard(opts) {
|
|
764
|
+
var board = getBoard(opts);
|
|
765
|
+
if (board.kind !== 'ui') throw new Error('This belongs on a UI element board (kind ui).');
|
|
766
|
+
return normalizeBoard(board);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Writes base64 screenshot data (inline, or a saved CDP Page.captureScreenshot
|
|
770
|
+
// response file) into the board's assets folder.
|
|
771
|
+
function saveBase64Image(bDir, tileId, data, label) {
|
|
772
|
+
if (!data || typeof data !== 'string') throw new Error('No screenshot data in ' + label);
|
|
773
|
+
var buf = Buffer.from(data.replace(/^data:image\/\w+;base64,/, ''), 'base64');
|
|
774
|
+
if (buf.length > 8 * 1024 * 1024) throw new Error('Screenshot is larger than 8 MB: ' + label);
|
|
775
|
+
var isPng = buf.slice(0, 4).toString('hex') === '89504e47';
|
|
776
|
+
var isJpg = buf.slice(0, 2).toString('hex') === 'ffd8';
|
|
777
|
+
if (!isPng && !isJpg) throw new Error('Screenshot data is not a PNG or JPEG: ' + label);
|
|
778
|
+
var name = tileId + (isPng ? '.png' : '.jpg');
|
|
779
|
+
ensureDir(path.join(bDir, 'assets'));
|
|
780
|
+
fs.writeFileSync(path.join(bDir, 'assets', name), buf);
|
|
781
|
+
return { image: 'assets/' + name, image_url: '' };
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function resolveImage(board, bDir, tileId, tile) {
|
|
785
|
+
if (tile.image_base64) return saveBase64Image(bDir, tileId, tile.image_base64, 'image_base64');
|
|
786
|
+
if (tile.image_path && path.extname(String(tile.image_path)).toLowerCase() === '.json') {
|
|
787
|
+
var jsonPath = path.resolve(String(tile.image_path));
|
|
788
|
+
var raw = loadJson(jsonPath);
|
|
789
|
+
return saveBase64Image(bDir, tileId, raw && (raw.data || (raw.result && raw.result.data)), jsonPath);
|
|
790
|
+
}
|
|
791
|
+
if (tile.image_path) {
|
|
792
|
+
var src = path.resolve(String(tile.image_path));
|
|
793
|
+
var ext = path.extname(src).toLowerCase();
|
|
794
|
+
if (IMAGE_EXTS.indexOf(ext) === -1) throw new Error('Image must be one of ' + IMAGE_EXTS.join(', ') + ': ' + src);
|
|
795
|
+
if (!fs.existsSync(src) || !fs.statSync(src).isFile()) throw new Error('Image file not found: ' + src);
|
|
796
|
+
if (fs.statSync(src).size > 8 * 1024 * 1024) throw new Error('Image is larger than 8 MB: ' + src);
|
|
797
|
+
var name = tileId + ext;
|
|
798
|
+
ensureDir(path.join(bDir, 'assets'));
|
|
799
|
+
fs.copyFileSync(src, path.join(bDir, 'assets', name));
|
|
800
|
+
return { image: 'assets/' + name, image_url: '' };
|
|
801
|
+
}
|
|
802
|
+
if (tile.image_url && isHttpUrl(tile.image_url)) return { image: '', image_url: String(tile.image_url) };
|
|
803
|
+
throw new Error('Each tile needs image_path (a saved crop) or an http(s) image_url.');
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Copies a scroll film (WebM/MP4, checked by file signature, not extension).
|
|
807
|
+
function resolveVideo(bDir, tileId, videoPath) {
|
|
808
|
+
if (!videoPath) return '';
|
|
809
|
+
var src = path.resolve(String(videoPath));
|
|
810
|
+
if (!fs.existsSync(src) || !fs.statSync(src).isFile()) throw new Error('Video file not found: ' + src);
|
|
811
|
+
if (fs.statSync(src).size > 40 * 1024 * 1024) throw new Error('Video is larger than 40 MB: ' + src);
|
|
812
|
+
var head = Buffer.alloc(12);
|
|
813
|
+
var fd = fs.openSync(src, 'r');
|
|
814
|
+
fs.readSync(fd, head, 0, 12, 0);
|
|
815
|
+
fs.closeSync(fd);
|
|
816
|
+
var ext = head.slice(0, 4).toString('hex') === '1a45dfa3' ? '.webm' : head.slice(4, 8).toString('latin1') === 'ftyp' ? '.mp4' : '';
|
|
817
|
+
if (!ext) throw new Error('Video must be WebM or MP4: ' + src);
|
|
818
|
+
ensureDir(path.join(bDir, 'assets'));
|
|
819
|
+
fs.copyFileSync(src, path.join(bDir, 'assets', tileId + ext));
|
|
820
|
+
return 'assets/' + tileId + ext;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Up to four waypoint stills shown as a strip under a film.
|
|
824
|
+
function resolveStills(board, bDir, tileId, stills) {
|
|
825
|
+
return (Array.isArray(stills) ? stills : []).slice(0, 4).map(function(p, i) {
|
|
826
|
+
return resolveImage(board, bDir, tileId + '-' + (i + 1), { image_path: p }).image;
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Adds Zara's (or the user's) element crops. Every tile must say where it
|
|
831
|
+
// came from; tiles the user already rejected are refused.
|
|
832
|
+
function addTiles(opts) {
|
|
833
|
+
opts = opts || {};
|
|
834
|
+
var incoming = opts.tiles || (opts.tile ? [opts.tile] : []);
|
|
835
|
+
if (!opts.boardId || !incoming.length) throw new Error('--board and at least one tile are required');
|
|
836
|
+
var board = loadUiBoard(opts);
|
|
837
|
+
var bDir = boardDir(board.project_id, board.board_id);
|
|
838
|
+
var round = (board.scout && board.scout.round) || 1;
|
|
839
|
+
var added = [];
|
|
840
|
+
var skipped = [];
|
|
841
|
+
for (var i = 0; i < incoming.length; i++) {
|
|
842
|
+
var t = incoming[i] || {};
|
|
843
|
+
var pageUrl = t.page_url || t.url || '';
|
|
844
|
+
var element = clip(t.element || t.title || '', 120).trim();
|
|
845
|
+
if (!isHttpUrl(pageUrl)) { skipped.push({ element: element, reason: 'missing or invalid page_url' }); continue; }
|
|
846
|
+
if (!element) { skipped.push({ page_url: pageUrl, reason: 'missing element name' }); continue; }
|
|
847
|
+
var key = tileKey(pageUrl, element);
|
|
848
|
+
if (board.blocked.indexOf(key) !== -1) { skipped.push({ element: element, reason: 'user rejected this before' }); continue; }
|
|
849
|
+
if (board.tiles.some(function(x) { return tileKey(x.page_url, x.element) === key; })) {
|
|
850
|
+
skipped.push({ element: element, reason: 'already on the board' });
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
var id = generateBoardId();
|
|
854
|
+
var img, video, stills;
|
|
855
|
+
try {
|
|
856
|
+
// A film may arrive without a poster: its first still becomes the poster.
|
|
857
|
+
img = resolveImage(board, bDir, id, (t.image_path || t.image_base64 || t.image_url) ? t : { image_path: (t.stills || [])[0] });
|
|
858
|
+
video = resolveVideo(bDir, id, t.video_path);
|
|
859
|
+
stills = resolveStills(board, bDir, id, t.stills);
|
|
860
|
+
} catch (e) { skipped.push({ element: element, reason: e.message }); continue; }
|
|
861
|
+
var tile = {
|
|
862
|
+
id: id,
|
|
863
|
+
n: board.tiles.reduce(function(m, x) { return Math.max(m, x.n || 0); }, 0) + 1,
|
|
864
|
+
section: SECTION_IDS.indexOf(t.section) !== -1 ? t.section : 'components',
|
|
865
|
+
element: element,
|
|
866
|
+
site: siteOf(pageUrl),
|
|
867
|
+
page_url: String(pageUrl),
|
|
868
|
+
image: img.image,
|
|
869
|
+
image_url: img.image_url,
|
|
870
|
+
video: video,
|
|
871
|
+
stills: stills,
|
|
872
|
+
tokens: sanitizeTokens(t.tokens),
|
|
873
|
+
steal: clip(t.steal, 200),
|
|
874
|
+
refuse: clip(t.refuse, 200),
|
|
875
|
+
peak: clip(t.peak, 200),
|
|
876
|
+
concept: !!t.concept,
|
|
877
|
+
round: round,
|
|
878
|
+
source: t.source === 'user' ? 'user' : 'zara_scout',
|
|
879
|
+
added_at: new Date().toISOString(),
|
|
880
|
+
votes: {},
|
|
881
|
+
user: null,
|
|
882
|
+
};
|
|
883
|
+
board.tiles.push(tile);
|
|
884
|
+
added.push(tile);
|
|
885
|
+
}
|
|
886
|
+
if (board.scout && added.length) board.scout.status = 'awaiting_jury';
|
|
887
|
+
var saved = persistBoard(board, opts.workspaceDir);
|
|
888
|
+
saved.added = added;
|
|
889
|
+
saved.skipped = skipped;
|
|
890
|
+
return saved;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Older `frames` input (url/title) is still accepted and becomes tiles.
|
|
894
|
+
function addFrames(opts) {
|
|
895
|
+
opts = opts || {};
|
|
896
|
+
var frames = opts.frames || (opts.frame ? [opts.frame] : []);
|
|
897
|
+
return addTiles({
|
|
898
|
+
project: opts.project,
|
|
899
|
+
boardId: opts.boardId,
|
|
900
|
+
workspaceDir: opts.workspaceDir,
|
|
901
|
+
tiles: frames.map(function(f) {
|
|
902
|
+
return {
|
|
903
|
+
page_url: f.page_url || f.url, element: f.element || f.title, section: f.section || f.surface,
|
|
904
|
+
image_path: f.image_path, image_url: f.image_url || f.url,
|
|
905
|
+
steal: f.steal, refuse: f.refuse, peak: f.peak, tokens: f.tokens,
|
|
906
|
+
};
|
|
907
|
+
}),
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function getBoard(opts) {
|
|
912
|
+
opts = opts || {};
|
|
913
|
+
var projectId = opts.project || session.getProjectId();
|
|
914
|
+
var boardId = opts.boardId;
|
|
915
|
+
if (!boardId) throw new Error('board is required');
|
|
916
|
+
var board = loadJson(path.join(boardDir(projectId, boardId), 'board.json'));
|
|
917
|
+
if (!board) throw new Error('Board not found: ' + boardId);
|
|
918
|
+
return board;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Persona votes are stored per tile as advice. Zara may not vote on tiles she
|
|
922
|
+
// scouted, and no persona vote can change a tile the user has decided.
|
|
923
|
+
function recordPersonaVotes(opts) {
|
|
924
|
+
opts = opts || {};
|
|
925
|
+
var board = loadUiBoard(opts);
|
|
926
|
+
var votes = opts.votes || [];
|
|
927
|
+
var ignored = [];
|
|
928
|
+
for (var i = 0; i < votes.length; i++) {
|
|
929
|
+
var v = votes[i] || {};
|
|
930
|
+
var persona = String(v.persona || '').toLowerCase();
|
|
931
|
+
var vote = String(v.vote || '').toLowerCase();
|
|
932
|
+
var tile = findTile(board, v.tile);
|
|
933
|
+
if (!tile) { ignored.push({ tile: v.tile, reason: 'unknown tile' }); continue; }
|
|
934
|
+
if (JURY.indexOf(persona) === -1) { ignored.push({ tile: v.tile, persona: persona, reason: 'not on the jury' }); continue; }
|
|
935
|
+
if (vote !== 'keep' && vote !== 'skip') { ignored.push({ tile: v.tile, reason: 'vote must be keep or skip' }); continue; }
|
|
936
|
+
tile.votes[persona] = { vote: vote, because: clip(v.because, 240), at: new Date().toISOString() };
|
|
937
|
+
}
|
|
938
|
+
var synthesis = { verdict: String(opts.verdict || '').toUpperCase(), summary: clip(opts.summary, 1000), top3: opts.top3 || [] };
|
|
939
|
+
applyJuryOutcome(board, synthesis);
|
|
940
|
+
board.synthesis = synthesis;
|
|
941
|
+
var saved = persistBoard(board, opts.workspaceDir);
|
|
942
|
+
saved.ignored = ignored;
|
|
943
|
+
return saved;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function recordJuryVerdict(opts) {
|
|
947
|
+
return recordPersonaVotes(opts);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// The user's choice on a tile is final. It is written to an append-only
|
|
951
|
+
// ledger, and a rejection blocks that element from ever being re-added.
|
|
952
|
+
function recordUserDecision(opts) {
|
|
953
|
+
opts = opts || {};
|
|
954
|
+
var board = loadUiBoard(opts);
|
|
955
|
+
var tile = findTile(board, opts.tile);
|
|
956
|
+
if (!tile) throw new Error('Tile not found: ' + opts.tile);
|
|
957
|
+
var decision = String(opts.decision || '').toLowerCase();
|
|
958
|
+
if (decision !== 'clear' && USER_DECISIONS.indexOf(decision) === -1) {
|
|
959
|
+
throw new Error('Decision must be keep, reject, maybe, or clear.');
|
|
960
|
+
}
|
|
961
|
+
var key = tileKey(tile.page_url, tile.element);
|
|
962
|
+
var now = new Date().toISOString();
|
|
963
|
+
tile.user = decision === 'clear' ? null : { decision: decision, note: clip(opts.note, 500), at: now };
|
|
964
|
+
board.blocked = board.blocked.filter(function(k) { return k !== key; });
|
|
965
|
+
if (decision === 'reject') board.blocked.push(key);
|
|
966
|
+
board.decisions.push({ tile_id: tile.id, n: tile.n, decision: decision, note: clip(opts.note, 500), at: now, by: 'user' });
|
|
967
|
+
return persistBoard(board, opts.workspaceDir);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// User-requested rounds have no cap. Zara gets the user's notes, keeps and
|
|
971
|
+
// rejections as the top constraints.
|
|
972
|
+
function requestRescout(opts) {
|
|
973
|
+
opts = opts || {};
|
|
974
|
+
var board = loadUiBoard(opts);
|
|
975
|
+
board.scout = board.scout || { persona: 'zara', round: 1 };
|
|
976
|
+
board.scout.round = (board.scout.round || 1) + 1;
|
|
977
|
+
board.scout.status = 'awaiting_scout';
|
|
978
|
+
board.scout.user_note = clip(opts.note, 500);
|
|
979
|
+
board.scout.brief = defaultScoutBrief(board.task, board.scout.round, scoutConstraints(board, opts.note));
|
|
980
|
+
board.decisions.push({ decision: 'rescout', note: clip(opts.note, 500), at: new Date().toISOString(), by: 'user' });
|
|
981
|
+
return persistBoard(board, opts.workspaceDir);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Locking writes the user's direction into the session so every persona
|
|
985
|
+
// reads it before designing.
|
|
986
|
+
function lockDirection(opts) {
|
|
987
|
+
opts = opts || {};
|
|
988
|
+
var board = loadUiBoard(opts);
|
|
989
|
+
var dir = computeDirection(board);
|
|
990
|
+
if (!dir.kept.length) throw new Error('Keep at least one tile before locking the direction.');
|
|
991
|
+
var now = new Date().toISOString();
|
|
992
|
+
board.direction = dir;
|
|
993
|
+
board.direction.locked_at = now;
|
|
994
|
+
board.direction.note = clip(opts.note, 500);
|
|
995
|
+
board.scout = board.scout || {};
|
|
996
|
+
board.scout.status = 'done';
|
|
997
|
+
board.decisions.push({ decision: 'lock', note: clip(opts.note, 500), at: now, by: 'user' });
|
|
998
|
+
var saved = persistBoard(board, opts.workspaceDir);
|
|
999
|
+
try {
|
|
1000
|
+
session.update({
|
|
1001
|
+
project: board.project_id,
|
|
1002
|
+
patch: {
|
|
1003
|
+
design_direction: {
|
|
1004
|
+
board_id: board.board_id,
|
|
1005
|
+
task: board.task,
|
|
1006
|
+
locked_at: now,
|
|
1007
|
+
locked_by: 'user',
|
|
1008
|
+
rule: 'User-locked mood board. Outranks every persona opinion. Do not reintroduce rejected elements.',
|
|
1009
|
+
adjectives: board.brief.adjectives,
|
|
1010
|
+
not_this: board.brief.not_this,
|
|
1011
|
+
note: board.direction.note,
|
|
1012
|
+
palette: dir.palette,
|
|
1013
|
+
fonts: dir.fonts,
|
|
1014
|
+
kept: dir.kept,
|
|
1015
|
+
rejected: dir.rejected,
|
|
1016
|
+
},
|
|
1017
|
+
},
|
|
1018
|
+
});
|
|
1019
|
+
} catch (e) {
|
|
1020
|
+
saved.sessionError = e.message;
|
|
1021
|
+
}
|
|
1022
|
+
return saved;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function setBrief(opts) {
|
|
1026
|
+
opts = opts || {};
|
|
1027
|
+
var board = loadUiBoard(opts);
|
|
1028
|
+
var next = defaultBrief(board.task, {
|
|
1029
|
+
job: opts.job || board.brief.job,
|
|
1030
|
+
adjectives: opts.adjectives || board.brief.adjectives,
|
|
1031
|
+
not_this: opts.not_this != null ? opts.not_this : board.brief.not_this,
|
|
1032
|
+
sections: opts.sections || board.brief.sections,
|
|
1033
|
+
confirmed_by_user: opts.confirmed_by_user != null ? opts.confirmed_by_user : board.brief.confirmed_by_user,
|
|
1034
|
+
});
|
|
1035
|
+
board.brief = next;
|
|
1036
|
+
if (board.scout) {
|
|
1037
|
+
board.scout.brief = defaultScoutBrief(board.task, board.scout.round || 1, scoutConstraints(board, board.scout.user_note));
|
|
1038
|
+
}
|
|
1039
|
+
return persistBoard(board, opts.workspaceDir);
|
|
493
1040
|
}
|
|
494
1041
|
|
|
495
1042
|
// ─── Add user reference and rerun ─────────────────────────────────────────────
|
|
@@ -552,12 +1099,76 @@ function listBoards(projectId) {
|
|
|
552
1099
|
.map(function(d) { return d.name; });
|
|
553
1100
|
}
|
|
554
1101
|
|
|
1102
|
+
// ─── Token capture script (run in the page via CDP Runtime.evaluate) ─────────
|
|
1103
|
+
|
|
1104
|
+
// Reads the element's real computed styles plus its children's, so the board
|
|
1105
|
+
// shows the site's actual colours and fonts instead of guesses.
|
|
1106
|
+
// `text` narrows the match to the first element whose visible text contains
|
|
1107
|
+
// it, because a bare `h1` often hits the logo instead of the headline.
|
|
1108
|
+
function captureScript(selector, text) {
|
|
1109
|
+
var sel = JSON.stringify(String(selector || 'body'));
|
|
1110
|
+
var txt = JSON.stringify(String(text || '').toLowerCase());
|
|
1111
|
+
return [
|
|
1112
|
+
'(function () {',
|
|
1113
|
+
' var all = Array.prototype.slice.call(document.querySelectorAll(' + sel + '));',
|
|
1114
|
+
' function area(el) { var r = el.getBoundingClientRect(); return r.width * r.height; }',
|
|
1115
|
+
' var hits = all.filter(function (el) { return area(el) > 0 && (!' + txt + ' || el.textContent.toLowerCase().indexOf(' + txt + ') !== -1); });',
|
|
1116
|
+
' var root = ' + txt + ' ? hits.sort(function (a, b) { return area(a) - area(b); })[0] : hits[0];',
|
|
1117
|
+
' if (!root) return { error: "No visible element matches " + ' + sel + ' + (' + txt + ' ? " with text " + ' + txt + ' : "") };',
|
|
1118
|
+
' function hex(c) {',
|
|
1119
|
+
' var m = String(c).match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)(?:,\\s*([\\d.]+))?/);',
|
|
1120
|
+
' if (!m || m[4] === "0") return null;',
|
|
1121
|
+
' return "#" + [m[1], m[2], m[3]].map(function (n) { return ("0" + parseInt(n, 10).toString(16)).slice(-2); }).join("");',
|
|
1122
|
+
' }',
|
|
1123
|
+
' var counts = {}, fonts = [];',
|
|
1124
|
+
' var nodes = [root].concat(Array.prototype.slice.call(root.querySelectorAll("*"), 0, 300));',
|
|
1125
|
+
' nodes.forEach(function (el) {',
|
|
1126
|
+
' var s = getComputedStyle(el);',
|
|
1127
|
+
' [s.color, s.backgroundColor, s.borderTopColor].forEach(function (c) { var h = hex(c); if (h) counts[h] = (counts[h] || 0) + 1; });',
|
|
1128
|
+
' var f = s.fontFamily.split(",")[0].replace(/["\']/g, "").trim();',
|
|
1129
|
+
' if (f && fonts.indexOf(f) === -1) fonts.push(f);',
|
|
1130
|
+
' });',
|
|
1131
|
+
// Capturing off-screen areas repeats content, so bring the element into
|
|
1132
|
+
// view and keep the crop within one viewport.
|
|
1133
|
+
' root.scrollIntoView({ block: "start" });',
|
|
1134
|
+
' var rs = getComputedStyle(root), r = root.getBoundingClientRect(), pad = 8;',
|
|
1135
|
+
' return {',
|
|
1136
|
+
' colors: Object.keys(counts).sort(function (a, b) { return counts[b] - counts[a]; }).slice(0, 8),',
|
|
1137
|
+
' fonts: fonts.slice(0, 4),',
|
|
1138
|
+
' radius: rs.borderRadius,',
|
|
1139
|
+
' spacing: rs.padding,',
|
|
1140
|
+
' font_size: rs.fontSize,',
|
|
1141
|
+
' size: Math.round(r.width) + "x" + Math.round(r.height),',
|
|
1142
|
+
' clip: { x: Math.max(0, r.left + scrollX - pad), y: Math.max(0, r.top + scrollY - pad), width: Math.ceil(r.width + pad * 2), height: Math.min(innerHeight, Math.ceil(r.height + pad * 2)), scale: 1 }',
|
|
1143
|
+
' };',
|
|
1144
|
+
'})()',
|
|
1145
|
+
].join('\n');
|
|
1146
|
+
}
|
|
1147
|
+
|
|
555
1148
|
module.exports = {
|
|
556
1149
|
buildMoodBoard: buildMoodBoard,
|
|
557
1150
|
critiqueMoodBoard: critiqueMoodBoard,
|
|
558
1151
|
addUserReferenceAndRerun: addUserReferenceAndRerun,
|
|
1152
|
+
addFrames: addFrames,
|
|
1153
|
+
addTiles: addTiles,
|
|
1154
|
+
getBoard: getBoard,
|
|
1155
|
+
loadUiBoard: loadUiBoard,
|
|
1156
|
+
recordJuryVerdict: recordJuryVerdict,
|
|
1157
|
+
recordPersonaVotes: recordPersonaVotes,
|
|
1158
|
+
recordUserDecision: recordUserDecision,
|
|
1159
|
+
requestRescout: requestRescout,
|
|
1160
|
+
lockDirection: lockDirection,
|
|
1161
|
+
setBrief: setBrief,
|
|
1162
|
+
computeDirection: computeDirection,
|
|
1163
|
+
tileStatus: tileStatus,
|
|
1164
|
+
juryPrompt: juryPrompt,
|
|
1165
|
+
captureScript: captureScript,
|
|
1166
|
+
SECTIONS: SECTIONS,
|
|
1167
|
+
TILE_TARGET: TILE_TARGET,
|
|
1168
|
+
defaultScoutBrief: defaultScoutBrief,
|
|
559
1169
|
listBoards: listBoards,
|
|
560
1170
|
boardDir: boardDir,
|
|
1171
|
+
siteOf: siteOf,
|
|
561
1172
|
formatDsPack: formatDsPack,
|
|
562
1173
|
tagHeuristics: tagHeuristics,
|
|
563
1174
|
};
|