wizards 0.3.6 → 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/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 +37 -1
- 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/web/dist/assets/AboutDialog-C0dN86zd.js +1 -0
- package/apps/web/dist/assets/{ChangelogDialog-m2M7gZiB.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-BdRy7FO7.js → FlowPreview-D4dp1MWS.js} +1 -1
- package/apps/web/dist/assets/{Harness-DmJkTwOO.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-CaGzgOPe.js → ReadOnly-B-UCgnqW.js} +1 -1
- package/apps/web/dist/assets/RunDrawer-C6pzi8AJ.js +4 -0
- package/apps/web/dist/assets/{RunnerView-CJ4cLA_0.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-CpuMIhbH.js → circle-alert-DLvC9ePT.js} +1 -1
- package/apps/web/dist/assets/{esm-DRaZAcXQ.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-BGotwAyN.js → lock-DR7NakPn.js} +1 -1
- package/apps/web/dist/assets/{phone-CA2A8W7I.js → phone-BORh3jfv.js} +1 -1
- package/apps/web/dist/assets/{settings-menu-DxswVOqQ.js → settings-menu-DV5D91TD.js} +1 -1
- package/apps/web/dist/assets/{ui-DAHMnAVc.js → ui-BOCXzzHi.js} +1 -1
- package/apps/web/dist/assets/{upload-DLbJVvfd.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/projects.d.ts +138 -3
- package/node_modules/@engenty-wizards/shared/dist/projects.js +153 -4
- package/package.json +1 -1
- package/apps/web/dist/assets/AboutDialog-Dz_tYBSU.js +0 -1
- package/apps/web/dist/assets/ChatView-D21o-lq8.js +0 -10
- package/apps/web/dist/assets/EditorPage-D6j3dQpu.js +0 -3
- package/apps/web/dist/assets/Marketplace-B-gy2onK.js +0 -2
- package/apps/web/dist/assets/NewWizardChannels-B3FEpsJ-.js +0 -1
- package/apps/web/dist/assets/NewWizardPage-CT1lThNM.js +0 -7
- package/apps/web/dist/assets/ProjectPage-BIanCF0m.js +0 -2
- package/apps/web/dist/assets/PublicRunner-H1fXa9BK.js +0 -1
- package/apps/web/dist/assets/RunDrawer-8sjkAbm5.js +0 -4
- package/apps/web/dist/assets/SettingsPage-BsdwE1OA.js +0 -1
- package/apps/web/dist/assets/SetupPage-sqrv7Auf.js +0 -1
- package/apps/web/dist/assets/SharePage-B6ets1ot.js +0 -1
- package/apps/web/dist/assets/brand-BRw18WJF.js +0 -2
- package/apps/web/dist/assets/changelog-BOFdRjx2.js +0 -1
- package/apps/web/dist/assets/index-DY9CslcT.css +0 -1
- package/apps/web/dist/assets/index-DkDBEMwz.js +0 -11
- package/apps/web/dist/assets/session-DBOQL_bg.js +0 -1
- package/apps/web/dist/assets/useMutation-DKyQXgyj.js +0 -1
- package/apps/web/dist/assets/volume-2-KaKQ4zMJ.js +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { anthropic } from "@ai-sdk/anthropic";
|
|
2
|
-
import { FACT_TYPES, factKey, PROJECT_FILE_KINDS, PROJECT_LIMITS,
|
|
2
|
+
import { cleanAssetSections, cleanLogoSections, FACT_TYPES, factKey, gradientCss, PROJECT_FILE_KINDS, PROJECT_LIMITS, parseGradient, RULE_MARKS, textStyleLine, } from "@engenty-wizards/shared/projects";
|
|
3
3
|
import { Agent } from "@mastra/core/agent";
|
|
4
4
|
import { createTool } from "@mastra/core/tools";
|
|
5
5
|
import { nanoid } from "nanoid";
|
|
@@ -9,12 +9,11 @@ import { PRIMER } from "../assistant/primer.js";
|
|
|
9
9
|
import { attachTools, costOf, gatewayTools, textModel } from "../models.js";
|
|
10
10
|
import { ServiceError } from "../services/errors.js";
|
|
11
11
|
import { knowledgeCounts } from "../services/knowledge.js";
|
|
12
|
-
import {
|
|
12
|
+
import { addProjectFileFromUrl, projectFileContent, projectFiles, updateProjectFile, } from "../services/project-files.js";
|
|
13
13
|
import { documentText } from "../services/project-index.js";
|
|
14
|
-
import {
|
|
14
|
+
import { ownedProject, updateProject } from "../services/projects.js";
|
|
15
15
|
import { categoryRows } from "../services/space-categories.js";
|
|
16
16
|
import { currentTenant } from "../tenants/tenant.js";
|
|
17
|
-
import { safeFetch } from "../tools/net-guard.js";
|
|
18
17
|
import { assistantPageOf, assistantToolsOf } from "../tools/plugin.js";
|
|
19
18
|
import { readWebsite } from "./website.js";
|
|
20
19
|
const MAX_STEPS = 24;
|
|
@@ -42,8 +41,11 @@ THE SPACE
|
|
|
42
41
|
- A website: read_website it first. You get the page's text, colours from its stylesheet, logo candidates and the links to imprint / contact / about. Read those pages too (at most four more) for address, VAT id, register number, phone, email, bank details, who runs it.
|
|
43
42
|
- Facts: one fact per thing, a short label in the person's language ("Adresse", "UID", "Telefon"), the value on one line where it fits. Only what a source or the person states — never guess, never invent. Keep the facts that are there; a fact with a known label is updated.
|
|
44
43
|
- About: two to five sentences — what they do, for whom, what sets them apart, the tone they write in. Rewrite it only when you learned something new or the person asks.
|
|
45
|
-
- Colours: two to six, the main one first, each with a name that says what it is for ("Primär", "Hintergrund", "Text"). Take them from the stylesheet's variables or its most used colours, never from photos. set_colors replaces the list: pass all colours that should remain.
|
|
44
|
+
- Colours: two to six, the main one first, each with a name that says what it is for ("Primär", "Hintergrund", "Text"). Take them from the stylesheet's variables or its most used colours, never from photos. set_colors replaces the list: pass all colours that should remain. Colours in no section are the standard ones; the first of them is the accent. Colour sections are optional like logo sections: only for a further palette a brand guide or the person names for its own use ("Illustration – drawings and charts"), never for the standard colours. A brand's gradients (set_gradients, as CSS) only where a brand guide or the site's stylesheet has them; patterns and background fills are pictures the person uploads.
|
|
46
45
|
- Logos: save the best candidate (an SVG or a large PNG rather than a favicon); a second one only if it truly differs (for dark ground, a signet). A few images that show the business (header, team, products) may be saved as assets — a handful, never everything.
|
|
46
|
+
- Logo sections and rules are optional, and most spaces need neither. Logos in no section are the standard ones; the first of them is the main logo. Add a section (set_logo_sections) only for further variants that a brand guide, a brand or press page or the person says are used elsewhere: the title names the variant ("Logomark", "Wordmark"), the description says when to use it ("Small spaces: app icons, favicons"). The standard logos never go into a section; one logo, or a light and a dark one whose descriptions already tell them apart, get no sections. Rules (set_rules, for the logos, the colours or the type) are short rows, one rule each, with a sign: do ("Keep clear space of the mark's height on all sides"), dont ("Place it on photos"), important, question (something to check with the person), number (steps in order), or none for a plain note (minimum size, file formats). Only what such a source or the person states; never invent rules and never write general advice a brand guide did not say.
|
|
47
|
+
- Typography: the brand's fonts (set_typography), each with what it is for ("Headings", "Body text") and the weights it uses — from the website's stylesheet (read_website lists its fonts) or a brand guide; system and generic faces are no brand fonts. A type scale (text styles: name, size, weight, line height, letter spacing) only when a brand guide gives one.
|
|
48
|
+
- Asset sections are optional too: tabs of one kind each ("Icons", "Background images"), made with set_asset_sections only when the space holds enough assets of clearly different kinds or the person asks; a handful of pictures stay in the general tab.
|
|
47
49
|
- Files the person attached are stored already; the message names them. look_at_file shows you a picture or a document's text. Give a file a description with describe_file when it has none or the person says what it is. Take facts from a document when asked.
|
|
48
50
|
- Wissen (German „Wissen“) is what the wizards' steps look things up in: pages, tables and files, kept apart by typed Kategorien. Documents the person attaches are read into Wissen by themselves: say so; do not copy their text into facts unless asked. A website, a sitemap or a folder whose entries should go into Wissen is a source: where a tool proposes sources, use it (the person takes the proposal on its card); where there is none, say that sources need the Sources plugin.
|
|
49
51
|
- Talking from another page than Info & brand, change title, about, logos, colours and facts only when the person asks.
|
|
@@ -55,6 +57,11 @@ HOW YOU WORK
|
|
|
55
57
|
const LABELS = {
|
|
56
58
|
save_logo: { de: "Holt das Logo …", en: "Fetching the logo …" },
|
|
57
59
|
save_file: { de: "Holt eine Datei …", en: "Fetching a file …" },
|
|
60
|
+
set_logo_sections: { de: "Ordnet die Logos …", en: "Grouping the logos …" },
|
|
61
|
+
set_asset_sections: { de: "Ordnet die Assets …", en: "Grouping the assets …" },
|
|
62
|
+
set_typography: { de: "Trägt die Schriften ein …", en: "Entering the fonts …" },
|
|
63
|
+
set_gradients: { de: "Trägt die Verläufe ein …", en: "Entering the gradients …" },
|
|
64
|
+
set_rules: { de: "Trägt Regeln ein …", en: "Entering rules …" },
|
|
58
65
|
set_profile: { de: "Schreibt Titel und Beschreibung …", en: "Writing title and description …" },
|
|
59
66
|
set_facts: { de: "Trägt Fakten ein …", en: "Entering facts …" },
|
|
60
67
|
set_colors: { de: "Trägt die Farben ein …", en: "Entering the colours …" },
|
|
@@ -107,7 +114,66 @@ async function projectState(userId, projectId) {
|
|
|
107
114
|
},
|
|
108
115
|
title: p.brand.name ?? "",
|
|
109
116
|
about: p.brand.about ?? "",
|
|
110
|
-
colors: p.brand.colors ?? [],
|
|
117
|
+
colors: (p.brand.colors ?? []).map(({ name, value, section }) => {
|
|
118
|
+
const s = p.brand.colorSections?.find((x) => x.id === section);
|
|
119
|
+
return { name, value, ...(s ? { section: s.title } : {}) };
|
|
120
|
+
}),
|
|
121
|
+
// Optional parts are only there when the space has them.
|
|
122
|
+
...(p.brand.colorSections?.length
|
|
123
|
+
? {
|
|
124
|
+
colorSections: p.brand.colorSections.map(({ title, description, kind }) => ({
|
|
125
|
+
title,
|
|
126
|
+
description,
|
|
127
|
+
kind: kind ?? "colors",
|
|
128
|
+
})),
|
|
129
|
+
}
|
|
130
|
+
: {}),
|
|
131
|
+
...(p.brand.gradients?.length
|
|
132
|
+
? {
|
|
133
|
+
gradients: p.brand.gradients.map((g) => ({
|
|
134
|
+
name: g.name,
|
|
135
|
+
section: p.brand.colorSections?.find((s) => s.id === g.section)?.title,
|
|
136
|
+
css: gradientCss(g),
|
|
137
|
+
})),
|
|
138
|
+
}
|
|
139
|
+
: {}),
|
|
140
|
+
...(p.brand.patterns?.length
|
|
141
|
+
? {
|
|
142
|
+
patterns: p.brand.patterns.map((x) => ({
|
|
143
|
+
name: x.name,
|
|
144
|
+
section: p.brand.colorSections?.find((s) => s.id === x.section)?.title,
|
|
145
|
+
tile: Boolean(x.tile),
|
|
146
|
+
})),
|
|
147
|
+
}
|
|
148
|
+
: {}),
|
|
149
|
+
...(p.brand.logoSections?.length
|
|
150
|
+
? {
|
|
151
|
+
logoSections: p.brand.logoSections.map(({ title, description, logos }) => ({
|
|
152
|
+
title,
|
|
153
|
+
description,
|
|
154
|
+
logos,
|
|
155
|
+
})),
|
|
156
|
+
}
|
|
157
|
+
: {}),
|
|
158
|
+
...(p.brand.assetSections?.length
|
|
159
|
+
? {
|
|
160
|
+
assetSections: p.brand.assetSections.map(({ title, assets }) => ({ title, assets })),
|
|
161
|
+
}
|
|
162
|
+
: {}),
|
|
163
|
+
...(p.brand.fonts?.length
|
|
164
|
+
? { fonts: p.brand.fonts.map(({ family, role, weights }) => ({ family, role, weights })) }
|
|
165
|
+
: {}),
|
|
166
|
+
...(p.brand.textStyles?.length
|
|
167
|
+
? {
|
|
168
|
+
textStyles: p.brand.textStyles.map((s) => ({
|
|
169
|
+
name: s.name,
|
|
170
|
+
style: textStyleLine(s, p.brand.fonts ?? []),
|
|
171
|
+
})),
|
|
172
|
+
}
|
|
173
|
+
: {}),
|
|
174
|
+
...(p.brand.logoRules ? { logoRules: p.brand.logoRules } : {}),
|
|
175
|
+
...(p.brand.colorRules ? { colorRules: p.brand.colorRules } : {}),
|
|
176
|
+
...(p.brand.typeRules ? { typeRules: p.brand.typeRules } : {}),
|
|
111
177
|
facts: p.facts.map(({ label, value }) => ({ label, value })),
|
|
112
178
|
files: files.map((f) => ({
|
|
113
179
|
id: f.id,
|
|
@@ -222,12 +288,220 @@ function spaceTools(input, project, wrote) {
|
|
|
222
288
|
}),
|
|
223
289
|
set_colors: createTool({
|
|
224
290
|
id: "set_colors",
|
|
225
|
-
description: "Set the brand's colours — the whole list, the main colour first. Values as #rrggbb.",
|
|
226
|
-
inputSchema: z.object({
|
|
227
|
-
|
|
291
|
+
description: "Set the brand's colours — the whole list, the main colour first. Values as #rrggbb. Optional: `sections` for a further palette of its own use (title, when to use it), and on a colour the `section` title it belongs to; colours without one are the standard ones. Without `sections` the space keeps its sections.",
|
|
292
|
+
inputSchema: z.object({
|
|
293
|
+
colors: z
|
|
294
|
+
.array(z.object({
|
|
295
|
+
name: z.string().max(120),
|
|
296
|
+
value: z.string().regex(/^#[0-9a-fA-F]{6}$/),
|
|
297
|
+
section: z.string().max(80).optional().describe("A section's title"),
|
|
298
|
+
}))
|
|
299
|
+
.max(PROJECT_LIMITS.colors),
|
|
300
|
+
sections: z
|
|
301
|
+
.array(z.object({ title: z.string().min(1).max(80), description: z.string().max(400) }))
|
|
302
|
+
.max(PROJECT_LIMITS.logoSections)
|
|
303
|
+
.optional(),
|
|
304
|
+
}),
|
|
305
|
+
execute: ({ colors, sections }) => attempt(() => inTurn(async () => {
|
|
228
306
|
doing("set_colors");
|
|
307
|
+
const p = await ownedProject(userId, projectId);
|
|
308
|
+
const groups = sections
|
|
309
|
+
? sections.map((s) => ({
|
|
310
|
+
id: p.brand.colorSections?.find((x) => x.title === s.title)?.id ?? nanoid(10),
|
|
311
|
+
...s,
|
|
312
|
+
}))
|
|
313
|
+
: (p.brand.colorSections ?? []);
|
|
314
|
+
const unknown = colors.filter((c) => c.section && !groups.some((s) => s.title === c.section));
|
|
315
|
+
if (unknown.length) {
|
|
316
|
+
throw new Error(`No colour section named: ${unknown.map((c) => c.section).join(", ")}`);
|
|
317
|
+
}
|
|
229
318
|
await updateProject(userId, projectId, {
|
|
230
|
-
brand: {
|
|
319
|
+
brand: {
|
|
320
|
+
colors: colors.map(({ section, ...c }) => {
|
|
321
|
+
const id = groups.find((s) => s.title === section)?.id;
|
|
322
|
+
return { ...c, value: c.value.toLowerCase(), ...(id ? { section: id } : {}) };
|
|
323
|
+
}),
|
|
324
|
+
colorSections: groups,
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
wrote();
|
|
328
|
+
return { ok: true };
|
|
329
|
+
})),
|
|
330
|
+
}),
|
|
331
|
+
set_logo_sections: createTool({
|
|
332
|
+
id: "set_logo_sections",
|
|
333
|
+
description: "Optional. Put further logo variants into sections, each with a title and when to use it (Logomark, Wordmark, Monochrome). Logos in no section are the standard ones and come first — never put those into a section. Use it only when a brand guide, a brand page or the person says a variant is used elsewhere — one logo or two plain variants need none. Replaces all sections: pass every section that should remain, an empty list removes them.",
|
|
334
|
+
inputSchema: z.object({
|
|
335
|
+
sections: z
|
|
336
|
+
.array(z.object({
|
|
337
|
+
title: z.string().min(1).max(80).describe("The variant: Logomark, Wordmark, …"),
|
|
338
|
+
description: z
|
|
339
|
+
.string()
|
|
340
|
+
.max(400)
|
|
341
|
+
.describe("When to use these logos, in the person's language"),
|
|
342
|
+
logos: z.array(z.string()).describe("Ids of the space's logo files, in order"),
|
|
343
|
+
}))
|
|
344
|
+
.max(PROJECT_LIMITS.logoSections),
|
|
345
|
+
}),
|
|
346
|
+
execute: ({ sections }) => attempt(() => inTurn(async () => {
|
|
347
|
+
doing("set_logo_sections");
|
|
348
|
+
const logos = (await projectFiles(projectId, "logo")).map((f) => f.id);
|
|
349
|
+
const unknown = sections.flatMap((s) => s.logos).filter((id) => !logos.includes(id));
|
|
350
|
+
if (unknown.length) {
|
|
351
|
+
throw new Error(`Not a logo of this space: ${unknown.join(", ")}`);
|
|
352
|
+
}
|
|
353
|
+
await updateProject(userId, projectId, {
|
|
354
|
+
brand: {
|
|
355
|
+
logoSections: cleanLogoSections(sections.map((s) => ({ ...s, id: nanoid(10) })), logos),
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
wrote();
|
|
359
|
+
return { ok: true };
|
|
360
|
+
})),
|
|
361
|
+
}),
|
|
362
|
+
set_asset_sections: createTool({
|
|
363
|
+
id: "set_asset_sections",
|
|
364
|
+
description: "Optional. Sort the space's assets into sections, each shown as a tab named by its title (Icons, Background images, Team photos). Assets in no section stay in the general tab. Use it only when there are enough assets of clearly different kinds, or the person asks. Replaces all sections: pass every section that should remain, an empty list removes them.",
|
|
365
|
+
inputSchema: z.object({
|
|
366
|
+
sections: z
|
|
367
|
+
.array(z.object({
|
|
368
|
+
title: z.string().min(1).max(80),
|
|
369
|
+
assets: z.array(z.string()).describe("Ids of the space's asset files, in order"),
|
|
370
|
+
}))
|
|
371
|
+
.max(PROJECT_LIMITS.logoSections),
|
|
372
|
+
}),
|
|
373
|
+
execute: ({ sections }) => attempt(() => inTurn(async () => {
|
|
374
|
+
doing("set_asset_sections");
|
|
375
|
+
const assets = (await projectFiles(projectId, "asset")).map((f) => f.id);
|
|
376
|
+
const unknown = sections.flatMap((s) => s.assets).filter((id) => !assets.includes(id));
|
|
377
|
+
if (unknown.length) {
|
|
378
|
+
throw new Error(`Not an asset of this space: ${unknown.join(", ")}`);
|
|
379
|
+
}
|
|
380
|
+
await updateProject(userId, projectId, {
|
|
381
|
+
brand: {
|
|
382
|
+
assetSections: cleanAssetSections(sections.map((s) => ({ ...s, id: nanoid(10) })), assets),
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
wrote();
|
|
386
|
+
return { ok: true };
|
|
387
|
+
})),
|
|
388
|
+
}),
|
|
389
|
+
set_typography: createTool({
|
|
390
|
+
id: "set_typography",
|
|
391
|
+
description: "Set the brand's fonts — the whole list, the main one first — each with what it is for and its weights. Optional: `textStyles`, the type scale a brand guide gives (name, font family, size px, weight, line height %, letter spacing %); without it the space keeps its text styles.",
|
|
392
|
+
inputSchema: z.object({
|
|
393
|
+
fonts: z
|
|
394
|
+
.array(z.object({
|
|
395
|
+
family: z.string().min(1).max(80),
|
|
396
|
+
role: z.string().max(200).describe("What it is for, in the person's language"),
|
|
397
|
+
weights: z.array(z.number().int().min(100).max(900)).max(9),
|
|
398
|
+
}))
|
|
399
|
+
.max(PROJECT_LIMITS.fonts),
|
|
400
|
+
textStyles: z
|
|
401
|
+
.array(z.object({
|
|
402
|
+
name: z.string().min(1).max(60),
|
|
403
|
+
family: z.string().max(80).optional().describe("One of the fonts; none: the first"),
|
|
404
|
+
size: z.number().min(6).max(400),
|
|
405
|
+
weight: z.number().int().min(100).max(900),
|
|
406
|
+
lineHeight: z.number().min(50).max(300).describe("% of the size"),
|
|
407
|
+
tracking: z.number().min(-20).max(50).describe("Letter spacing, % of the size"),
|
|
408
|
+
uppercase: z.boolean().optional(),
|
|
409
|
+
}))
|
|
410
|
+
.max(PROJECT_LIMITS.textStyles)
|
|
411
|
+
.optional(),
|
|
412
|
+
}),
|
|
413
|
+
execute: ({ fonts, textStyles }) => attempt(() => inTurn(async () => {
|
|
414
|
+
doing("set_typography");
|
|
415
|
+
const p = await ownedProject(userId, projectId);
|
|
416
|
+
const faces = fonts.map((f) => ({
|
|
417
|
+
...f,
|
|
418
|
+
id: p.brand.fonts?.find((x) => x.family === f.family)?.id ?? nanoid(10),
|
|
419
|
+
}));
|
|
420
|
+
await updateProject(userId, projectId, {
|
|
421
|
+
brand: {
|
|
422
|
+
fonts: faces,
|
|
423
|
+
...(textStyles
|
|
424
|
+
? {
|
|
425
|
+
textStyles: textStyles.map(({ family, ...s }) => ({
|
|
426
|
+
...s,
|
|
427
|
+
id: nanoid(10),
|
|
428
|
+
font: faces.find((f) => f.family === family)?.id,
|
|
429
|
+
})),
|
|
430
|
+
}
|
|
431
|
+
: {}),
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
wrote();
|
|
435
|
+
return { ok: true };
|
|
436
|
+
})),
|
|
437
|
+
}),
|
|
438
|
+
set_gradients: createTool({
|
|
439
|
+
id: "set_gradients",
|
|
440
|
+
description: "Optional. Set the brand's gradients, as a brand guide or the site's stylesheet gives them: each with a name and its CSS (linear-, radial- or conic-gradient with hex stops). They go into a gradients section, made when there is none with that title. Replaces the gradients of that section.",
|
|
441
|
+
inputSchema: z.object({
|
|
442
|
+
section: z.string().min(1).max(80).describe("The gradients section's title"),
|
|
443
|
+
description: z.string().max(400).optional().describe("When to use them"),
|
|
444
|
+
gradients: z
|
|
445
|
+
.array(z.object({ name: z.string().min(1).max(80), css: z.string().max(1000) }))
|
|
446
|
+
.max(PROJECT_LIMITS.gradients),
|
|
447
|
+
}),
|
|
448
|
+
execute: ({ section, description, gradients }) => attempt(() => inTurn(async () => {
|
|
449
|
+
doing("set_gradients");
|
|
450
|
+
const p = await ownedProject(userId, projectId);
|
|
451
|
+
const sections = p.brand.colorSections ?? [];
|
|
452
|
+
const found = sections.find((s) => s.title === section && s.kind === "gradients");
|
|
453
|
+
const id = found?.id ?? nanoid(10);
|
|
454
|
+
const parsed = gradients.map((g) => ({ g, read: parseGradient(g.css) }));
|
|
455
|
+
const bad = parsed.filter((x) => !x.read).map((x) => x.g.name);
|
|
456
|
+
if (bad.length) {
|
|
457
|
+
throw new Error(`Not a gradient with hex stops: ${bad.join(", ")}`);
|
|
458
|
+
}
|
|
459
|
+
await updateProject(userId, projectId, {
|
|
460
|
+
brand: {
|
|
461
|
+
colorSections: found
|
|
462
|
+
? sections.map((s) => (s.id === id && description ? { ...s, description } : s))
|
|
463
|
+
: [
|
|
464
|
+
...sections,
|
|
465
|
+
{ id, title: section, description: description ?? "", kind: "gradients" },
|
|
466
|
+
],
|
|
467
|
+
gradients: [
|
|
468
|
+
...(p.brand.gradients ?? []).filter((g) => g.section !== id),
|
|
469
|
+
...parsed.map(({ g, read }) => ({
|
|
470
|
+
id: nanoid(10),
|
|
471
|
+
name: g.name,
|
|
472
|
+
section: id,
|
|
473
|
+
...read,
|
|
474
|
+
})),
|
|
475
|
+
],
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
wrote();
|
|
479
|
+
return { ok: true };
|
|
480
|
+
})),
|
|
481
|
+
}),
|
|
482
|
+
set_rules: createTool({
|
|
483
|
+
id: "set_rules",
|
|
484
|
+
description: "Optional. Set how the logos, the colours or the type may be used: one short rule per row, with its sign (do, dont, important, question, number; none for a plain note). Only rules a brand guide, the website or the person states — never invented ones, never general advice. Replaces that list; an empty list removes it.",
|
|
485
|
+
inputSchema: z.object({
|
|
486
|
+
of: z.enum(["logos", "colors", "type"]),
|
|
487
|
+
rules: z
|
|
488
|
+
.array(z.object({
|
|
489
|
+
text: z.string().min(1).max(200),
|
|
490
|
+
mark: z
|
|
491
|
+
.enum(RULE_MARKS)
|
|
492
|
+
.optional()
|
|
493
|
+
.describe("do, dont, important, question, number; none for a plain note"),
|
|
494
|
+
}))
|
|
495
|
+
.max(PROJECT_LIMITS.logoRules),
|
|
496
|
+
}),
|
|
497
|
+
execute: ({ of, rules }) => attempt(() => inTurn(async () => {
|
|
498
|
+
doing("set_rules");
|
|
499
|
+
await updateProject(userId, projectId, {
|
|
500
|
+
brand: of === "logos"
|
|
501
|
+
? { logoRules: rules }
|
|
502
|
+
: of === "colors"
|
|
503
|
+
? { colorRules: rules }
|
|
504
|
+
: { typeRules: rules },
|
|
231
505
|
});
|
|
232
506
|
wrote();
|
|
233
507
|
return { ok: true };
|
|
@@ -247,27 +521,7 @@ function spaceTools(input, project, wrote) {
|
|
|
247
521
|
}),
|
|
248
522
|
execute: ({ url, kind, description }) => attempt(async () => {
|
|
249
523
|
doing("save_file", { kind });
|
|
250
|
-
const
|
|
251
|
-
headers: { "user-agent": "Mozilla/5.0 (compatible; engenty-wizards/0.1)" },
|
|
252
|
-
signal: AbortSignal.any([AbortSignal.timeout(60_000), ...(signal ? [signal] : [])]),
|
|
253
|
-
});
|
|
254
|
-
if (!res.ok) {
|
|
255
|
-
throw new Error(`The address answered ${res.status}.`);
|
|
256
|
-
}
|
|
257
|
-
const mime = (res.headers.get("content-type") ?? "").split(";")[0].trim().toLowerCase();
|
|
258
|
-
if (Number(res.headers.get("content-length") ?? 0) > projectFileLimit(kind, mime)) {
|
|
259
|
-
throw new Error("The file is too large.");
|
|
260
|
-
}
|
|
261
|
-
const data = new Uint8Array(await res.arrayBuffer());
|
|
262
|
-
const name = decodeURIComponent(new URL(res.url).pathname.split("/").pop() || kind);
|
|
263
|
-
const row = await addProjectFile(projectId, {
|
|
264
|
-
kind,
|
|
265
|
-
name,
|
|
266
|
-
mime: mime === "text/html" && kind !== "document" ? "" : mime,
|
|
267
|
-
data,
|
|
268
|
-
description,
|
|
269
|
-
source: res.url,
|
|
270
|
-
});
|
|
524
|
+
const row = await addProjectFileFromUrl(projectId, { url, kind, description, signal });
|
|
271
525
|
wrote();
|
|
272
526
|
return { ok: true, id: row.id, name: row.name, mime: row.mime, size: row.size };
|
|
273
527
|
}),
|