wizards 0.4.4 → 0.4.6
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/architect.js +32 -13
- package/apps/runtime/dist/agents/subscription.js +12 -12
- package/apps/runtime/dist/app.js +9 -5
- package/apps/runtime/dist/assistant/knows.js +2 -2
- package/apps/runtime/dist/authoring/example.js +21 -20
- package/apps/runtime/dist/authoring/guide.js +4 -2
- package/apps/runtime/dist/connectors/imap.js +4 -4
- package/apps/runtime/dist/connectors/index.js +40 -15
- package/apps/runtime/dist/connectors/plugin-connections.js +3 -2
- package/apps/runtime/dist/connectors/via-account.js +8 -6
- package/apps/runtime/dist/docs/dev/plugins/connections.md +1 -1
- package/apps/runtime/dist/docs/dev/plugins/reference.md +3 -2
- package/apps/runtime/dist/docs/dev/plugins/runners.md +38 -2
- package/apps/runtime/dist/docs/dev/plugins/server.md +3 -2
- package/apps/runtime/dist/docs/dev/plugins/starters.md +1 -1
- package/apps/runtime/dist/docs/dev/plugins/studio.md +2 -1
- package/apps/runtime/dist/documents/parse.js +5 -3
- package/apps/runtime/dist/engine/events.js +1 -1
- package/apps/runtime/dist/engine/input.js +28 -8
- package/apps/runtime/dist/engine/prepare.js +2 -2
- package/apps/runtime/dist/engine/requirements.js +12 -3
- package/apps/runtime/dist/engine/runner.js +34 -23
- package/apps/runtime/dist/engine/steps.js +6 -5
- package/apps/runtime/dist/engine/surface.js +2 -1
- package/apps/runtime/dist/engine/template.js +1 -1
- package/apps/runtime/dist/engine/types.js +2 -1
- package/apps/runtime/dist/film/project.js +1 -1
- package/apps/runtime/dist/film/skills.js +2 -2
- package/apps/runtime/dist/film/sounds.js +1 -1
- package/apps/runtime/dist/film/step.js +7 -4
- package/apps/runtime/dist/film/tools.js +1 -1
- package/apps/runtime/dist/geocode.js +1 -1
- package/apps/runtime/dist/harness/apple.js +0 -5
- package/apps/runtime/dist/harness/claude.js +2 -1
- package/apps/runtime/dist/harness/codex.js +2 -1
- package/apps/runtime/dist/harness/model.js +4 -3
- package/apps/runtime/dist/harness/terminal.js +4 -3
- package/apps/runtime/dist/model-errors.js +8 -3
- package/apps/runtime/dist/models.js +58 -38
- package/apps/runtime/dist/plugins/channels.js +1 -0
- package/apps/runtime/dist/plugins/runs.js +2 -1
- package/apps/runtime/dist/routes/api.js +16 -5
- package/apps/runtime/dist/routes/lang.js +13 -0
- package/apps/runtime/dist/routes/runs.js +12 -19
- package/apps/runtime/dist/routes/studio.js +40 -28
- package/apps/runtime/dist/services/access.js +4 -3
- package/apps/runtime/dist/services/architect.js +15 -4
- package/apps/runtime/dist/services/assistant-chats.js +2 -1
- package/apps/runtime/dist/services/brand.js +3 -2
- package/apps/runtime/dist/services/errors.js +7 -5
- package/apps/runtime/dist/services/knowledge-model.js +3 -2
- package/apps/runtime/dist/services/marketplace.js +6 -5
- package/apps/runtime/dist/services/package.js +10 -3
- package/apps/runtime/dist/services/profile.js +12 -0
- package/apps/runtime/dist/services/project-files.js +13 -12
- package/apps/runtime/dist/services/projects.js +11 -6
- package/apps/runtime/dist/services/runners.js +17 -2
- package/apps/runtime/dist/services/runs.js +18 -11
- package/apps/runtime/dist/services/shares.js +2 -1
- package/apps/runtime/dist/services/site.js +21 -10
- package/apps/runtime/dist/services/space-categories.js +7 -6
- package/apps/runtime/dist/services/space-data.js +19 -16
- package/apps/runtime/dist/services/spaces.js +8 -7
- package/apps/runtime/dist/services/wizards.js +41 -20
- package/apps/runtime/dist/store/errors.js +2 -1
- package/apps/runtime/dist/store/index.js +21 -4
- package/apps/runtime/dist/store/shared-lists.js +2 -3
- package/apps/runtime/dist/tools/index.js +17 -2
- package/apps/runtime/dist/tools/plugin.js +3 -2
- package/apps/runtime/dist/widgets/render.js +1 -1
- package/apps/runtime/dist/widgets/step.js +2 -1
- package/apps/web/dist/assets/{AboutDialog-nLn3vZAN.js → AboutDialog-i-qD_Nea.js} +1 -1
- package/apps/web/dist/assets/{ChangelogDialog-Cbx3mN2p.js → ChangelogDialog-BYuVcmKR.js} +1 -1
- package/apps/web/dist/assets/ChatView-DMEYEsA7.js +10 -0
- package/apps/web/dist/assets/EditorPage-Bry35WHx.js +3 -0
- package/apps/web/dist/assets/{FlowPreview-Bvj3R8g4.js → FlowPreview-BsfjDVv8.js} +1 -1
- package/apps/web/dist/assets/{Harness-D-Z3MbYq.js → Harness-BxQDTGJk.js} +2 -2
- package/apps/web/dist/assets/NewWizardChannels-DPnLT-md.js +1 -0
- package/apps/web/dist/assets/NewWizardPage-BSh_bUIJ.js +7 -0
- package/apps/web/dist/assets/{OnboardingPage-CdoAfnI2.js → OnboardingPage-BZrRdVwr.js} +1 -1
- package/apps/web/dist/assets/ProjectPage-_3L9R3SZ.js +1 -0
- package/apps/web/dist/assets/PublicRunner-DXbh8uhe.js +1 -0
- package/apps/web/dist/assets/{ReadOnly-DQPHmvS0.js → ReadOnly-DFysMEMF.js} +1 -1
- package/apps/web/dist/assets/RunDrawer-DpUakYb9.js +4 -0
- package/apps/web/dist/assets/RunnerView-DQ-NgcCa.js +1 -0
- package/apps/web/dist/assets/SettingsPage-DyUCIRjo.js +1 -0
- package/apps/web/dist/assets/SetupPage-BgVHvN6j.js +1 -0
- package/apps/web/dist/assets/SharePage-BI6vkc9c.js +1 -0
- package/apps/web/dist/assets/changelog-Cnv7ZZEL.js +1 -0
- package/apps/web/dist/assets/{circle-alert-DCBV7n5k.js → circle-alert-BVj6idzu.js} +1 -1
- package/apps/web/dist/assets/esm-hSq-jeOG.js +7 -0
- package/apps/web/dist/assets/index-Ca1841qE.js +14 -0
- package/apps/web/dist/assets/index-GSl2Rcjy.css +1 -0
- package/apps/web/dist/assets/{lock-DlzTfwxP.js → lock-BoWS-JhN.js} +1 -1
- package/apps/web/dist/assets/{phone-BFD815nc.js → phone-CeqyvwK-.js} +1 -1
- package/apps/web/dist/assets/settings-menu-BK8WpcGv.js +1 -0
- package/apps/web/dist/assets/ui-BbIh6SOJ.js +134 -0
- package/apps/web/dist/assets/{upload-BEf5-d6X.js → upload-BQrAlHIG.js} +1 -1
- package/apps/web/dist/index.html +4 -4
- package/apps/web/dist/mcp/flow.html +76 -76
- package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +28 -4
- package/node_modules/@engenty-wizards/plugin-sdk/dist/studio.d.ts +52 -1
- package/node_modules/@engenty-wizards/plugin-sdk/dist/thread.js +6 -1
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +6 -2
- package/node_modules/@engenty-wizards/shared/dist/definition.js +13 -5
- package/node_modules/@engenty-wizards/shared/dist/knowledge.js +1 -1
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +11 -1
- package/node_modules/@engenty-wizards/shared/dist/run.js +11 -1
- package/node_modules/@engenty-wizards/shared/dist/runners.d.ts +12 -0
- package/node_modules/@engenty-wizards/shared/dist/store.d.ts +8 -0
- package/node_modules/@engenty-wizards/shared/dist/store.js +8 -0
- package/package.json +1 -1
- package/apps/web/dist/assets/ChatView-1VuHE2Q7.js +0 -10
- package/apps/web/dist/assets/EditorPage-Cp3jqQsk.js +0 -3
- package/apps/web/dist/assets/NewWizardChannels-DCF7JoL0.js +0 -1
- package/apps/web/dist/assets/NewWizardPage-BTlgq7yN.js +0 -7
- package/apps/web/dist/assets/ProjectPage-CoPSdDO9.js +0 -1
- package/apps/web/dist/assets/PublicRunner-CHwK3X75.js +0 -1
- package/apps/web/dist/assets/RunDrawer-CmGuqp0p.js +0 -4
- package/apps/web/dist/assets/RunnerView-DiaQiDbw.js +0 -1
- package/apps/web/dist/assets/SettingsPage-NzmFrJXb.js +0 -1
- package/apps/web/dist/assets/SetupPage-8mBuezdY.js +0 -1
- package/apps/web/dist/assets/SharePage-C-T5fkp1.js +0 -1
- package/apps/web/dist/assets/changelog-CsxPWKJ-.js +0 -1
- package/apps/web/dist/assets/esm-CnR8rDPi.js +0 -7
- package/apps/web/dist/assets/index-CmxVazOf.css +0 -1
- package/apps/web/dist/assets/index-D57rKyGN.js +0 -14
- package/apps/web/dist/assets/settings-menu-D-UqnL1p.js +0 -1
- package/apps/web/dist/assets/ui-DTuixH4y.js +0 -134
|
@@ -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.4.
|
|
4
|
+
version: 0.4.4
|
|
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
|
|
@@ -37,7 +37,7 @@ server.registerRunner({
|
|
|
37
37
|
| `kind` | `page`: the person opens an address. `channel`: the runner has an address of its own (a number, a bot) and drives runs from its webhooks |
|
|
38
38
|
| `capabilities` | What the runner takes in and shows; see below |
|
|
39
39
|
| `page` | A page runner's handler: what `/w/<token>/<id>` answers with. Nobody is signed in. A page runner without one throws when it is registered |
|
|
40
|
-
| `problem` | `() => string \| null`, may be async: why the runner cannot be used in the current tenant right now ("Not connected: Settings → WhatsApp"), asked inside the tenant whenever the studio lists the runners.
|
|
40
|
+
| `problem` | `() => string \| { text, kind?, note?, plan? } \| null`, may be async: why the runner cannot be used in the current tenant right now ("Not connected: Settings → WhatsApp"), asked inside the tenant whenever the studio lists the runners. A text is something to set up: the share dialog's row says "Noch nicht eingerichtet" with "Einrichten". An object says its `kind`: `setup` (the same), `waiting` (asked for, waiting for someone else; the row has no button), `plan` (the tenant's plan does not have it; the row offers the plans, "`plan` ansehen"); `note` (`{ de, en }`) is what the row says instead ("Nummer angefragt", "Ab Pro") |
|
|
41
41
|
| `address` | A channel runner: `(wizard: { id, title, token }) => { number?, keyword?, link? } \| null`, may be async. Where a person reaches that wizard: the number, the keyword that picks it there, and a link that opens the channel with the keyword written (`sms:…`, `https://wa.me/…`). The share dialog shows the keyword and number under the runner's name, copies the link (else the number) and offers the link as a QR code. Asked for each wizard that has the runner on; null while it is not set up |
|
|
42
42
|
|
|
43
43
|
A page runner is served at `/w/<token>/<id>` for every wizard that switched it on. From
|
|
@@ -130,6 +130,42 @@ One section per runner. After a change, invalidating the query `["wizard-runners
|
|
|
130
130
|
redraws the row. The settings a channel needs once per tenant (the number, the token)
|
|
131
131
|
go into a [settings section](./studio.md).
|
|
132
132
|
|
|
133
|
+
### Setting up inside the dialog
|
|
134
|
+
|
|
135
|
+
What "Einrichten" in a row opens: without more, the plugin's settings section. A setup the studio
|
|
136
|
+
half registers opens inside the dialog instead, as a view with a back arrow, at the dialog's
|
|
137
|
+
height:
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
studio.registerShareSetup({
|
|
141
|
+
runner: "sms",
|
|
142
|
+
title: () => t("setupTitle"),
|
|
143
|
+
component: Setup,
|
|
144
|
+
notice: Notice,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
function Setup({ wizard, Actions, done }: StudioShareSetupProps) {
|
|
148
|
+
return (
|
|
149
|
+
<>
|
|
150
|
+
<p>…</p>
|
|
151
|
+
<Actions>
|
|
152
|
+
<Button onClick={() => ask.mutate()}>Nummer anfragen</Button>
|
|
153
|
+
</Actions>
|
|
154
|
+
</>
|
|
155
|
+
);
|
|
156
|
+
// done({ title: "Nummer angefragt", text: "…", tone: "waiting" }) goes back.
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
| | |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `title` | The view's heading, a function: the language can change |
|
|
163
|
+
| `component` | Gets `{ wizard: { id, title }, Actions, done }`. What it renders inside `Actions` goes to the foot of the dialog, where the dialog's own buttons are. `done(banner?)` goes back to the main view and asks the rows again; `banner` (`{ title, text?, tone? }`, `tone` `done` green or `waiting` plain) shows on top |
|
|
164
|
+
| `notice` | Optional: a banner above the rows while the runner has news ("Deine Nummer ist da"), drawn by the plugin, nothing while it has none. Gets `{ wizard, open }`; `open()` opens the setup. The dialog shows the first one of the runners that are on |
|
|
165
|
+
|
|
166
|
+
One setup per runner. A setup that asks the server for something calls the plugin's own routes;
|
|
167
|
+
the rows show what `problem` answers after `done`.
|
|
168
|
+
|
|
133
169
|
## A thread door from the SDK
|
|
134
170
|
|
|
135
171
|
A messenger runs a wizard one message at a time: a field per question, options to tap or to
|
|
@@ -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.4.
|
|
4
|
+
version: 0.4.4
|
|
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
|
|
@@ -97,7 +97,8 @@ the prices are the Manage-App's ([manage contract](https://github.com/engenty/wi
|
|
|
97
97
|
const channels = await server.channels.get();
|
|
98
98
|
// { number: { number: "+43…", country: "AT", kinds: ["sms", "call"], providerRef: "PN…" } | null,
|
|
99
99
|
// request: { id, country, wish, createdAt } | null,
|
|
100
|
-
// shared: [{ number, country, kinds }]
|
|
100
|
+
// shared: [{ number, country, kinds }],
|
|
101
|
+
// allowed: true } // false: the plan has no number of its own (Free)
|
|
101
102
|
|
|
102
103
|
await server.channels.request({ userId: request.user.id, country: "AT", lang: "de" });
|
|
103
104
|
await server.channels.book({ kind: "sms", quantity: segments, key: messageSid, runId });
|
|
@@ -27,7 +27,7 @@ server.registerStarter({
|
|
|
27
27
|
| `id` | Lower case, digits and `-`, starting with a letter, at most 40 characters. Unique in the plugin |
|
|
28
28
|
| `title` | What the list of templates calls it |
|
|
29
29
|
| `description` | What it makes, in one or two sentences. Shown under the title |
|
|
30
|
-
| `language` | `"de"` or `"en"`, the language its texts are in. Default: `"
|
|
30
|
+
| `language` | `"de"` or `"en"`, the language its texts are in. Default: `"en"` |
|
|
31
31
|
| `definition` | A complete wizard definition: the JSON the studio keeps and `get_authoring_guide` describes |
|
|
32
32
|
| `files` | Files of the wizard's workspace by path: widgets, sample data. A string is written as UTF-8, a `Uint8Array` as it is |
|
|
33
33
|
|
|
@@ -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.
|
|
4
|
+
version: 0.4.4
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
The studio half is `ui/plugin.tsx`. Its default export is a function; the studio calls it after
|
|
@@ -89,6 +89,7 @@ export default defineStudioPlugin((studio) => {
|
|
|
89
89
|
| `studio.registerSettingsSection({ id, label, icon, component, menu? })` | A section of the settings at `/studio/settings/<id>`. `menu`: the user menu links to it directly |
|
|
90
90
|
| `studio.registerSpaceSection({ id, group?, label, hint?, component })` | A section of a part of the space page at `/studio/space/<group>#<id>`, below the part's own. See [The space](#the-space) |
|
|
91
91
|
| `studio.registerShareSection({ runner, component })` | A runner's own settings for a wizard (a keyword), under the share dialog's gear once the runner is on and set up. See [Runners](./runners.md#the-runners-part-of-the-share-dialog) |
|
|
92
|
+
| `studio.registerShareSetup({ runner, title, component, notice? })` | What "Einrichten" in the runner's row opens: a view inside the share dialog, with its buttons at the dialog's foot, and a banner of the runner's news. See [Runners](./runners.md#setting-up-inside-the-dialog) |
|
|
92
93
|
| `studio.registerAssistantCard({ tool, component })` | A card in the assistant's chat for what a tool of the server half returned. See [The space](./space.md#a-card-in-the-chat) |
|
|
93
94
|
|
|
94
95
|
- The studio's own addresses are taken: `/new`, `/edit`, `/settings`, `/space`, `/setup`,
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { existsSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { bilingual } from "@engenty-wizards/shared/run";
|
|
5
6
|
import { generateText, Output } from "ai";
|
|
6
7
|
import PostalMime from "postal-mime";
|
|
7
8
|
import { z } from "zod";
|
|
@@ -11,9 +12,10 @@ import { splitMarkdownByPageBreaks } from "../engenty/doc-converter/page-break.j
|
|
|
11
12
|
import { LocalProvider } from "../engenty/doc-converter/providers/local/index.js";
|
|
12
13
|
import { invoiceSchema, receiptSchema } from "../engenty/document-scanner/schemas/index.js";
|
|
13
14
|
import { env } from "../env.js";
|
|
15
|
+
import { NotedError } from "../model-errors.js";
|
|
14
16
|
import { costOf, textModel } from "../models.js";
|
|
15
17
|
import { htmlToMarkdown } from "../render/convert.js";
|
|
16
|
-
export class UnreadableDocument extends
|
|
18
|
+
export class UnreadableDocument extends NotedError {
|
|
17
19
|
}
|
|
18
20
|
const DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
19
21
|
const XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
@@ -168,7 +170,7 @@ async function read(input, opts) {
|
|
|
168
170
|
const vision = new VisionProvider(opts.charge, opts.signal, opts.call);
|
|
169
171
|
if (mime.startsWith("image/")) {
|
|
170
172
|
if (!vision.canConvert(mime)) {
|
|
171
|
-
throw new UnreadableDocument(`Bilder vom Typ ${mime} kann ich nicht lesen (PNG, JPEG, WebP gehen).`);
|
|
173
|
+
throw new UnreadableDocument(bilingual(`Bilder vom Typ ${mime} kann ich nicht lesen (PNG, JPEG, WebP gehen).`, `I cannot read images of type ${mime} (PNG, JPEG and WebP work).`));
|
|
172
174
|
}
|
|
173
175
|
const result = await vision.convert(data, name, mime);
|
|
174
176
|
return { markdown: result.markdown, pages: 1, via: "vision" };
|
|
@@ -212,7 +214,7 @@ async function read(input, opts) {
|
|
|
212
214
|
if (mime === "application/json" || mime.startsWith("text/")) {
|
|
213
215
|
return { markdown: text(), pages: null, via: "text" };
|
|
214
216
|
}
|
|
215
|
-
throw new UnreadableDocument(`„${name}“ (${mime}) kann ich nicht lesen.`);
|
|
217
|
+
throw new UnreadableDocument(bilingual(`„${name}“ (${mime}) kann ich nicht lesen.`, `I cannot read “${name}” (${mime}).`));
|
|
216
218
|
}
|
|
217
219
|
// --- structured extraction -----------------------------------------------------
|
|
218
220
|
/** The scanner's schemas say nothing about the currency; SaaS invoices come in several. */
|
|
@@ -7,7 +7,7 @@ const bus = new EventEmitter();
|
|
|
7
7
|
bus.setMaxListeners(0);
|
|
8
8
|
export async function emitEvent(runId, stepId, type, said, asset) {
|
|
9
9
|
const note = typeof said === "string" ? null : said;
|
|
10
|
-
const message = note ? noteText(note, "
|
|
10
|
+
const message = note ? noteText(note, "en") : said;
|
|
11
11
|
const [row] = await db
|
|
12
12
|
.insert(schema.runEvent)
|
|
13
13
|
.values({
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { isDecidedField, isLocationValue, MAX_FILES, offeredSlot, shownFields, } from "@engenty-wizards/shared/definition";
|
|
2
|
+
import { bilingual, saidText } from "@engenty-wizards/shared/run";
|
|
3
|
+
/** The field errors in a language, for whoever reads them. */
|
|
4
|
+
export function fieldErrorsIn(errors, lang) {
|
|
5
|
+
return errors.map((e) => ({ field: e.field, message: saidText(e.message, lang) }));
|
|
6
|
+
}
|
|
2
7
|
function empty(v) {
|
|
3
8
|
return v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0);
|
|
4
9
|
}
|
|
@@ -137,36 +142,48 @@ export function readPageInput(step, input, context = {}) {
|
|
|
137
142
|
continue;
|
|
138
143
|
}
|
|
139
144
|
if (field.required && empty(v) && field.kind !== "toggle") {
|
|
140
|
-
errors.push({ field: field.id, message: "Pflichtfeld" });
|
|
145
|
+
errors.push({ field: field.id, message: bilingual("Pflichtfeld", "Required") });
|
|
141
146
|
continue;
|
|
142
147
|
}
|
|
143
148
|
if (Array.isArray(v) && (field.kind === "image" || field.kind === "file")) {
|
|
144
149
|
if (field.min && v.length < field.min) {
|
|
145
|
-
errors.push({
|
|
150
|
+
errors.push({
|
|
151
|
+
field: field.id,
|
|
152
|
+
message: bilingual(`Bitte mindestens ${field.min} auswählen`, `Please choose at least ${field.min}`),
|
|
153
|
+
});
|
|
146
154
|
continue;
|
|
147
155
|
}
|
|
148
156
|
if (field.max && v.length > field.max) {
|
|
149
157
|
errors.push({
|
|
150
158
|
field: field.id,
|
|
151
|
-
message: `Höchstens ${field.max} – bitte einige entfernen`,
|
|
159
|
+
message: bilingual(`Höchstens ${field.max} – bitte einige entfernen`, `At most ${field.max} – please remove some`),
|
|
152
160
|
});
|
|
153
161
|
continue;
|
|
154
162
|
}
|
|
155
163
|
}
|
|
156
164
|
if (field.kind === "number" && Number.isNaN(v)) {
|
|
157
|
-
errors.push({
|
|
165
|
+
errors.push({
|
|
166
|
+
field: field.id,
|
|
167
|
+
message: bilingual("Bitte eine Zahl eingeben", "Please enter a number"),
|
|
168
|
+
});
|
|
158
169
|
continue;
|
|
159
170
|
}
|
|
160
171
|
if (field.kind === "select" && !empty(v) && !field.options?.includes(String(v))) {
|
|
161
|
-
errors.push({ field: field.id, message: "Ungültige Auswahl" });
|
|
172
|
+
errors.push({ field: field.id, message: bilingual("Ungültige Auswahl", "Invalid choice") });
|
|
162
173
|
continue;
|
|
163
174
|
}
|
|
164
175
|
if (field.kind === "slot" && !empty(v) && !field.options?.includes(String(v))) {
|
|
165
|
-
errors.push({
|
|
176
|
+
errors.push({
|
|
177
|
+
field: field.id,
|
|
178
|
+
message: bilingual("Dieser Termin wird nicht angeboten", "This time is not offered"),
|
|
179
|
+
});
|
|
166
180
|
continue;
|
|
167
181
|
}
|
|
168
182
|
if (field.kind === "email" && !empty(v) && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(v))) {
|
|
169
|
-
errors.push({
|
|
183
|
+
errors.push({
|
|
184
|
+
field: field.id,
|
|
185
|
+
message: bilingual("Keine gültige E-Mail-Adresse", "Not a valid email address"),
|
|
186
|
+
});
|
|
170
187
|
continue;
|
|
171
188
|
}
|
|
172
189
|
if (field.kind === "url" && !empty(v)) {
|
|
@@ -174,7 +191,10 @@ export function readPageInput(step, input, context = {}) {
|
|
|
174
191
|
new URL(String(v));
|
|
175
192
|
}
|
|
176
193
|
catch {
|
|
177
|
-
errors.push({
|
|
194
|
+
errors.push({
|
|
195
|
+
field: field.id,
|
|
196
|
+
message: bilingual("Keine gültige URL", "Not a valid URL"),
|
|
197
|
+
});
|
|
178
198
|
continue;
|
|
179
199
|
}
|
|
180
200
|
}
|
|
@@ -68,7 +68,7 @@ async function listenTo(ctx, id, label, value) {
|
|
|
68
68
|
if (!found || found.row.runId !== ctx.runId || !/^(audio|video)\//.test(found.row.mime)) {
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
71
|
-
await ctx.emit("info",
|
|
71
|
+
await ctx.emit("info", { code: "listen", params: { label } });
|
|
72
72
|
try {
|
|
73
73
|
const { pieces, costUsd } = await listenPieces({
|
|
74
74
|
bytes: new Uint8Array(found.data),
|
|
@@ -85,6 +85,6 @@ async function listenTo(ctx, id, label, value) {
|
|
|
85
85
|
}
|
|
86
86
|
// The step goes on without the words; the next one tries again.
|
|
87
87
|
console.error(`[run ${ctx.runId}] listen ${id}`, err);
|
|
88
|
-
await ctx.emit("info",
|
|
88
|
+
await ctx.emit("info", { code: "listenFailed", params: { label } });
|
|
89
89
|
}
|
|
90
90
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { conditionMatches, conditionsOf, isDecidedField, isDecisionStep, isOtherwise, MODEL_CLASSES, } from "@engenty-wizards/shared/definition";
|
|
2
2
|
import { closedChoicesFor, openValue, otherwiseIndex, unavoidableFrom, unavoidableSteps, } from "@engenty-wizards/shared/fit";
|
|
3
|
+
import { bilingual, saidText } from "@engenty-wizards/shared/run";
|
|
3
4
|
import { classProblem, filmClient } from "../models.js";
|
|
4
5
|
// The steps every run passes through, and the choices that lead around a missing model, are
|
|
5
6
|
// pure and shared (packages/shared/src/fit.ts): the standalone check uses the same.
|
|
@@ -220,8 +221,12 @@ export async function stepsWithoutModel(def) {
|
|
|
220
221
|
const missing = await missingClasses();
|
|
221
222
|
return new Set(def.steps.filter((s) => stepClasses(s).some((cls) => missing.has(cls))).map((s) => s.id));
|
|
222
223
|
}
|
|
224
|
+
/** The classes this wizard needs that this runtime cannot serve, in a language; empty = every step can run. */
|
|
225
|
+
export async function missingModels(def, lang = "en") {
|
|
226
|
+
return (await missingModelsSaid(def)).map((m) => ({ ...m, problem: saidText(m.problem, lang) }));
|
|
227
|
+
}
|
|
223
228
|
/** The classes this wizard needs that this runtime cannot serve; empty = every step can run. */
|
|
224
|
-
export async function
|
|
229
|
+
export async function missingModelsSaid(def) {
|
|
225
230
|
const unavoidable = unavoidableSteps(def);
|
|
226
231
|
const needs = new Map();
|
|
227
232
|
for (const step of def.steps) {
|
|
@@ -268,9 +273,13 @@ export function blockingMessage(missing) {
|
|
|
268
273
|
if (!blocking.length) {
|
|
269
274
|
return null;
|
|
270
275
|
}
|
|
271
|
-
|
|
272
|
-
.map((m) =>
|
|
276
|
+
const line = (lang) => blocking
|
|
277
|
+
.map((m) => {
|
|
278
|
+
const steps = m.steps.map((s) => (lang === "de" ? `„${s.title}“` : `“${s.title}”`));
|
|
279
|
+
return `${steps.join(", ")}: ${saidText(m.problem, lang)}`;
|
|
280
|
+
})
|
|
273
281
|
.join(" ");
|
|
282
|
+
return bilingual(line("de"), line("en"));
|
|
274
283
|
}
|
|
275
284
|
/** The class a marketplace capability stands for; the rest need no model of their own. */
|
|
276
285
|
const CAPABILITY_CLASS = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { marginOf, } from "@engenty-wizards/shared/decision";
|
|
2
2
|
import { branchValues, cardsFromData, conditionsOf, dataRef, followRules, formatsFor, isDecidedField, LIST_FORMATS, listRef, loopKey, MAX_SHOWN_FIELDS, optionsFromData, otherwiseOf, pageNeeds, whenHolds, wizardLang, } from "@engenty-wizards/shared/definition";
|
|
3
|
-
import {
|
|
3
|
+
import { bilingual, saidText, } from "@engenty-wizards/shared/run";
|
|
4
4
|
import { CONVERSATION_RUNNERS, DEFAULT_RUNNER_SETTINGS } from "@engenty-wizards/shared/runners";
|
|
5
5
|
import { eq } from "drizzle-orm";
|
|
6
6
|
import { nanoid } from "nanoid";
|
|
@@ -12,7 +12,7 @@ import { env } from "../env.js";
|
|
|
12
12
|
import { saveAsset } from "../files/storage.js";
|
|
13
13
|
import { managed, tenantInfo } from "../manage.js";
|
|
14
14
|
import { hasFfmpeg } from "../media/ffmpeg.js";
|
|
15
|
-
import { NotedError, noteOf } from "../model-errors.js";
|
|
15
|
+
import { NotedError, noteOf, saidOf } from "../model-errors.js";
|
|
16
16
|
import { conversationAvailable, freeTierRefusal, freeTierText, ModelUnavailableError, noteFreeTierRefusal, } from "../models.js";
|
|
17
17
|
import { runEnded } from "../plugins/events.js";
|
|
18
18
|
import { projectFiles } from "../services/project-files.js";
|
|
@@ -26,7 +26,7 @@ import { clearDeviceCalls } from "./device.js";
|
|
|
26
26
|
import { emitEvent, recentEvents, signalChanged } from "./events.js";
|
|
27
27
|
import { readPageInput } from "./input.js";
|
|
28
28
|
import { pushRun } from "./push.js";
|
|
29
|
-
import { blockingMessage, closedChoices,
|
|
29
|
+
import { blockingMessage, closedChoices, missingModelsSaid, openValue, stepsWithoutModel, } from "./requirements.js";
|
|
30
30
|
import { releaseResources, resourcesFor } from "./resources.js";
|
|
31
31
|
import { runAutomaticStep } from "./steps.js";
|
|
32
32
|
import { answersAsText, resolveRef } from "./template.js";
|
|
@@ -41,9 +41,9 @@ export class RunInputError extends NotedError {
|
|
|
41
41
|
this.errors = errors;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
export class NoCreditsError extends
|
|
44
|
+
export class NoCreditsError extends NotedError {
|
|
45
45
|
constructor() {
|
|
46
|
-
super("Dieser Wizard hat gerade kein Guthaben mehr. Bitte später erneut versuchen.");
|
|
46
|
+
super(bilingual("Dieser Wizard hat gerade kein Guthaben mehr. Bitte später erneut versuchen.", "This wizard has no credits left right now. Please try again later."));
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
/** The runs this process drives right now, with the handle that stops each. */
|
|
@@ -99,7 +99,7 @@ function stepOf(def, id) {
|
|
|
99
99
|
}
|
|
100
100
|
function friendly(err) {
|
|
101
101
|
if (err instanceof StepError || err instanceof ModelUnavailableError) {
|
|
102
|
-
return err
|
|
102
|
+
return saidOf(err);
|
|
103
103
|
}
|
|
104
104
|
// An own AI Gateway key on Vercel's free tier: the person can top it up or pick another way.
|
|
105
105
|
const refused = freeTierRefusal(err);
|
|
@@ -109,32 +109,32 @@ function friendly(err) {
|
|
|
109
109
|
const msg = err?.message ?? String(err);
|
|
110
110
|
// The model-gateway answers 402 once the tenant's credits are used up.
|
|
111
111
|
if (/insufficient_credits|\b402\b/i.test(msg)) {
|
|
112
|
-
return new NoCreditsError()
|
|
112
|
+
return saidOf(new NoCreditsError());
|
|
113
113
|
}
|
|
114
114
|
// A runtime that runs alone pays its provider itself: its admin can top the account up.
|
|
115
115
|
if (!managed && /minimum balance|insufficient_funds|top up your credits/i.test(msg)) {
|
|
116
|
-
return "Das Guthaben beim Modell-Anbieter reicht dafür nicht aus (Videos brauchen dort ein Mindestguthaben). Bitte dort aufladen und noch einmal versuchen.";
|
|
116
|
+
return bilingual("Das Guthaben beim Modell-Anbieter reicht dafür nicht aus (Videos brauchen dort ein Mindestguthaben). Bitte dort aufladen und noch einmal versuchen.", "The credit with the model provider is not enough for this (videos need a minimum balance there). Please top up there and try again.");
|
|
117
117
|
}
|
|
118
118
|
if (/rate.?limit|429/i.test(msg)) {
|
|
119
|
-
return "Der KI-Dienst ist gerade ausgelastet. Bitte gleich noch einmal versuchen.";
|
|
119
|
+
return bilingual("Der KI-Dienst ist gerade ausgelastet. Bitte gleich noch einmal versuchen.", "The AI service is busy right now. Please try again in a moment.");
|
|
120
120
|
}
|
|
121
121
|
if (/timeout|timed out/i.test(msg)) {
|
|
122
|
-
return "Der Schritt hat zu lange gedauert. Bitte noch einmal versuchen.";
|
|
122
|
+
return bilingual("Der Schritt hat zu lange gedauert. Bitte noch einmal versuchen.", "The step took too long. Please try again.");
|
|
123
123
|
}
|
|
124
124
|
// The platform's own provider account (budget, quota, key): nothing the person can fix.
|
|
125
125
|
if (/budget|quota|insufficient|billing|credit balance|api key|unauthori[sz]ed|401|403/i.test(msg)) {
|
|
126
|
-
return "Der KI-Dienst ist gerade nicht erreichbar. Bitte später noch einmal versuchen.";
|
|
126
|
+
return bilingual("Der KI-Dienst ist gerade nicht erreichbar. Bitte später noch einmal versuchen.", "The AI service cannot be reached right now. Please try again later.");
|
|
127
127
|
}
|
|
128
|
-
return `Dieser Schritt ist fehlgeschlagen: ${msg.slice(0, 300)}
|
|
128
|
+
return bilingual(`Dieser Schritt ist fehlgeschlagen: ${msg.slice(0, 300)}`, `This step failed: ${msg.slice(0, 300)}`);
|
|
129
129
|
}
|
|
130
130
|
/**
|
|
131
131
|
* A step that failed ends the run. A failure with a note (Apple Intelligence's) goes into the log
|
|
132
132
|
* as that note, so a runner shows it in its language; the run's error is in the wizard's.
|
|
133
133
|
*/
|
|
134
134
|
async function failRun(run, stepId, err) {
|
|
135
|
-
const
|
|
136
|
-
const message =
|
|
137
|
-
await emitEvent(run.id, stepId, "error",
|
|
135
|
+
const said = noteOf(err) ?? friendly(err);
|
|
136
|
+
const message = saidText(said, wizardLang(run.definition));
|
|
137
|
+
await emitEvent(run.id, stepId, "error", said);
|
|
138
138
|
await updateRun(run.id, { status: "failed", error: message });
|
|
139
139
|
}
|
|
140
140
|
// --- creating & driving ------------------------------------------------------
|
|
@@ -143,7 +143,7 @@ export async function createRun(input) {
|
|
|
143
143
|
const first = input.definition.steps[0];
|
|
144
144
|
const state = { values: {}, outputs: {}, history: [], notes: {} };
|
|
145
145
|
// A step every run passes through needs a model this runtime cannot serve: refuse before anything is held.
|
|
146
|
-
const blocked = blockingMessage(await
|
|
146
|
+
const blocked = blockingMessage(await missingModelsSaid(input.definition));
|
|
147
147
|
if (blocked) {
|
|
148
148
|
throw new ModelUnavailableError(blocked);
|
|
149
149
|
}
|
|
@@ -219,7 +219,11 @@ async function take(runId, tenant) {
|
|
|
219
219
|
await withTenant(tenant, () => drive(runId, abort.signal))
|
|
220
220
|
.catch(async (err) => {
|
|
221
221
|
console.error(`[run ${runId}]`, err);
|
|
222
|
-
await withTenant(tenant, () =>
|
|
222
|
+
await withTenant(tenant, async () => {
|
|
223
|
+
const run = await loadRun(runId);
|
|
224
|
+
const lang = run ? wizardLang(run.definition) : "en";
|
|
225
|
+
await updateRun(runId, { status: "failed", error: saidText(friendly(err), lang) });
|
|
226
|
+
}).catch(() => undefined);
|
|
223
227
|
})
|
|
224
228
|
.finally(async () => {
|
|
225
229
|
clearInterval(beat);
|
|
@@ -631,14 +635,17 @@ export async function submitPage(runId, stepId, input) {
|
|
|
631
635
|
const answer = values[id];
|
|
632
636
|
if (answer !== undefined &&
|
|
633
637
|
(choice.values.length === 0 || choice.values.some((v) => String(v) === String(answer)))) {
|
|
634
|
-
errors.push({
|
|
638
|
+
errors.push({
|
|
639
|
+
field: id,
|
|
640
|
+
message: bilingual("Das ist hier gerade nicht verfügbar.", "That is not available here right now."),
|
|
641
|
+
});
|
|
635
642
|
}
|
|
636
643
|
}
|
|
637
644
|
if (errors.length) {
|
|
638
645
|
throw new RunInputError(errors);
|
|
639
646
|
}
|
|
640
647
|
const connections = step.fields.some((f) => f.kind === "connection")
|
|
641
|
-
? await connectionViews(run.definition.connections ?? [], scopeOf(run), await projectIdOf(run))
|
|
648
|
+
? await connectionViews(run.definition.connections ?? [], scopeOf(run), await projectIdOf(run), wizardLang(run.definition))
|
|
642
649
|
: [];
|
|
643
650
|
for (const field of step.fields) {
|
|
644
651
|
if (field.kind === "image" || field.kind === "file") {
|
|
@@ -646,7 +653,9 @@ export async function submitPage(runId, stepId, input) {
|
|
|
646
653
|
for (const id of Array.isArray(value) ? value : value ? [value] : []) {
|
|
647
654
|
const asset = await db.query.asset.findFirst({ where: eq(schema.asset.id, String(id)) });
|
|
648
655
|
if (asset?.runId !== runId) {
|
|
649
|
-
throw new RunInputError([
|
|
656
|
+
throw new RunInputError([
|
|
657
|
+
{ field: field.id, message: bilingual("Datei nicht gefunden", "File not found") },
|
|
658
|
+
]);
|
|
650
659
|
}
|
|
651
660
|
}
|
|
652
661
|
}
|
|
@@ -657,7 +666,9 @@ export async function submitPage(runId, stepId, input) {
|
|
|
657
666
|
values[field.id] = account.label;
|
|
658
667
|
}
|
|
659
668
|
else if (field.required) {
|
|
660
|
-
throw new RunInputError([
|
|
669
|
+
throw new RunInputError([
|
|
670
|
+
{ field: field.id, message: bilingual("Bitte zuerst verbinden", "Please connect first") },
|
|
671
|
+
]);
|
|
661
672
|
}
|
|
662
673
|
}
|
|
663
674
|
}
|
|
@@ -701,7 +712,7 @@ export async function reviewStep(runId, stepId, action) {
|
|
|
701
712
|
if (!step.show.includes(action.target)) {
|
|
702
713
|
throw new RunConflict("Unknown target");
|
|
703
714
|
}
|
|
704
|
-
state.notes[action.target] = action.note.slice(0, 2000) || "
|
|
715
|
+
state.notes[action.target] = action.note.slice(0, 2000) || "Please make a new variant.";
|
|
705
716
|
// A step with several results makes only the named ones again.
|
|
706
717
|
if (action.items?.length) {
|
|
707
718
|
state.redo = { ...state.redo, [action.target]: action.items };
|
|
@@ -884,7 +895,7 @@ export async function runView(run, brand) {
|
|
|
884
895
|
ask: run.status === "running" ? run.ask : null,
|
|
885
896
|
lists,
|
|
886
897
|
connections: step?.type === "page" && step.fields.some((f) => f.kind === "connection")
|
|
887
|
-
? await connectionViews(def.connections ?? [], scopeOf(run), await projectIdOf(run))
|
|
898
|
+
? await connectionViews(def.connections ?? [], scopeOf(run), await projectIdOf(run), wizardLang(def))
|
|
888
899
|
: [],
|
|
889
900
|
keeps: Boolean(def.lists?.length || def.connections?.length),
|
|
890
901
|
shareUrl: run.shareToken ? `${env.publicUrl}/s/${run.shareToken}` : null,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { pOf } from "@engenty-wizards/shared/decision";
|
|
2
2
|
import { allFields, dataRef, isDecisionStep, LOCALES, templateRefs, wizardLang, } from "@engenty-wizards/shared/definition";
|
|
3
3
|
import { mainLogo, ruleLines } from "@engenty-wizards/shared/projects";
|
|
4
|
+
import { bilingual } from "@engenty-wizards/shared/run";
|
|
4
5
|
import { SURFACE_GUIDE, validateSurface, } from "@engenty-wizards/shared/surface";
|
|
5
6
|
import { Agent } from "@mastra/core/agent";
|
|
6
7
|
import { generateText, Output } from "ai";
|
|
@@ -378,7 +379,7 @@ function agentEntries(step, ctx) {
|
|
|
378
379
|
async function runAgentEach(step, ctx) {
|
|
379
380
|
const entries = agentEntries(step, ctx);
|
|
380
381
|
if (!entries.length) {
|
|
381
|
-
throw new StepError("Es gibt keine Einträge, für die dieser Schritt arbeiten könnte.");
|
|
382
|
+
throw new StepError(bilingual("Es gibt keine Einträge, für die dieser Schritt arbeiten könnte.", "There are no entries for this step to work on."));
|
|
382
383
|
}
|
|
383
384
|
// The view, if any, is composed once over the table of all entries.
|
|
384
385
|
const single = {
|
|
@@ -553,7 +554,7 @@ export async function runAgentStep(step, ctx) {
|
|
|
553
554
|
const text = step.output.format === "json" ? everything : closing || everything;
|
|
554
555
|
if (step.output.format !== "json") {
|
|
555
556
|
if (!text && !assets.length) {
|
|
556
|
-
throw new StepError("Der Schritt hat kein Ergebnis geliefert.");
|
|
557
|
+
throw new StepError(bilingual("Der Schritt hat kein Ergebnis geliefert.", "The step did not deliver a result."));
|
|
557
558
|
}
|
|
558
559
|
return { text, assets: assets.length ? assets : undefined, at: new Date().toISOString() };
|
|
559
560
|
}
|
|
@@ -754,7 +755,7 @@ async function writeHtml(step, ctx, kind) {
|
|
|
754
755
|
.replace(/```\s*$/, "")
|
|
755
756
|
.trim();
|
|
756
757
|
if (!/<html|<!doctype/i.test(html)) {
|
|
757
|
-
throw new StepError("Das Dokument konnte nicht erstellt werden.");
|
|
758
|
+
throw new StepError(bilingual("Das Dokument konnte nicht erstellt werden.", "The document could not be made."));
|
|
758
759
|
}
|
|
759
760
|
// On screen (preview, PNG) a print document needs the margin its @page rule gives it on paper.
|
|
760
761
|
return kind === "document"
|
|
@@ -805,7 +806,7 @@ async function inParallel(items, limit, work) {
|
|
|
805
806
|
async function runMediaStep(step, ctx, asset) {
|
|
806
807
|
const entries = entriesOf(step, ctx);
|
|
807
808
|
if (!entries.length) {
|
|
808
|
-
throw new StepError("Für diesen Schritt fehlen die Vorlagen (Bilder oder Szenen).");
|
|
809
|
+
throw new StepError(bilingual("Für diesen Schritt fehlen die Vorlagen (Bilder oder Szenen).", "This step is missing its sources (images or scenes)."));
|
|
809
810
|
}
|
|
810
811
|
const previous = ctx.state.outputs[step.id];
|
|
811
812
|
const before = previous?.assets?.filter((a) => a.kind === asset) ?? [];
|
|
@@ -897,7 +898,7 @@ export async function runGenerateStep(step, ctx) {
|
|
|
897
898
|
case "voice": {
|
|
898
899
|
const text = renderTemplate(step.prompt, ctx.scope).trim().slice(0, 4000);
|
|
899
900
|
if (!text) {
|
|
900
|
-
throw new StepError("Es gibt keinen Text zum Vorlesen.");
|
|
901
|
+
throw new StepError(bilingual("Es gibt keinen Text zum Vorlesen.", "There is no text to read aloud."));
|
|
901
902
|
}
|
|
902
903
|
const note = ctx.state.notes[step.id];
|
|
903
904
|
const media = await generateSpeechMedia({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { assembleSurface, dataRef, listRef, } from "@engenty-wizards/shared/definition";
|
|
2
|
+
import { bilingual } from "@engenty-wizards/shared/run";
|
|
2
3
|
import { validateSurface } from "@engenty-wizards/shared/surface";
|
|
3
4
|
import { decide } from "./decide.js";
|
|
4
5
|
import { answersAsText, resolveRef } from "./template.js";
|
|
@@ -13,7 +14,7 @@ export async function runSurfaceStep(step, ctx) {
|
|
|
13
14
|
const components = step.components ?? (await decidedComponents(step, ctx));
|
|
14
15
|
const issues = validateSurface(components, Object.keys(data));
|
|
15
16
|
if (issues.length) {
|
|
16
|
-
throw new StepError(`Die Ansicht ist fehlerhaft: ${issues.map((i) => i.message).join(" ")}`);
|
|
17
|
+
throw new StepError(bilingual(`Die Ansicht ist fehlerhaft: ${issues.map((i) => i.message).join(" ")}`, `The view is faulty: ${issues.map((i) => i.message).join(" ")}`));
|
|
17
18
|
}
|
|
18
19
|
return { surface: { components, data }, at: new Date().toISOString() };
|
|
19
20
|
}
|
|
@@ -11,7 +11,7 @@ function money(n, currency, locale) {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
/** Line items as a Markdown table with their totals — what a model needs to write a document. */
|
|
14
|
-
export function itemsAsMarkdown(field, value, values, lang = "
|
|
14
|
+
export function itemsAsMarkdown(field, value, values, lang = "en") {
|
|
15
15
|
const locale = LOCALES[lang];
|
|
16
16
|
const totals = itemsTotals(field, value, values);
|
|
17
17
|
const cols = field.columns ?? [];
|
|
@@ -250,7 +250,7 @@ export async function prepareFilmFolder(step, ctx) {
|
|
|
250
250
|
const preset = { "16:9": "landscape", "9:16": "portrait", "1:1": "square" }[step.format];
|
|
251
251
|
const init = await runHyperframes(["init", "film", "--non-interactive", "--skip-transcribe", `--resolution=${preset}`], { cwd: root, signal: ctx.signal, timeoutMs: 5 * 60_000 });
|
|
252
252
|
if (init.code !== 0 || !existsSync(join(root, "film"))) {
|
|
253
|
-
throw new Error(`
|
|
253
|
+
throw new Error(`The film project could not be set up: ${init.out.slice(-300)}`);
|
|
254
254
|
}
|
|
255
255
|
await put(root, ".engenty/inputs", stamp);
|
|
256
256
|
return { root, brief };
|
|
@@ -21,7 +21,7 @@ const fetching = new Map();
|
|
|
21
21
|
export function platformSkills(name) {
|
|
22
22
|
const pkg = PLATFORM_SKILLS[name];
|
|
23
23
|
if (!pkg) {
|
|
24
|
-
return Promise.reject(new Error(`
|
|
24
|
+
return Promise.reject(new Error(`There is no skill package “${name}”.`));
|
|
25
25
|
}
|
|
26
26
|
const dir = join(env.dataDir, "skills", `${pkg.name}@${pkg.version.slice(0, 12)}`);
|
|
27
27
|
if (existsSync(join(dir, ".complete"))) {
|
|
@@ -37,7 +37,7 @@ export function platformSkills(name) {
|
|
|
37
37
|
async function fetchPackage(pkg, dir) {
|
|
38
38
|
const res = await fetch(pkg.tarball);
|
|
39
39
|
if (!res.ok) {
|
|
40
|
-
throw new Error(`
|
|
40
|
+
throw new Error(`The skill package “${pkg.name}” could not be loaded (${res.status}).`);
|
|
41
41
|
}
|
|
42
42
|
const tmp = `${dir}.part`;
|
|
43
43
|
await rm(tmp, { recursive: true, force: true });
|
|
@@ -108,7 +108,7 @@ async function build() {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
if (!sounds.length) {
|
|
111
|
-
throw new Error("
|
|
111
|
+
throw new Error("The sounds for films could not be loaded.");
|
|
112
112
|
}
|
|
113
113
|
sounds.sort((a, b) => a.file.localeCompare(b.file));
|
|
114
114
|
await writeFile(join(tmp, "catalog.json"), JSON.stringify({
|
|
@@ -2,10 +2,12 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { readFile, rm, stat } from "node:fs/promises";
|
|
4
4
|
import { isAbsolute, join, normalize } from "node:path";
|
|
5
|
+
import { bilingual } from "@engenty-wizards/shared/run";
|
|
5
6
|
import { StepError } from "../engine/types.js";
|
|
6
7
|
import { CLAUDE_BIN, claudeEnv } from "../harness/claude.js";
|
|
7
8
|
import { failureOf } from "../harness/model.js";
|
|
8
9
|
import { openBridge } from "../mcp/bridge.js";
|
|
10
|
+
import { errorText } from "../model-errors.js";
|
|
9
11
|
import { filmClient } from "../models.js";
|
|
10
12
|
import { prepareFilmFolder, savedSession, saveSession } from "./project.js";
|
|
11
13
|
import { runHyperframes } from "./tools.js";
|
|
@@ -227,12 +229,13 @@ export async function runFilmStep(step, ctx) {
|
|
|
227
229
|
if (ctx.signal.aborted) {
|
|
228
230
|
throw err;
|
|
229
231
|
}
|
|
230
|
-
throw new StepError(`${client.name} hat beim Film aufgehört: ${err.message}. Die Arbeit ist gespeichert; ein neuer Versuch macht dort weiter.`);
|
|
232
|
+
throw new StepError(bilingual(`${client.name} hat beim Film aufgehört: ${err.message}. Die Arbeit ist gespeichert; ein neuer Versuch macht dort weiter.`, `${client.name} stopped working on the film: ${err.message}. The work is saved; a new attempt carries on from there.`));
|
|
231
233
|
});
|
|
232
234
|
const result = last;
|
|
233
235
|
if (!result || result.is_error) {
|
|
234
|
-
const detail = String(result?.result ?? (stderr.trim() || "
|
|
235
|
-
|
|
236
|
+
const detail = String(result?.result ?? (stderr.trim() || "no result")).slice(0, 400);
|
|
237
|
+
const failure = failureOf(client, detail);
|
|
238
|
+
throw new StepError(bilingual(`${errorText(failure, "de")} Die Arbeit ist gespeichert; ein neuer Versuch macht dort weiter.`, `${errorText(failure, "en")} The work is saved; a new attempt carries on from there.`));
|
|
236
239
|
}
|
|
237
240
|
return result;
|
|
238
241
|
};
|
|
@@ -262,7 +265,7 @@ export async function runFilmStep(step, ctx) {
|
|
|
262
265
|
}
|
|
263
266
|
const problem = renderProblem(render.out);
|
|
264
267
|
if (attempt >= RENDER_FIXES) {
|
|
265
|
-
throw new StepError(`Der Film ließ sich nicht rendern: ${problem.slice(0, 400)}`);
|
|
268
|
+
throw new StepError(bilingual(`Der Film ließ sich nicht rendern: ${problem.slice(0, 400)}`, `The film could not be rendered: ${problem.slice(0, 400)}`));
|
|
266
269
|
}
|
|
267
270
|
await ctx.emit("tool", { code: "filmFix" });
|
|
268
271
|
const fixed = await turn(`The final render of film/ failed:\n\n${problem}\n\nFind the cause in the composition's code, fix it, run \`check\` and a draft render through the hyperframes tool until both pass, and end with one line on what you fixed.`, await savedSession(root));
|