wizards 0.4.0 → 0.4.2

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.
Files changed (124) hide show
  1. package/apps/runtime/dist/agents/studio-assistant.js +67 -20
  2. package/apps/runtime/dist/docs/dev/plugins/reference.md +7 -4
  3. package/apps/runtime/dist/docs/dev/plugins/reloading.md +3 -3
  4. package/apps/runtime/dist/docs/dev/plugins/runners.md +9 -12
  5. package/apps/runtime/dist/docs/dev/plugins/server.md +3 -1
  6. package/apps/runtime/dist/docs/dev/plugins/space.md +19 -1
  7. package/apps/runtime/dist/docs/dev/plugins/studio.md +43 -6
  8. package/apps/runtime/dist/docs/dev/plugins/styling.md +8 -1
  9. package/apps/runtime/dist/docs/user/space.md +4 -4
  10. package/apps/runtime/dist/docs/user/test-and-share.md +22 -58
  11. package/apps/runtime/dist/engine/steps.js +3 -2
  12. package/apps/runtime/dist/engine/template.js +14 -10
  13. package/apps/runtime/dist/film/project.js +15 -3
  14. package/apps/runtime/dist/plugins/loader.js +38 -2
  15. package/apps/runtime/dist/routes/studio.js +28 -1
  16. package/apps/runtime/dist/services/brand.js +61 -1
  17. package/apps/runtime/dist/services/projects.js +19 -10
  18. package/apps/runtime/dist/tools/plugin.js +11 -3
  19. package/apps/web/dist/assets/AboutDialog-CH1rQdia.js +1 -0
  20. package/apps/web/dist/assets/{ChangelogDialog-DynvQuuR.js → ChangelogDialog-78MQLHfA.js} +1 -1
  21. package/apps/web/dist/assets/ChatView-DLJfy69U.js +10 -0
  22. package/apps/web/dist/assets/EditorPage-BYrZokVk.js +3 -0
  23. package/apps/web/dist/assets/{FlowPreview-BZLnG7GV.js → FlowPreview-xIWPdOCV.js} +1 -1
  24. package/apps/web/dist/assets/{Harness-CNh3nbB_.js → Harness-CsDhNLSW.js} +2 -2
  25. package/apps/web/dist/assets/NewWizardChannels-JB3V6DCL.js +1 -0
  26. package/apps/web/dist/assets/NewWizardPage-Bkh-hjSe.js +7 -0
  27. package/apps/web/dist/assets/OnboardingPage-Diw1ILjC.js +1 -0
  28. package/apps/web/dist/assets/ProjectPage-FgoZDzPI.js +1 -0
  29. package/apps/web/dist/assets/PublicRunner-ClT6B7t5.js +1 -0
  30. package/apps/web/dist/assets/{ReadOnly-DTjv2Jzm.js → ReadOnly-DQPHmvS0.js} +1 -1
  31. package/apps/web/dist/assets/RunDrawer-DAwTEyjM.js +4 -0
  32. package/apps/web/dist/assets/{RunnerView-BPPmlv9z.js → RunnerView-B_tJroFM.js} +1 -1
  33. package/apps/web/dist/assets/SettingsPage-BsMN-XQs.js +1 -0
  34. package/apps/web/dist/assets/SetupPage-C7FuIpSt.js +1 -0
  35. package/apps/web/dist/assets/SharePage-CbFe83L9.js +1 -0
  36. package/apps/web/dist/assets/changelog-mYclku73.js +1 -0
  37. package/apps/web/dist/assets/{circle-alert-CVOH4wAv.js → circle-alert-DCBV7n5k.js} +1 -1
  38. package/apps/web/dist/assets/{esm-BfXdeWaI.js → esm-BhIeKBJY.js} +1 -1
  39. package/apps/web/dist/assets/index-CmxVazOf.css +1 -0
  40. package/apps/web/dist/assets/index-fflYi8P1.js +14 -0
  41. package/apps/web/dist/assets/{lock-DHBh62LA.js → lock-DlzTfwxP.js} +1 -1
  42. package/apps/web/dist/assets/{phone-Bt1cyWpY.js → phone-BFD815nc.js} +1 -1
  43. package/apps/web/dist/assets/{settings-menu-ace1vI3i.js → settings-menu-C5Y8SOhG.js} +1 -1
  44. package/apps/web/dist/assets/ui-DTuixH4y.js +134 -0
  45. package/apps/web/dist/assets/{upload-CCEq88l4.js → upload-BEf5-d6X.js} +1 -1
  46. package/apps/web/dist/index.html +4 -5
  47. package/apps/web/dist/mcp/flow.html +2 -2
  48. package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +36 -2
  49. package/node_modules/@engenty-wizards/plugin-sdk/dist/studio.d.ts +44 -1
  50. package/node_modules/@engenty-wizards/shared/dist/feature-flags.d.ts +18 -0
  51. package/node_modules/@engenty-wizards/shared/dist/feature-flags.js +18 -0
  52. package/node_modules/@engenty-wizards/shared/dist/projects.d.ts +22 -6
  53. package/node_modules/@engenty-wizards/shared/dist/projects.js +20 -0
  54. package/package.json +1 -1
  55. package/apps/web/dist/assets/AboutDialog-CPQiN7H9.js +0 -1
  56. package/apps/web/dist/assets/ChatView-CKo_9IZD.js +0 -10
  57. package/apps/web/dist/assets/EditorPage-CDIJXC4T.js +0 -3
  58. package/apps/web/dist/assets/NewWizardChannels-C1Ss9UMY.js +0 -1
  59. package/apps/web/dist/assets/NewWizardPage-2qLAbvDL.js +0 -7
  60. package/apps/web/dist/assets/OnboardingPage-C1FJwEsT.js +0 -1
  61. package/apps/web/dist/assets/ProjectPage-CGuLUuOs.js +0 -1
  62. package/apps/web/dist/assets/PublicRunner-BKw1lOW4.js +0 -1
  63. package/apps/web/dist/assets/RunDrawer-D1I5xqVf.js +0 -4
  64. package/apps/web/dist/assets/SettingsPage-zbomQfGy.js +0 -1
  65. package/apps/web/dist/assets/SetupPage-D3F7M6Pz.js +0 -1
  66. package/apps/web/dist/assets/SharePage-BY49IJgT.js +0 -1
  67. package/apps/web/dist/assets/changelog-DYZC2E6q.js +0 -1
  68. package/apps/web/dist/assets/index-B9xVkr2X.css +0 -1
  69. package/apps/web/dist/assets/index-BH0PYgov.js +0 -15
  70. package/apps/web/dist/assets/logo-C4V2_3Ar.js +0 -1
  71. package/apps/web/dist/assets/ui-D2SqIZeQ.js +0 -133
  72. package/modules/calls/dist/client.css +0 -3
  73. package/modules/calls/dist/client.js +0 -1
  74. package/modules/calls/engenty.plugin.json +0 -8
  75. package/modules/calls/migrations/0001_init.sql +0 -26
  76. package/modules/calls/package.json +0 -25
  77. package/modules/calls/src/openai.ts +0 -155
  78. package/modules/calls/src/plugin.ts +0 -407
  79. package/modules/calls/src/rules.ts +0 -226
  80. package/modules/calls/src/schema.ts +0 -39
  81. package/modules/calls/src/session.ts +0 -353
  82. package/modules/calls/src/store.ts +0 -124
  83. package/modules/calls/src/twilio.ts +0 -90
  84. package/modules/calls/tsconfig.json +0 -10
  85. package/modules/calls/ui/Settings.tsx +0 -137
  86. package/modules/calls/ui/app.ts +0 -50
  87. package/modules/calls/ui/messages.ts +0 -81
  88. package/modules/calls/ui/parts.tsx +0 -36
  89. package/modules/calls/ui/plugin.tsx +0 -15
  90. package/modules/sms/dist/client.css +0 -3
  91. package/modules/sms/dist/client.js +0 -1
  92. package/modules/sms/engenty.plugin.json +0 -8
  93. package/modules/sms/migrations/0001_init.sql +0 -26
  94. package/modules/sms/package.json +0 -24
  95. package/modules/sms/src/plugin.ts +0 -329
  96. package/modules/sms/src/schema.ts +0 -26
  97. package/modules/sms/src/store.ts +0 -140
  98. package/modules/sms/src/surface.ts +0 -70
  99. package/modules/sms/src/twilio.ts +0 -130
  100. package/modules/sms/tsconfig.json +0 -10
  101. package/modules/sms/ui/Settings.tsx +0 -113
  102. package/modules/sms/ui/Share.tsx +0 -60
  103. package/modules/sms/ui/app.ts +0 -51
  104. package/modules/sms/ui/messages.ts +0 -70
  105. package/modules/sms/ui/parts.tsx +0 -36
  106. package/modules/sms/ui/plugin.tsx +0 -20
  107. package/modules/whatsapp/dist/client.css +0 -3
  108. package/modules/whatsapp/dist/client.js +0 -1
  109. package/modules/whatsapp/engenty.plugin.json +0 -8
  110. package/modules/whatsapp/migrations/0001_init.sql +0 -30
  111. package/modules/whatsapp/package.json +0 -24
  112. package/modules/whatsapp/src/graph.ts +0 -271
  113. package/modules/whatsapp/src/inbound.ts +0 -143
  114. package/modules/whatsapp/src/plugin.ts +0 -374
  115. package/modules/whatsapp/src/schema.ts +0 -32
  116. package/modules/whatsapp/src/store.ts +0 -156
  117. package/modules/whatsapp/src/surface.ts +0 -127
  118. package/modules/whatsapp/tsconfig.json +0 -10
  119. package/modules/whatsapp/ui/Settings.tsx +0 -141
  120. package/modules/whatsapp/ui/Share.tsx +0 -60
  121. package/modules/whatsapp/ui/app.ts +0 -54
  122. package/modules/whatsapp/ui/messages.ts +0 -80
  123. package/modules/whatsapp/ui/parts.tsx +0 -36
  124. package/modules/whatsapp/ui/plugin.tsx +0 -20
