Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/shot_vocabulary.mjs

11.5 KiB, 1 run

created by r2519314175:205, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// shot_vocabulary.mjs — the cropped screenshots for the guide's visual glossary.
2//
3// One crop per term on `guide/social.html`, taken from the running app rather
4// than drawn, because the whole point of that page is that a reader can see the
5// thing and recognise it again. A drawing of a chip proves nothing about what a
6// chip looks like.
7//
8// Each crop is taken by SELECTOR, with a little air around it, so a control that
9// moves keeps its picture and a control that is renamed loses it loudly: a
10// selector that matches nothing is reported and the shot is skipped rather than
11// a full window being written under a crop's name.
12//
13// Everything is captured at the default palette the harness boots into, which is
14// the dark one. The zone diagram beside these is drawn in CSS variables and
15// follows the reader's own palette; a photograph cannot, and pretending
16// otherwise would mean eleven copies of every crop.
17//
18// eval "$(bash dev/world.sh 6 --up)"
19// node dev/shot_vocabulary.mjs
20//
21// Writes into www/guide/shots/. Needs dev/serve.mjs and dev/mockllm.mjs up.
22import fs from 'node:fs';
23import path from 'node:path';
24import { fileURLToPath } from 'node:url';
25import { open, signInAs, chat } from './harness.mjs';
26
27const HERE = path.dirname(fileURLToPath(import.meta.url));
28const OUT = path.join(HERE, '..', 'www', 'guide', 'shots');
29fs.mkdirSync(OUT, { recursive: true });
30
31const s = await open({ name: 'Vocab' });
32const p = s.page;
33const made = [], missed = [];
34
35/// Crop by selector, with `pad` pixels of the surface around it.
36///
37/// Clipped to the viewport, because a clip that runs off the screen is refused
38/// by the browser and takes the whole run with it.
39async function grab(name, sel, { pad = 6, scale = 2 } = {}) {
40 const clip = await p.evaluate(({ sel, pad }) => {
41 const el = document.querySelector(sel);
42 if (!el) return null;
43 const r = el.getBoundingClientRect();
44 if (!r.width || !r.height) return null;
45 return {
46 x: Math.max(0, Math.round(r.x - pad)),
47 y: Math.max(0, Math.round(r.y - pad)),
48 width: Math.min(Math.round(r.width + pad * 2), window.innerWidth - Math.max(0, Math.round(r.x - pad))),
49 height: Math.min(Math.round(r.height + pad * 2), window.innerHeight - Math.max(0, Math.round(r.y - pad))),
50 };
51 }, { sel, pad });
52 if (!clip) { missed.push(`${name} — ${sel} is not on screen`); console.log(` MISS ${name} — ${sel}`); return false; }
53 await p.screenshot({ path: path.join(OUT, name), clip, scale: scale > 1 ? 'device' : 'css', timeout: 10000 });
54 console.log(` ${name} (${clip.width}x${clip.height})`);
55 made.push(name);
56 return true;
57}
58
59/// Crop `sel` but stop where `until` begins, for a container holding two things
60/// the glossary names separately.
61async function grabTo(name, sel, until, { pad = 8 } = {}) {
62 const clip = await p.evaluate(({ sel, until, pad }) => {
63 const el = document.querySelector(sel), u = document.querySelector(until);
64 if (!el) return null;
65 const r = el.getBoundingClientRect();
66 const bottom = u ? Math.min(r.bottom, u.getBoundingClientRect().top) : r.bottom;
67 if (!r.width || bottom - r.top <= 0) return null;
68 return {
69 x: Math.max(0, Math.round(r.x - pad)),
70 y: Math.max(0, Math.round(r.y - pad)),
71 width: Math.round(r.width + pad * 2),
72 height: Math.round(bottom - r.top + pad),
73 };
74 }, { sel, until, pad });
75 if (!clip) { missed.push(`${name} — ${sel} is not on screen`); console.log(` MISS ${name} — ${sel}`); return false; }
76 await p.screenshot({ path: path.join(OUT, name), clip, scale: 'device', timeout: 10000 });
77 console.log(` ${name} (${clip.width}x${clip.height})`);
78 made.push(name);
79 return true;
80}
81
82const pause = (ms) => p.waitForTimeout(ms);
83
84// ── The scene ────────────────────────────────────────────────────────
85// Tags first: they are set in the store and read back on a reload, which is the
86// one path that agrees with what a person would see.
87await pause(1200);
88await p.evaluate(async () => {
89 const m = await import('/pkg/oxedyne_daimond.js');
90 const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 4096, '', true);
91 const list = JSON.parse(await app.list_diamonds());
92 const tags = [['project', 'urgent'], ['topic']];
93 for (let i = 0; i < list.length && i < tags.length; i++) {
94 await app.set_tags(list[i].id, JSON.stringify(tags[i]));
95 }
96});
97await p.reload({ waitUntil: 'domcontentloaded' });
98await signInAs(s, 'Vocab');
99await pause(1500);
100await p.evaluate(() => { const b = document.getElementById('admin-close'); if (b) b.click(); });
101await pause(500);
102
103// ── The rail ─────────────────────────────────────────────────────────
104await grab('vocab-chips.png', '#panel-tags', { pad: 8 });
105await grab('vocab-everything.png', '#pptw-global-row', { pad: 8 });
106await grab('vocab-head.png', '.rail-top .railhead', { pad: 8 });
107await grab('vocab-tile-diamond.png', '.diamond-list .session-box', { pad: 8 });
108await grab('vocab-tagfilter.png', '#diamond-filter', { pad: 8 });
109await grab('vocab-cog.png', '.diamond-list .session-box .tile-cog', { pad: 14 });
110// The rail's own closer, which is the one that speaks for the whole rail.
111await grab('vocab-closer.png', '#pptw-global-row [data-close]', { pad: 14 });
112
113// The divider between the two lists: a 2px rule that turns accent under the
114// pointer, shown hovered with the Chats head below it, because at rest it is a
115// hairline and a picture of a hairline teaches nobody its name.
116await p.hover('#handle-rail-split', { force: true }).catch(() => {});
117await pause(400);
118await grab('vocab-divider.png', '#handle-rail-split', { pad: 26 });
119
120// ── The chat ─────────────────────────────────────────────────────────
121await chat(s, 'Which zone is the dock in?');
122await pause(1200);
123await grab('vocab-tile-chat.png', '.session-list .session-box', { pad: 8 });
124await grab('vocab-chead.png', '#panel-ai .chead', { pad: 8 });
125await grab('vocab-composer.png', '#panel-ai .chat-input-bar', { pad: 8 });
126await grab('vocab-spendrow.png', '#spend-row', { pad: 10 });
127
128// ── The admin panel ──────────────────────────────────────────────────
129await p.evaluate(() => { const b = document.getElementById('settings-btn'); if (b) b.click(); });
130await pause(900);
131await p.evaluate(() => { const b = document.getElementById('admin-close'); if (b) b.click(); });
132await pause(500);
133// Stopped above the spend row, which has a crop of its own: one picture per
134// term, or a reader cannot tell which part of it the word names.
135await grabTo('vocab-rows.png', '#admin-status', '#spend-row');
136
137// ── A Diamond's two faces ────────────────────────────────────────────
138await p.evaluate(() => {
139 const t = document.querySelector('.diamond-list .session-box');
140 if (t) t.click();
141});
142await pause(1500);
143await grab('vocab-face.png', '#diamond-view', { pad: 10 });
144
145// ── The paperclip ────────────────────────────────────────────────────
146// On every row of the Workspace panel, and drawn only while something has the
147// focus to attach to -- so this follows the Diamond above rather than standing
148// on its own.
149await p.evaluate(() => { if (window.DaimondPanels) DaimondPanels.show('work'); });
150await pause(1500);
151// The clip is `visibility: hidden` until the pointer is on its row, so the row
152// is hovered and the WHOLE row is cropped: a paperclip on its own on a black
153// square teaches a reader nothing about where to find one.
154await p.hover('.files-row:has(.attach-btn)', { force: true }).catch(() => {});
155await pause(400);
156await grab('vocab-paperclip.png', '.files-row:has(.attach-btn)', { pad: 8 });
157
158// ── The Go to box ────────────────────────────────────────────────────
159// Ctrl-K is deliberately ignored while the caret is in a text field, and after
160// the steps above it is in the composer.
161await p.evaluate(() => { if (document.activeElement && document.activeElement.blur) document.activeElement.blur(); });
162await p.keyboard.press('Control+k');
163await pause(900);
164// Left empty: the box's own placeholder says what it is for, and the resting
165// list is panels, which is what a reader will have come to it for.
166await grab('vocab-goto.png', '.pal-box', { pad: 10 });
167await p.keyboard.press('Escape');
168await pause(400);
169
170// ── A dialog ─────────────────────────────────────────────────────────
171// A chat tile's own cog dialog, which is the shape every dialog in the app has:
172// a title, a closer on the same row, and the settings under it.
173await p.evaluate(() => { const c = document.querySelector('.session-list .session-box .tile-cog'); if (c) c.click(); });
174await pause(1000);
175await grab('vocab-dialog.png', '.dlg-card', { pad: 10 });
176await p.keyboard.press('Escape');
177await pause(400);
178
179// ── The gallery ──────────────────────────────────────────────────────
180// The "⋯" chip only exists once the row cannot hold every chip, so the window is
181// narrowed to make one rather than a panel being unpinned behind the reader's
182// back.
183await p.setViewportSize({ width: 1000, height: 900 });
184await pause(800);
185await p.evaluate(() => { const b = document.getElementById('panel-more'); if (b) b.click(); });
186await pause(700);
187await grab('vocab-gallery.png', '#panel-gallery', { pad: 10 });
188await p.keyboard.press('Escape');
189await pause(400);
190
191// ── The phone shell: the drawer and the sheet ────────────────────────
192// Reloaded at the phone size rather than resized into it. A window taken from
193// 1500 to 1000 and then to 390 leaves the rail `display: none` and open at the
194// same time, which is a layout the phone shell never reaches on its own and not
195// what a reader would be looking at.
196await p.setViewportSize({ width: 390, height: 844 });
197await p.reload({ waitUntil: 'domcontentloaded' });
198await signInAs(s, 'Vocab');
199await pause(2000);
200await p.evaluate(() => { if (window.DaimondPanels) DaimondPanels.show('rail'); });
201await pause(600);
202await p.click('#drawer-btn', { force: true }).catch(() => {});
203await pause(1000);
204// The hamburger toggles, so a rail that was already open when the window
205// narrowed ends up shut. Asked for by state rather than by another press.
206await p.evaluate(() => { if (!document.body.classList.contains('drawer-open')) { const b = document.getElementById('drawer-btn'); if (b) b.click(); } });
207await pause(900);
208await grab('vocab-drawer.png', '#panel-rail', { pad: 0 });
209await p.evaluate(() => { const sc = document.getElementById('scrim'); if (sc) sc.click(); });
210await pause(800);
211await p.evaluate(() => { if (window.DaimondSheet) DaimondSheet.open('work'); });
212await pause(1500);
213await grab('vocab-sheet.png', '#msheet', { pad: 0 });
214
215console.log(`\n${made.length} crops written to www/guide/shots/`);
216if (missed.length) { console.log('MISSED:'); missed.forEach((m) => console.log(' ' + m)); }
217await s.close();
218process.exit(missed.length ? 1 : 0);