wizards 0.2.0 → 0.2.1
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/app.js +36 -4
- package/apps/runtime/dist/auth/local-ticket.js +8 -5
- package/apps/runtime/dist/cli/connect.js +531 -0
- package/apps/runtime/dist/cli/environment.js +3 -1
- package/apps/runtime/dist/cli/main.js +17 -0
- package/apps/runtime/dist/cli/mcp.js +175 -0
- package/apps/runtime/dist/cli/setup.js +49 -0
- package/apps/runtime/dist/mcp/auth.js +26 -0
- package/apps/runtime/dist/mcp/flow-app.js +94 -0
- package/apps/runtime/dist/mcp/handler.js +15 -5
- package/apps/runtime/dist/mcp/seen.js +65 -0
- package/apps/runtime/dist/mcp/tools.js +156 -4
- package/apps/runtime/dist/routes/runs.js +7 -3
- package/apps/runtime/dist/routes/studio.js +20 -0
- package/apps/runtime/dist/secrets/signing.js +21 -3
- package/apps/runtime/dist/services/runs.js +190 -28
- package/apps/runtime/dist/services/wizards.js +1 -1
- package/apps/web/dist/assets/{ChangelogDialog-COqPzrKT.js → ChangelogDialog-Co_9fs61.js} +1 -1
- package/apps/web/dist/assets/ChatPanel-Br3nYkUc.js +1 -0
- package/apps/web/dist/assets/{CreditsDialog-pRbTW7Xh.js → CreditsDialog-D3fZRqXH.js} +1 -1
- package/apps/web/dist/assets/EditorPage-BSjQC6FE.js +2 -0
- package/apps/web/dist/assets/{FlowPreview-CDmcN1oX.js → FlowPreview-CjUK5046.js} +1 -1
- package/apps/web/dist/assets/{HomePage-DUAiS9pg.js → HomePage-8v4HqIbP.js} +1 -1
- package/apps/web/dist/assets/{ImportWizard-pzljVvcr.js → ImportWizard-DiV62Uno.js} +1 -1
- package/apps/web/dist/assets/{LocalRuntime-DPJMJlWc.js → LocalRuntime-CTyLGC98.js} +1 -1
- package/apps/web/dist/assets/NewWizardPage-BBL2yenw.js +2 -0
- package/apps/web/dist/assets/{PublicRunner-DoZdJuyW.js → PublicRunner-BwtoEVlE.js} +1 -1
- package/apps/web/dist/assets/{RunnerView-XKosfgO-.js → RunnerView-DOizlTad.js} +1 -1
- package/apps/web/dist/assets/SettingsPage-CBDv04YS.js +2 -0
- package/apps/web/dist/assets/{SetupPage-BFQ0V__v.js → SetupPage-CfXTOaIm.js} +1 -1
- package/apps/web/dist/assets/{SharePage-C1yg5bc2.js → SharePage-CgByqrAF.js} +1 -1
- package/apps/web/dist/assets/{api-DijQGkp-.js → api-KMi35npD.js} +1 -1
- package/apps/web/dist/assets/changelog-03JgixQA.js +1 -0
- package/apps/web/dist/assets/index-B9YVjvVY.css +1 -0
- package/apps/web/dist/assets/index-BmpnE7IO.js +10 -0
- package/apps/web/dist/assets/meta-4R1Lnr25.js +1 -0
- package/apps/web/dist/assets/oss-credits-roB62QJu.js +1 -0
- package/apps/web/dist/assets/{outputs-37WFkpRS.js → outputs-COE_nvAw.js} +1 -1
- package/apps/web/dist/assets/plug-CLjyCoHB.js +1 -0
- package/apps/web/dist/assets/{session-BerOchdC.js → session-B6-6VomQ.js} +1 -1
- package/apps/web/dist/assets/type-Bj9H3eOK.js +1 -0
- package/apps/web/dist/assets/ui-Cs6lpsmj.js +1 -0
- package/apps/web/dist/index.html +6 -5
- package/apps/web/dist/mcp/flow.html +748 -0
- package/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/use-wizard/SKILL.md +18 -0
- package/apps/web/dist/assets/ChatPanel-aQP8iz10.js +0 -1
- package/apps/web/dist/assets/EditorPage-xkq6pvw-.js +0 -2
- package/apps/web/dist/assets/NewWizardPage-TwYgdbKl.js +0 -2
- package/apps/web/dist/assets/SettingsPage-C0_peJuV.js +0 -2
- package/apps/web/dist/assets/changelog-sa4SXBrg.js +0 -1
- package/apps/web/dist/assets/index-BAObuqzs.js +0 -10
- package/apps/web/dist/assets/index-BOl3401k.css +0 -1
- package/apps/web/dist/assets/meta-ijlGGCOr.js +0 -1
- package/apps/web/dist/assets/oss-credits-ClwxOPrf.js +0 -1
- package/apps/web/dist/assets/type-c_dt5Ne8.js +0 -1
- package/apps/web/dist/assets/ui-DKc92qJ1.js +0 -1
|
@@ -8,9 +8,10 @@ import { ServiceError } from "../services/errors.js";
|
|
|
8
8
|
import { deleteFile, listFiles, readFileText, writeFile } from "../services/files.js";
|
|
9
9
|
import { marketplaceWizard, searchMarketplace } from "../services/marketplace.js";
|
|
10
10
|
import { defaultProject, listProjects, ownedProject } from "../services/projects.js";
|
|
11
|
-
import { startTestRun, testRunReport } from "../services/runs.js";
|
|
11
|
+
import { answerRunAsk, answerRunPage, controlRun, reviewRun, runReport, startRun, startTestRun, testRunReport, } from "../services/runs.js";
|
|
12
12
|
import { checkDraftWidget } from "../services/widgets.js";
|
|
13
13
|
import { createWizard, editWizard, listWizards, ownedWizard, parseDraft, publishWizard, shareUrl, studioUrl, updateWizardSettings, wizardState, writeDraft, } from "../services/wizards.js";
|
|
14
|
+
import { FLOW_VIEW_KEY, flowAppMeta, flowView } from "./flow-app.js";
|
|
14
15
|
const definition = z
|
|
15
16
|
.record(z.string(), z.unknown())
|
|
16
17
|
.describe("The complete wizard JSON as described by get_authoring_guide.");
|
|
@@ -68,6 +69,31 @@ export function registerTools(server, who) {
|
|
|
68
69
|
}
|
|
69
70
|
}));
|
|
70
71
|
};
|
|
72
|
+
/** A tool whose result also opens the flow widget, in hosts that render MCP Apps. */
|
|
73
|
+
const appTool = (name, scope, config, run) => {
|
|
74
|
+
if (!who.scopes.includes(scope)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
server.registerTool(name, {
|
|
78
|
+
title: config.title,
|
|
79
|
+
description: config.description,
|
|
80
|
+
inputSchema: config.input,
|
|
81
|
+
annotations: { readOnlyHint: config.readOnly ?? false, openWorldHint: false },
|
|
82
|
+
_meta: flowAppMeta,
|
|
83
|
+
}, (async (args) => {
|
|
84
|
+
try {
|
|
85
|
+
const { result, view } = await run(args);
|
|
86
|
+
return {
|
|
87
|
+
...ok(result),
|
|
88
|
+
structuredContent: result,
|
|
89
|
+
_meta: { [FLOW_VIEW_KEY]: view },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
return failure(err);
|
|
94
|
+
}
|
|
95
|
+
}));
|
|
96
|
+
};
|
|
71
97
|
// --- read ------------------------------------------------------------------
|
|
72
98
|
tool("get_authoring_guide", "wizards:read", {
|
|
73
99
|
title: "Authoring guide",
|
|
@@ -275,9 +301,9 @@ Returns the new revision and the issues.`,
|
|
|
275
301
|
};
|
|
276
302
|
});
|
|
277
303
|
// --- test runs ---------------------------------------------------------------
|
|
278
|
-
|
|
304
|
+
appTool("start_test_run", "runs:test", {
|
|
279
305
|
title: "Start test run",
|
|
280
|
-
description: "Run the draft once with your answers. SPENDS THE ADMIN'S CREDITS (short copy + image ~15, research ~150, video ~250). Pages are filled from `answers`, reviews accepted when acceptReviews is true. Follow it with get_test_run.",
|
|
306
|
+
description: "Run the draft once with your answers. SPENDS THE ADMIN'S CREDITS (short copy + image ~15, research ~150, video ~250). Pages are filled from `answers`, reviews accepted when acceptReviews is true. Follow it with get_test_run. Shows the flow with the run on it.",
|
|
281
307
|
input: z.object({
|
|
282
308
|
wizardId: z.string(),
|
|
283
309
|
answers: z
|
|
@@ -286,7 +312,14 @@ Returns the new revision and the issues.`,
|
|
|
286
312
|
.describe("Field id → value, for every page (field ids are unique in a wizard). Items fields take an array of row objects."),
|
|
287
313
|
acceptReviews: z.boolean().default(true),
|
|
288
314
|
}),
|
|
289
|
-
}, async ({ wizardId, answers, acceptReviews }) =>
|
|
315
|
+
}, async ({ wizardId, answers, acceptReviews }) => {
|
|
316
|
+
const { runId } = await startTestRun(who.userId, wizardId, { answers, acceptReviews });
|
|
317
|
+
const report = await runReport(who.userId, runId, 2, "test");
|
|
318
|
+
return {
|
|
319
|
+
result: report,
|
|
320
|
+
view: await flowView(who.userId, wizardId, { shows: "draft", run: report }),
|
|
321
|
+
};
|
|
322
|
+
});
|
|
290
323
|
tool("get_test_run", "runs:test", {
|
|
291
324
|
title: "Get test run",
|
|
292
325
|
description: "Status, recent events, outputs (text clipped) with signed download links valid one hour, credits spent, and what the run waits for.",
|
|
@@ -302,6 +335,125 @@ Returns the new revision and the issues.`,
|
|
|
302
335
|
}),
|
|
303
336
|
readOnly: true,
|
|
304
337
|
}, async ({ runId, waitSeconds }) => testRunReport(who.userId, runId, waitSeconds));
|
|
338
|
+
// --- using wizards: the flow, and runs of the published version -------------
|
|
339
|
+
appTool("show_wizard", "wizards:read", {
|
|
340
|
+
title: "Show wizard",
|
|
341
|
+
description: "Show a wizard as its flow diagram: its pages, AI steps, reviews and branches. The published version when there is one, else the draft. Also returns the steps as text.",
|
|
342
|
+
input: z.object({
|
|
343
|
+
wizardId: z.string(),
|
|
344
|
+
version: z.enum(["published", "draft"]).optional().describe("Default: published, if any."),
|
|
345
|
+
}),
|
|
346
|
+
readOnly: true,
|
|
347
|
+
}, async ({ wizardId, version }) => {
|
|
348
|
+
const view = await flowView(who.userId, wizardId, { shows: version });
|
|
349
|
+
return {
|
|
350
|
+
result: {
|
|
351
|
+
wizardId,
|
|
352
|
+
title: view.wizard.title,
|
|
353
|
+
shows: view.wizard.shows,
|
|
354
|
+
published: view.wizard.published,
|
|
355
|
+
steps: view.definition.steps.map((s) => ({
|
|
356
|
+
id: s.id,
|
|
357
|
+
type: s.type,
|
|
358
|
+
title: s.title,
|
|
359
|
+
...(s.type === "page"
|
|
360
|
+
? { fields: s.fields.map((f) => ({ id: f.id, label: f.label, kind: f.kind })) }
|
|
361
|
+
: {}),
|
|
362
|
+
})),
|
|
363
|
+
},
|
|
364
|
+
view,
|
|
365
|
+
};
|
|
366
|
+
});
|
|
367
|
+
appTool("run_wizard", "runs:test", {
|
|
368
|
+
title: "Run wizard",
|
|
369
|
+
description: `Start a run of a published wizard for the person, here in the chat. Spends credits like any run.
|
|
370
|
+
Start it right away — no show_wizard first: the widget shows the wizard's pages and the person can answer there. What the person already said goes to the first page with answer_page, using the field ids in waitingFor. Then follow waitingFor:
|
|
371
|
+
- page: ask the person for the fields with inChat=true, answer_page; fields with inChat=false (files, recordings, signatures) need the run page at browserUrl
|
|
372
|
+
- review: show the outputs, then review_step
|
|
373
|
+
- ask: answer_ask (allow/skip a change in a connected account) or the run page for a sign-in
|
|
374
|
+
While status is running, get_run with waitSeconds. Hosts with MCP Apps also show the run as a widget the person can answer in.`,
|
|
375
|
+
input: z.object({
|
|
376
|
+
wizardId: z.string(),
|
|
377
|
+
answers: z.record(z.string(), z.unknown()).default({}),
|
|
378
|
+
}),
|
|
379
|
+
}, async ({ wizardId, answers }) => {
|
|
380
|
+
const { runId, refused } = await startRun(who.userId, wizardId, answers);
|
|
381
|
+
const report = await runReport(who.userId, runId, 2);
|
|
382
|
+
return {
|
|
383
|
+
result: { ...report, ...(refused ? { refusedAnswers: refused } : {}) },
|
|
384
|
+
view: await flowView(who.userId, wizardId, { run: report }),
|
|
385
|
+
};
|
|
386
|
+
});
|
|
387
|
+
const waitSeconds = z
|
|
388
|
+
.number()
|
|
389
|
+
.int()
|
|
390
|
+
.min(0)
|
|
391
|
+
.max(45)
|
|
392
|
+
.default(0)
|
|
393
|
+
.describe("Wait up to this long while the run is still working.");
|
|
394
|
+
tool("get_run", "runs:test", {
|
|
395
|
+
title: "Get run",
|
|
396
|
+
description: "A run's status, what it waits for, its outputs (text clipped) with signed download links valid one hour, and the credits it spent.",
|
|
397
|
+
input: z.object({ runId: z.string(), waitSeconds }),
|
|
398
|
+
readOnly: true,
|
|
399
|
+
}, async ({ runId, waitSeconds }) => runReport(who.userId, runId, waitSeconds));
|
|
400
|
+
tool("answer_page", "runs:test", {
|
|
401
|
+
title: "Answer page",
|
|
402
|
+
description: "Fill the page a run waits for: field id → value (items: an array of row objects; toggle: true/false; multiselect: an array). Returns the run, after waitSeconds.",
|
|
403
|
+
input: z.object({
|
|
404
|
+
runId: z.string(),
|
|
405
|
+
stepId: z.string().describe("waitingFor.page"),
|
|
406
|
+
values: z.record(z.string(), z.unknown()),
|
|
407
|
+
waitSeconds,
|
|
408
|
+
}),
|
|
409
|
+
}, async ({ runId, stepId, values, waitSeconds }) => {
|
|
410
|
+
await answerRunPage(who.userId, runId, stepId, values);
|
|
411
|
+
return runReport(who.userId, runId, waitSeconds);
|
|
412
|
+
});
|
|
413
|
+
tool("review_step", "runs:test", {
|
|
414
|
+
title: "Review step",
|
|
415
|
+
description: "Answer the review a run waits for: accept (optionally with edited texts, step id → text, when the review is editable) or regenerate one step it shows, with a note what to change.",
|
|
416
|
+
input: z.object({
|
|
417
|
+
runId: z.string(),
|
|
418
|
+
stepId: z.string().describe("waitingFor.review"),
|
|
419
|
+
action: z.discriminatedUnion("type", [
|
|
420
|
+
z.object({
|
|
421
|
+
type: z.literal("accept"),
|
|
422
|
+
edits: z.record(z.string(), z.string()).optional(),
|
|
423
|
+
}),
|
|
424
|
+
z.object({
|
|
425
|
+
type: z.literal("regenerate"),
|
|
426
|
+
target: z.string().describe("One of waitingFor.show."),
|
|
427
|
+
note: z.string().max(2000),
|
|
428
|
+
}),
|
|
429
|
+
]),
|
|
430
|
+
waitSeconds,
|
|
431
|
+
}),
|
|
432
|
+
}, async ({ runId, stepId, action, waitSeconds }) => {
|
|
433
|
+
await reviewRun(who.userId, runId, stepId, action);
|
|
434
|
+
return runReport(who.userId, runId, waitSeconds);
|
|
435
|
+
});
|
|
436
|
+
tool("answer_ask", "runs:test", {
|
|
437
|
+
title: "Answer ask",
|
|
438
|
+
description: "A running step asks before it changes something in a connected account (waitingFor.ask, kind confirm): allow it once, or skip it. Ask the person first.",
|
|
439
|
+
input: z.object({
|
|
440
|
+
runId: z.string(),
|
|
441
|
+
askId: z.string(),
|
|
442
|
+
answer: z.enum(["allow", "skip"]),
|
|
443
|
+
waitSeconds,
|
|
444
|
+
}),
|
|
445
|
+
}, async ({ runId, askId, answer, waitSeconds }) => {
|
|
446
|
+
await answerRunAsk(who.userId, runId, askId, answer);
|
|
447
|
+
return runReport(who.userId, runId, waitSeconds);
|
|
448
|
+
});
|
|
449
|
+
tool("control_run", "runs:test", {
|
|
450
|
+
title: "Control run",
|
|
451
|
+
description: "back: to the previous page or review. retry: a failed step again. cancel: stop the run.",
|
|
452
|
+
input: z.object({ runId: z.string(), action: z.enum(["back", "retry", "cancel"]) }),
|
|
453
|
+
}, async ({ runId, action }) => {
|
|
454
|
+
await controlRun(who.userId, runId, action);
|
|
455
|
+
return runReport(who.userId, runId);
|
|
456
|
+
});
|
|
305
457
|
// --- workspace (widget code, libraries, reference data) --------------------
|
|
306
458
|
tool("list_files", "wizards:read", {
|
|
307
459
|
title: "List workspace files",
|
|
@@ -21,7 +21,7 @@ import { hashIp, verifyTurnstile, wizardUnavailable } from "../limits.js";
|
|
|
21
21
|
import { ModelUnavailableError } from "../model-errors.js";
|
|
22
22
|
import { listDownload, renderDownload, stepHtml } from "../render/downloads.js";
|
|
23
23
|
import { HTML_RESPONSE_CSP } from "../render/guard.js";
|
|
24
|
-
import { verifySignedUrl } from "../secrets/signing.js";
|
|
24
|
+
import { runTicketValid, verifySignedUrl } from "../secrets/signing.js";
|
|
25
25
|
import { brandView } from "../services/brand.js";
|
|
26
26
|
import { ServiceError } from "../services/errors.js";
|
|
27
27
|
import { sharedRun, shareImage, shareRun, shareView, unshareRun } from "../services/shares.js";
|
|
@@ -62,8 +62,9 @@ function visitorId(c, create, embedded = false) {
|
|
|
62
62
|
return vid;
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
|
-
* The run, if the caller may see it: its visitor, the admin who owns the wizard,
|
|
66
|
-
* read-only file routes — a signed link
|
|
65
|
+
* The run, if the caller may see it: its visitor, the admin who owns the wizard, the widget of
|
|
66
|
+
* the owner's AI app with its run ticket, or — on the read-only file routes — a signed link
|
|
67
|
+
* handed out to the owner's MCP client.
|
|
67
68
|
*/
|
|
68
69
|
async function accessibleRun(c, runId, opts = {}) {
|
|
69
70
|
const run = await db.query.run.findFirst({ where: eq(schema.run.id, runId) });
|
|
@@ -73,6 +74,9 @@ async function accessibleRun(c, runId, opts = {}) {
|
|
|
73
74
|
if (opts.signed && verifySignedUrl(c.req.url)) {
|
|
74
75
|
return run;
|
|
75
76
|
}
|
|
77
|
+
if (runTicketValid(run.id, c.req.query("rt"))) {
|
|
78
|
+
return run;
|
|
79
|
+
}
|
|
76
80
|
const vid = visitorId(c, false);
|
|
77
81
|
if (vid && run.visitorId === vid) {
|
|
78
82
|
return run;
|
|
@@ -10,6 +10,8 @@ import { runProjectAssistant } from "../agents/project-assistant.js";
|
|
|
10
10
|
import { chatEngine, setChatEngine, subscriptionClients } from "../agents/subscription.js";
|
|
11
11
|
import { accountOverview, linkedAccount, startLink, unlink } from "../auth/account.js";
|
|
12
12
|
import { createLocalKey, deleteLocalKey, listLocalKeys } from "../auth/keys.js";
|
|
13
|
+
import { appStates, connectApps } from "../cli/connect.js";
|
|
14
|
+
import { layout } from "../cli/home.js";
|
|
13
15
|
import { connectorImportSchema, importConnector, listConnectors, previewSource, refreshConnector, registryService, removeConnector, searchRegistry, } from "../connectors/external.js";
|
|
14
16
|
import { balanceCredits, canSpend } from "../credits/credits.js";
|
|
15
17
|
import { estimateRun } from "../credits/estimate.js";
|
|
@@ -19,6 +21,7 @@ import { freshAuth, loginEnv } from "../harness/env.js";
|
|
|
19
21
|
import { detectHarness, detectHarnesses, HARNESS_IDS, harness, installSpec, } from "../harness/index.js";
|
|
20
22
|
import { closeTerminal, openTerminal, resizeTerminal, subscribeTerminal, writeTerminal, } from "../harness/terminal.js";
|
|
21
23
|
import { managed } from "../manage.js";
|
|
24
|
+
import { seenApps } from "../mcp/seen.js";
|
|
22
25
|
import { transcribeAudio } from "../media/transcribe.js";
|
|
23
26
|
import { embeddingModel, hasTextModel, LOCAL_KEYS, localModelSettings, ModelUnavailableError, saveLocalModels, textModel, } from "../models.js";
|
|
24
27
|
import { HTML_RESPONSE_CSP } from "../render/guard.js";
|
|
@@ -530,6 +533,23 @@ export const studio = new Hono()
|
|
|
530
533
|
await deleteLocalKey(c.req.param("id"));
|
|
531
534
|
return c.json({ ok: true });
|
|
532
535
|
})
|
|
536
|
+
// The person's AI apps on this machine (Claude Desktop, Cursor, Codex …) and whether they
|
|
537
|
+
// have this install's MCP server: the same as `wizards connect`.
|
|
538
|
+
.get("/local/apps", async (c) => (managed ? c.notFound() : c.json(await appStates(layout()))))
|
|
539
|
+
.post("/local/apps/:id", async (c) => {
|
|
540
|
+
if (managed) {
|
|
541
|
+
return c.notFound();
|
|
542
|
+
}
|
|
543
|
+
const { connect } = z.object({ connect: z.boolean() }).parse(await c.req.json());
|
|
544
|
+
const [done] = await connectApps(layout(), [c.req.param("id")], !connect);
|
|
545
|
+
if (!done) {
|
|
546
|
+
return c.json({ error: "Unknown app" }, 404);
|
|
547
|
+
}
|
|
548
|
+
// Not done is an answer too: the app's file is no plain JSON, here is what to paste.
|
|
549
|
+
return c.json(done.outcome);
|
|
550
|
+
})
|
|
551
|
+
// What each AI client did with the MCP server since the start: the test on "Integrate".
|
|
552
|
+
.get("/integrations/seen", (c) => c.json(seenApps(c.get("user").tenantId)))
|
|
533
553
|
.get("/local/models", (c) => (managed ? c.notFound() : c.json(localModelSettings())))
|
|
534
554
|
.put("/local/models", async (c) => {
|
|
535
555
|
if (managed) {
|
|
@@ -21,6 +21,11 @@ export function signedUrl(path, ttlSeconds = 3600) {
|
|
|
21
21
|
url.searchParams.set("sig", sign(payload(url, exp)));
|
|
22
22
|
return url.toString();
|
|
23
23
|
}
|
|
24
|
+
function sameSignature(data, sig) {
|
|
25
|
+
const expected = Buffer.from(sign(data));
|
|
26
|
+
const given = Buffer.from(sig);
|
|
27
|
+
return expected.length === given.length && timingSafeEqual(expected, given);
|
|
28
|
+
}
|
|
24
29
|
export function verifySignedUrl(href) {
|
|
25
30
|
const url = new URL(href, env.appUrl);
|
|
26
31
|
const exp = url.searchParams.get("exp");
|
|
@@ -28,7 +33,20 @@ export function verifySignedUrl(href) {
|
|
|
28
33
|
if (!exp || !sig || Number(exp) * 1000 < Date.now()) {
|
|
29
34
|
return false;
|
|
30
35
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
return sameSignature(payload(url, exp), sig);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A ticket to one run's routes without a session: the widget an AI app shows runs on the host's
|
|
40
|
+
* origin and reaches the run with it. Sent as `?rt=`, since an EventSource sends no headers.
|
|
41
|
+
*/
|
|
42
|
+
export function runTicket(runId, ttlSeconds = 12 * 3600) {
|
|
43
|
+
const exp = String(Math.floor(Date.now() / 1000) + ttlSeconds);
|
|
44
|
+
return `${exp}.${sign(`run:${runId}|${exp}`)}`;
|
|
45
|
+
}
|
|
46
|
+
export function runTicketValid(runId, ticket) {
|
|
47
|
+
const [exp, sig] = (ticket ?? "").split(".");
|
|
48
|
+
if (!exp || !sig || Number(exp) * 1000 < Date.now()) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return sameSignature(`run:${runId}|${exp}`, sig);
|
|
34
52
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { formatsFor } from "@engenty-wizards/shared/definition";
|
|
2
|
-
import { and, count, desc, eq, inArray } from "drizzle-orm";
|
|
2
|
+
import { and, count, desc, eq, inArray, or } from "drizzle-orm";
|
|
3
3
|
import { canSpend, MICROS_PER_CREDIT } from "../credits/credits.js";
|
|
4
4
|
import { db, schema } from "../db/client.js";
|
|
5
|
-
import { unattended } from "../engine/asks.js";
|
|
5
|
+
import { answerAsk, unattended } from "../engine/asks.js";
|
|
6
6
|
import { emitEvent, recentEvents, subscribe } from "../engine/events.js";
|
|
7
|
-
import { createRun, RunInputError, reviewStep, submitPage } from "../engine/runner.js";
|
|
7
|
+
import { cancel, createRun, goBack, NoCreditsError, RunConflict, RunInputError, retry, reviewStep, submitPage, } from "../engine/runner.js";
|
|
8
8
|
import { ModelUnavailableError } from "../model-errors.js";
|
|
9
9
|
import { signedUrl } from "../secrets/signing.js";
|
|
10
10
|
import { currentTenant } from "../tenants/tenant.js";
|
|
11
11
|
import { notFound, ServiceError } from "./errors.js";
|
|
12
|
-
import { ownedWizard, requireClean, studioUrl } from "./wizards.js";
|
|
12
|
+
import { ownedWizard, requireClean, shareUrl, studioUrl } from "./wizards.js";
|
|
13
13
|
/** Test runs an MCP client started with answers: pages are filled and reviews accepted for it. */
|
|
14
14
|
const autopilots = new Set();
|
|
15
15
|
export async function startTestRun(userId, wizardId, autopilot) {
|
|
@@ -95,9 +95,15 @@ function fly(runId, answers, acceptReviews) {
|
|
|
95
95
|
const timeout = setTimeout(land, 30 * 60_000);
|
|
96
96
|
void step();
|
|
97
97
|
}
|
|
98
|
-
|
|
98
|
+
/**
|
|
99
|
+
* A run the caller may see: any test run of their tenant, or a run they started themselves
|
|
100
|
+
* from an MCP client (run_wizard). `test` narrows it to test runs.
|
|
101
|
+
*/
|
|
102
|
+
async function ownedRun(userId, runId, only) {
|
|
99
103
|
const run = await db.query.run.findFirst({
|
|
100
|
-
where: and(eq(schema.run.id, runId), eq(schema.run.tenantId, currentTenant()),
|
|
104
|
+
where: and(eq(schema.run.id, runId), eq(schema.run.tenantId, currentTenant()), only === "test"
|
|
105
|
+
? eq(schema.run.mode, "test")
|
|
106
|
+
: or(eq(schema.run.mode, "test"), eq(schema.run.userId, userId))),
|
|
101
107
|
});
|
|
102
108
|
if (!run) {
|
|
103
109
|
throw notFound();
|
|
@@ -108,9 +114,9 @@ async function ownedTestRun(_userId, runId) {
|
|
|
108
114
|
function moving(run) {
|
|
109
115
|
return run.status === "running" || (run.status === "waiting_input" && autopilots.has(run.id));
|
|
110
116
|
}
|
|
111
|
-
async function waitWhileMoving(userId, runId, seconds) {
|
|
117
|
+
async function waitWhileMoving(userId, runId, seconds, only) {
|
|
112
118
|
const deadline = Date.now() + seconds * 1000;
|
|
113
|
-
let run = await
|
|
119
|
+
let run = await ownedRun(userId, runId, only);
|
|
114
120
|
while (moving(run) && Date.now() < deadline) {
|
|
115
121
|
await new Promise((resolve) => {
|
|
116
122
|
const unsubscribe = subscribe(runId, () => {
|
|
@@ -123,7 +129,7 @@ async function waitWhileMoving(userId, runId, seconds) {
|
|
|
123
129
|
resolve();
|
|
124
130
|
}, deadline - Date.now());
|
|
125
131
|
});
|
|
126
|
-
run = await
|
|
132
|
+
run = await ownedRun(userId, runId, only);
|
|
127
133
|
}
|
|
128
134
|
return run;
|
|
129
135
|
}
|
|
@@ -139,9 +145,88 @@ function deliverableFormats(run, step) {
|
|
|
139
145
|
.find((d) => d.from === step.id);
|
|
140
146
|
return listed ? listed.formats.filter((f) => possible.includes(f)) : [];
|
|
141
147
|
}
|
|
142
|
-
/**
|
|
143
|
-
|
|
144
|
-
|
|
148
|
+
/** Field kinds an MCP client can answer with plain values; the others need the run page. */
|
|
149
|
+
const CHAT_KINDS = new Set([
|
|
150
|
+
"text",
|
|
151
|
+
"textarea",
|
|
152
|
+
"number",
|
|
153
|
+
"select",
|
|
154
|
+
"multiselect",
|
|
155
|
+
"date",
|
|
156
|
+
"email",
|
|
157
|
+
"url",
|
|
158
|
+
"toggle",
|
|
159
|
+
"color",
|
|
160
|
+
"items",
|
|
161
|
+
"location",
|
|
162
|
+
]);
|
|
163
|
+
/** Where the person can go on in a browser: the run page of a live run, the studio for a test. */
|
|
164
|
+
/** The live run's page — the wizard itself, never the studio. A test run has none. */
|
|
165
|
+
async function browserUrl(run) {
|
|
166
|
+
if (run.mode === "test") {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
const w = await db.query.wizard.findFirst({ where: eq(schema.wizard.id, run.wizardId) });
|
|
170
|
+
return w ? `${shareUrl(w.shareToken)}/${run.id}` : null;
|
|
171
|
+
}
|
|
172
|
+
function waitingFor(run, current) {
|
|
173
|
+
if (run.status === "running" && run.ask) {
|
|
174
|
+
const ask = run.ask;
|
|
175
|
+
return ask.kind === "confirm"
|
|
176
|
+
? {
|
|
177
|
+
ask: ask.id,
|
|
178
|
+
kind: ask.kind,
|
|
179
|
+
reason: ask.reason,
|
|
180
|
+
service: ask.service,
|
|
181
|
+
action: ask.action.summary,
|
|
182
|
+
input: clip(ask.input, 1500),
|
|
183
|
+
hint: "Ask the person, then answer_ask with allow or skip.",
|
|
184
|
+
}
|
|
185
|
+
: {
|
|
186
|
+
ask: ask.id,
|
|
187
|
+
kind: ask.kind,
|
|
188
|
+
reason: ask.reason,
|
|
189
|
+
site: ask.site.host,
|
|
190
|
+
hint: "A sign-in: the person types it on the run page (browserUrl).",
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (run.status !== "waiting_input" || !current) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
if (current.type === "page") {
|
|
197
|
+
return {
|
|
198
|
+
page: current.id,
|
|
199
|
+
title: current.title,
|
|
200
|
+
fields: current.fields.map((f) => ({
|
|
201
|
+
id: f.id,
|
|
202
|
+
label: f.label,
|
|
203
|
+
kind: f.kind,
|
|
204
|
+
required: Boolean(f.required),
|
|
205
|
+
options: f.options,
|
|
206
|
+
placeholder: f.placeholder,
|
|
207
|
+
help: f.help,
|
|
208
|
+
default: f.default,
|
|
209
|
+
value: run.state.values[f.id],
|
|
210
|
+
columns: f.columns,
|
|
211
|
+
// A file, a recording or a signature comes from the person's device: the run page.
|
|
212
|
+
inChat: CHAT_KINDS.has(f.kind),
|
|
213
|
+
})),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (current.type === "review") {
|
|
217
|
+
return {
|
|
218
|
+
review: current.id,
|
|
219
|
+
title: current.title,
|
|
220
|
+
show: current.show,
|
|
221
|
+
editable: Boolean(current.edit),
|
|
222
|
+
hint: "Show the person the outputs, then review_step: accept, or regenerate one with a note.",
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
/** What a run did so far, for a client without the run page: outputs, links, cost, errors. */
|
|
228
|
+
export async function runReport(userId, runId, waitSeconds = 0, only) {
|
|
229
|
+
const run = await waitWhileMoving(userId, runId, waitSeconds, only);
|
|
145
230
|
const current = run.definition.steps.find((s) => s.id === run.cursor) ?? null;
|
|
146
231
|
const base = `/api/runs/${run.id}`;
|
|
147
232
|
const outputs = run.definition.steps.flatMap((step) => {
|
|
@@ -167,25 +252,19 @@ export async function testRunReport(userId, runId, waitSeconds = 0) {
|
|
|
167
252
|
},
|
|
168
253
|
];
|
|
169
254
|
});
|
|
170
|
-
const waitingFor = run.status === "waiting_input" && current?.type === "page"
|
|
171
|
-
? {
|
|
172
|
-
page: current.id,
|
|
173
|
-
fields: current.fields.map((f) => ({
|
|
174
|
-
id: f.id,
|
|
175
|
-
label: f.label,
|
|
176
|
-
kind: f.kind,
|
|
177
|
-
required: Boolean(f.required),
|
|
178
|
-
options: f.options,
|
|
179
|
-
})),
|
|
180
|
-
}
|
|
181
|
-
: run.status === "waiting_input" && current?.type === "review"
|
|
182
|
-
? { review: current.id, hint: "Accept or regenerate in the studio." }
|
|
183
|
-
: undefined;
|
|
184
255
|
return {
|
|
185
256
|
runId: run.id,
|
|
257
|
+
wizardId: run.wizardId,
|
|
258
|
+
mode: run.mode,
|
|
186
259
|
status: moving(run) ? "running" : run.status,
|
|
187
260
|
step: current ? { id: current.id, type: current.type, title: current.title } : null,
|
|
188
|
-
|
|
261
|
+
/** Steps behind the run: answered pages and reviews, every step with a result, the end. */
|
|
262
|
+
passed: run.definition.steps
|
|
263
|
+
.filter((s) => run.state.history.includes(s.id) ||
|
|
264
|
+
run.state.outputs[s.id] ||
|
|
265
|
+
(run.status === "done" && s.id === run.cursor))
|
|
266
|
+
.map((s) => s.id),
|
|
267
|
+
waitingFor: waitingFor(run, current),
|
|
189
268
|
error: run.error,
|
|
190
269
|
credits: Math.ceil(run.costMicros / MICROS_PER_CREDIT),
|
|
191
270
|
events: (await recentEvents(run.id, 0, 15)).map((e) => ({
|
|
@@ -194,9 +273,92 @@ export async function testRunReport(userId, runId, waitSeconds = 0) {
|
|
|
194
273
|
message: e.message,
|
|
195
274
|
})),
|
|
196
275
|
outputs,
|
|
197
|
-
|
|
276
|
+
browserUrl: await browserUrl(run),
|
|
277
|
+
...(run.mode === "test" ? { studioUrl: studioUrl(run.wizardId) } : {}),
|
|
198
278
|
};
|
|
199
279
|
}
|
|
280
|
+
export const testRunReport = (userId, runId, waitSeconds = 0) => runReport(userId, runId, waitSeconds, "test");
|
|
281
|
+
/** A command the run refused, as a tool error the client can act on. */
|
|
282
|
+
function refusedInput(err) {
|
|
283
|
+
if (err instanceof RunInputError) {
|
|
284
|
+
throw new ServiceError("invalid", err.message, { fields: err.errors });
|
|
285
|
+
}
|
|
286
|
+
if (err instanceof RunConflict) {
|
|
287
|
+
throw new ServiceError("refused", err.message);
|
|
288
|
+
}
|
|
289
|
+
throw err;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Runs the published version of a wizard for the admin, from their own AI client: a live run
|
|
293
|
+
* that is theirs — no visitor, no share link needed. With answers the first page is filled.
|
|
294
|
+
*/
|
|
295
|
+
export async function startRun(userId, wizardId, answers = {}) {
|
|
296
|
+
const w = await ownedWizard(userId, wizardId);
|
|
297
|
+
if (w.publishedVersion === null) {
|
|
298
|
+
throw new ServiceError("refused", "This wizard is not published yet. Publish it, or try the draft with start_test_run.");
|
|
299
|
+
}
|
|
300
|
+
const version = await db.query.wizardVersion.findFirst({
|
|
301
|
+
where: and(eq(schema.wizardVersion.wizardId, w.id), eq(schema.wizardVersion.version, w.publishedVersion)),
|
|
302
|
+
});
|
|
303
|
+
if (!version) {
|
|
304
|
+
throw notFound();
|
|
305
|
+
}
|
|
306
|
+
if (!(await canSpend())) {
|
|
307
|
+
throw new ServiceError("no_credits", "Dein Guthaben ist aufgebraucht.");
|
|
308
|
+
}
|
|
309
|
+
const runId = await createRun({
|
|
310
|
+
wizardId: w.id,
|
|
311
|
+
definition: version.definition,
|
|
312
|
+
files: version.files,
|
|
313
|
+
version: w.publishedVersion,
|
|
314
|
+
mode: "live",
|
|
315
|
+
userId,
|
|
316
|
+
}).catch((err) => {
|
|
317
|
+
if (err instanceof ModelUnavailableError) {
|
|
318
|
+
throw new ServiceError("refused", err.message);
|
|
319
|
+
}
|
|
320
|
+
if (err instanceof NoCreditsError) {
|
|
321
|
+
throw new ServiceError("no_credits", "Dein Guthaben ist aufgebraucht.");
|
|
322
|
+
}
|
|
323
|
+
throw err;
|
|
324
|
+
});
|
|
325
|
+
const first = version.definition.steps[0];
|
|
326
|
+
if (first?.type !== "page" || !Object.keys(answers).length) {
|
|
327
|
+
return { runId, refused: null };
|
|
328
|
+
}
|
|
329
|
+
const values = Object.fromEntries(first.fields.filter((f) => f.id in answers).map((f) => [f.id, answers[f.id]]));
|
|
330
|
+
// The run is there either way; what did not fit is answered again with answer_page.
|
|
331
|
+
try {
|
|
332
|
+
await submitPage(runId, first.id, values);
|
|
333
|
+
return { runId, refused: null };
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
if (err instanceof RunInputError) {
|
|
337
|
+
return { runId, refused: err.errors };
|
|
338
|
+
}
|
|
339
|
+
throw err;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
export async function answerRunPage(userId, runId, stepId, values) {
|
|
343
|
+
await ownedRun(userId, runId);
|
|
344
|
+
await submitPage(runId, stepId, values).catch(refusedInput);
|
|
345
|
+
}
|
|
346
|
+
export async function reviewRun(userId, runId, stepId, action) {
|
|
347
|
+
await ownedRun(userId, runId);
|
|
348
|
+
await reviewStep(runId, stepId, action).catch(refusedInput);
|
|
349
|
+
}
|
|
350
|
+
export async function controlRun(userId, runId, action) {
|
|
351
|
+
await ownedRun(userId, runId);
|
|
352
|
+
const command = { back: goBack, retry, cancel }[action];
|
|
353
|
+
await command(runId).catch(refusedInput);
|
|
354
|
+
}
|
|
355
|
+
/** Answers what a running step asks: allow a change in a connected account, or skip it. */
|
|
356
|
+
export async function answerRunAsk(userId, runId, askId, answer) {
|
|
357
|
+
await ownedRun(userId, runId);
|
|
358
|
+
if (!answerAsk(runId, askId, answer === "allow" ? { type: "done" } : { type: "skip" })) {
|
|
359
|
+
throw new ServiceError("refused", "This question is no longer open.");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
200
362
|
export async function listRuns(userId, wizardId) {
|
|
201
363
|
const w = await ownedWizard(userId, wizardId);
|
|
202
364
|
const rows = await db.query.run.findMany({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a as e,n as t,t as n}from"./jsx-runtime-D3jfb0Ew.js";import{t as r}from"./preload-helper-CNEI7N0N.js";import{a as i,d as a,h as o,i as s,s as c,y as l}from"./ui-
|
|
1
|
+
import{a as e,n as t,t as n}from"./jsx-runtime-D3jfb0Ew.js";import{t as r}from"./preload-helper-CNEI7N0N.js";import{a as i,d as a,h as o,i as s,s as c,y as l}from"./ui-Cs6lpsmj.js";var u=e(t(),1),d=e(n(),1);function f(e){switch(e.group){case`Added`:return{label:`NEW`,dot:`bg-moss`};case`Fixed`:return{label:`FIXED`,dot:`bg-amber`};case`Changed`:return{label:`CHANGED`,dot:`bg-cobalt`};case`Performance`:return{label:`PERFORMANCE`,dot:`bg-ember`};case`Deploy`:return{label:`DEPLOY`,dot:`bg-ink-4`};case`Docs`:return{label:`DOCS`,dot:`bg-ink-4`};default:return{label:`OTHER`,dot:`bg-ink-4`}}}var p=e=>e.version?e.version.replace(/^v/,``):l(`about.unreleased`),m=e=>e.timestamp?new Date(e.timestamp*1e3).toISOString().slice(0,10):``,h=e=>e.charAt(0).toUpperCase()+e.slice(1);function g(e,t){let n=t.trim().toLowerCase();if(!n)return e;let r=[];for(let t of e){if(`${p(t)} ${m(t)}`.toLowerCase().includes(n)){r.push(t);continue}let e=t.commits.filter(e=>`${f(e).label} ${e.scope??``} ${e.message}`.toLowerCase().includes(n));e.length>0&&r.push({...t,commits:e})}return r}function _({open:e,onClose:t}){let[n,_]=(0,u.useState)(null),[v,y]=(0,u.useState)(``);(0,u.useEffect)(()=>{if(!e||n)return;let t=!1;return r(()=>import(`./changelog-03JgixQA.js`).then(e=>{t||_(e.default)}),[]),()=>{t=!0}},[e,n]),(0,u.useEffect)(()=>{e||y(``)},[e]);let b=n?g(n,v):[];return(0,d.jsxs)(s,{open:e,onClose:t,title:l(`about.changelog`),wide:!0,children:[(0,d.jsx)(`p`,{className:`-mt-3 mb-4 text-[14px] text-ink-3`,children:l(`about.changelogHint`)}),n===null?(0,d.jsx)(`div`,{className:`flex justify-center py-10 text-ink-3`,children:(0,d.jsx)(a,{})}):n.length===0?(0,d.jsx)(i,{children:l(`about.changelogEmpty`)}):(0,d.jsxs)(d.Fragment,{children:[(0,d.jsx)(c,{type:`search`,value:v,placeholder:l(`about.search`),onChange:e=>y(e.target.value)}),b.length===0?(0,d.jsx)(i,{children:l(`about.noMatches`)}):null,b.map(e=>(0,d.jsxs)(`section`,{className:`mt-6`,children:[(0,d.jsxs)(`h3`,{className:`flex items-baseline gap-3 border-border-soft border-b pb-2 font-display font-semibold text-[16px]`,children:[p(e),(0,d.jsx)(`span`,{className:`font-normal font-sans text-[13px] text-ink-3 tabular-nums`,children:m(e)})]}),(0,d.jsx)(`ul`,{className:`mt-3 flex flex-col gap-2`,children:e.commits.map(e=>{let t=f(e);return(0,d.jsxs)(`li`,{className:`flex items-start gap-2.5 text-[14px]`,children:[(0,d.jsx)(`span`,{className:o(`mt-[7px] size-1.5 shrink-0 rounded-full`,t.dot)}),(0,d.jsxs)(`span`,{className:`min-w-0 text-ink-2`,children:[(0,d.jsxs)(`span`,{className:`font-semibold text-[12px] text-ink tracking-wide`,children:[t.label,e.scope?` [${e.scope}]`:``]}),` `,h(e.message),e.breaking?(0,d.jsx)(`span`,{className:`ml-1.5 rounded-full bg-rose-tint px-1.5 py-0.5 font-medium text-[12px] text-rose`,children:l(`about.breaking`)}):null]})]},e.id)})})]},e.version??`unreleased`))]})]})}export{_ as ChangelogDialog};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as e,n as t,t as n}from"./jsx-runtime-D3jfb0Ew.js";import{c as r,d as i,i as a,n as o,nt as s}from"./api-KMi35npD.js";import{t as c}from"./createLucideIcon-q3MZMmnf.js";import{t as l}from"./plug-CLjyCoHB.js";import{S as u,g as d,h as f,r as p,v as m}from"./outputs-COE_nvAw.js";import{r as h}from"./wand-sparkles-CKxArWPY.js";import{t as g}from"./plus-Du-Jx8aI.js";import{t as _}from"./mic-Bvz4mA2j.js";import{_ as v,d as y,h as b,o as x,y as S}from"./ui-Cs6lpsmj.js";import{l as C}from"./index-BmpnE7IO.js";var w={name:`chevron-up`,size:24,node:[[`path`,{d:`m18 15-6-6-6 6`,key:`153udz`}]]};w.node;var T=c(w),E=e(t(),1),D=!1;function O(){if(!(typeof window>`u`||D))return window.SpeechRecognition??window.webkitSpeechRecognition}var k=typeof navigator<`u`&&!!navigator.mediaDevices?.getUserMedia&&typeof MediaRecorder<`u`;function A(e,t){let n=e.trimEnd(),r=t.trim();return n&&r?`${n} ${r}`:n||r}function j(e,t){let[n,r]=(0,E.useState)(!1),[i,a]=(0,E.useState)(!1),[s,c]=(0,E.useState)(null),l=(0,E.useRef)(e);l.current=e;let u=(0,E.useRef)(t);u.current=t;let d=(0,E.useRef)(null);(0,E.useEffect)(()=>()=>d.current?.stop(),[]);let f=async()=>{let e=l.current,t;try{t=await navigator.mediaDevices.getUserMedia({audio:!0})}catch{c(`denied`);return}let n=[],i=new MediaRecorder(t);i.ondataavailable=e=>e.data.size&&n.push(e.data),i.onstop=async()=>{for(let e of t.getTracks())e.stop();d.current=null,r(!1);let s=(i.mimeType||`audio/webm`).split(`;`)[0],l=new File(n,`dictation`,{type:s});if(l.size){a(!0);try{let{text:t}=await o.upload(`/api/studio/transcribe`,l);t.trim()&&u.current(A(e,t))}catch(e){c(e.message)}finally{a(!1)}}},d.current={stop:()=>i.state!==`inactive`&&i.stop()},i.start(),r(!0)},p=e=>{let t=l.current,n=``,i=new e;i.continuous=!0,i.interimResults=!0,i.lang=v===`de`?`de-DE`:`en-US`,i.onresult=e=>{let r=``;for(let t=Math.max(0,e.resultIndex);t<e.results.length;t++){let i=e.results[t],a=i[0]?.transcript??``;i.isFinal?n=A(n,a):r+=a}u.current(A(t,A(n,r)))},i.onerror=e=>{if(e.error!==`aborted`&&e.error!==`no-speech`){if(e.error===`not-allowed`){c(`denied`);return}D=!0,k&&!n?(i.onend=null,d.current=null,f()):c(e.error)}},i.onend=()=>{d.current=null,r(!1)},d.current={stop:()=>i.stop()},i.start(),r(!0)};return{supported:!!O()||k,listening:n,processing:i,error:s,toggle:()=>{if(i)return;if(d.current){d.current.stop();return}c(null);let e=O();e?p(e):f()}}}var M=e(n(),1),N=/\.(png|jpe?g|gif|webp|avif|svg)$/i;function P(e){let t=e.filter(e=>e.type.startsWith(`image/`));return t.length?Object.fromEntries(t.map(e=>[e.name,URL.createObjectURL(e)])):void 0}function F(e){let t=e.match(/(?:^|\n\n)(?:Angehängt|Attached): ([^\n]+)$/);return t?{text:e.slice(0,t.index).trim(),names:t[1].split(`, `).filter(Boolean)}:{text:e,names:[]}}function I(e,t){if(e.name&&e.name!==`image.png`)return e;let n=e.type.split(`/`)[1]?.replace(`jpeg`,`jpg`)||`png`,r=new Date().toISOString().slice(0,19).replace(/[-:]/g,``).replace(`T`,`-`);return new File([e],`paste-${r}${t?`-${t+1}`:``}.${n}`,{type:e.type})}function L({url:e,label:t,children:n}){return(0,M.jsxs)(`div`,{className:`group relative`,title:t,children:[(0,M.jsx)(`img`,{src:e,alt:t,width:64,height:64,className:`size-16 rounded-lg bg-paper-2 object-cover ring-1 ring-border`}),n]})}function R({names:e,previews:t,fileUrl:n}){let r=e.filter(e=>t?.[e]||n&&N.test(e)),i=e.filter(e=>!r.includes(e));return(0,M.jsxs)(`div`,{className:`flex flex-col items-end gap-1.5`,children:[r.map(e=>{let r=t?.[e]??n?.(e)??``;return(0,M.jsx)(`a`,{href:r,target:`_blank`,rel:`noreferrer`,title:e,children:(0,M.jsx)(`img`,{src:r,alt:e,className:`max-h-60 w-auto max-w-full rounded-xl bg-paper-2 object-contain ring-1 ring-border`})},e)}),i.length?(0,M.jsx)(`div`,{className:`flex flex-wrap justify-end gap-1.5`,children:i.map(e=>(0,M.jsxs)(`span`,{className:`inline-flex max-w-full items-center gap-1.5 rounded-full bg-paper-2 px-2.5 py-1 text-[12px] text-ink-2`,children:[(0,M.jsx)(m,{className:`size-3 shrink-0 text-ink-4`}),(0,M.jsx)(`span`,{className:`truncate`,children:e})]},e))}):null]})}function z(e,t){let n=s(),[i,o]=(0,E.useState)([]),[c,l]=(0,E.useState)(`idle`),[u,d]=(0,E.useState)(null),[f,p]=(0,E.useState)(null),[m,h]=(0,E.useState)(null),[g,_]=(0,E.useState)(null),v=(0,E.useRef)(null);return(0,E.useEffect)(()=>{if(e){if(v.current!==e.id){v.current=e.id,o(e.messages);return}c===`idle`&&o(t=>e.messages.length>=t.length?e.messages:t)}},[e,c]),{messages:i,phase:c,activity:f,thought:m,startedAt:g,error:u,send:async(i,s=[])=>{if(!e||!(i.trim()||s.length)||c!==`idle`)return!1;if(d(null),_(Date.now()),h(null),s.length){l(`thinking`);try{for(let t of s){let n=await fetch(r(`/api/studio/wizards/${e.id}/files/${encodeURIComponent(t.name)}`),{method:`PUT`,credentials:`include`,headers:{"content-type":t.type||`application/octet-stream`},body:t});if(!n.ok){let e=await n.json().catch(()=>({}));throw Error(e.error??n.statusText)}}}catch(t){return d(t.message),l(`idle`),_(null),await n.invalidateQueries({queryKey:[`wizard`,e.id]}),!1}let t=`${S(`editor.attached`)}: ${s.map(e=>e.name).join(`, `)}`;i=i.trim()?`${i}\n\n${t}`:t}let u=`p-${Date.now()}`;o(e=>[...e,{id:`u-${Date.now()}`,role:`user`,content:i,previews:P(s)},{id:u,role:`assistant`,content:``,pending:!0}]),l(`thinking`),p(null);let f=!1;try{await a(`/api/studio/wizards/${e.id}/chat`,{message:i},(e,n)=>{if(e===`text`){let e=JSON.parse(n);o(t=>t.map(t=>t.id===u?{...t,content:t.content+e}:t))}else if(e===`activity`)p(JSON.parse(n));else if(e===`thought`)h(JSON.parse(n));else if(e===`building`)l(`building`);else if(e===`done`){f=!0;let e=JSON.parse(n);o(t=>t.map(t=>t.id===u?{...t,content:e.reply,changed:e.changed,pending:!1}:t)),e.changed&&t(e.draft,e.revision)}else e===`error`&&(f=!0,d(JSON.parse(n).message))}),f||d(S(`editor.chatBroken`))}catch(e){d(e.message)}finally{p(null),h(null),_(null),o(e=>e.filter(e=>!(e.id===u&&!e.content)).map(e=>({...e,pending:!1}))),l(`idle`),await n.invalidateQueries({queryKey:[`wizard`,e.id]}),await n.invalidateQueries({queryKey:[`me`]})}return!0},fileUrl:t=>r(`/api/studio/wizards/${e?.id}/files/${encodeURIComponent(t)}`)}}function B(e){let[t,n]=(0,E.useState)(()=>Date.now());return(0,E.useEffect)(()=>{if(e===null)return;n(Date.now());let t=setInterval(()=>n(Date.now()),1e3);return()=>clearInterval(t)},[e]),e===null?null:Math.max(0,Math.floor((t-e)/1e3))}function V(e){return e<60?`${e} s`:`${Math.floor(e/60)}:${String(e%60).padStart(2,`0`)} min`}function H({chat:e,thought:t,className:n,spinner:r=`size-3.5`}){let i=B(e.startedAt);return(0,M.jsxs)(`div`,{className:b(`min-w-0`,n),children:[(0,M.jsxs)(`div`,{className:`flex min-w-0 items-center gap-2`,children:[(0,M.jsx)(y,{className:b(`shrink-0`,r)}),(0,M.jsx)(`span`,{className:`min-w-0 truncate`,children:e.activity??(e.phase===`building`?S(`editor.building`):S(`editor.thinking`))}),i===null?null:(0,M.jsx)(`span`,{className:`shrink-0 text-ink-4 tabular-nums`,children:V(i)})]}),t&&e.thought?(0,M.jsx)(`p`,{className:`mt-0.5 truncate pl-5.5 text-[12px] text-ink-4`,children:e.thought}):null]})}function U({message:e,fileUrl:t}){let{text:n,names:r}=F(e.content);return(0,M.jsxs)(`div`,{className:`ml-8 flex flex-col items-end gap-1.5 self-end`,children:[r.length?(0,M.jsx)(R,{names:r,previews:e.previews,fileUrl:t}):null,n?(0,M.jsx)(`div`,{className:`rounded-xl rounded-br-md bg-paper-2 px-4 py-2.5 text-[14px] leading-relaxed`,children:(0,M.jsx)(`span`,{className:`whitespace-pre-wrap`,children:n})}):null]})}function W({chat:e,avatar:t,hello:n=S(`editor.chatHello`),placeholder:r=S(`editor.composer`),changedLabel:a=`Ablauf aktualisiert`,compact:o,intro:s}){let[c,v]=(0,E.useState)(!1),[w,D]=(0,E.useState)(``),[O,k]=(0,E.useState)([]),A=(0,E.useRef)(null),N=(0,E.useRef)(null),P=(0,E.useRef)(null),F=j(w,D);(0,E.useEffect)(()=>{A.current?.scrollTo({top:A.current.scrollHeight,behavior:`smooth`})},[e.messages,e.phase]),(0,E.useEffect)(()=>{let e=N.current;e&&(e.style.height=`auto`,e.style.height=`${Math.min(e.scrollHeight,200)}px`)},[w]);let R=(w.trim()||O.length>0)&&e.phase===`idle`,z=e=>k(t=>[...t.filter(t=>!e.some(e=>e.name===t.name)),...e]),[B,V]=(0,E.useState)(new Map);(0,E.useEffect)(()=>{let e=new Map(O.filter(e=>e.type.startsWith(`image/`)).map(e=>[e,URL.createObjectURL(e)]));return V(e),()=>{for(let t of e.values())URL.revokeObjectURL(t)}},[O]);let W=async()=>{if(!R)return;F.listening&&F.toggle();let t={text:w,files:O};D(``),k([]),await e.send(t.text.trim(),t.files)||(D(t.text),k(t.files))},G=!o||c,K=e.messages.at(-1);return(0,M.jsxs)(`div`,{className:`flex h-full min-h-0 flex-col`,children:[o&&(K||e.error)?(0,M.jsxs)(`button`,{type:`button`,"aria-expanded":c,"aria-label":S(`editor.thread`),onClick:()=>v(e=>!e),className:`flex items-center gap-2 px-4 pt-2.5 pb-0.5 text-left text-[13px] text-ink-3 transition hover:text-ink`,children:[!(e.error||c)&&K?.pending?(0,M.jsx)(H,{chat:e,className:`flex-1`}):(0,M.jsx)(`span`,{className:b(`min-w-0 flex-1 truncate`,e.error&&`text-rose`),children:e.error??(c?S(`editor.thread`):K?.content)}),c?(0,M.jsx)(h,{className:`size-4 shrink-0`}):(0,M.jsx)(T,{className:`size-4 shrink-0`})]}):null,(0,M.jsxs)(`div`,{ref:A,className:b(`min-h-0 flex-1 overflow-y-auto px-5 py-5`,!G&&`hidden`),children:[e.messages.length===0?s??(0,M.jsxs)(`div`,{className:`flex items-start gap-3`,children:[(0,M.jsx)(i,{kind:t,size:32,interactive:!1}),(0,M.jsx)(`p`,{className:`pt-1 text-[14px] text-ink-2 leading-relaxed`,children:n})]}):null,(0,M.jsx)(`div`,{className:`flex flex-col gap-4`,children:e.messages.map(n=>n.role===`user`?(0,M.jsx)(U,{message:n,fileUrl:e.fileUrl},n.id):(0,M.jsxs)(`div`,{className:`flex items-start gap-3`,children:[(0,M.jsx)(`div`,{className:`shrink-0`,children:(0,M.jsx)(i,{kind:t,size:28,interactive:!1})}),(0,M.jsxs)(`div`,{className:`min-w-0 pt-0.5 text-[14px]`,children:[n.source===`mcp`?(0,M.jsxs)(`div`,{className:`mb-1 inline-flex items-center gap-1.5 text-[12px] text-ink-3`,children:[(0,M.jsx)(l,{className:`size-3`}),` `,S(`editor.viaClient`,{client:n.client??`MCP`})]}):null,n.content?(0,M.jsx)(p,{text:n.content,className:`text-[14px]`}):null,n.pending?(0,M.jsx)(H,{chat:e,thought:!0,className:`mt-1 text-[13px] text-ink-3`}):null,n.changed?(0,M.jsxs)(`div`,{className:`mt-2 inline-flex items-center gap-1.5 rounded-full bg-ember-tint px-2.5 py-0.5 text-[12px] text-ember-strong`,children:[(0,M.jsx)(d,{className:`size-3`}),` `,a]}):null]})]},n.id))}),e.error?(0,M.jsx)(`div`,{className:`mt-4 rounded-lg bg-rose-tint px-3 py-2 text-[13px] text-rose`,children:e.error}):null]}),(0,M.jsxs)(`div`,{className:o?`p-2`:`p-3`,children:[(0,M.jsxs)(`div`,{className:`rounded-xl bg-card p-1.5 shadow-soft ring-1 ring-border focus-within:ring-focus`,children:[B.size?(0,M.jsx)(`div`,{className:`flex flex-wrap gap-2 px-1 pt-1 pb-1.5`,children:O.map((e,t)=>{let n=B.get(e);return n?(0,M.jsx)(L,{url:n,label:e.name,children:(0,M.jsx)(`button`,{type:`button`,"aria-label":S(`editor.detach`),onClick:()=>k(e=>e.filter((e,n)=>n!==t)),className:`-top-1.5 -right-1.5 absolute flex size-5 items-center justify-center rounded-full bg-ink text-paper opacity-0 shadow-soft transition focus-visible:opacity-100 group-hover:opacity-100`,children:(0,M.jsx)(C,{className:`size-3`})})},`${e.name}-${t}`):null})}):null,O.some(e=>!B.has(e))?(0,M.jsx)(`ul`,{className:`flex flex-wrap gap-1.5 px-1 pt-1 pb-1.5`,children:O.map((e,t)=>B.has(e)?null:(0,M.jsxs)(`li`,{className:`inline-flex max-w-full items-center gap-1.5 rounded-full bg-paper-2 py-1 pr-1 pl-2.5 text-[12px] text-ink-2`,children:[(0,M.jsx)(m,{className:`size-3 shrink-0 text-ink-4`}),(0,M.jsx)(`span`,{className:`truncate`,children:e.name}),(0,M.jsx)(`button`,{type:`button`,"aria-label":S(`editor.detach`),onClick:()=>k(e=>e.filter((e,n)=>n!==t)),className:`inline-flex size-5 shrink-0 items-center justify-center rounded-full text-ink-3 hover:bg-paper-3 hover:text-ink`,children:(0,M.jsx)(C,{className:`size-3`})})]},`${e.name}-${t}`))}):null,(0,M.jsxs)(`div`,{className:`flex items-end gap-1`,children:[(0,M.jsx)(x,{label:S(`editor.attach`),onClick:()=>P.current?.click(),children:(0,M.jsx)(g,{className:`size-4`})}),(0,M.jsx)(`input`,{ref:P,type:`file`,multiple:!0,hidden:!0,onChange:e=>{let t=Array.from(e.target.files??[]);e.target.value=``,z(t)}}),(0,M.jsx)(`textarea`,{ref:N,rows:1,value:w,onChange:e=>D(e.target.value),onPaste:e=>{let t=Array.from(e.clipboardData.files);t.length&&(e.preventDefault(),z(t.map(I)))},onKeyDown:e=>{e.key===`Enter`&&!e.shiftKey&&(e.preventDefault(),W())},placeholder:F.listening?S(`editor.listening`):r,className:`max-h-[200px] min-h-[40px] flex-1 resize-none bg-transparent px-1.5 py-2 text-[14px] outline-none placeholder:text-ink-4`}),F.supported?(0,M.jsx)(x,{label:F.processing?S(`editor.transcribing`):F.listening?S(`editor.voiceStop`):S(`editor.voice`),"aria-pressed":F.listening,disabled:F.processing,onClick:F.toggle,className:b(F.listening&&`bg-rose-tint text-rose hover:bg-rose-tint hover:text-rose`),children:F.processing?(0,M.jsx)(y,{className:`size-4`}):F.listening?(0,M.jsx)(f,{className:`size-3.5 animate-pulse-dot fill-current`}):(0,M.jsx)(_,{className:`size-4`})}):null,(0,M.jsx)(`button`,{type:`button`,onClick:()=>void W(),disabled:!R,className:b(`inline-flex size-9 shrink-0 items-center justify-center rounded-full transition`,R?`bg-primary text-primary-foreground`:`bg-paper-2 text-ink-4`),"aria-label":`Send`,children:(0,M.jsx)(u,{className:`size-4`})})]})]}),F.error?(0,M.jsx)(`p`,{className:`mt-2 px-1 text-[12px] text-rose`,children:F.error===`denied`?S(`editor.micDenied`):F.error}):null]})]})}export{z as i,H as n,P as r,W as t};
|