wizards 0.4.3 → 0.4.5
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 +5 -1
- package/apps/runtime/dist/docs/dev/plugins/routes.md +35 -1
- package/apps/runtime/dist/docs/dev/plugins/server.md +27 -1
- package/apps/runtime/dist/docs/dev/plugins/starters.md +1 -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/manage.js +29 -0
- package/apps/runtime/dist/model-errors.js +8 -3
- package/apps/runtime/dist/models.js +58 -38
- package/apps/runtime/dist/plugins/channels.js +77 -0
- package/apps/runtime/dist/plugins/loader.js +9 -1
- package/apps/runtime/dist/plugins/public.js +16 -2
- package/apps/runtime/dist/plugins/registry.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/plugin-public.js +65 -12
- 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/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-DWaBKvI6.js → AboutDialog-BCtnYRj2.js} +1 -1
- package/apps/web/dist/assets/{ChangelogDialog-BcM1gXPM.js → ChangelogDialog-DBjoIIHL.js} +1 -1
- package/apps/web/dist/assets/ChatView-BcyUYN0Q.js +10 -0
- package/apps/web/dist/assets/EditorPage-Ce1QD-DK.js +3 -0
- package/apps/web/dist/assets/{FlowPreview-S1s1yRaP.js → FlowPreview-BtpAdyt-.js} +1 -1
- package/apps/web/dist/assets/{Harness-DILx5Edb.js → Harness-DJJ_yMLY.js} +2 -2
- package/apps/web/dist/assets/NewWizardChannels-BrYLMV6F.js +1 -0
- package/apps/web/dist/assets/NewWizardPage-827zwhhW.js +7 -0
- package/apps/web/dist/assets/{OnboardingPage-xoGrKIJ-.js → OnboardingPage-jC6FidpF.js} +1 -1
- package/apps/web/dist/assets/ProjectPage-BYcM1-1H.js +1 -0
- package/apps/web/dist/assets/{PublicRunner-Cab2jUy6.js → PublicRunner-BofZl0MO.js} +1 -1
- package/apps/web/dist/assets/{ReadOnly-DQPHmvS0.js → ReadOnly-B4nIGkoj.js} +1 -1
- package/apps/web/dist/assets/RunDrawer-Dx-BRCkY.js +4 -0
- package/apps/web/dist/assets/RunnerView-CDCp61aq.js +1 -0
- package/apps/web/dist/assets/SettingsPage-B49nLHwv.js +1 -0
- package/apps/web/dist/assets/SetupPage-4tzzHUq0.js +1 -0
- package/apps/web/dist/assets/SharePage-Bs4s73h4.js +1 -0
- package/apps/web/dist/assets/changelog-D-12LwZB.js +1 -0
- package/apps/web/dist/assets/{circle-alert-DCBV7n5k.js → circle-alert-BueS7qO8.js} +1 -1
- package/apps/web/dist/assets/esm-CjGdsKU4.js +7 -0
- package/apps/web/dist/assets/index-BRDn-5v_.js +14 -0
- package/apps/web/dist/assets/index-mlnAPOdf.css +1 -0
- package/apps/web/dist/assets/{lock-DlzTfwxP.js → lock-CkXYnQux.js} +1 -1
- package/apps/web/dist/assets/{phone-BFD815nc.js → phone-CHkEf-Dz.js} +1 -1
- package/apps/web/dist/assets/settings-menu-CrVpArM_.js +1 -0
- package/apps/web/dist/assets/ui-DrdqFaQt.js +134 -0
- package/apps/web/dist/assets/{upload-BEf5-d6X.js → upload-CzkkZqAo.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 +98 -2
- 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/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-D0RRIl15.js +0 -10
- package/apps/web/dist/assets/EditorPage-DrJ6e_2q.js +0 -3
- package/apps/web/dist/assets/NewWizardChannels-CEILDADp.js +0 -1
- package/apps/web/dist/assets/NewWizardPage-DxOtK2E-.js +0 -7
- package/apps/web/dist/assets/ProjectPage-6E5z4nZA.js +0 -1
- package/apps/web/dist/assets/RunDrawer-B3FNYpgZ.js +0 -4
- package/apps/web/dist/assets/RunnerView-D6sGVnRy.js +0 -1
- package/apps/web/dist/assets/SettingsPage-DHtIqmOJ.js +0 -1
- package/apps/web/dist/assets/SetupPage-Cn9iQbJx.js +0 -1
- package/apps/web/dist/assets/SharePage-CMr72PeU.js +0 -1
- package/apps/web/dist/assets/changelog-DfmWTlav.js +0 -1
- package/apps/web/dist/assets/esm-BPfTI3lL.js +0 -7
- package/apps/web/dist/assets/index-B-D8UvL9.js +0 -14
- package/apps/web/dist/assets/index-CmxVazOf.css +0 -1
- package/apps/web/dist/assets/settings-menu-BJm0CwOj.js +0 -1
- package/apps/web/dist/assets/ui-DTuixH4y.js +0 -134
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GRADIENT_TYPES, gradientCss, mainLogo, } from "@engenty-wizards/shared/projects";
|
|
2
|
+
import { bilingual } from "@engenty-wizards/shared/run";
|
|
2
3
|
import { generateText, Output } from "ai";
|
|
3
4
|
import { and, asc, eq } from "drizzle-orm";
|
|
4
5
|
import { z } from "zod";
|
|
@@ -65,14 +66,14 @@ export async function describeGradient(brand, text, current) {
|
|
|
65
66
|
answer = result.output;
|
|
66
67
|
}
|
|
67
68
|
catch {
|
|
68
|
-
throw new ServiceError("invalid", "Daraus ließ sich kein Verlauf machen. Beschreib ihn bitte anders.");
|
|
69
|
+
throw new ServiceError("invalid", bilingual("Daraus ließ sich kein Verlauf machen. Beschreib ihn bitte anders.", "No gradient could be made from that. Please describe it differently."));
|
|
69
70
|
}
|
|
70
71
|
const stops = answer.stops
|
|
71
72
|
.filter((s) => HEX.test(s.color))
|
|
72
73
|
.slice(0, 12)
|
|
73
74
|
.map((s) => ({ color: s.color.toLowerCase(), at: Math.min(100, Math.max(0, s.at)) }));
|
|
74
75
|
if (stops.length < 2) {
|
|
75
|
-
throw new ServiceError("invalid", "Daraus ließ sich kein Verlauf machen. Beschreib ihn bitte anders.");
|
|
76
|
+
throw new ServiceError("invalid", bilingual("Daraus ließ sich kein Verlauf machen. Beschreib ihn bitte anders.", "No gradient could be made from that. Please describe it differently."));
|
|
76
77
|
}
|
|
77
78
|
return {
|
|
78
79
|
name: answer.name.trim().slice(0, 80),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { errorText, NotedError } from "../model-errors.js";
|
|
1
2
|
const STATUS = {
|
|
2
3
|
not_found: 404,
|
|
3
4
|
invalid: 400,
|
|
@@ -8,19 +9,20 @@ const STATUS = {
|
|
|
8
9
|
read_only: 403,
|
|
9
10
|
forbidden: 403,
|
|
10
11
|
};
|
|
11
|
-
export class ServiceError extends
|
|
12
|
+
export class ServiceError extends NotedError {
|
|
12
13
|
code;
|
|
13
14
|
data;
|
|
14
|
-
constructor(code,
|
|
15
|
-
super(
|
|
15
|
+
constructor(code, said, data = {}) {
|
|
16
|
+
super(said);
|
|
16
17
|
this.code = code;
|
|
17
18
|
this.data = data;
|
|
18
19
|
}
|
|
19
20
|
get status() {
|
|
20
21
|
return STATUS[this.code];
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
/** The answer, its words in the asker's language; English where none is known. */
|
|
24
|
+
toJSON(lang = "en") {
|
|
25
|
+
return { error: errorText(this, lang), code: this.code, ...this.data };
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
export const notFound = () => new ServiceError("not_found", "not found");
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dayOf, slugOf } from "@engenty-wizards/shared/knowledge";
|
|
2
|
+
import { bilingual } from "@engenty-wizards/shared/run";
|
|
2
3
|
import { columnIdOf } from "@engenty-wizards/shared/space-data";
|
|
3
4
|
import { generateText, Output } from "ai";
|
|
4
5
|
import { and, eq, inArray, isNull } from "drizzle-orm";
|
|
@@ -222,7 +223,7 @@ function titleOf(markdown, name) {
|
|
|
222
223
|
function reviewOf(parsed, markdown) {
|
|
223
224
|
const illegible = (markdown.match(/\[unleserlich\]/gi) ?? []).length;
|
|
224
225
|
if (parsed.via === "vision" && (illegible >= 3 || markdown.replace(/\s/g, "").length < 200)) {
|
|
225
|
-
return "
|
|
226
|
+
return "Hard to read: please compare with the original.";
|
|
226
227
|
}
|
|
227
228
|
return null;
|
|
228
229
|
}
|
|
@@ -503,7 +504,7 @@ export async function writeSummary(valueId) {
|
|
|
503
504
|
where: eq(schema.spaceCategoryValue.id, valueId),
|
|
504
505
|
});
|
|
505
506
|
if (!value) {
|
|
506
|
-
throw new ServiceError("not_found", "Diesen Wert gibt es nicht.");
|
|
507
|
+
throw new ServiceError("not_found", bilingual("Diesen Wert gibt es nicht.", "There is no such value."));
|
|
507
508
|
}
|
|
508
509
|
const category = await categoryRow(value.categoryId);
|
|
509
510
|
const { items, total } = await itemsOfValue(value.id);
|
|
@@ -4,6 +4,7 @@ import { DEFINITION_VERSION, readable, wizardSchema, } from "@engenty-wizards/sh
|
|
|
4
4
|
import { capabilitiesOf, costTierOf, effortOf, formatsOf, MARKETPLACE_LANGS, pluginsOf, } from "@engenty-wizards/shared/marketplace";
|
|
5
5
|
import { wizardOutline } from "@engenty-wizards/shared/marketplace-entry";
|
|
6
6
|
import { searchEntries } from "@engenty-wizards/shared/marketplace-search";
|
|
7
|
+
import { bilingual } from "@engenty-wizards/shared/run";
|
|
7
8
|
import { and, eq, inArray, notInArray } from "drizzle-orm";
|
|
8
9
|
import { formulaEstimate } from "../credits/estimate.js";
|
|
9
10
|
import { control, controlDb, db, schema, withTenant } from "../db/client.js";
|
|
@@ -25,7 +26,7 @@ import { ServiceError } from "./errors.js";
|
|
|
25
26
|
* listed first and never fetched. They are known as `<plugin id>.<starter id>`; a marketplace
|
|
26
27
|
* entry's id has no dot, so the two never meet.
|
|
27
28
|
*/
|
|
28
|
-
export const asLang = (raw) => MARKETPLACE_LANGS.includes(raw ?? "") ? raw : "
|
|
29
|
+
export const asLang = (raw) => MARKETPLACE_LANGS.includes(raw ?? "") ? raw : "en";
|
|
29
30
|
/** A search waits this long for the marketplace; then it searches what is kept here. */
|
|
30
31
|
const TIMEOUT_MS = 5000;
|
|
31
32
|
/** One call to the marketplace's API; null without an answer, or with an error. */
|
|
@@ -114,7 +115,7 @@ function pluginSummary({ id, plugin, starter }) {
|
|
|
114
115
|
.slice(0, 16),
|
|
115
116
|
updatedAt: (changed?.mtime ?? new Date()).toISOString(),
|
|
116
117
|
starter: true,
|
|
117
|
-
language: starter.language ?? "
|
|
118
|
+
language: starter.language ?? "en",
|
|
118
119
|
title: starter.title,
|
|
119
120
|
pitch: starter.description,
|
|
120
121
|
// Found by the plugin's name as well: "Termine" finds the appointment plugin's starters.
|
|
@@ -390,7 +391,7 @@ export async function marketplaceWizard(id, lang) {
|
|
|
390
391
|
revision: null,
|
|
391
392
|
plugin: plugin.source.id,
|
|
392
393
|
title: starter.title,
|
|
393
|
-
language: starter.language ?? "
|
|
394
|
+
language: starter.language ?? "en",
|
|
394
395
|
definition: structuredClone(starter.definition),
|
|
395
396
|
files: Object.fromEntries(Object.entries(starter.files ?? {}).map(([path, data]) => [
|
|
396
397
|
path,
|
|
@@ -403,13 +404,13 @@ export async function marketplaceWizard(id, lang) {
|
|
|
403
404
|
return null;
|
|
404
405
|
}
|
|
405
406
|
if (!readable(found.definition)) {
|
|
406
|
-
throw new ServiceError("refused", "Diese Vorlage braucht eine neuere Version der App. Bitte aktualisiere die App.");
|
|
407
|
+
throw new ServiceError("refused", bilingual("Diese Vorlage braucht eine neuere Version der App. Bitte aktualisiere die App.", "This template needs a newer version of the app. Please update the app."));
|
|
407
408
|
}
|
|
408
409
|
// A wizard whose plugins are not here would stop at its first step that uses them.
|
|
409
410
|
const have = await pluginIdsOf(currentTenant());
|
|
410
411
|
const needs = pluginsOf(wizardSchema.parse(found.definition)).filter((p) => !have.has(p));
|
|
411
412
|
if (needs.length) {
|
|
412
|
-
throw new ServiceError("refused", `Diese Vorlage braucht ${needs.length === 1 ? "das Plugin" : "die Plugins"} ${needs.join(", ")}: mit Pro auf engenty.ai dabei.`);
|
|
413
|
+
throw new ServiceError("refused", bilingual(`Diese Vorlage braucht ${needs.length === 1 ? "das Plugin" : "die Plugins"} ${needs.join(", ")}: mit Pro auf engenty.ai dabei.`, `This template needs the ${needs.length === 1 ? "plugin" : "plugins"} ${needs.join(", ")}: included with Pro on engenty.ai.`));
|
|
413
414
|
}
|
|
414
415
|
return {
|
|
415
416
|
id: found.id,
|
|
@@ -136,16 +136,23 @@ async function missing(userId, projectId, draft, manifest) {
|
|
|
136
136
|
];
|
|
137
137
|
}
|
|
138
138
|
/** Makes a new wizard of a package in the project: the draft and its workspace, not published. */
|
|
139
|
-
export async function importWizard(userId, projectId, data, name
|
|
139
|
+
export async function importWizard(userId, projectId, data, name,
|
|
140
|
+
/** The language the admin reads the import's note in. */
|
|
141
|
+
lang = "en") {
|
|
140
142
|
await requireAdmin();
|
|
141
143
|
const { manifest, files } = readPackage(data);
|
|
142
144
|
// The shape is checked before anything is written; a wrong one leaves no wizard behind.
|
|
143
145
|
const { draft } = parseDraft(manifest.definition, files.map((f) => f.path));
|
|
144
146
|
const lacks = await missing(userId, projectId, draft, manifest);
|
|
147
|
+
const de = lang === "de";
|
|
145
148
|
const note = [
|
|
146
|
-
|
|
149
|
+
de
|
|
150
|
+
? `Importiert${name ? ` aus „${name}“` : ""}: ${draft.steps.length} Schritte, ${files.length} ${files.length === 1 ? "Datei" : "Dateien"}.`
|
|
151
|
+
: `Imported${name ? ` from “${name}”` : ""}: ${draft.steps.length} steps, ${files.length} ${files.length === 1 ? "file" : "files"}.`,
|
|
147
152
|
lacks.length
|
|
148
|
-
?
|
|
153
|
+
? de
|
|
154
|
+
? `Diesem Projekt fehlt noch: ${lacks.join(", ")} – in den Einstellungen unter „Connectors“ hinzufügen.`
|
|
155
|
+
: `This project still lacks: ${lacks.join(", ")} – add it under “Connectors” in the settings.`
|
|
149
156
|
: "",
|
|
150
157
|
]
|
|
151
158
|
.filter(Boolean)
|
|
@@ -27,6 +27,18 @@ export async function userProfile(user) {
|
|
|
27
27
|
phone: stored.phone ?? "",
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
+
const langKey = (user) => `lang:${user.tenantId}:${user.id}`;
|
|
31
|
+
/**
|
|
32
|
+
* The language the person picked in the studio, kept with them rather than one browser: a new
|
|
33
|
+
* browser or device opens in it. Null until they pick one; the browser's language stands then.
|
|
34
|
+
*/
|
|
35
|
+
export async function userLang(user) {
|
|
36
|
+
const lang = await readSetting(langKey(user));
|
|
37
|
+
return lang === "de" || lang === "en" ? lang : null;
|
|
38
|
+
}
|
|
39
|
+
export async function saveUserLang(user, lang) {
|
|
40
|
+
await writeSetting(langKey(user), lang);
|
|
41
|
+
}
|
|
30
42
|
export async function saveProfile(user, profile) {
|
|
31
43
|
// Name and e-mail an account gives stay the person's own underneath: they come back unlinked.
|
|
32
44
|
const stored = (await readSetting(key(user))) ?? {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PROJECT_LIMITS, projectFileLimit, } from "@engenty-wizards/shared/projects";
|
|
2
|
+
import { bilingual } from "@engenty-wizards/shared/run";
|
|
2
3
|
import { generateText } from "ai";
|
|
3
4
|
import { and, asc, count, eq, max } from "drizzle-orm";
|
|
4
5
|
import { db, schema } from "../db/client.js";
|
|
@@ -27,7 +28,7 @@ export function fileView(row) {
|
|
|
27
28
|
description: row.description,
|
|
28
29
|
source: row.source,
|
|
29
30
|
status: stale ? "failed" : row.status,
|
|
30
|
-
error: stale ? "
|
|
31
|
+
error: stale ? "Interrupted. Please read it in again." : row.error,
|
|
31
32
|
indexed: row.indexed,
|
|
32
33
|
pages: row.pages,
|
|
33
34
|
chars: row.chars,
|
|
@@ -78,14 +79,14 @@ export async function addProjectFile(projectId, input) {
|
|
|
78
79
|
const mime = documentMime(input.name, input.mime) || "application/octet-stream";
|
|
79
80
|
if (!accepts(input.kind, mime)) {
|
|
80
81
|
throw new ServiceError("invalid", input.kind === "logo"
|
|
81
|
-
? "Ein Logo ist ein Bild (PNG, SVG, JPEG, WebP)."
|
|
82
|
+
? bilingual("Ein Logo ist ein Bild (PNG, SVG, JPEG, WebP).", "A logo is an image (PNG, SVG, JPEG, WebP).")
|
|
82
83
|
: input.kind === "pattern"
|
|
83
|
-
? "Ein Muster ist ein Bild (SVG, PNG, JPEG, WebP)."
|
|
84
|
-
: "Hierher gehören Bilder, Videos, Audio, PDFs, Schriften, Lottie und 3D-Modelle. Texte zum Nachschlagen bitte zu den Dokumenten.");
|
|
84
|
+
? bilingual("Ein Muster ist ein Bild (SVG, PNG, JPEG, WebP).", "A pattern is an image (SVG, PNG, JPEG, WebP).")
|
|
85
|
+
: bilingual("Hierher gehören Bilder, Videos, Audio, PDFs, Schriften, Lottie und 3D-Modelle. Texte zum Nachschlagen bitte zu den Dokumenten.", "Images, videos, audio, PDFs, fonts, Lottie and 3D models belong here. Texts to look things up in go to the documents."));
|
|
85
86
|
}
|
|
86
87
|
const limit = projectFileLimit(input.kind, mime);
|
|
87
88
|
if (!input.data.byteLength || input.data.byteLength > limit) {
|
|
88
|
-
throw new ServiceError("invalid", `Bitte eine Datei bis ${Math.round(limit / 1_000_000)} MB wählen.`);
|
|
89
|
+
throw new ServiceError("invalid", bilingual(`Bitte eine Datei bis ${Math.round(limit / 1_000_000)} MB wählen.`, `Please choose a file of up to ${Math.round(limit / 1_000_000)} MB.`));
|
|
89
90
|
}
|
|
90
91
|
const scope = and(eq(schema.projectFile.projectId, projectId), eq(schema.projectFile.kind, input.kind));
|
|
91
92
|
const [{ n, last }] = await db
|
|
@@ -93,7 +94,7 @@ export async function addProjectFile(projectId, input) {
|
|
|
93
94
|
.from(schema.projectFile)
|
|
94
95
|
.where(scope);
|
|
95
96
|
if (n >= PROJECT_LIMITS.files[input.kind]) {
|
|
96
|
-
throw new ServiceError("refused", `Höchstens ${PROJECT_LIMITS.files[input.kind]} Dateien.`);
|
|
97
|
+
throw new ServiceError("refused", bilingual(`Höchstens ${PROJECT_LIMITS.files[input.kind]} Dateien.`, `At most ${PROJECT_LIMITS.files[input.kind]} files.`));
|
|
97
98
|
}
|
|
98
99
|
const name = input.name.slice(0, 120) || "datei";
|
|
99
100
|
const ref = await saveAsset({
|
|
@@ -149,21 +150,21 @@ export async function addProjectFileFromUrl(projectId, input) {
|
|
|
149
150
|
headers: { "user-agent": "Mozilla/5.0 (compatible; engenty-wizards/0.1)" },
|
|
150
151
|
signal: AbortSignal.any([AbortSignal.timeout(60_000), ...(signal ? [signal] : [])]),
|
|
151
152
|
}).catch(() => {
|
|
152
|
-
throw new ServiceError("invalid", "Die Adresse ist nicht erreichbar.");
|
|
153
|
+
throw new ServiceError("invalid", bilingual("Die Adresse ist nicht erreichbar.", "The address cannot be reached."));
|
|
153
154
|
});
|
|
154
155
|
if (!res.ok) {
|
|
155
|
-
throw new ServiceError("invalid", `Die Adresse antwortete ${res.status}.`);
|
|
156
|
+
throw new ServiceError("invalid", bilingual(`Die Adresse antwortete ${res.status}.`, `The address answered ${res.status}.`));
|
|
156
157
|
}
|
|
157
158
|
const mime = (res.headers.get("content-type") ?? "").split(";")[0].trim().toLowerCase();
|
|
158
159
|
if (mime === "text/html" && kind !== "document") {
|
|
159
160
|
await res.body?.cancel();
|
|
160
|
-
throw new ServiceError("invalid", "Unter der Adresse steht eine Webseite, kein Bild.", {
|
|
161
|
+
throw new ServiceError("invalid", bilingual("Unter der Adresse steht eine Webseite, kein Bild.", "The address holds a web page, not an image."), {
|
|
161
162
|
reason: "page",
|
|
162
163
|
url: res.url,
|
|
163
164
|
});
|
|
164
165
|
}
|
|
165
166
|
if (Number(res.headers.get("content-length") ?? 0) > projectFileLimit(kind, mime)) {
|
|
166
|
-
throw new ServiceError("invalid", "Die Datei ist zu groß.");
|
|
167
|
+
throw new ServiceError("invalid", bilingual("Die Datei ist zu groß.", "The file is too large."));
|
|
167
168
|
}
|
|
168
169
|
const data = new Uint8Array(await res.arrayBuffer());
|
|
169
170
|
const name = decodeURIComponent(new URL(res.url).pathname.split("/").pop() || kind);
|
|
@@ -297,7 +298,7 @@ async function prepare(row) {
|
|
|
297
298
|
status: "failed",
|
|
298
299
|
error: err instanceof UnreadableDocument
|
|
299
300
|
? err.message
|
|
300
|
-
: "
|
|
301
|
+
: "Reading did not work. Please try again.",
|
|
301
302
|
});
|
|
302
303
|
}
|
|
303
304
|
});
|
|
@@ -306,7 +307,7 @@ export async function reindexProjectFile(projectId, fileId) {
|
|
|
306
307
|
await requireWritableProject(projectId);
|
|
307
308
|
const row = await projectFile(projectId, fileId);
|
|
308
309
|
if (row.kind !== "document") {
|
|
309
|
-
throw new ServiceError("invalid", "Nur Dokumente stehen im Index.");
|
|
310
|
+
throw new ServiceError("invalid", bilingual("Nur Dokumente stehen im Index.", "Only documents are in the index."));
|
|
310
311
|
}
|
|
311
312
|
const [pending] = await db
|
|
312
313
|
.update(schema.projectFile)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { COLOR_SECTION_KINDS, cleanAssetSections, cleanLogoSections, FACT_TYPES, FONT_USES, FONT_WEIGHTS, GRADIENT_TYPES, orderColors, PROJECT_LIMITS, RULE_MARKS, } from "@engenty-wizards/shared/projects";
|
|
2
|
+
import { bilingual } from "@engenty-wizards/shared/run";
|
|
2
3
|
import { and, count, eq, isNull } from "drizzle-orm";
|
|
3
4
|
import { nanoid } from "nanoid";
|
|
4
5
|
import { z } from "zod";
|
|
@@ -154,9 +155,11 @@ async function ensureProject() {
|
|
|
154
155
|
const any = await db.query.project.findFirst({ where: madeHere() });
|
|
155
156
|
if (!any) {
|
|
156
157
|
const id = nanoid(12);
|
|
157
|
-
await db
|
|
158
|
-
|
|
159
|
-
|
|
158
|
+
await db.insert(schema.project).values({
|
|
159
|
+
id,
|
|
160
|
+
tenantId: currentTenant(),
|
|
161
|
+
name: env.manage.url ? "Meine Wizards" : "Local Space",
|
|
162
|
+
});
|
|
160
163
|
spaceChanged("space.created", id);
|
|
161
164
|
}
|
|
162
165
|
}
|
|
@@ -222,7 +225,9 @@ export async function createProject(_userId, name) {
|
|
|
222
225
|
.where(eq(schema.project.tenantId, currentTenant()));
|
|
223
226
|
const limit = await projectLimit();
|
|
224
227
|
if (n >= limit) {
|
|
225
|
-
throw new ServiceError("refused", limit === 1
|
|
228
|
+
throw new ServiceError("refused", limit === 1
|
|
229
|
+
? bilingual("Hier gibt es ein Projekt.", "There is one project here.")
|
|
230
|
+
: bilingual(`Höchstens ${limit} Projekte.`, `At most ${limit} projects.`));
|
|
226
231
|
}
|
|
227
232
|
const id = nanoid(12);
|
|
228
233
|
await db
|
|
@@ -246,7 +251,7 @@ export async function updateProject(userId, projectId, patch) {
|
|
|
246
251
|
return { ...s, headers };
|
|
247
252
|
});
|
|
248
253
|
if (patch.facts && new Set(patch.facts.map((f) => f.key)).size !== patch.facts.length) {
|
|
249
|
-
throw new ServiceError("invalid", "Zwei Einträge haben denselben Schlüssel.");
|
|
254
|
+
throw new ServiceError("invalid", bilingual("Zwei Einträge haben denselben Schlüssel.", "Two entries have the same key."));
|
|
250
255
|
}
|
|
251
256
|
await db
|
|
252
257
|
.update(schema.project)
|
|
@@ -360,7 +365,7 @@ export async function deleteProject(userId, projectId) {
|
|
|
360
365
|
await requireAdmin();
|
|
361
366
|
const all = await db.query.project.findMany({ where: madeHere() });
|
|
362
367
|
if (all.length <= 1) {
|
|
363
|
-
throw new ServiceError("refused", "Das letzte Projekt bleibt.");
|
|
368
|
+
throw new ServiceError("refused", bilingual("Das letzte Projekt bleibt.", "The last project stays."));
|
|
364
369
|
}
|
|
365
370
|
await removeProject(projectId);
|
|
366
371
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { FIELD_WAYS, formatsFor, isDecidedField, shownFields, slotText, wizardLang, } from "@engenty-wizards/shared/definition";
|
|
2
|
+
import { bilingual } from "@engenty-wizards/shared/run";
|
|
2
3
|
import { and, count, desc, eq, gte, inArray, max, or, sql } from "drizzle-orm";
|
|
3
4
|
import { canSpend, MICROS_PER_CREDIT } from "../credits/credits.js";
|
|
4
5
|
import { db, schema } from "../db/client.js";
|
|
5
6
|
import { answerAsk } from "../engine/asks.js";
|
|
6
7
|
import { emitEvent, recentEvents, watchRun } from "../engine/events.js";
|
|
8
|
+
import { fieldErrorsIn } from "../engine/input.js";
|
|
7
9
|
import { cancel, createRun, goBack, NoCreditsError, pageContext, RunConflict, RunInputError, retry, reviewStep, submitPage, } from "../engine/runner.js";
|
|
8
|
-
import { ModelUnavailableError } from "../model-errors.js";
|
|
10
|
+
import { ModelUnavailableError, saidOf } from "../model-errors.js";
|
|
9
11
|
import { signedUrl } from "../secrets/signing.js";
|
|
10
12
|
import { currentTenant } from "../tenants/tenant.js";
|
|
11
13
|
import { notFound, ServiceError } from "./errors.js";
|
|
@@ -16,7 +18,7 @@ export async function startTestRun(userId, wizardId, autopilot) {
|
|
|
16
18
|
const w = await ownedWizard(userId, wizardId);
|
|
17
19
|
const { definition, files } = await requireClean(w);
|
|
18
20
|
if (!(await canSpend())) {
|
|
19
|
-
throw new ServiceError("no_credits", "Dein Guthaben ist aufgebraucht.");
|
|
21
|
+
throw new ServiceError("no_credits", bilingual("Dein Guthaben ist aufgebraucht.", "Your credits are used up."));
|
|
20
22
|
}
|
|
21
23
|
const runId = await createRun({
|
|
22
24
|
wizardId: w.id,
|
|
@@ -27,7 +29,7 @@ export async function startTestRun(userId, wizardId, autopilot) {
|
|
|
27
29
|
userId,
|
|
28
30
|
unattended: Boolean(autopilot),
|
|
29
31
|
}).catch((err) => {
|
|
30
|
-
throw err instanceof ModelUnavailableError ? new ServiceError("refused", err
|
|
32
|
+
throw err instanceof ModelUnavailableError ? new ServiceError("refused", saidOf(err)) : err;
|
|
31
33
|
});
|
|
32
34
|
if (autopilot) {
|
|
33
35
|
fly(runId, autopilot.answers, autopilot.acceptReviews);
|
|
@@ -78,9 +80,14 @@ function fly(runId, answers, acceptReviews) {
|
|
|
78
80
|
} while (again);
|
|
79
81
|
}
|
|
80
82
|
catch (err) {
|
|
83
|
+
const answers = (lang) => err instanceof RunInputError
|
|
84
|
+
? fieldErrorsIn(err.errors, lang)
|
|
85
|
+
.map((e) => `${e.field} – ${e.message}`)
|
|
86
|
+
.join("; ")
|
|
87
|
+
: "";
|
|
81
88
|
const message = err instanceof RunInputError
|
|
82
|
-
? `Antworten fehlen oder passen nicht: ${
|
|
83
|
-
: err
|
|
89
|
+
? bilingual(`Antworten fehlen oder passen nicht: ${answers("de")}`, `Answers are missing or do not fit: ${answers("en")}`)
|
|
90
|
+
: saidOf(err);
|
|
84
91
|
await emitEvent(runId, null, "error", message).catch(() => undefined);
|
|
85
92
|
land();
|
|
86
93
|
}
|
|
@@ -347,10 +354,10 @@ export const testRunReport = (userId, runId, waitSeconds = 0) => runReport(userI
|
|
|
347
354
|
/** A command the run refused, as a tool error the client can act on. */
|
|
348
355
|
export function refusedInput(err) {
|
|
349
356
|
if (err instanceof RunInputError) {
|
|
350
|
-
throw new ServiceError("invalid", err
|
|
357
|
+
throw new ServiceError("invalid", saidOf(err), { fields: fieldErrorsIn(err.errors, "en") });
|
|
351
358
|
}
|
|
352
359
|
if (err instanceof RunConflict) {
|
|
353
|
-
throw new ServiceError("refused", err
|
|
360
|
+
throw new ServiceError("refused", saidOf(err));
|
|
354
361
|
}
|
|
355
362
|
throw err;
|
|
356
363
|
}
|
|
@@ -370,7 +377,7 @@ export async function startRun(userId, wizardId, answers = {}) {
|
|
|
370
377
|
throw notFound();
|
|
371
378
|
}
|
|
372
379
|
if (!(await canSpend())) {
|
|
373
|
-
throw new ServiceError("no_credits", "Dein Guthaben ist aufgebraucht.");
|
|
380
|
+
throw new ServiceError("no_credits", bilingual("Dein Guthaben ist aufgebraucht.", "Your credits are used up."));
|
|
374
381
|
}
|
|
375
382
|
const runId = await createRun({
|
|
376
383
|
wizardId: w.id,
|
|
@@ -381,10 +388,10 @@ export async function startRun(userId, wizardId, answers = {}) {
|
|
|
381
388
|
userId,
|
|
382
389
|
}).catch((err) => {
|
|
383
390
|
if (err instanceof ModelUnavailableError) {
|
|
384
|
-
throw new ServiceError("refused", err
|
|
391
|
+
throw new ServiceError("refused", saidOf(err));
|
|
385
392
|
}
|
|
386
393
|
if (err instanceof NoCreditsError) {
|
|
387
|
-
throw new ServiceError("no_credits", "Dein Guthaben ist aufgebraucht.");
|
|
394
|
+
throw new ServiceError("no_credits", bilingual("Dein Guthaben ist aufgebraucht.", "Your credits are used up."));
|
|
388
395
|
}
|
|
389
396
|
throw err;
|
|
390
397
|
});
|
|
@@ -400,7 +407,7 @@ export async function startRun(userId, wizardId, answers = {}) {
|
|
|
400
407
|
}
|
|
401
408
|
catch (err) {
|
|
402
409
|
if (err instanceof RunInputError) {
|
|
403
|
-
return { runId, refused: err.errors };
|
|
410
|
+
return { runId, refused: fieldErrorsIn(err.errors, "en") };
|
|
404
411
|
}
|
|
405
412
|
throw err;
|
|
406
413
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bilingual } from "@engenty-wizards/shared/run";
|
|
1
2
|
import { and, eq, isNotNull, lt } from "drizzle-orm";
|
|
2
3
|
import { nanoid } from "nanoid";
|
|
3
4
|
import { db, schema } from "../db/client.js";
|
|
@@ -12,7 +13,7 @@ export const shareUrlFor = (token) => `${env.publicUrl}/s/${token}`;
|
|
|
12
13
|
/** Shares a finished run. Idempotent: the same run keeps the same link. */
|
|
13
14
|
export async function shareRun(run) {
|
|
14
15
|
if (run.status !== "done") {
|
|
15
|
-
throw new ServiceError("refused", "Nur fertige Ergebnisse lassen sich teilen.");
|
|
16
|
+
throw new ServiceError("refused", bilingual("Nur fertige Ergebnisse lassen sich teilen.", "Only finished results can be shared."));
|
|
16
17
|
}
|
|
17
18
|
let token = run.shareToken;
|
|
18
19
|
if (!token) {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { factKey, PROJECT_LIMITS } from "@engenty-wizards/shared/projects";
|
|
2
|
+
import { bilingual } from "@engenty-wizards/shared/run";
|
|
2
3
|
import { nanoid } from "nanoid";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { runStudioAssistant } from "../agents/studio-assistant.js";
|
|
5
6
|
import { canSpend } from "../credits/credits.js";
|
|
7
|
+
import { errorText } from "../model-errors.js";
|
|
6
8
|
import { currentTenant } from "../tenants/tenant.js";
|
|
7
9
|
import { projectWritable, requireWritable } from "./access.js";
|
|
8
10
|
import { apiSpace, apiSpaceDetails } from "./api.js";
|
|
@@ -55,6 +57,8 @@ export const siteSchema = z.object({
|
|
|
55
57
|
/** Let the space assistant fill in about and facts; false writes only what the site states. */
|
|
56
58
|
assist: z.boolean().default(true),
|
|
57
59
|
});
|
|
60
|
+
/** The language the site's people read: German for a German site, else English. */
|
|
61
|
+
const siteLang = (input) => input.language?.toLowerCase().startsWith("de") ? "de" : "en";
|
|
58
62
|
const jobs = new Map();
|
|
59
63
|
const KEEP_MS = 60 * 60 * 1000;
|
|
60
64
|
function forget() {
|
|
@@ -146,16 +150,16 @@ function assistMessage(input) {
|
|
|
146
150
|
.slice(0, 6)
|
|
147
151
|
.map((p) => `## ${p.title}${p.url ? ` (${p.url})` : ""}${p.role ? ` [${p.role}]` : ""}\n\n${p.markdown.slice(0, 6000)}`);
|
|
148
152
|
return [
|
|
149
|
-
`
|
|
150
|
-
"
|
|
151
|
-
`Name: ${input.name || "—"}\
|
|
153
|
+
`This is the WordPress website ${input.url}. Fill the space from what it says about itself: a title, a description (about), facts (address, phone, email, VAT id, company register, opening hours, who runs it) and the colours, if there are none yet. Its pages are already in Knowledge; do not copy them. Write in the website's own language.`,
|
|
154
|
+
"The details below come straight from WordPress. Read the website with read_website only when something important is missing. Invent nothing.",
|
|
155
|
+
`Name: ${input.name || "—"}\nTagline: ${input.tagline || "—"}\nLanguage: ${input.language ?? "—"}`,
|
|
152
156
|
input.facts.length
|
|
153
|
-
? `
|
|
157
|
+
? `What the website states:\n${input.facts.map((f) => `- ${f.label}: ${f.value}`).join("\n")}`
|
|
154
158
|
: "",
|
|
155
159
|
input.colors.length
|
|
156
|
-
? `
|
|
160
|
+
? `The theme's colours: ${input.colors.map((c) => `${c.name} ${c.value}`).join(", ")}`
|
|
157
161
|
: "",
|
|
158
|
-
pages.length ? `
|
|
162
|
+
pages.length ? `Pages:\n\n${pages.join("\n\n")}` : "",
|
|
159
163
|
]
|
|
160
164
|
.filter(Boolean)
|
|
161
165
|
.join("\n\n")
|
|
@@ -163,7 +167,10 @@ function assistMessage(input) {
|
|
|
163
167
|
}
|
|
164
168
|
async function work(job, projectId, input) {
|
|
165
169
|
try {
|
|
166
|
-
job.activity =
|
|
170
|
+
job.activity =
|
|
171
|
+
siteLang(input) === "de"
|
|
172
|
+
? "Übernimmt die Angaben der Website …"
|
|
173
|
+
: "Taking over what the website says …";
|
|
167
174
|
await fillFromSite(job.userId, projectId, input);
|
|
168
175
|
job.activity = "Schreibt die Seiten in Wissen …";
|
|
169
176
|
job.pages = await writePages(projectId, input);
|
|
@@ -181,7 +188,7 @@ async function work(job, projectId, input) {
|
|
|
181
188
|
message: assistMessage(input),
|
|
182
189
|
page: null,
|
|
183
190
|
browserTools: [],
|
|
184
|
-
lang: input
|
|
191
|
+
lang: siteLang(input),
|
|
185
192
|
onActivity: (label) => {
|
|
186
193
|
job.activity = label;
|
|
187
194
|
},
|
|
@@ -201,7 +208,11 @@ async function work(job, projectId, input) {
|
|
|
201
208
|
catch (err) {
|
|
202
209
|
job.status = "failed";
|
|
203
210
|
job.error =
|
|
204
|
-
err instanceof ServiceError
|
|
211
|
+
err instanceof ServiceError
|
|
212
|
+
? errorText(err, siteLang(input))
|
|
213
|
+
: siteLang(input) === "de"
|
|
214
|
+
? "Die Website ließ sich nicht übernehmen."
|
|
215
|
+
: "The website could not be taken over.";
|
|
205
216
|
console.error("[site]", err);
|
|
206
217
|
}
|
|
207
218
|
}
|
|
@@ -256,7 +267,7 @@ function siteJobView(job) {
|
|
|
256
267
|
export function siteJob(jobId) {
|
|
257
268
|
const job = jobs.get(jobId);
|
|
258
269
|
if (!job || job.tenantId !== currentTenant()) {
|
|
259
|
-
throw new ServiceError("not_found", "Diesen Vorgang gibt es nicht (mehr).");
|
|
270
|
+
throw new ServiceError("not_found", bilingual("Diesen Vorgang gibt es nicht (mehr).", "There is no such job (any more)."));
|
|
260
271
|
}
|
|
261
272
|
return siteJobView(job);
|
|
262
273
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CATEGORY_TYPES, dayOf, } from "@engenty-wizards/shared/knowledge";
|
|
2
|
+
import { bilingual } from "@engenty-wizards/shared/run";
|
|
2
3
|
import { and, asc, eq, inArray, isNotNull, or, sql } from "drizzle-orm";
|
|
3
4
|
import { nanoid } from "nanoid";
|
|
4
5
|
import { db, schema } from "../db/client.js";
|
|
@@ -272,10 +273,10 @@ const MAX_CATEGORIES = 60;
|
|
|
272
273
|
function checkDraft(draft) {
|
|
273
274
|
const name = draft.name.replace(/\s+/g, " ").trim().slice(0, 80);
|
|
274
275
|
if (!name) {
|
|
275
|
-
throw new ServiceError("invalid", "Eine Kategorie braucht einen Namen.");
|
|
276
|
+
throw new ServiceError("invalid", bilingual("Eine Kategorie braucht einen Namen.", "A category needs a name."));
|
|
276
277
|
}
|
|
277
278
|
if (!CATEGORY_TYPES.includes(draft.type)) {
|
|
278
|
-
throw new ServiceError("invalid", `Unbekannter Typ „${draft.type}
|
|
279
|
+
throw new ServiceError("invalid", bilingual(`Unbekannter Typ „${draft.type}“.`, `Unknown type “${draft.type}”.`));
|
|
279
280
|
}
|
|
280
281
|
return name;
|
|
281
282
|
}
|
|
@@ -329,7 +330,7 @@ export async function putCategory(projectId, draft, opts = {}) {
|
|
|
329
330
|
return known;
|
|
330
331
|
}
|
|
331
332
|
if (rows.length >= MAX_CATEGORIES) {
|
|
332
|
-
throw new ServiceError("refused", `Ein Space hat höchstens ${MAX_CATEGORIES} Kategorien.`);
|
|
333
|
+
throw new ServiceError("refused", bilingual(`Ein Space hat höchstens ${MAX_CATEGORIES} Kategorien.`, `A space has at most ${MAX_CATEGORIES} categories.`));
|
|
333
334
|
}
|
|
334
335
|
const id = nanoid(12);
|
|
335
336
|
const [row] = await db
|
|
@@ -408,7 +409,7 @@ export async function updateCategory(categoryId, patch) {
|
|
|
408
409
|
if (name !== row.name) {
|
|
409
410
|
const clash = byName(await categoryRows(row.projectId), name);
|
|
410
411
|
if (clash && clash.id !== row.id) {
|
|
411
|
-
throw new ServiceError("invalid", `Die Kategorie „${name}“ gibt es schon.`);
|
|
412
|
+
throw new ServiceError("invalid", bilingual(`Die Kategorie „${name}“ gibt es schon.`, `The category “${name}” already exists.`));
|
|
412
413
|
}
|
|
413
414
|
}
|
|
414
415
|
await db
|
|
@@ -458,7 +459,7 @@ export async function addCategoryValue(categoryId, value) {
|
|
|
458
459
|
const category = await categoryRow(categoryId);
|
|
459
460
|
await requireWritableProject(category.projectId);
|
|
460
461
|
if (category.type !== "choice") {
|
|
461
|
-
throw new ServiceError("invalid", "Nur eine Auswahl hat Werte.");
|
|
462
|
+
throw new ServiceError("invalid", bilingual("Nur eine Auswahl hat Werte.", "Only a choice has values."));
|
|
462
463
|
}
|
|
463
464
|
const ids = await addValues(category.id, [value]);
|
|
464
465
|
return { id: [...ids.values()].at(-1) };
|
|
@@ -488,7 +489,7 @@ export async function mergeCategoryValues(fromId, intoId) {
|
|
|
488
489
|
const from = await valueRow(fromId);
|
|
489
490
|
const into = await valueRow(intoId);
|
|
490
491
|
if (from.category.id !== into.category.id || fromId === intoId) {
|
|
491
|
-
throw new ServiceError("invalid", "Nur Werte einer Kategorie lassen sich zusammenführen.");
|
|
492
|
+
throw new ServiceError("invalid", bilingual("Nur Werte einer Kategorie lassen sich zusammenführen.", "Only values of one category can be merged."));
|
|
492
493
|
}
|
|
493
494
|
await requireWritableProject(from.category.projectId);
|
|
494
495
|
const t = schema.spaceItemCategory;
|