@@ -1,11 +1,12 @@
1
1
  import { anthropic } from "@ai-sdk/anthropic";
2
- import { cleanAssetSections, cleanLogoSections, FACT_TYPES, factKey, gradientCss, PROJECT_FILE_KINDS, PROJECT_LIMITS, parseGradient, RULE_MARKS, textStyleLine, } from "@engenty-wizards/shared/projects";
2
+ import { cleanAssetSections, cleanLogoSections, FACT_TYPES, FONT_USES, 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";
6
6
  import { z } from "zod";
7
7
  import { knowsTools } from "../assistant/knows.js";
8
8
  import { PRIMER } from "../assistant/primer.js";
9
+ import { currentFeatures } from "../features.js";
9
10
  import { attachTools, contextWindowOf, costOf, gatewayTools, textModel, usageOf, } from "../models.js";
10
11
  import { ServiceError } from "../services/errors.js";
11
12
  import { knowledgeCounts } from "../services/knowledge.js";
@@ -48,7 +49,7 @@ THE SPACE
48
49
  - 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.
49
50
  - 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.
50
51
  - 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.
51
- - 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.
52
+ - 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 font's licence and what it may be used for (web, documents, print, images, video) only where a brand guide, the font's own page or the person says so; the wizards keep to it. A type scale (text styles: name, size, weight, line height, letter spacing) only when a brand guide gives one.
52
53
  - 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.
53
54
  - 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.
54
55
  - 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.
@@ -125,9 +126,8 @@ async function projectState(userId, projectId) {
125
126
  // Optional parts are only there when the space has them.
126
127
  ...(p.brand.colorSections?.length
127
128
  ? {
128
- colorSections: p.brand.colorSections.map(({ title, description, kind }) => ({
129
+ colorSections: p.brand.colorSections.map(({ title, kind }) => ({
129
130
  title,
130
- description,
131
131
  kind: kind ?? "colors",
132
132
  })),
133
133
  }
@@ -136,6 +136,7 @@ async function projectState(userId, projectId) {
136
136
  ? {
137
137
  gradients: p.brand.gradients.map((g) => ({
138
138
  name: g.name,
139
+ ...(g.description ? { description: g.description } : {}),
139
140
  section: p.brand.colorSections?.find((s) => s.id === g.section)?.title,
140
141
  css: gradientCss(g),
141
142
  })),
@@ -145,6 +146,7 @@ async function projectState(userId, projectId) {
145
146
  ? {
146
147
  patterns: p.brand.patterns.map((x) => ({
147
148
  name: x.name,
149
+ ...(x.description ? { description: x.description } : {}),
148
150
  section: p.brand.colorSections?.find((s) => s.id === x.section)?.title,
149
151
  tile: Boolean(x.tile),
150
152
  })),
@@ -165,7 +167,15 @@ async function projectState(userId, projectId) {
165
167
  }
166
168
  : {}),
167
169
  ...(p.brand.fonts?.length
168
- ? { fonts: p.brand.fonts.map(({ family, role, weights }) => ({ family, role, weights })) }
170
+ ? {
171
+ fonts: p.brand.fonts.map(({ family, role, weights, license, uses }) => ({
172
+ family,
173
+ role,
174
+ weights,
175
+ ...(license ? { license } : {}),
176
+ ...(uses?.length ? { uses } : {}),
177
+ })),
178
+ }
169
179
  : {}),
170
180
  ...(p.brand.textStyles?.length
171
181
  ? {
@@ -188,8 +198,7 @@ async function projectState(userId, projectId) {
188
198
  })),
189
199
  };
190
200
  }
191
- /** The space's own tools: what fills it in. */
192
- function spaceTools(input, project, wrote) {
201
+ function spaceTools(input, project, wrote, on) {
193
202
  const { signal } = input;
194
203
  const userId = input.user.id;
195
204
  const projectId = project.id;
@@ -292,7 +301,7 @@ function spaceTools(input, project, wrote) {
292
301
  }),
293
302
  set_colors: createTool({
294
303
  id: "set_colors",
295
- 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.",
304
+ 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 (its title), and on a colour the `section` title it belongs to; colours without one are the standard ones. Without `sections` the space keeps its sections.",
296
305
  inputSchema: z.object({
297
306
  colors: z
298
307
  .array(z.object({
@@ -302,12 +311,15 @@ function spaceTools(input, project, wrote) {
302
311
  }))
303
312
  .max(PROJECT_LIMITS.colors),
304
313
  sections: z
305
- .array(z.object({ title: z.string().min(1).max(80), description: z.string().max(400) }))
314
+ .array(z.object({ title: z.string().min(1).max(80) }))
306
315
  .max(PROJECT_LIMITS.logoSections)
307
316
  .optional(),
308
317
  }),
309
318
  execute: ({ colors, sections }) => attempt(() => inTurn(async () => {
310
319
  doing("set_colors");
320
+ if (!on.brandSections && (sections || colors.some((c) => c.section))) {
321
+ throw new Error("Colour sections are not available in this studio: pass the colours without them.");
322
+ }
311
323
  const p = await ownedProject(userId, projectId);
312
324
  const groups = sections
313
325
  ? sections.map((s) => ({
@@ -392,13 +404,22 @@ function spaceTools(input, project, wrote) {
392
404
  }),
393
405
  set_typography: createTool({
394
406
  id: "set_typography",
395
- 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.",
407
+ description: "Set the brand's fonts — the whole list, the main one first — each with what it is for, its weights and, where a source says so, its licence and what it may be used for. 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.",
396
408
  inputSchema: z.object({
397
409
  fonts: z
398
410
  .array(z.object({
399
411
  family: z.string().min(1).max(80),
400
412
  role: z.string().max(200).describe("What it is for, in the person's language"),
401
413
  weights: z.array(z.number().int().min(100).max(900)).max(9),
414
+ license: z
415
+ .string()
416
+ .max(200)
417
+ .optional()
418
+ .describe("The licence, only where a source names it: 'SIL Open Font License'"),
419
+ uses: z
420
+ .array(z.enum(FONT_USES))
421
+ .optional()
422
+ .describe("All the licence allows it for, only where a source says so"),
402
423
  }))
403
424
  .max(PROJECT_LIMITS.fonts),
404
425
  textStyles: z
@@ -441,15 +462,18 @@ function spaceTools(input, project, wrote) {
441
462
  }),
442
463
  set_gradients: createTool({
443
464
  id: "set_gradients",
444
- 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.",
465
+ description: "Optional. Set the brand's gradients, as a brand guide or the site's stylesheet gives them: each with a name, when to use it where the source says so, 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.",
445
466
  inputSchema: z.object({
446
467
  section: z.string().min(1).max(80).describe("The gradients section's title"),
447
- description: z.string().max(400).optional().describe("When to use them"),
448
468
  gradients: z
449
- .array(z.object({ name: z.string().min(1).max(80), css: z.string().max(1000) }))
469
+ .array(z.object({
470
+ name: z.string().min(1).max(80),
471
+ description: z.string().max(400).optional().describe("When to use it"),
472
+ css: z.string().max(1000),
473
+ }))
450
474
  .max(PROJECT_LIMITS.gradients),
451
475
  }),
452
- execute: ({ section, description, gradients }) => attempt(() => inTurn(async () => {
476
+ execute: ({ section, gradients }) => attempt(() => inTurn(async () => {
453
477
  doing("set_gradients");
454
478
  const p = await ownedProject(userId, projectId);
455
479
  const sections = p.brand.colorSections ?? [];
@@ -463,16 +487,14 @@ function spaceTools(input, project, wrote) {
463
487
  await updateProject(userId, projectId, {
464
488
  brand: {
465
489
  colorSections: found
466
- ? sections.map((s) => (s.id === id && description ? { ...s, description } : s))
467
- : [
468
- ...sections,
469
- { id, title: section, description: description ?? "", kind: "gradients" },
470
- ],
490
+ ? sections
491
+ : [...sections, { id, title: section, kind: "gradients" }],
471
492
  gradients: [
472
493
  ...(p.brand.gradients ?? []).filter((g) => g.section !== id),
473
494
  ...parsed.map(({ g, read }) => ({
474
495
  id: nanoid(10),
475
496
  name: g.name,
497
+ ...(g.description ? { description: g.description } : {}),
476
498
  section: id,
477
499
  ...read,
478
500
  })),
@@ -582,6 +604,16 @@ function spaceTools(input, project, wrote) {
582
604
  },
583
605
  }),
584
606
  };
607
+ if (!on.brandSections) {
608
+ delete tools.set_logo_sections;
609
+ delete tools.set_gradients;
610
+ }
611
+ if (!on.brandRules) {
612
+ delete tools.set_rules;
613
+ }
614
+ if (!on.typography) {
615
+ delete tools.set_typography;
616
+ }
585
617
  return tools;
586
618
  }
587
619
  /** A browser tool as the model calls it: sent to the page, its answer awaited. */
@@ -673,6 +705,12 @@ export async function runStudioAssistant(input) {
673
705
  const cards = [];
674
706
  const { project } = input;
675
707
  const space = project && input.writable ? project : null;
708
+ const switches = await currentFeatures();
709
+ const brandOn = {
710
+ brandSections: switches.brandSections === true,
711
+ brandRules: switches.brandRules === true,
712
+ typography: switches.typography === true,
713
+ };
676
714
  const tools = {
677
715
  ...knowsTools({
678
716
  user: input.user,
@@ -687,7 +725,7 @@ export async function runStudioAssistant(input) {
687
725
  input.onCard?.(card);
688
726
  },
689
727
  }),
690
- ...(space ? spaceTools(input, space, wrote) : {}),
728
+ ...(space ? spaceTools(input, space, wrote, brandOn) : {}),
691
729
  };
692
730
  // What the tenant's plugins let the assistant do besides: a source of knowledge, say.
693
731
  if (space) {
@@ -763,6 +801,15 @@ export async function runStudioAssistant(input) {
763
801
  part === "knowledge"
764
802
  ? "The person talks to you from Wissen: they want the wizards to know something."
765
803
  : "",
804
+ space && !(brandOn.brandSections && brandOn.brandRules && brandOn.typography)
805
+ ? `Not in this studio (no tools for it; never offer it): ${[
806
+ brandOn.brandSections ? "" : "sections of logos and colours, gradients and patterns",
807
+ brandOn.brandRules ? "" : "rules for logos, colours and type",
808
+ brandOn.typography ? "" : "typography (fonts, type scale)",
809
+ ]
810
+ .filter(Boolean)
811
+ .join("; ")}.`
812
+ : "",
766
813
  input.message,
767
814
  ]
768
815
  .filter(Boolean)
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Reference
3
3
  description: Every call, type, address and setting of the plugin framework on one page.
4
- version: 0.3.4
4
+ version: 0.3.7
5
5
  ---
6
6
 
7
7
  ## Server half
@@ -32,7 +32,8 @@ The default export is a `WizardsPluginFactory`: `(wizards: WizardsPluginApi) =>
32
32
  | `registerRunner(runner: PluginRunner)` | [Runners](./runners.md). A door a wizard runs through; `page` runners answer at `/w/<token>/<id>` |
33
33
  | `runs` | [Runners](./runners.md#drive-a-run). `start`, `report`, `wizards`, `answerPage`, `review`, `answerAsk`, `control`, `subscribe`, `handoffUrl`, `upload`, `transcribe`: the functions behind the MCP tools, for the plugin's door |
34
34
  | `speech.conversation()` | [Runners](./runners.md#a-call). `{ provider: "openai", apiKey, model }`: the tenant's realtime speech model, for a call a plugin drives itself. Throws `503` / `no_model` while none is set up |
35
- | `registerSpaceContext({ block, tools? })` | [The space](./space.md). `block(space): string \| null` |
35
+ | `registerSpaceContext({ block, tools?, steps? })` | [The space](./space.md). `block(space): string \| null`; `steps`: `agent` (default), `html` (steps that write a document or a dashboard, without the tools) |
36
+ | `spaces.brand(space)` | [The space](./space.md#the-spaces-brand). `{ name, colors, fonts }` of the space, or `null` |
36
37
  | `registerAssistantTool(tool: PluginAssistantTool)` | [The space](./space.md#tools-of-the-space-assistant) |
37
38
  | `index.put({ space, key, title, text, link? })`, `index.remove(space, key?)` | [The space](./space.md#the-search-index) |
38
39
  | `spaceData.putCategories`, `.putPage`, `.putTable`, `.putFile`, `.putDocument`, `.list`, `.readPages`, `.files`, `.readFile`, `.remove`, `.summarize` | [The space](./space.md#knowledge-a-plugin-writes). Knowledge of a space: pages, tables and files under keys of the plugin's own, with their Kategorien; and the space's own pages and files to read |
@@ -45,7 +46,7 @@ The default export is a `WizardsPluginFactory`: `(wizards: WizardsPluginApi) =>
45
46
  | `web.fetch(url, init?)`, `web.read(url, { signal? })` | [The web](./server.md#the-web) |
46
47
  | `documents.parse({ data, name, mime }, { signal? })` | [Documents](./server.md#documents). `{ markdown, pages }` |
47
48
  | `getTenantDb(): PluginDb` | The current tenant's database, a Drizzle libSQL database |
48
- | `generate(request): Promise<{ text, object }>` | Asks a model of the current tenant: `prompt`, `system?`, `schema?`, `model?`, `maxOutputTokens?`, `signal?`. See [The server half](./server.md#asking-a-model) |
49
+ | `generate(request): Promise<{ text, object }>` | Asks a model of the current tenant: `prompt`, `system?`, `schema?`, `model?`, `maxOutputTokens?`, `signal?`, `files?` (pictures and PDFs: `{ data, mediaType, name? }[]`). See [The server half](./server.md#asking-a-model) |
49
50
  | `connections.available(ids)`, `.start({ space, connector, actions })`, `.connect({ space, connector, actions, credentials })`, `.list(space)`, `.get(id)`, `.call(id, action, input)`, `.remove(id)` | [Accounts](./connections.md) |
50
51
  | `secrets.seal(value): string`, `secrets.unseal(sealed)` | [Keys of the plugin's own](./server.md#keys-of-the-plugins-own). `unseal` gives null for a value sealed on another install |
51
52
  | `registerAssistantContext({ block })` | [The space](./space.md#the-assistant-on-the-plugins-own-page). `block(space): string \| null` |
@@ -240,11 +241,13 @@ The default export is a `StudioPlugin`: `(studio: StudioPluginContext) => void`.
240
241
  | `studio.registerShareSection({ runner, component })` | [Runners](./runners.md#the-runners-part-of-the-share-dialog). The runner's own settings for a wizard (a keyword), under the share dialog's gear once it is switched on and set up. `component` gets `{ wizard: { id, title } }` |
241
242
  | `studio.registerAssistantCard({ tool, component })` | Draws a tool's result in the space assistant's chat. `component` gets `{ data, send }` |
242
243
  | `studio.registerSpaceSection({ id, group?, label, hint?, component })` | A section of a part of the space page at `/studio/space/<group>#<id>`; `group`: `info`, `knowledge` (default), `data` or `results` |
244
+ | `studio.registerSpacePart({ id, label, icon, after?, component })` | [A part of the plugin's own](./studio.md#a-part-of-the-plugins-own) at `/studio/space/<id>`, after `info`, `knowledge` or `data` (default) in the menu |
245
+ | `studio.useSpaceMenu(entries)` | A hook in a plugin's part: its menu, `{ id, label, icon? }[]`, each jumping to the element with that id |
243
246
  | `studio.useCrumbs(items)` | A hook: the trail in the top bar for the page shown, `{ label, to? }[]`, instead of a heading of the page's own |
244
247
  | `studio.Assistant` | Tells the studio's assistant what to say on a page of the plugin: `title?`, `hello?`, `placeholder?`, `suggestions?: { label, message?, fill? }[]`, `onChanged?`. Draws nothing |
245
248
  | `studio.useAssistantPage(page)` | A hook: what the page shows, for the assistant: `open?: { kind, id?, label }`, `selection?`, `summary?`, `fields?: { id, label, value }[]`, `compact?`, and the words of `studio.Assistant` |
246
249
  | `studio.useAssistantTool(name, { description, parameters? }, run)` | A hook: a tool the assistant runs in the page while it is shown, as `<plugin>_<name>`. `parameters`: JSON Schema; `run(args)` returns what the assistant learns |
247
- | `studio.useSpace()` | A hook: `{ id, name, readOnly }` of the space the studio shows, or `null` |
250
+ | `studio.useSpace()` | A hook: `{ id, name, readOnly, brand }` of the space the studio shows, or `null`; `brand` is `{ name, colors, fonts }` |
248
251
  | `studio.i18n.register({ de, en })` | Gives `t(key, vars?)` |
249
252
  | `studio.i18n.lang()` | `"de"` or `"en"` |
250
253
  | `studio.api.get`, `.post`, `.put`, `.patch`, `.del` | The plugin's own routes |
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: While it runs
3
3
  description: Save a file and the plugin loads again — what reloads, what survives, and how to work on a plugin.
4
- version: 0.3.2
4
+ version: 0.4.0
5
5
  ---
6
6
 
7
7
  A runtime that runs alone watches its plugins. You do not restart it while you write one.
@@ -67,8 +67,8 @@ A loop that needs no restart, from a checkout:
67
67
  4. Open the plugin's page in the studio and edit. The server half reloads on save; the page
68
68
  updates when the build finished.
69
69
 
70
- The runtime's log names the plugins it loaded at start, the ones that ship with it first
71
- (`plugins: calls, sms, whatsapp, contacts, workdays`), and prints why one did not load.
70
+ The runtime's log names the plugins it loaded at start (`plugins: contacts, workdays`), and
71
+ prints why one did not load.
72
72
 
73
73
  ## On engenty.ai
74
74
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Runners
3
3
  description: A door a wizard runs through, beside the page and the chat — a call, a messenger, a kiosk — and the run API that drives runs from it.
4
- version: 0.3.4
4
+ version: 0.4.0
5
5
  ---
6
6
 
7
7
  A runner is a way to run a wizard. The runtime has four: the stepped page at `/w/<token>`, the
@@ -163,10 +163,9 @@ const door = new ThreadDoor({ runs: server.runs, surface, offered, watch, unwatc
163
163
  | `watch(thread)`, `unwatch(runId)` | Hear the run (`runs.subscribe`) and call `door.resume` when it moves; the plugin keeps the subscriptions |
164
164
  | `Thread` | `{ id, name, runId, state, lastInboundAt, lastMessageId }`: the plugin keeps it in a table per person and hands it to the door before and after |
165
165
 
166
- The WhatsApp module in `modules/whatsapp` and the SMS module in `modules/sms` are two surfaces
167
- of this door: the webhook on a public route, the keyword table, the number in the settings and
168
- the keyword in the share dialog are theirs; buttons and lists, the location request and files
169
- through the Cloud API are WhatsApp's surface, numbered lines and links are SMS's.
166
+ A messenger plugin is a surface of this door: the webhook on a public route, the keyword table,
167
+ the number and the keyword in the share dialog are the plugin's; buttons and lists, or numbered
168
+ lines and links, are its surface.
170
169
 
171
170
  ## A call
172
171
 
@@ -179,13 +178,11 @@ const { apiKey, model } = await server.speech.conversation();
179
178
  // Throws with the status 503 and the code `no_model` while no OpenAI key is set under Models.
180
179
  ```
181
180
 
182
- The calls module in `modules/calls` is the whole of it: Twilio carries the call (a WhatsApp
183
- call arrives over Meta's SIP on a Twilio SIP domain, a phone call on a Twilio number) and asks
184
- the plugin for TwiML, which hands the call to the tenant's OpenAI project over SIP with a token
185
- in a header; OpenAI posts the incoming call to the plugin's public route, the plugin accepts it
186
- with the wizard's rules and tools and answers the model's tool calls over a WebSocket, driving
187
- the run with `server.runs`. The audio never touches the runtime. What a voice cannot take is
188
- texted as a link.
181
+ A call plugin keeps the audio out of the runtime: the telephony provider hands the call to the
182
+ speech model's SIP endpoint with a token in a header, the model's provider posts the incoming
183
+ call to the plugin's public route, and the plugin accepts it with the wizard's rules and tools
184
+ and answers the model's tool calls over a WebSocket, driving the run with `server.runs`. What a
185
+ voice cannot take goes out as a link.
189
186
 
190
187
  ## Not there yet
191
188
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: The server half
3
3
  description: The function a plugin exports, what it is handed, and the rules it runs under.
4
- version: 0.3.2
4
+ version: 0.3.7
5
5
  ---
6
6
 
7
7
  The server half is a file whose default export is a function. The runtime calls it once when the
@@ -39,6 +39,7 @@ export default definePlugin((wizards) => {
39
39
  | `registerAssistantTool(tool)` | A tool of the space assistant | [The space](./space.md#tools-of-the-space-assistant) |
40
40
  | `registerAssistantContext(context)` | What the space assistant is told on the plugin's own page | [The space](./space.md#the-assistant-on-the-plugins-own-page) |
41
41
  | `spaceData.putPage(input)`, `putTable`, `putFile`, `putDocument`, `putCategories`, … | Knowledge of a space: pages, tables, files and Kategorien | [The space](./space.md#knowledge-a-plugin-writes) |
42
+ | `spaces.brand(space)` | The space's look: its title, colours and fonts | [The space](./space.md#the-spaces-brand) |
42
43
  | `index.put(entry)`, `index.remove(space, key?)` | Texts in the space's search index | [The space](./space.md#the-search-index) |
43
44
  | `registerHttpRoute(route)` | A route below `/api/studio/plugins/<id>` | [Routes](./routes.md) |
44
45
  | `registerPublicRoute(route)`, `publicUrl(path)` | A route anyone may call, and its address | [Routes](./routes.md#public-routes) |
@@ -106,6 +107,7 @@ const { object } = await server.generate({
106
107
  | `model` | `classifier` (fast, cheap), `standard`, `high`, `highest` | `standard` |
107
108
  | `maxOutputTokens` | A limit on the answer | the model's |
108
109
  | `signal` | Aborts the call | two minutes |
110
+ | `files` | Pictures (PNG, JPEG, WebP, GIF) and PDFs the model looks at with the prompt: `{ data, mediaType, name? }[]`, `data` a `Uint8Array` | none |
109
111
 
110
112
  It answers `{ text, object }`. Without a model set up it throws an error with the status `503`
111
113
  and the code `no_model`: a route that lets it through answers just that, and the studio half
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: The space
3
3
  description: Knowledge a plugin writes, a block and tools for every AI step of a space, texts in its search index, tools and cards of its assistant.
4
- version: 0.3.2
4
+ version: 0.3.7
5
5
  ---
6
6
 
7
7
  A space holds what all its wizards share: a title, logos, colours, facts, and Knowledge —
@@ -93,6 +93,24 @@ server.registerSpaceContext({
93
93
  - A model on the device calls no tools: Apple Intelligence on a Mac, or the phone a run is
94
94
  followed on, which answers only steps that list no tools themselves. The step then gets the
95
95
  block without the tools.
96
+ - `steps` says which steps get it: `["agent"]` when left out. `"html"` is the steps that write a
97
+ document or a dashboard as HTML; they get the block without the tools, after the space's
98
+ brand: how the brand's elements look, say.
99
+
100
+ ## The space's brand
101
+
102
+ `server.spaces.brand(space)` reads what Info & brand holds of the space's look, for a block, a
103
+ page or a model call in it:
104
+
105
+ ```ts
106
+ const brand = await server.spaces.brand(space.id);
107
+ // { name: "Mira Kaffee", colors: [{ name: "Primär", value: "#7a3e1d" }, …],
108
+ // fonts: [{ family: "Fraunces", role: "Headings", weights: [600] }, …] }
109
+ ```
110
+
111
+ - The first colour is the accent. A font's `role` is what it is for, in the person's words.
112
+ - `null` for a space the tenant does not have. The studio half gets the same as
113
+ `studio.useSpace().brand`.
96
114
 
97
115
  ## The search index
98
116
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: The studio half
3
3
  description: Pages, icons in the app bar, sections of the settings and of the space page — written in React, built once, drawn inside the studio.
4
- version: 0.3.4
4
+ version: 0.3.7
5
5
  ---
6
6
 
7
7
  The studio half is `ui/plugin.tsx`. Its default export is a function; the studio calls it after
@@ -123,7 +123,7 @@ space page, behind the house icon (Space) in the app bar, shows them in four par
123
123
 
124
124
  | Part | `group` | Holds |
125
125
  |---|---|---|
126
- | Info & brand | `info` | Basics, Logos, Colours, Assets, Facts |
126
+ | Info & brand | `info` | Basics, Facts, Logos, Colours, Brand assets |
127
127
  | Knowledge | `knowledge` | Documents, pages, tables and files with their Kategorien: what every AI step looks things up in |
128
128
  | Data | `data` | What the wizards keep |
129
129
  | Results | `results` | The runs that reached their result |
@@ -157,13 +157,50 @@ studio.registerSpaceSection({
157
157
  - `id` is the section's anchor: `/studio/space/data#contacts` opens the page there, and so does
158
158
  `/studio/space#contacts`. In Knowledge a section is a page of its own:
159
159
  `/studio/space/knowledge?show=s:<id>`, where `#<id>` leads too. Ids are unique on the whole
160
- page; the space's own are taken: `base`, `logos`, `colors`, `assets`, `documents`, `facts`.
161
- - `studio.useSpace()` is a hook: `{ id, name, readOnly }`, or `null` while there is none. A
162
- component that calls it draws again when the person picks another space. `readOnly`: the
163
- space came from a local install, or nothing is built on this runtime; show it, change nothing.
160
+ page; the space's own are taken: `base`, `facts`, `logos`, `colors`, `typography`, `assets`,
161
+ `documents`.
162
+ - `studio.useSpace()` is a hook: `{ id, name, readOnly, brand }`, or `null` while there is none.
163
+ A component that calls it draws again when the person picks another space, or changes its
164
+ brand. `readOnly`: the space came from a local install, or nothing is built on this runtime;
165
+ show it, change nothing. `brand` is what Info & brand holds of the look: `name`, `colors`
166
+ (`{ name, value }`, the first is the accent) and `fonts` (`{ family, role, weights }`).
164
167
  - Every wizard belongs to a space: a [run event](./events.md) names it as
165
168
  `wizard.projectId`. Keep it with what the plugin stores to show a space's part of it.
166
169
 
170
+ ### A part of the plugin's own
171
+
172
+ A plugin whose subject is as big as one of the parts adds a part of its own: an entry in the
173
+ parts' menu, with a page at `/studio/space/<id>` that the plugin draws whole. The part's menu
174
+ lists what the page holds; `studio.useSpaceMenu` fills it while the page is drawn.
175
+
176
+ ```tsx
177
+ function Library() {
178
+ const space = studio.useSpace();
179
+ const sections = useSections(space?.id);
180
+ studio.useSpaceMenu(sections.map((s) => ({ id: s.id, label: s.title, icon: Component })));
181
+ return sections.map((s) => (
182
+ <Anchor key={s.id} id={s.id}>
183
+ <Section title={s.title}>…</Section>
184
+ </Anchor>
185
+ ));
186
+ }
187
+
188
+ studio.registerSpacePart({
189
+ id: "library",
190
+ label: () => t("title"),
191
+ icon: Blocks,
192
+ after: "info",
193
+ component: Library,
194
+ });
195
+ ```
196
+
197
+ - `id` is the address: `/studio/space/library`. The parts' own (`info`, `knowledge`, `data`,
198
+ `results`) are taken.
199
+ - `after` is the part it follows in the menu: `info`, `knowledge` or `data` (default).
200
+ - Each entry of `useSpaceMenu` jumps to the element with its `id` in what the part draws, and
201
+ the menu marks the one in view. `Anchor` and `Section` draw them as the space's own parts do.
202
+ - The component is drawn again for another space: keep the space's data under its id.
203
+
167
204
  ## Words in two languages
168
205
 
169
206
  The studio speaks German and English. `studio.i18n.register` takes the plugin's words and gives
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Styles and components
3
3
  description: The studio's components, Tailwind against the studio's theme, and why a plugin's styles never leak.
4
- version: 0.3.2
4
+ version: 0.3.7
5
5
  ---
6
6
 
7
7
  A plugin's page should look like the studio. Two things make that easy: the studio's components
@@ -34,6 +34,13 @@ and its theme.
34
34
  | `LinkedText` | A sentence with its web addresses as links, named by their host | `text` |
35
35
  | `cn(...)` | Joins class names, skipping what is false | |
36
36
  | `dateText(date, withWeekday?)` | A "YYYY-MM-DD" date in the studio's language: "Wed 14 Oct 2026" | |
37
+ | `Anchor`, `Section` | A section of the space page and the spot the menu jumps to: the heading and its hint above the card | `id` on the anchor · `title`, `hint`, `save` (how the last save went), `action` (beside the heading), `plain` (the content draws its own cards) |
38
+ | `InlineText` | Text edited where it stands, without a field's frame | `value`, `onChange`, `placeholder`, `label`, `maxLength`, `fit` (as wide as its text) |
39
+ | `AddMenu` | The "Add" menu of a section's heading | `entries: { label, icon, onPick }[]` |
40
+ | `AddRow` | A quiet "+ …" row at the end of a list | `label`, `onClick` |
41
+ | `RuleRows`, `useRuleRows(saved)` | Rules as rows, each with its sign (bullet, do, don't, to check, important, numbered) | `rules` (what the hook gives), `placeholder`, `readOnly` |
42
+ | `useAutosave(value, save)` | Saves a value a moment after it last changed; gives the state for a section's `save` | |
43
+ | `newId()` | A short id for something new | |
37
44
 
38
45
  ```tsx
39
46
  import { Button, Card, Chip, Label, Input } from "@engenty-wizards/web/ui";
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Space
3
3
  description: What your wizards share and produce — who you are, what they look things up in, and the results of their runs.
4
- version: 0.3.2
4
+ version: 0.3.7
5
5
  ---
6
6
 
7
7
  The house in the app bar, after the space's tile, opens your space: what all your wizards share,
@@ -13,7 +13,7 @@ the top leads back. On a phone the parts, a part's sections and the page take tu
13
13
 
14
14
  | Part | Sections | Used for |
15
15
  |---|---|---|
16
- | Info & brand | Basics, Logos, Colours, Assets, Facts | Who you are and how you look |
16
+ | Info & brand | Basics, Facts, Logos, Colours, Brand assets | Who you are and how you look |
17
17
  | Knowledge | All, then each Kategorie, then the sections plugins add | What every AI step looks things up in |
18
18
  | Data | All data, then each wizard | Tables and pages the wizards keep |
19
19
  | Results | All results, then each wizard | The runs that reached their result |
@@ -23,10 +23,10 @@ the top leads back. On a phone the parts, a part's sections and the page take tu
23
23
  | Section | Holds | Used for |
24
24
  |---|---|---|
25
25
  | Basics | Title and "About": what you do, for whom, and the tone you write in | Texts sound like you. People who run a wizard see the title too |
26
+ | Facts | Labelled values: address, VAT id, opening hours | Steps use them as written |
26
27
  | Logos | One or several, each with a description of what it is for | People see the main logo. The description tells a step what a variant is for |
27
28
  | Colours | Named colours, each with a note on its use | The first colour is the accent of documents, dashboards and public pages |
28
- | Assets | Images, graphics, videos, each with a description | AI steps place them in documents and choose by the description |
29
- | Facts | Labelled values: address, VAT id, opening hours | Steps use them as written |
29
+ | Brand assets | Images, graphics, videos, sound, PDFs, fonts, Lottie and 3D files, in tabs | AI steps place pictures in documents and choose by the description |
30
30
 
31
31
  ## Knowledge
32
32
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Test and share
3
3
  description: Try the wizard, publish it, and hand out its link or put it on a website.
4
- version: 0.3.4
4
+ version: 0.4.0
5
5
  ---
6
6
 
7
7
  ## Test
@@ -81,65 +81,29 @@ The link opens the first browser channel that is on. The others answer under the
81
81
  (`/steps`, `/chat`, `/talk` for live voice, `/video` for the video call) and are offered on the
82
82
  wizard's first page: "Or: Chat". A switched-off address sends people to the link instead.
83
83
 
84
- ### WhatsApp
85
-
86
- With the WhatsApp module a wizard runs in a WhatsApp thread on your own business number. Set
87
- the number up once under Settings → WhatsApp: the phone number ID and a permanent access token
88
- from your Meta app, then the webhook address and the verify token the page shows go into the
89
- Meta app's WhatsApp configuration. Until a number is connected, the channel shows "not set up"
90
- in the list.
91
-
92
- Switch WhatsApp on for a wizard and its row in the share dialog shows its **keyword** and the
93
- number; "Link" copies the link that writes it (`wa.me/<number>?text=<keyword>`), and the QR code
94
- view draws it. The keyword is changed under the gear. Whoever sends the keyword to the number
95
- starts the wizard; without a keyword, the number offers the wizards that are on.
96
-
97
- In the thread the wizard asks one field per message: buttons for up to three choices, a list
98
- for more, yes/no as buttons, a photo or a file sent in the chat, "send location" for a place, a
99
- voice message for a text field where a listener is set up under Models. A page with a field only
100
- the screen can take (a signature, a sign-in) arrives as a link to the wizard's page; the thread
101
- goes on by itself once it was filled there. A review is the text with "Looks good" and "New
102
- version"; the result arrives as text, pictures and documents, a widget or dashboard as a picture,
103
- and a link to everything. "stop" ends a run, the keyword starts a new one.
104
-
105
- What WhatsApp allows: a free message within 24 hours of the person's last one. A wizard that
106
- finishes later can only reach back with an approved utility template with one placeholder; its
107
- name goes into the settings, the link into the placeholder. Without one, the result waits for
108
- the person's next message.
109
-
110
- ### SMS
111
-
112
- With the SMS module a wizard runs by text message, through a Twilio account and number of your
113
- own. Settings → SMS takes the Account SID, the auth token and the number; the webhook address
114
- the page shows goes into the number's "A message comes in" field in Twilio. Switch SMS on for a
115
- wizard and its row shows the keyword and the number; "Link" copies an `sms:` link that types it,
84
+ ### WhatsApp, SMS and calls
85
+
86
+ On engenty.ai a wizard can also run in a WhatsApp thread, by text message and as a phone call,
87
+ where the team's plan includes them. These channels are not part of the open runtime: a local
88
+ install has them through its account in the cloud.
89
+
90
+ Switch one on for a wizard and its row in the share dialog shows the number and the **keyword**:
91
+ whoever sends the keyword to the number starts the wizard. "Link" copies a link that writes it,
116
92
  and the QR code view draws it. The keyword is changed under the gear.
117
93
 
118
- Text only: one field per text, choices as numbered lines answered with the number (several
119
- numbers with commas for a multiple choice), yes or no, a place as a typed address. A page with
120
- a photo, a file or a signature arrives as a link to the wizard's page and the texts go on once it
121
- was filled there. Long texts come as their gist; every picture, document and the whole result
122
- are links. Each text costs what Twilio charges; there is no window, a wizard that finishes
123
- later still texts.
124
-
125
- ### Calls
126
-
127
- With the calls module a wizard runs as a conversation on the phone: a WhatsApp call to your
128
- business number, or a phone call to a Twilio number. Twilio carries the call and the speech
129
- model of your OpenAI account (Settings → Models) speaks, as the chat's live voice does, so the
130
- same rules apply: one field at a time, choices read out, e-mail addresses and numbers read back.
131
-
132
- Settings → Calls takes the Twilio account, a Twilio number that can text, the OpenAI project ID,
133
- the webhook secret of a webhook you add in the OpenAI dashboard for incoming calls and the
134
- region (EU or US), and shows what to enter where: the voice webhook for Twilio's SIP domain and
135
- phone number, the OpenAI webhook, and for WhatsApp the SIP server to set on the number in Meta's
136
- calling settings (the Twilio SIP domain, port 5061). Switch "Call" on for a wizard; with several
137
- on, the wizard asks which one at the start of the call. A WhatsApp call button or deep link whose
138
- payload is a wizard's link token picks that wizard.
139
-
140
- What a voice cannot take (a photo, a file, a signature) is texted as a link to the wizard's page,
141
- and the call goes on once it was filled there. At the end the result is texted as a link too.
142
- Each minute costs what OpenAI and Twilio charge.
94
+ - **WhatsApp**: one field per message, buttons for up to three choices, a list for more, a photo
95
+ or a file sent in the chat, "send location" for a place, a voice message for a text field. A
96
+ free message goes out within 24 hours of the person's last one; a wizard that finishes later
97
+ reaches back with an approved template, or waits for the person's next message.
98
+ - **SMS**: text only, choices as numbered lines answered with the number, a place as a typed
99
+ address; long texts come as their gist, every picture, document and the whole result as links.
100
+ - **Calls**: the speech model talks, as the chat's live voice does: one field at a time, choices
101
+ read out, e-mail addresses and numbers read back. With several wizards on for calls, the call
102
+ asks which one first.
103
+
104
+ What a channel cannot take (a signature, a sign-in; by text and on the phone also a photo or a
105
+ file) arrives as a link to the wizard's page, and the conversation goes on once it was filled
106
+ there. "stop" ends a run in a thread, the keyword starts a new one.
143
107
 
144
108
  ### Who can open the link
145
109