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.
Files changed (58) hide show
  1. package/apps/runtime/dist/app.js +36 -4
  2. package/apps/runtime/dist/auth/local-ticket.js +8 -5
  3. package/apps/runtime/dist/cli/connect.js +531 -0
  4. package/apps/runtime/dist/cli/environment.js +3 -1
  5. package/apps/runtime/dist/cli/main.js +17 -0
  6. package/apps/runtime/dist/cli/mcp.js +175 -0
  7. package/apps/runtime/dist/cli/setup.js +49 -0
  8. package/apps/runtime/dist/mcp/auth.js +26 -0
  9. package/apps/runtime/dist/mcp/flow-app.js +94 -0
  10. package/apps/runtime/dist/mcp/handler.js +15 -5
  11. package/apps/runtime/dist/mcp/seen.js +65 -0
  12. package/apps/runtime/dist/mcp/tools.js +156 -4
  13. package/apps/runtime/dist/routes/runs.js +7 -3
  14. package/apps/runtime/dist/routes/studio.js +20 -0
  15. package/apps/runtime/dist/secrets/signing.js +21 -3
  16. package/apps/runtime/dist/services/runs.js +190 -28
  17. package/apps/runtime/dist/services/wizards.js +1 -1
  18. package/apps/web/dist/assets/{ChangelogDialog-COqPzrKT.js → ChangelogDialog-Co_9fs61.js} +1 -1
  19. package/apps/web/dist/assets/ChatPanel-Br3nYkUc.js +1 -0
  20. package/apps/web/dist/assets/{CreditsDialog-pRbTW7Xh.js → CreditsDialog-D3fZRqXH.js} +1 -1
  21. package/apps/web/dist/assets/EditorPage-BSjQC6FE.js +2 -0
  22. package/apps/web/dist/assets/{FlowPreview-CDmcN1oX.js → FlowPreview-CjUK5046.js} +1 -1
  23. package/apps/web/dist/assets/{HomePage-DUAiS9pg.js → HomePage-8v4HqIbP.js} +1 -1
  24. package/apps/web/dist/assets/{ImportWizard-pzljVvcr.js → ImportWizard-DiV62Uno.js} +1 -1
  25. package/apps/web/dist/assets/{LocalRuntime-DPJMJlWc.js → LocalRuntime-CTyLGC98.js} +1 -1
  26. package/apps/web/dist/assets/NewWizardPage-BBL2yenw.js +2 -0
  27. package/apps/web/dist/assets/{PublicRunner-DoZdJuyW.js → PublicRunner-BwtoEVlE.js} +1 -1
  28. package/apps/web/dist/assets/{RunnerView-XKosfgO-.js → RunnerView-DOizlTad.js} +1 -1
  29. package/apps/web/dist/assets/SettingsPage-CBDv04YS.js +2 -0
  30. package/apps/web/dist/assets/{SetupPage-BFQ0V__v.js → SetupPage-CfXTOaIm.js} +1 -1
  31. package/apps/web/dist/assets/{SharePage-C1yg5bc2.js → SharePage-CgByqrAF.js} +1 -1
  32. package/apps/web/dist/assets/{api-DijQGkp-.js → api-KMi35npD.js} +1 -1
  33. package/apps/web/dist/assets/changelog-03JgixQA.js +1 -0
  34. package/apps/web/dist/assets/index-B9YVjvVY.css +1 -0
  35. package/apps/web/dist/assets/index-BmpnE7IO.js +10 -0
  36. package/apps/web/dist/assets/meta-4R1Lnr25.js +1 -0
  37. package/apps/web/dist/assets/oss-credits-roB62QJu.js +1 -0
  38. package/apps/web/dist/assets/{outputs-37WFkpRS.js → outputs-COE_nvAw.js} +1 -1
  39. package/apps/web/dist/assets/plug-CLjyCoHB.js +1 -0
  40. package/apps/web/dist/assets/{session-BerOchdC.js → session-B6-6VomQ.js} +1 -1
  41. package/apps/web/dist/assets/type-Bj9H3eOK.js +1 -0
  42. package/apps/web/dist/assets/ui-Cs6lpsmj.js +1 -0
  43. package/apps/web/dist/index.html +6 -5
  44. package/apps/web/dist/mcp/flow.html +748 -0
  45. package/package.json +2 -1
  46. package/plugin/.claude-plugin/plugin.json +1 -1
  47. package/plugin/skills/use-wizard/SKILL.md +18 -0
  48. package/apps/web/dist/assets/ChatPanel-aQP8iz10.js +0 -1
  49. package/apps/web/dist/assets/EditorPage-xkq6pvw-.js +0 -2
  50. package/apps/web/dist/assets/NewWizardPage-TwYgdbKl.js +0 -2
  51. package/apps/web/dist/assets/SettingsPage-C0_peJuV.js +0 -2
  52. package/apps/web/dist/assets/changelog-sa4SXBrg.js +0 -1
  53. package/apps/web/dist/assets/index-BAObuqzs.js +0 -10
  54. package/apps/web/dist/assets/index-BOl3401k.css +0 -1
  55. package/apps/web/dist/assets/meta-ijlGGCOr.js +0 -1
  56. package/apps/web/dist/assets/oss-credits-ClwxOPrf.js +0 -1
  57. package/apps/web/dist/assets/type-c_dt5Ne8.js +0 -1
  58. 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
- tool("start_test_run", "runs:test", {
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 }) => startTestRun(who.userId, 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, or — on the
66
- * read-only file routes — a signed link handed out to the owner's MCP client.
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
- const expected = Buffer.from(sign(payload(url, exp)));
32
- const given = Buffer.from(sig);
33
- return expected.length === given.length && timingSafeEqual(expected, given);
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
- async function ownedTestRun(_userId, runId) {
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()), eq(schema.run.mode, "test")),
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 ownedTestRun(userId, runId);
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 ownedTestRun(userId, runId);
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
- /** What a test run did so far, for a client without the studio: outputs, links, cost, errors. */
143
- export async function testRunReport(userId, runId, waitSeconds = 0) {
144
- const run = await waitWhileMoving(userId, runId, waitSeconds);
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
- waitingFor,
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
- studioUrl: studioUrl(run.wizardId),
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({
@@ -65,7 +65,7 @@ export async function listWizards(_userId, projectId) {
65
65
  });
66
66
  return rows.map(wizardSummary);
67
67
  }
68
- async function publishedVersion(w) {
68
+ export async function publishedVersion(w) {
69
69
  if (w.publishedVersion === null) {
70
70
  return null;
71
71
  }
@@ -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-DKc92qJ1.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-sa4SXBrg.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};
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};