wizards 0.3.5 → 0.3.7
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/apps/runtime/dist/agents/studio-assistant.js +285 -31
- package/apps/runtime/dist/agents/website.js +256 -34
- package/apps/runtime/dist/db/schema.js +1 -1
- package/apps/runtime/dist/docs/user/test-and-share.md +21 -11
- package/apps/runtime/dist/documents/parse.js +14 -0
- package/apps/runtime/dist/engine/steps.js +42 -6
- package/apps/runtime/dist/engine/template.js +50 -3
- package/apps/runtime/dist/film/project.js +54 -4
- package/apps/runtime/dist/routes/studio.js +52 -3
- package/apps/runtime/dist/services/brand.js +6 -3
- package/apps/runtime/dist/services/project-files.js +59 -3
- package/apps/runtime/dist/services/projects.js +150 -2
- package/apps/runtime/dist/services/wizards.js +25 -0
- package/apps/web/dist/assets/AboutDialog-C0dN86zd.js +1 -0
- package/apps/web/dist/assets/{ChangelogDialog-VEBTlC3f.js → ChangelogDialog-Cv81t7tj.js} +1 -1
- package/apps/web/dist/assets/ChatView-uogJ3nht.js +10 -0
- package/apps/web/dist/assets/EditorPage-n8YuTh6j.js +3 -0
- package/apps/web/dist/assets/{FlowPreview-86xWWfdk.js → FlowPreview-D4dp1MWS.js} +1 -1
- package/apps/web/dist/assets/{Harness-CF6H2TYJ.js → Harness-v7r0e6vK.js} +2 -2
- package/apps/web/dist/assets/NewWizardChannels-Be-7VhBi.js +1 -0
- package/apps/web/dist/assets/NewWizardPage-C8hPvrK8.js +7 -0
- package/apps/web/dist/assets/ProjectPage-B686ArYy.js +1 -0
- package/apps/web/dist/assets/PublicRunner-DeCpVfZP.js +1 -0
- package/apps/web/dist/assets/{ReadOnly-mhKW496R.js → ReadOnly-B-UCgnqW.js} +1 -1
- package/apps/web/dist/assets/RunDrawer-C6pzi8AJ.js +4 -0
- package/apps/web/dist/assets/{RunnerView-CTRFgyu5.js → RunnerView-Deenymq0.js} +1 -1
- package/apps/web/dist/assets/SettingsPage-CK6L0yEe.js +1 -0
- package/apps/web/dist/assets/SetupPage-9CH3qXdH.js +1 -0
- package/apps/web/dist/assets/SharePage-B778xUFo.js +1 -0
- package/apps/web/dist/assets/changelog-DCJo2mbx.js +1 -0
- package/apps/web/dist/assets/{circle-alert-CYs622_r.js → circle-alert-DLvC9ePT.js} +1 -1
- package/apps/web/dist/assets/{esm-B52LIMhP.js → esm-BJskogLD.js} +1 -1
- package/apps/web/dist/assets/index-D6hUM8NU.css +1 -0
- package/apps/web/dist/assets/index-Df6UxElH.js +12 -0
- package/apps/web/dist/assets/{lock-BvSWZOin.js → lock-DR7NakPn.js} +1 -1
- package/apps/web/dist/assets/{phone-CI2Flerg.js → phone-BORh3jfv.js} +1 -1
- package/apps/web/dist/assets/{settings-menu-BOOPxcJH.js → settings-menu-DV5D91TD.js} +1 -1
- package/apps/web/dist/assets/{ui-CMdSOoUZ.js → ui-BOCXzzHi.js} +1 -1
- package/apps/web/dist/assets/{upload-Cz9HgBT9.js → upload-B3yv8gsk.js} +1 -1
- package/apps/web/dist/index.html +3 -8
- package/apps/web/dist/mcp/flow.html +2 -2
- package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +3 -3
- package/node_modules/@engenty-wizards/shared/dist/changes.d.ts +22 -0
- package/node_modules/@engenty-wizards/shared/dist/changes.js +61 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +8 -8
- package/node_modules/@engenty-wizards/shared/dist/marketplace.d.ts +1 -1
- package/node_modules/@engenty-wizards/shared/dist/projects.d.ts +138 -3
- package/node_modules/@engenty-wizards/shared/dist/projects.js +153 -4
- package/node_modules/@engenty-wizards/shared/dist/wizard-package.d.ts +1 -1
- package/package.json +1 -1
- package/apps/web/dist/assets/AboutDialog-D1N4-3FE.js +0 -1
- package/apps/web/dist/assets/ChatView-DSRUEpb6.js +0 -10
- package/apps/web/dist/assets/EditorPage-UkaHDklX.js +0 -3
- package/apps/web/dist/assets/Marketplace-C-Vz3yEk.js +0 -2
- package/apps/web/dist/assets/NewWizardChannels-Cc7gDitg.js +0 -1
- package/apps/web/dist/assets/NewWizardPage-sY5q-l99.js +0 -7
- package/apps/web/dist/assets/ProjectPage-iSdpJAZS.js +0 -2
- package/apps/web/dist/assets/PublicRunner-BgRXMUxS.js +0 -1
- package/apps/web/dist/assets/RunDrawer-B0gJzdxH.js +0 -4
- package/apps/web/dist/assets/SettingsPage-CBF8i6j6.js +0 -1
- package/apps/web/dist/assets/SetupPage-Fpz9aSLN.js +0 -1
- package/apps/web/dist/assets/SharePage-Dv3Csh4A.js +0 -1
- package/apps/web/dist/assets/brand-BamUjXVr.js +0 -2
- package/apps/web/dist/assets/changelog-ViHoNW7e.js +0 -1
- package/apps/web/dist/assets/index-Bdx1Qity.js +0 -11
- package/apps/web/dist/assets/index-CDdJDojq.css +0 -1
- package/apps/web/dist/assets/session-BEHF6g4Y.js +0 -1
- package/apps/web/dist/assets/useMutation-DKyQXgyj.js +0 -1
- package/apps/web/dist/assets/volume-2-CyQuz1nn.js +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FONT_WEIGHTS } from "@engenty-wizards/shared/projects";
|
|
1
2
|
import { webContext } from "../render/chromium.js";
|
|
2
3
|
import { htmlToMarkdown } from "../render/convert.js";
|
|
3
4
|
import { safeFetch } from "../tools/net-guard.js";
|
|
@@ -29,28 +30,60 @@ function hex(value) {
|
|
|
29
30
|
}
|
|
30
31
|
const rgb = v.match(/^rgba?\(\s*(\d{1,3})[\s,]+(\d{1,3})[\s,]+(\d{1,3})/);
|
|
31
32
|
if (rgb) {
|
|
32
|
-
return
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
return toHex(rgb.slice(1, 4).map(Number));
|
|
34
|
+
}
|
|
35
|
+
const hsl = v.match(/^hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
|
|
36
|
+
if (hsl) {
|
|
37
|
+
const [h, sat, l] = [Number(hsl[1]), Number(hsl[2]) / 100, Number(hsl[3]) / 100];
|
|
38
|
+
const f = (n) => {
|
|
39
|
+
const k = (n + h / 30) % 12;
|
|
40
|
+
return 255 * (l - sat * Math.min(l, 1 - l) * Math.max(-1, Math.min(k - 3, 9 - k, 1)));
|
|
41
|
+
};
|
|
42
|
+
return toHex([f(0), f(8), f(4)]);
|
|
43
|
+
}
|
|
44
|
+
const ok = v.match(/^oklch\(\s*([\d.]+)(%?)\s+([\d.]+)\s+([\d.]+)/);
|
|
45
|
+
if (ok) {
|
|
46
|
+
return toHex(oklchToRgb(Number(ok[1]) / (ok[2] ? 100 : 1), Number(ok[3]), Number(ok[4])));
|
|
36
47
|
}
|
|
37
48
|
return null;
|
|
38
49
|
}
|
|
50
|
+
const toHex = (rgb) => `#${rgb
|
|
51
|
+
.map((n) => Math.round(Math.max(0, Math.min(255, n)))
|
|
52
|
+
.toString(16)
|
|
53
|
+
.padStart(2, "0"))
|
|
54
|
+
.join("")}`;
|
|
55
|
+
/** OKLCH (as CSS writes it, Tailwind 4 among others) in sRGB, 0–255. */
|
|
56
|
+
function oklchToRgb(l, c, h) {
|
|
57
|
+
const a = c * Math.cos((h * Math.PI) / 180);
|
|
58
|
+
const b = c * Math.sin((h * Math.PI) / 180);
|
|
59
|
+
const l_ = (l + 0.3963377774 * a + 0.2158037573 * b) ** 3;
|
|
60
|
+
const m_ = (l - 0.1055613458 * a - 0.0638541728 * b) ** 3;
|
|
61
|
+
const s_ = (l - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
62
|
+
const linear = [
|
|
63
|
+
4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_,
|
|
64
|
+
-1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_,
|
|
65
|
+
-0.0041960863 * l_ - 0.7034186147 * m_ + 1.707614701 * s_,
|
|
66
|
+
];
|
|
67
|
+
return linear.map((x) => 255 * (x <= 0.0031308 ? 12.92 * x : 1.055 * Math.max(0, x) ** (1 / 2.4) - 0.055));
|
|
68
|
+
}
|
|
39
69
|
/** How far a colour is from grey, 0–255. */
|
|
40
70
|
function chroma(color) {
|
|
41
71
|
const [r, g, b] = [1, 3, 5].map((i) => Number.parseInt(color.slice(i, i + 2), 16));
|
|
42
72
|
return Math.max(r, g, b) - Math.min(r, g, b);
|
|
43
73
|
}
|
|
74
|
+
/** A colour as CSS writes it: hex, rgb(), hsl() or oklch(). */
|
|
75
|
+
const COLOUR = /#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\)|oklch\([^)]*\)/;
|
|
44
76
|
function coloursOf(css) {
|
|
45
77
|
const variables = [];
|
|
46
|
-
for (const m of css.matchAll(
|
|
78
|
+
for (const m of css.matchAll(new RegExp(`(--[\\w-]+)\\s*:\\s*(${COLOUR.source})`, "g"))) {
|
|
47
79
|
const value = hex(m[2]);
|
|
48
|
-
|
|
80
|
+
// Tailwind's own variables are its machinery, not the site's colours.
|
|
81
|
+
if (value && !m[1].startsWith("--tw-") && !variables.some((v) => v.name === m[1])) {
|
|
49
82
|
variables.push({ name: m[1], value });
|
|
50
83
|
}
|
|
51
84
|
}
|
|
52
85
|
const counts = new Map();
|
|
53
|
-
for (const m of css.matchAll(
|
|
86
|
+
for (const m of css.matchAll(new RegExp(COLOUR.source, "g"))) {
|
|
54
87
|
const value = hex(m[0]);
|
|
55
88
|
if (value) {
|
|
56
89
|
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
@@ -173,59 +206,247 @@ export async function readPage(raw, signal) {
|
|
|
173
206
|
jsonld: jsonLdOf(html),
|
|
174
207
|
};
|
|
175
208
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
209
|
+
const saysLogo = (tag) => /logo/i.test([
|
|
210
|
+
attr(tag, "src"),
|
|
211
|
+
attr(tag, "alt"),
|
|
212
|
+
attr(tag, "class"),
|
|
213
|
+
attr(tag, "id"),
|
|
214
|
+
attr(tag, "aria-label"),
|
|
215
|
+
]
|
|
216
|
+
.filter(Boolean)
|
|
217
|
+
.join(" "));
|
|
218
|
+
/**
|
|
219
|
+
* The logos a page shows, best first: the logo its structured data names, pictures that say
|
|
220
|
+
* "logo", what its home link in the header shows, and its icons. `inline`: SVGs drawn into the
|
|
221
|
+
* page count too, as `data:` addresses — for a person to pick from, too long for a model to read.
|
|
222
|
+
*/
|
|
223
|
+
function logosOf(html, base, inline = false) {
|
|
180
224
|
const logos = [];
|
|
181
|
-
const
|
|
182
|
-
const url = absolute(href, base);
|
|
225
|
+
const add = (href, hint) => {
|
|
226
|
+
const url = href.startsWith("data:image/svg+xml") ? href : absolute(href, base);
|
|
183
227
|
if (url && !logos.some((l) => l.url === url)) {
|
|
184
228
|
logos.push({ url, hint });
|
|
185
229
|
}
|
|
186
230
|
};
|
|
187
|
-
const
|
|
188
|
-
|
|
231
|
+
for (const item of jsonLdOf(html)) {
|
|
232
|
+
const logo = item?.logo;
|
|
233
|
+
const href = typeof logo === "string" ? logo : logo?.url;
|
|
234
|
+
if (typeof href === "string") {
|
|
235
|
+
add(href, "the logo the page names in its structured data");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (const tag of tagsOf(html, "img")) {
|
|
189
239
|
const src = attr(tag, "src") || attr(tag, "data-src");
|
|
190
|
-
|
|
191
|
-
|
|
240
|
+
if (src && !src.startsWith("data:") && saysLogo(tag)) {
|
|
241
|
+
const alt = attr(tag, "alt");
|
|
242
|
+
add(src, `image on the page${alt ? `, alt "${alt}"` : ""}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// What the link home shows is the logo on most sites, said so or not.
|
|
246
|
+
for (const m of html.matchAll(/<a\b([^>]*)>([\s\S]*?)<\/a>/gi)) {
|
|
247
|
+
const href = absolute(attr(`<a ${m[1]}>`, "href"), base);
|
|
248
|
+
if (!href || new URL(href).pathname !== "/" || new URL(href).host !== new URL(base).host) {
|
|
192
249
|
continue;
|
|
193
250
|
}
|
|
194
|
-
|
|
195
|
-
|
|
251
|
+
const img = tagsOf(m[2], "img")[0];
|
|
252
|
+
const src = img && (attr(img, "src") || attr(img, "data-src"));
|
|
253
|
+
if (src && !src.startsWith("data:")) {
|
|
254
|
+
add(src, "image in the link to the home page");
|
|
255
|
+
}
|
|
256
|
+
const svg = inline ? m[2].match(/<svg\b[\s\S]*?<\/svg>/i)?.[0] : undefined;
|
|
257
|
+
if (svg) {
|
|
258
|
+
add(svgUrl(svg), "drawing in the link to the home page");
|
|
196
259
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
260
|
+
}
|
|
261
|
+
if (inline) {
|
|
262
|
+
for (const m of html.matchAll(/<svg\b[^>]*>[\s\S]*?<\/svg>/gi)) {
|
|
263
|
+
const open = m[0].match(/^<svg\b[^>]*>/i)?.[0] ?? "";
|
|
264
|
+
const title = m[0].match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1] ?? "";
|
|
265
|
+
if (saysLogo(open) || /logo/i.test(title)) {
|
|
266
|
+
add(svgUrl(m[0]), "drawing on the page");
|
|
201
267
|
}
|
|
202
268
|
}
|
|
203
269
|
}
|
|
204
|
-
for (const tag of
|
|
270
|
+
for (const tag of tagsOf(html, "link")) {
|
|
205
271
|
const rel = attr(tag, "rel").toLowerCase();
|
|
206
272
|
if (/apple-touch-icon|mask-icon|(^|\s)icon/.test(rel)) {
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
const og = meta("og:image");
|
|
211
|
-
if (og) {
|
|
212
|
-
const url = absolute(og, base);
|
|
213
|
-
if (url) {
|
|
214
|
-
images.unshift({ url, alt: "share image (og:image)" });
|
|
273
|
+
add(attr(tag, "href"), `${rel} ${attr(tag, "sizes")}`.trim());
|
|
215
274
|
}
|
|
216
275
|
}
|
|
217
|
-
|
|
276
|
+
return logos;
|
|
277
|
+
}
|
|
278
|
+
/** An inline SVG as a picture of its own: with its namespace, as a `data:` address. */
|
|
279
|
+
function svgUrl(svg) {
|
|
280
|
+
const own = /\bxmlns\s*=/.test(svg.match(/^<svg\b[^>]*>/i)?.[0] ?? "")
|
|
281
|
+
? svg
|
|
282
|
+
: svg.replace(/^<svg\b/i, '<svg xmlns="http://www.w3.org/2000/svg"');
|
|
283
|
+
return `data:image/svg+xml;base64,${Buffer.from(own).toString("base64")}`;
|
|
284
|
+
}
|
|
285
|
+
/** The logos on a web page, for a person to pick from (Logos › from a web address). */
|
|
286
|
+
export async function scanLogos(raw, signal) {
|
|
287
|
+
const { base, html } = await loadPage(raw, signal);
|
|
288
|
+
return logosOf(html, base, true)
|
|
289
|
+
.filter((l) => !l.url.startsWith("data:") || l.url.length < 400_000)
|
|
290
|
+
.slice(0, 16);
|
|
291
|
+
}
|
|
292
|
+
/** The CSS a page brings: its `<style>` blocks, style attributes and first stylesheets. */
|
|
293
|
+
async function cssOf(html, base, signal) {
|
|
294
|
+
const sheets = tagsOf(html, "link")
|
|
218
295
|
.filter((t) => /stylesheet/i.test(attr(t, "rel")))
|
|
219
296
|
.map((t) => absolute(attr(t, "href"), base))
|
|
220
297
|
.filter((u) => Boolean(u))
|
|
221
298
|
.slice(0, 4);
|
|
222
299
|
const inline = [...html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)].map((m) => m[1]);
|
|
223
300
|
const styled = [...html.matchAll(/\bstyle\s*=\s*"([^"]*)"/gi)].map((m) => m[1]);
|
|
224
|
-
|
|
301
|
+
return [
|
|
225
302
|
...inline,
|
|
226
303
|
...styled,
|
|
227
304
|
...(await Promise.all(sheets.map((s) => stylesheet(s, signal)))),
|
|
228
305
|
].join("\n");
|
|
306
|
+
}
|
|
307
|
+
/** Family names that are no brand's own: generic and system faces. */
|
|
308
|
+
const GENERIC_FONT = /^(serif|sans-serif|monospace|cursive|fantasy|system-ui|ui-[a-z-]+|-apple-system|blinkmacsystemfont|inherit|initial|unset|revert|emoji|math|fangsong|segoe ui( emoji| symbol)?|helvetica( neue)?|arial|roboto|noto (sans|color emoji)|apple color emoji|courier( new)?|times( new roman)?|georgia|menlo|monaco|consolas|var\(.*|liberation mono|sfmono-regular|sf mono|sf pro( text| display)?|dejavu sans( mono)?)$/i;
|
|
309
|
+
/** "Geist Variable" is Geist: a variable font's file name, not another family. */
|
|
310
|
+
const familyName = (raw) => raw
|
|
311
|
+
.replace(/^[\s"'(]+|[\s"')]+$/g, "")
|
|
312
|
+
.replace(/\s+variable$/i, "")
|
|
313
|
+
.trim();
|
|
314
|
+
/**
|
|
315
|
+
* The typefaces a page uses, most used first, with the weights its `@font-face` rules or its
|
|
316
|
+
* Google / Bunny Fonts links name.
|
|
317
|
+
*/
|
|
318
|
+
function fontsOf(css, html) {
|
|
319
|
+
const counts = new Map();
|
|
320
|
+
const weights = new Map();
|
|
321
|
+
const count = (family, n = 1) => counts.set(family, (counts.get(family) ?? 0) + n);
|
|
322
|
+
const weigh = (family, list) => {
|
|
323
|
+
const set = weights.get(family) ?? new Set();
|
|
324
|
+
for (const w of list) {
|
|
325
|
+
set.add(w);
|
|
326
|
+
}
|
|
327
|
+
weights.set(family, set);
|
|
328
|
+
};
|
|
329
|
+
const first = (decl) => decl
|
|
330
|
+
.split(",")
|
|
331
|
+
.map(familyName)
|
|
332
|
+
.find((f) => /[a-z]/i.test(f) && !GENERIC_FONT.test(f));
|
|
333
|
+
// --font-weight-…, --font-size-… and the like are no families.
|
|
334
|
+
for (const m of css.matchAll(/(?:font-family|--font-(?!weight|size|feature|variation|style|stretch)[\w-]+)\s*:\s*([^;}]+)/gi)) {
|
|
335
|
+
const family = first(m[1]);
|
|
336
|
+
if (family) {
|
|
337
|
+
count(family);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
for (const m of css.matchAll(/@font-face\s*\{([^}]*)\}/gi)) {
|
|
341
|
+
const family = first(m[1].match(/font-family\s*:\s*([^;]+)/i)?.[1] ?? "");
|
|
342
|
+
const range = (m[1].match(/font-weight\s*:\s*([^;]+)/i)?.[1] ?? "400")
|
|
343
|
+
.split(/\s+/)
|
|
344
|
+
.map(Number)
|
|
345
|
+
.filter((n) => n >= 100 && n <= 900);
|
|
346
|
+
if (family && range.length) {
|
|
347
|
+
const [lo, hi] = [Math.min(...range), Math.max(...range)];
|
|
348
|
+
weigh(family, FONT_WEIGHTS.filter((w) => w >= lo && w <= hi));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// fonts.googleapis.com/css2?family=Inter:wght@400;700 and fonts.bunny.net/css?family=inter:400,700
|
|
352
|
+
for (const m of html.matchAll(/fonts\.(?:googleapis\.com|bunny\.net)\/css2?\?([^"'\s>]+)/gi)) {
|
|
353
|
+
for (const part of m[1].replace(/&/g, "&").split("&")) {
|
|
354
|
+
if (!part.startsWith("family=")) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
for (const one of decodeURIComponent(part.slice(7)).split("|")) {
|
|
358
|
+
const [name, spec = ""] = one.split(":");
|
|
359
|
+
const family = familyName(name.replace(/\+/g, " ").replace(/-/g, " ")).replace(/\b\w/g, (c) => c.toUpperCase());
|
|
360
|
+
count(family, 3);
|
|
361
|
+
weigh(family, (spec.match(/\d{3}/g) ?? []).map(Number).filter((w) => w >= 100 && w <= 900));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return [...counts.entries()]
|
|
366
|
+
.sort((a, b) => b[1] - a[1])
|
|
367
|
+
.slice(0, 6)
|
|
368
|
+
.map(([family]) => ({
|
|
369
|
+
family,
|
|
370
|
+
weights: [...(weights.get(family) ?? [])].sort((a, b) => a - b),
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
/** The typefaces of a web page, for a person to pick from (Typography › from a web address). */
|
|
374
|
+
export async function scanFonts(raw, signal) {
|
|
375
|
+
const { base, html } = await loadPage(raw, signal);
|
|
376
|
+
return fontsOf(await cssOf(html, base, signal), html);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* The colours of a web page, for a person to pick from (Colours › from a web address): its
|
|
380
|
+
* theme colour, the colours its stylesheet names, then the ones it uses most. A named one is
|
|
381
|
+
* named after its variable ("--brand-primary" → "brand primary").
|
|
382
|
+
*/
|
|
383
|
+
export async function scanColors(raw, signal) {
|
|
384
|
+
const { base, html } = await loadPage(raw, signal);
|
|
385
|
+
const { variables, frequent } = coloursOf(await cssOf(html, base, signal));
|
|
386
|
+
const out = [];
|
|
387
|
+
const add = (name, value) => {
|
|
388
|
+
if (value && !out.some((c) => c.value === value)) {
|
|
389
|
+
out.push({ name, value });
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
add("theme color", hex(metaOf(html, "theme-color")));
|
|
393
|
+
for (const v of variables) {
|
|
394
|
+
add(v.name.replace(/^--/, "").replace(/[-_]+/g, " "), v.value);
|
|
395
|
+
}
|
|
396
|
+
for (const value of frequent) {
|
|
397
|
+
add("", value);
|
|
398
|
+
}
|
|
399
|
+
return out.slice(0, 24);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* The pictures of a web page, for a person to pick assets from: its share image, then the
|
|
403
|
+
* images it shows, logos left out.
|
|
404
|
+
*/
|
|
405
|
+
export async function scanImages(raw, signal) {
|
|
406
|
+
const { base, html } = await loadPage(raw, signal);
|
|
407
|
+
const out = [];
|
|
408
|
+
const add = (href, hint) => {
|
|
409
|
+
const url = absolute(href, base);
|
|
410
|
+
if (url && !out.some((i) => i.url === url)) {
|
|
411
|
+
out.push({ url, hint });
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
const og = metaOf(html, "og:image");
|
|
415
|
+
if (og) {
|
|
416
|
+
add(og, "share image");
|
|
417
|
+
}
|
|
418
|
+
for (const tag of tagsOf(html, "img")) {
|
|
419
|
+
const src = attr(tag, "src") || attr(tag, "data-src");
|
|
420
|
+
if (src && !src.startsWith("data:") && !saysLogo(tag)) {
|
|
421
|
+
add(src, attr(tag, "alt"));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return out.slice(0, 24);
|
|
425
|
+
}
|
|
426
|
+
export async function readWebsite(raw, signal) {
|
|
427
|
+
const { res, base, html } = await loadPage(raw, signal);
|
|
428
|
+
const tags = (name) => tagsOf(html, name);
|
|
429
|
+
const meta = (key) => metaOf(html, key);
|
|
430
|
+
const logos = logosOf(html, base);
|
|
431
|
+
const images = [];
|
|
432
|
+
for (const tag of tags("img")) {
|
|
433
|
+
const src = attr(tag, "src") || attr(tag, "data-src");
|
|
434
|
+
if (!src || src.startsWith("data:") || saysLogo(tag)) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const url = absolute(src, base);
|
|
438
|
+
if (url && images.length < 12 && !images.some((i) => i.url === url)) {
|
|
439
|
+
images.push({ url, alt: attr(tag, "alt") });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const og = meta("og:image");
|
|
443
|
+
if (og) {
|
|
444
|
+
const url = absolute(og, base);
|
|
445
|
+
if (url) {
|
|
446
|
+
images.unshift({ url, alt: "share image (og:image)" });
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const css = await cssOf(html, base, signal);
|
|
229
450
|
const host = new URL(base).hostname;
|
|
230
451
|
const links = [];
|
|
231
452
|
for (const m of html.matchAll(/<a\b([^>]*)>([\s\S]*?)<\/a>/gi)) {
|
|
@@ -252,6 +473,7 @@ export async function readWebsite(raw, signal) {
|
|
|
252
473
|
...coloursOf(css),
|
|
253
474
|
logos: logos.slice(0, 10),
|
|
254
475
|
images,
|
|
476
|
+
fonts: fontsOf(css, html),
|
|
255
477
|
},
|
|
256
478
|
links: links.slice(0, 10),
|
|
257
479
|
};
|
|
@@ -209,7 +209,7 @@ export const projectFile = sqliteTable("project_file", {
|
|
|
209
209
|
projectId: text("project_id")
|
|
210
210
|
.notNull()
|
|
211
211
|
.references(() => project.id, { onDelete: "cascade" }),
|
|
212
|
-
kind: text("kind", { enum: ["logo", "asset", "document"] }).notNull(),
|
|
212
|
+
kind: text("kind", { enum: ["logo", "asset", "document", "pattern"] }).notNull(),
|
|
213
213
|
name: text("name").notNull(),
|
|
214
214
|
mime: text("mime").notNull(),
|
|
215
215
|
size: integer("size").notNull(),
|
|
@@ -15,18 +15,30 @@ measured from the latest runs after five of them.
|
|
|
15
15
|
|
|
16
16
|
## Publish
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
A wizard goes live the first time you share it: "Share" publishes the draft as version 1 as the
|
|
19
|
+
dialog opens (nobody has the link yet, and "Only you" under Access keeps it closed). A draft with
|
|
20
|
+
issues is not published; the dialog lists what to fix first, with "Let the assistant fix it".
|
|
21
|
+
|
|
22
|
+
After that the editor's top bar has "Make changes live" whenever the draft differs from the
|
|
23
|
+
version people run. It opens a dialog that says:
|
|
24
|
+
|
|
25
|
+
| Part | Says |
|
|
26
|
+
|---|---|
|
|
27
|
+
| What changed | The wizard's own settings, each step by its title (new, removed, or which of its parts: fields, instructions, result …), the steps' order, the files |
|
|
28
|
+
| Tested | Whether a test run used exactly this draft: "Tested 4 minutes ago – with these changes" and "See the run", or "Not tested with these changes yet" with "Test now" |
|
|
29
|
+
| Notes | Anyone in the middle of a run carries on with the version they started; what a run costs now where that changed; with an account, that the version goes to the cloud too |
|
|
30
|
+
|
|
31
|
+
"Go live" makes the draft the next version; the link stays the same. Issues stop it, as on the
|
|
32
|
+
first share. The editor's top bar and the wizard's card on the start page say where it runs:
|
|
20
33
|
|
|
21
34
|
| It says | Means |
|
|
22
35
|
|---|---|
|
|
23
|
-
| Not published yet · Draft | Only you can run it, as a test |
|
|
36
|
+
| Not published yet · Draft | Only you can run it, as a test. Sharing it publishes it |
|
|
24
37
|
| Live · version 3 | The link runs this version. With a cloud icon: on engenty.ai, for everyone with the link |
|
|
25
38
|
| Live · 1 note | It runs in the cloud, one step there with less (see "Share") |
|
|
26
39
|
| Local | Published on an install without an account: the link answers only on this computer |
|
|
27
40
|
| Being sent | The version did not reach engenty.ai yet; the studio tries again by itself |
|
|
28
41
|
| Not in the cloud | engenty.ai does not run this version. "Share" says why and what helps |
|
|
29
|
-
| Unpublished changes | You changed the draft since. The link still runs the published version |
|
|
30
42
|
|
|
31
43
|
On a card, "Link" copies the address others can open, and the arrow opens it. A wizard that
|
|
32
44
|
runs only on this computer has no address for others: its "Link" says so and how an account
|
|
@@ -34,8 +46,7 @@ gives it one, and the arrow opens it here.
|
|
|
34
46
|
|
|
35
47
|
## Share
|
|
36
48
|
|
|
37
|
-
"Share"
|
|
38
|
-
it, and copies the link.
|
|
49
|
+
"Share" says who may open the wizard and how others reach it, and copies the link.
|
|
39
50
|
|
|
40
51
|
| Part | Does |
|
|
41
52
|
|---|---|
|
|
@@ -46,8 +57,7 @@ it, and copies the link.
|
|
|
46
57
|
| Copy link | The link people open. It opens the first browser channel, and the wizard offers the others on its first page |
|
|
47
58
|
| The gear | Runs a day (50 unless you change it, with what that costs at most), each message channel's keyword, and "Create a new link": the old link and its QR code stop working, in the cloud too |
|
|
48
59
|
|
|
49
|
-
Where the
|
|
50
|
-
publishes from there.
|
|
60
|
+
Where the draft has changes, the dialog says so on top and leads to "Make changes live".
|
|
51
61
|
|
|
52
62
|
One visitor can start 6 runs an hour.
|
|
53
63
|
|
|
@@ -138,13 +148,13 @@ for anyone else. To let others run the wizard:
|
|
|
138
148
|
|
|
139
149
|
| Way | How |
|
|
140
150
|
|---|---|
|
|
141
|
-
| The cloud | With an [account](./models.md#account) signed in, "
|
|
151
|
+
| The cloud | With an [account](./models.md#account) signed in, every version that goes live (the first share, and each "Go live") also goes to engenty.ai. Its link there runs on the account's credits, also while your computer is off |
|
|
142
152
|
| Your own server | [Run engenty wizards on a server](./server.md) with a public https address |
|
|
143
153
|
|
|
144
154
|
### In the cloud
|
|
145
155
|
|
|
146
|
-
Once an account is signed in under Settings → Account, every
|
|
147
|
-
|
|
156
|
+
Once an account is signed in under Settings → Account, every version that goes live is sent to
|
|
157
|
+
engenty.ai too. The avatar then carries a small cloud, and its menu says "Connected to
|
|
148
158
|
engenty.ai" with the credits. The share dialog gives the wizard's address at engenty.ai, which
|
|
149
159
|
stays the same when you publish again. Where the cloud does not run this version, the dialog
|
|
150
160
|
says so on top:
|
|
@@ -35,6 +35,20 @@ const MIME_BY_EXT = {
|
|
|
35
35
|
jpeg: "image/jpeg",
|
|
36
36
|
webp: "image/webp",
|
|
37
37
|
gif: "image/gif",
|
|
38
|
+
svg: "image/svg+xml",
|
|
39
|
+
avif: "image/avif",
|
|
40
|
+
mp4: "video/mp4",
|
|
41
|
+
mov: "video/quicktime",
|
|
42
|
+
webm: "video/webm",
|
|
43
|
+
mp3: "audio/mpeg",
|
|
44
|
+
wav: "audio/wav",
|
|
45
|
+
m4a: "audio/mp4",
|
|
46
|
+
woff2: "font/woff2",
|
|
47
|
+
woff: "font/woff",
|
|
48
|
+
ttf: "font/ttf",
|
|
49
|
+
otf: "font/otf",
|
|
50
|
+
glb: "model/gltf-binary",
|
|
51
|
+
gltf: "model/gltf+json",
|
|
38
52
|
};
|
|
39
53
|
/** The type to read a file as: mail servers and browsers often only say "octet-stream". */
|
|
40
54
|
export function documentMime(name, mime) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { pOf } from "@engenty-wizards/shared/decision";
|
|
2
2
|
import { allFields, dataRef, isDecisionStep, LOCALES, templateRefs, wizardLang, } from "@engenty-wizards/shared/definition";
|
|
3
|
+
import { mainLogo, ruleLines } from "@engenty-wizards/shared/projects";
|
|
3
4
|
import { SURFACE_GUIDE, validateSurface, } from "@engenty-wizards/shared/surface";
|
|
4
5
|
import { Agent } from "@mastra/core/agent";
|
|
5
6
|
import { generateText, Output } from "ai";
|
|
@@ -18,7 +19,7 @@ import { runWidgetStep } from "../widgets/step.js";
|
|
|
18
19
|
import { decide } from "./decide.js";
|
|
19
20
|
import { prepareInputs } from "./prepare.js";
|
|
20
21
|
import { runSurfaceStep } from "./surface.js";
|
|
21
|
-
import { answersAsText, colorLine, factLines, renderTemplate, resolveRef, } from "./template.js";
|
|
22
|
+
import { answersAsText, assetSectionLine, colorLine, factLines, logoSectionLine, renderTemplate, resolveRef, typeLines, } from "./template.js";
|
|
22
23
|
import { StepError } from "./types.js";
|
|
23
24
|
/** Today's date as the wizard's people write it. */
|
|
24
25
|
function today(ctx) {
|
|
@@ -28,13 +29,26 @@ function today(ctx) {
|
|
|
28
29
|
day: "numeric",
|
|
29
30
|
});
|
|
30
31
|
}
|
|
32
|
+
const sectionTitle = (s) => s.description ? `${s.title} – ${s.description}` : s.title;
|
|
31
33
|
function brandBlock(ctx) {
|
|
32
34
|
const b = ctx.scope.brand;
|
|
33
35
|
const parts = [
|
|
34
36
|
b.name,
|
|
35
37
|
b.about,
|
|
36
38
|
b.facts?.length ? `Facts:\n${factLines(b.facts)}` : "",
|
|
37
|
-
b.colors?.length
|
|
39
|
+
b.colors?.length
|
|
40
|
+
? `Colours (the first is the accent): ${colorLine(b.colors, b.colorSections, b.gradients)}`
|
|
41
|
+
: "",
|
|
42
|
+
b.colorRules?.length
|
|
43
|
+
? `Rules for the colours, in the brand's words:\n${ruleLines(b.colorRules)}`
|
|
44
|
+
: "",
|
|
45
|
+
b.logoRules?.length
|
|
46
|
+
? `Rules for the logo, in the brand's words:\n${ruleLines(b.logoRules)}`
|
|
47
|
+
: "",
|
|
48
|
+
b.fonts?.length || b.textStyles?.length
|
|
49
|
+
? `Typography (set text in these fonts, with system fallbacks):\n${typeLines(b.fonts, b.textStyles)}`
|
|
50
|
+
: "",
|
|
51
|
+
b.typeRules?.length ? `Rules for type, in the brand's words:\n${ruleLines(b.typeRules)}` : "",
|
|
38
52
|
].filter(Boolean);
|
|
39
53
|
return parts.length ? `# WHO THIS WIZARD BELONGS TO\n${parts.join("\n")}` : "";
|
|
40
54
|
}
|
|
@@ -656,13 +670,35 @@ function placeableImages(step, ctx) {
|
|
|
656
670
|
}
|
|
657
671
|
}
|
|
658
672
|
const logos = ctx.projectFiles.filter((f) => f.kind === "logo");
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
673
|
+
const main = mainLogo(logos, ctx.scope.brand.logoSections);
|
|
674
|
+
for (const f of logos) {
|
|
675
|
+
const section = logoSectionLine(ctx.scope.brand.logoSections, f.id);
|
|
676
|
+
out.push({
|
|
677
|
+
id: f.id,
|
|
678
|
+
label: [
|
|
679
|
+
f === main ? "Company logo" : "Logo variant",
|
|
680
|
+
section ? ` — section ${section}` : "",
|
|
681
|
+
f.description ? `: ${f.description}` : "",
|
|
682
|
+
].join(""),
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
for (const p of ctx.scope.brand.patterns ?? []) {
|
|
686
|
+
const file = ctx.projectFiles.find((f) => f.id === p.file);
|
|
687
|
+
const section = ctx.scope.brand.colorSections?.find((s) => s.id === p.section);
|
|
688
|
+
if (file) {
|
|
689
|
+
out.push({
|
|
690
|
+
id: file.id,
|
|
691
|
+
label: `${p.tile ? "Pattern to tile" : "Background fill"}: ${p.name || file.name}${section ? ` (${sectionTitle(section)})` : ""}`,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
662
694
|
}
|
|
663
695
|
for (const f of ctx.projectFiles) {
|
|
664
696
|
if (f.kind === "asset" && f.mime.startsWith("image/")) {
|
|
665
|
-
|
|
697
|
+
const section = assetSectionLine(ctx.scope.brand.assetSections, f.id);
|
|
698
|
+
out.push({
|
|
699
|
+
id: f.id,
|
|
700
|
+
label: `${f.description || f.name} (from the project's assets${section ? `, section ${section}` : ""})`,
|
|
701
|
+
});
|
|
666
702
|
}
|
|
667
703
|
}
|
|
668
704
|
return out;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { allFields, isAudioValue, isLocationValue, itemsTotals, LOCALES, locationText, wizardLang, } from "@engenty-wizards/shared/definition";
|
|
2
|
+
import { gradientCss, textStyleLine, } from "@engenty-wizards/shared/projects";
|
|
2
3
|
import { listAsMarkdown } from "@engenty-wizards/shared/store";
|
|
3
4
|
const TEMPLATE_RE = /\{\{\s*([^}]+?)\s*\}\}/g;
|
|
4
5
|
function money(n, currency, locale) {
|
|
@@ -34,8 +35,52 @@ export function itemsAsMarkdown(field, value, values, lang = "de") {
|
|
|
34
35
|
export function factLines(facts) {
|
|
35
36
|
return facts.map((f) => `${f.label}: ${f.value}`).join("\n");
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* The colours as a model reads them; a section's after the standard ones, with when to use them.
|
|
40
|
+
* A gradients section names its gradients as CSS.
|
|
41
|
+
*/
|
|
42
|
+
export function colorLine(colors, sections = [], gradients = []) {
|
|
43
|
+
const line = (list) => list.map((c) => (c.name ? `${c.name} ${c.value}` : c.value)).join(", ");
|
|
44
|
+
const known = (c) => sections.some((s) => s.id === c.section);
|
|
45
|
+
return [
|
|
46
|
+
line(colors.filter((c) => !known(c))),
|
|
47
|
+
...sections.map((s) => {
|
|
48
|
+
const head = s.description ? `${s.title} (${s.description})` : s.title;
|
|
49
|
+
if (s.kind === "gradients") {
|
|
50
|
+
const own = gradients.filter((g) => g.section === s.id);
|
|
51
|
+
return own.length
|
|
52
|
+
? `${head}: ${own.map((g) => `${g.name || "gradient"} ${gradientCss(g)}`).join("; ")}`
|
|
53
|
+
: "";
|
|
54
|
+
}
|
|
55
|
+
const own = colors.filter((c) => c.section === s.id);
|
|
56
|
+
return own.length ? `${head}: ${line(own)}` : "";
|
|
57
|
+
}),
|
|
58
|
+
]
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
.join(" · ");
|
|
61
|
+
}
|
|
62
|
+
/** The brand's type as a model reads it: its fonts, then its text styles. */
|
|
63
|
+
export function typeLines(fonts = [], styles = []) {
|
|
64
|
+
const families = fonts.map((f) => {
|
|
65
|
+
const weights = f.weights.length ? `, weights ${f.weights.join("/")}` : "";
|
|
66
|
+
return `- ${f.family}${f.role ? ` (${f.role})` : ""}${weights}`;
|
|
67
|
+
});
|
|
68
|
+
const scale = styles.map((s) => `- ${s.name}: ${textStyleLine(s, fonts)}`);
|
|
69
|
+
return [
|
|
70
|
+
families.length ? `Fonts:\n${families.join("\n")}` : "",
|
|
71
|
+
scale.length ? `Text styles:\n${scale.join("\n")}` : "",
|
|
72
|
+
]
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.join("\n");
|
|
75
|
+
}
|
|
76
|
+
const sectionWords = (s) => !s ? "" : s.description ? `${s.title} (${s.description})` : s.title;
|
|
77
|
+
/** The logo's section in words — "Logomark (small spaces, app icons)" — or "" where it has none. */
|
|
78
|
+
export function logoSectionLine(sections, fileId) {
|
|
79
|
+
return sectionWords(sections?.find((x) => x.logos.includes(fileId)));
|
|
80
|
+
}
|
|
81
|
+
/** The title of the asset's section — "Icons" — or "" where it has none. */
|
|
82
|
+
export function assetSectionLine(sections, fileId) {
|
|
83
|
+
return sections?.find((x) => x.assets.includes(fileId))?.title ?? "";
|
|
39
84
|
}
|
|
40
85
|
function brandRef(brand, key) {
|
|
41
86
|
switch (key) {
|
|
@@ -49,7 +94,9 @@ function brandRef(brand, key) {
|
|
|
49
94
|
case "accent":
|
|
50
95
|
return brand.colors?.[0]?.value ?? "";
|
|
51
96
|
case "colors":
|
|
52
|
-
return colorLine(brand.colors ?? []);
|
|
97
|
+
return colorLine(brand.colors ?? [], brand.colorSections, brand.gradients);
|
|
98
|
+
case "fonts":
|
|
99
|
+
return typeLines(brand.fonts, brand.textStyles);
|
|
53
100
|
default:
|
|
54
101
|
return "";
|
|
55
102
|
}
|