@signalcore/ui 1.0.0-beta.2 → 1.0.0-beta.4

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 (99) hide show
  1. package/README.md +126 -21
  2. package/dist/AgentAbout.js +6 -2
  3. package/dist/AgentAbout.js.map +1 -1
  4. package/dist/AgentChat.d.ts +25 -3
  5. package/dist/AgentChat.js +302 -28
  6. package/dist/AgentChat.js.map +1 -1
  7. package/dist/AgentSpace.d.ts +25 -4
  8. package/dist/AgentSpace.js +55 -11
  9. package/dist/AgentSpace.js.map +1 -1
  10. package/dist/Threads.d.ts +73 -0
  11. package/dist/Threads.js +140 -0
  12. package/dist/Threads.js.map +1 -0
  13. package/dist/View.d.ts +48 -0
  14. package/dist/View.js +101 -0
  15. package/dist/View.js.map +1 -0
  16. package/dist/automation/marks.js +7 -3
  17. package/dist/automation/marks.js.map +1 -1
  18. package/dist/automation/messages.d.ts +4 -0
  19. package/dist/automation/messages.js +5 -1
  20. package/dist/automation/messages.js.map +1 -1
  21. package/dist/automation/when.js +8 -0
  22. package/dist/automation/when.js.map +1 -1
  23. package/dist/blocks.d.ts +83 -2
  24. package/dist/client.d.ts +23 -5
  25. package/dist/client.js +8 -2
  26. package/dist/client.js.map +1 -1
  27. package/dist/contract.d.ts +9 -4
  28. package/dist/es.d.ts +2 -0
  29. package/dist/es.js +34 -3
  30. package/dist/es.js.map +1 -1
  31. package/dist/frames.d.ts +7 -1
  32. package/dist/frames.js.map +1 -1
  33. package/dist/index.d.ts +14 -7
  34. package/dist/index.js +11 -4
  35. package/dist/kit/Agenda.d.ts +25 -0
  36. package/dist/kit/Agenda.js +119 -0
  37. package/dist/kit/Agenda.js.map +1 -0
  38. package/dist/kit/Combobox.js +1 -1
  39. package/dist/kit/DateField.js +1 -1
  40. package/dist/kit/Kanban.d.ts +17 -0
  41. package/dist/kit/Kanban.js +157 -0
  42. package/dist/kit/Kanban.js.map +1 -0
  43. package/dist/kit/TimeField.js +1 -1
  44. package/dist/kit/Timeline.d.ts +14 -0
  45. package/dist/kit/Timeline.js +71 -0
  46. package/dist/kit/Timeline.js.map +1 -0
  47. package/dist/kit/context.d.ts +1 -1
  48. package/dist/kit/context.js.map +1 -1
  49. package/dist/kit/messages.d.ts +22 -0
  50. package/dist/kit/messages.js +10 -0
  51. package/dist/kit/messages.js.map +1 -1
  52. package/dist/surface/Form.js +1 -1
  53. package/dist/surface/Placed.d.ts +20 -0
  54. package/dist/surface/Placed.js +70 -0
  55. package/dist/surface/Placed.js.map +1 -0
  56. package/dist/surface/Surface.d.ts +4 -2
  57. package/dist/surface/Surface.js +78 -2
  58. package/dist/surface/Surface.js.map +1 -1
  59. package/dist/surface/places.d.ts +16 -0
  60. package/dist/surface/places.js +15 -0
  61. package/dist/surface/places.js.map +1 -0
  62. package/dist/surface/scope.d.ts +17 -1
  63. package/dist/surface/scope.js +1 -0
  64. package/dist/surface/scope.js.map +1 -1
  65. package/dist/types.d.ts +23 -0
  66. package/dist/types.js.map +1 -1
  67. package/dist/useConversation.d.ts +2 -0
  68. package/dist/useConversation.js +16 -0
  69. package/dist/useConversation.js.map +1 -1
  70. package/dist/widget.js +17 -16
  71. package/package.json +1 -1
  72. package/src/AgentAbout.tsx +4 -0
  73. package/src/AgentChat.tsx +417 -34
  74. package/src/AgentSpace.tsx +106 -14
  75. package/src/Threads.tsx +218 -0
  76. package/src/View.tsx +117 -0
  77. package/src/automation/marks.tsx +7 -0
  78. package/src/automation/messages.ts +8 -0
  79. package/src/automation/when.ts +4 -0
  80. package/src/blocks.ts +100 -2
  81. package/src/client.ts +42 -10
  82. package/src/contract.ts +404 -29
  83. package/src/es.ts +30 -0
  84. package/src/frames.ts +3 -1
  85. package/src/index.ts +36 -3
  86. package/src/kit/Agenda.tsx +161 -0
  87. package/src/kit/Kanban.tsx +182 -0
  88. package/src/kit/Timeline.tsx +58 -0
  89. package/src/kit/context.tsx +45 -1
  90. package/src/kit/messages.ts +31 -0
  91. package/src/styles.css +825 -66
  92. package/src/surface/Placed.tsx +98 -0
  93. package/src/surface/Surface.tsx +92 -1
  94. package/src/surface/places.ts +19 -0
  95. package/src/surface/scope.ts +24 -1
  96. package/src/types.ts +24 -0
  97. package/src/useConversation.ts +10 -0
  98. package/src/widget/element.tsx +54 -7
  99. package/src/widget/widget.css +13 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"View.js","names":[],"sources":["../src/View.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\n\nimport { classes } from \"./classes\";\nimport { ApiError, type Client } from \"./client\";\nimport type { KitLocale, KitMessages } from \"./kit/messages\";\nimport type { HostBlocks, SurfaceClassNames, SurfaceComponents } from \"./surface/scope\";\nimport { Surface } from \"./surface/Surface\";\nimport type { SurfaceAction } from \"./surface/values\";\nimport type { ViewRendered } from \"./types\";\n\n/** How often a view is read again while it is shown: the figures move as the rows do. */\nexport const VIEW_POLL_MS = 30_000;\n\n/** A view as it was last read: the view once it came, why it failed, or that it isn't there. */\nexport interface ViewRead {\n view: ViewRendered | null;\n problem: unknown;\n /** No such view, or not one for this person (404). */\n gone: boolean;\n}\n\n/**\n * An agent's view (`name`), rendered for the person the page speaks for (their token: their\n * own) or, for the business, for the contact it names: read when shown and again every\n * `VIEW_POLL_MS`, what was read staying while the next read is on its way.\n */\nexport function useView(client: Client, name: string, contact?: string): ViewRead {\n const key = `${name}\\n${contact ?? \"\"}`;\n const [read, setRead] = useState<ViewRead & { key: string }>({\n key,\n view: null,\n problem: null,\n gone: false,\n });\n const latest = useRef({ client, name, contact });\n useEffect(() => {\n latest.current = { client, name, contact };\n });\n useEffect(() => {\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const load = async () => {\n const now = latest.current;\n try {\n const view = await now.client.views.render(now.name, now.contact);\n if (stopped) return;\n setRead({ key, view, problem: null, gone: false });\n } catch (error) {\n if (stopped) return;\n const gone = error instanceof ApiError && error.status === 404;\n setRead((last) => ({ ...last, key, problem: gone ? null : error, gone }));\n }\n if (!stopped) timer = setTimeout(() => void load(), VIEW_POLL_MS);\n };\n void load();\n return () => {\n stopped = true;\n clearTimeout(timer);\n };\n }, [client, key]);\n // Another view's read is not this one's: until its own comes, nothing is shown.\n return read.key === key ? read : { view: null, problem: null, gone: false };\n}\n\nexport interface ViewProps {\n client: Client;\n /** The view's name, as its agent or the business wrote it (`proxima_reunion`). */\n name: string;\n /** For the business: the contact it is rendered for (their id). A person's token: their own. */\n contact?: string | undefined;\n /** The host's own blocks, by the name each was declared with. */\n blocks?: HostBlocks | undefined;\n /** The host's own drawing of a block type. */\n components?: SurfaceComponents | undefined;\n classNames?: SurfaceClassNames;\n /** The words of its blocks, in the host's language; English by default. */\n messages?: Partial<KitMessages> | undefined;\n /** The language of its days, times and numbers; English by default. */\n locale?: KitLocale | undefined;\n /** What the person does on a host's block that sends something; without it, they only read. */\n onAction?: ((action: SurfaceAction) => void) | undefined;\n}\n\n/**\n * An agent's view where the host's page puts it (a rep's next meeting on its home, their week\n * beside a record), drawn with the kit's blocks in the host's style, live: read again every half\n * minute, so it shows what the rows hold now, never a copy. While it is first read, an empty\n * box holds its place; a view that isn't there (or isn't the person's) draws nothing.\n */\nexport function View({\n client,\n name,\n contact,\n blocks,\n components,\n classNames,\n messages,\n locale,\n onAction,\n}: ViewProps) {\n const { view } = useView(client, name, contact);\n if (view === null) {\n return <div aria-busy=\"true\" className={`sc ${classes(classNames, \"sc-surface\")}`} />;\n }\n return (\n <Surface\n surface={{ blocks: view.blocks, text: view.text }}\n active={onAction !== undefined}\n onAction={onAction ?? (() => undefined)}\n blocks={blocks}\n components={components}\n classNames={classNames}\n messages={messages}\n locale={locale}\n />\n );\n}\n"],"mappings":";;;;;;;AAWA,MAAa,eAAe;;;;;;AAe5B,SAAgB,QAAQ,QAAgB,MAAc,SAA4B;CAChF,MAAM,MAAM,GAAG,KAAK,IAAI,WAAW;CACnC,MAAM,CAAC,MAAM,WAAW,SAAqC;EAC3D;EACA,MAAM;EACN,SAAS;EACT,MAAM;CACR,CAAC;CACD,MAAM,SAAS,OAAO;EAAE;EAAQ;EAAM;CAAQ,CAAC;CAC/C,gBAAgB;EACd,OAAO,UAAU;GAAE;GAAQ;GAAM;EAAQ;CAC3C,CAAC;CACD,gBAAgB;EACd,IAAI,UAAU;EACd,IAAI;EACJ,MAAM,OAAO,YAAY;GACvB,MAAM,MAAM,OAAO;GACnB,IAAI;IACF,MAAM,OAAO,MAAM,IAAI,OAAO,MAAM,OAAO,IAAI,MAAM,IAAI,OAAO;IAChE,IAAI,SAAS;IACb,QAAQ;KAAE;KAAK;KAAM,SAAS;KAAM,MAAM;IAAM,CAAC;GACnD,SAAS,OAAO;IACd,IAAI,SAAS;IACb,MAAM,OAAO,iBAAiB,YAAY,MAAM,WAAW;IAC3D,SAAS,UAAU;KAAE,GAAG;KAAM;KAAK,SAAS,OAAO,OAAO;KAAO;IAAK,EAAE;GAC1E;GACA,IAAI,CAAC,SAAS,QAAQ,iBAAiB,KAAK,KAAK,GAAG,YAAY;EAClE;EACA,KAAU;EACV,aAAa;GACX,UAAU;GACV,aAAa,KAAK;EACpB;CACF,GAAG,CAAC,QAAQ,GAAG,CAAC;CAEhB,OAAO,KAAK,QAAQ,MAAM,OAAO;EAAE,MAAM;EAAM,SAAS;EAAM,MAAM;CAAM;AAC5E;;;;;;;AA2BA,SAAgB,KAAK,EACnB,QACA,MACA,SACA,QACA,YACA,YACA,UACA,QACA,YACY;CACZ,MAAM,EAAE,SAAS,QAAQ,QAAQ,MAAM,OAAO;CAC9C,IAAI,SAAS,MACX,OAAO,oBAAC,OAAD;EAAK,aAAU;EAAO,WAAW,MAAM,QAAQ,YAAY,YAAY;CAAM,CAAA;CAEtF,OACE,oBAAC,SAAD;EACE,SAAS;GAAE,QAAQ,KAAK;GAAQ,MAAM,KAAK;EAAK;EAChD,QAAQ,aAAa,KAAA;EACrB,UAAU,mBAAmB,KAAA;EACrB;EACI;EACA;EACF;EACF;CACT,CAAA;AAEL"}
@@ -1,6 +1,6 @@
1
1
  import { Mark } from "../kit/Mark.js";
2
2
  import { sentOn } from "./record.js";
3
- import { BellIcon, BrainIcon, ChatCircleIcon, ChatTextIcon, ChatsCircleIcon, ClockIcon, FileTextIcon, FlaskIcon, GlobeIcon, LightningIcon, SparkleIcon, TableIcon, WebhooksLogoIcon } from "@phosphor-icons/react";
3
+ import { ArrowBendDownRightIcon, BellIcon, BrainIcon, ChatCircleIcon, ChatTextIcon, ChatsCircleIcon, ClockIcon, FileTextIcon, FlaskIcon, GlobeIcon, HandTapIcon, LightningIcon, PlugsConnectedIcon, SparkleIcon, TableIcon, WebhooksLogoIcon } from "@phosphor-icons/react";
4
4
  import { Fragment, jsx } from "react/jsx-runtime";
5
5
  //#region src/automation/marks.tsx
6
6
  /** Only an https logo is loaded: never a script, never a local file. */
@@ -28,7 +28,9 @@ function AppMark({ name, icon, size, apps }) {
28
28
  const KINDS = {
29
29
  schedule: ClockIcon,
30
30
  webhook: WebhooksLogoIcon,
31
- platform: ChatCircleIcon
31
+ platform: ChatCircleIcon,
32
+ manual: HandTapIcon,
33
+ after: ArrowBendDownRightIcon
32
34
  };
33
35
  /** What starts an automation, as a mark: a clock, a webhook, a message, the app whose event it
34
36
  * waits for. */
@@ -56,7 +58,9 @@ const BY = {
56
58
  webhook: WebhooksLogoIcon,
57
59
  platform: ChatCircleIcon,
58
60
  agent: ChatCircleIcon,
59
- test: FlaskIcon
61
+ test: FlaskIcon,
62
+ api: PlugsConnectedIcon,
63
+ after: ArrowBendDownRightIcon
60
64
  };
61
65
  function ByMark({ by, trigger, size, apps }) {
62
66
  if (by !== "event") return /* @__PURE__ */ jsx(Mark, {
@@ -1 +1 @@
1
- {"version":3,"file":"marks.js","names":[],"sources":["../../src/automation/marks.tsx"],"sourcesContent":["import {\n BellIcon,\n BrainIcon,\n ChatCircleIcon,\n ChatTextIcon,\n ChatsCircleIcon,\n ClockIcon,\n FileTextIcon,\n FlaskIcon,\n GlobeIcon,\n LightningIcon,\n SparkleIcon,\n TableIcon,\n WebhooksLogoIcon,\n type Icon,\n} from \"@phosphor-icons/react\";\nimport type { ReactNode } from \"react\";\n\nimport type { AppDirectory } from \"../AgentAbout\";\nimport { Mark } from \"../kit/Mark\";\nimport { sentOn } from \"./record\";\nimport type { RunBy, RunStep, Trigger, Use } from \"./types\";\n\n/** Only an https logo is loaded: never a script, never a local file. */\nconst https = (url: string | null): url is string => url !== null && /^https:\\/\\//i.test(url);\n\n/**\n * An app's mark: the host's logo when it draws apps (`apps`), else the logo the API gives, else\n * the app's initial; each bare, as `Mark` draws it.\n */\nexport function AppMark({\n name,\n icon,\n size,\n apps,\n}: {\n name: string;\n icon: string | null;\n size: number;\n apps: AppDirectory | undefined;\n}) {\n if (apps !== undefined) return <>{apps.logo({ provider: \"\", name, icon, size })}</>;\n if (https(icon)) return <Mark src={icon} size={size} />;\n return (\n <Mark size={size}>\n <span>{name.trim().charAt(0).toUpperCase() || \"?\"}</span>\n </Mark>\n );\n}\n\nconst KINDS: Readonly<Record<Exclude<Trigger[\"kind\"], \"event\">, Icon>> = {\n schedule: ClockIcon,\n webhook: WebhooksLogoIcon,\n platform: ChatCircleIcon,\n};\n\n/** What starts an automation, as a mark: a clock, a webhook, a message, the app whose event it\n * waits for. */\nexport function TriggerMark({\n trigger,\n size,\n apps,\n}: {\n trigger: Trigger;\n size: number;\n apps: AppDirectory | undefined;\n}) {\n if (trigger.kind !== \"event\") return <Mark icon={KINDS[trigger.kind]} size={size} />;\n const name = trigger.app_title ?? trigger.app;\n if (name === null) return <Mark icon={LightningIcon} size={size} />;\n return <AppMark name={name} icon={trigger.icon} size={size} apps={apps} />;\n}\n\n/** What started a run: its time, a call, its app's event, a message, a chat with the agent, a\n * test. */\nconst BY: Readonly<Record<Exclude<RunBy, \"event\">, Icon>> = {\n schedule: ClockIcon,\n webhook: WebhooksLogoIcon,\n platform: ChatCircleIcon,\n agent: ChatCircleIcon,\n test: FlaskIcon,\n};\n\nexport function ByMark({\n by,\n trigger,\n size,\n apps,\n}: {\n by: RunBy;\n /** The automation's, for an event's app; none, a plain mark. */\n trigger: Trigger | null;\n size: number;\n apps: AppDirectory | undefined;\n}) {\n if (by !== \"event\") return <Mark icon={BY[by]} size={size} />;\n return trigger === null ? (\n <Mark icon={LightningIcon} size={size} />\n ) : (\n <TriggerMark trigger={trigger} size={size} apps={apps} />\n );\n}\n\n/**\n * What a step uses, other than an app: the model, the agent, the inbox, a message on a channel, a\n * table, the web.\n */\nexport const USES: Readonly<Record<Exclude<Use[\"kind\"], \"app\">, Icon>> = {\n ai: SparkleIcon,\n agent: ChatsCircleIcon,\n notify: BellIcon,\n send: ChatTextIcon,\n data: TableIcon,\n http: GlobeIcon,\n};\n\n/** A call of a run's record, by what it reached; an app's tool by the app's mark. */\nconst CALLS: Readonly<Record<string, Icon>> = {\n ai: SparkleIcon,\n notify: BellIcon,\n send: ChatTextIcon,\n \"agent.start\": ChatsCircleIcon,\n \"data.find\": TableIcon,\n \"data.add\": TableIcon,\n \"data.update\": TableIcon,\n http: GlobeIcon,\n \"files.search\": FileTextIcon,\n \"files.read\": FileTextIcon,\n \"files.write\": FileTextIcon,\n \"files.list\": FileTextIcon,\n \"memory.remember\": BrainIcon,\n \"memory.recall\": BrainIcon,\n};\n\nexport function callMark(\n step: RunStep,\n app: { name: string; icon: string | null } | null,\n apps: AppDirectory | undefined,\n size: number,\n): ReactNode {\n if (app !== null) return <AppMark name={app.name} icon={app.icon} size={size} apps={apps} />;\n // A message sent on a channel: the channel's brand, when the host draws brands and it is known.\n const channel = step.call === \"send\" ? sentOn(step) : null;\n if (channel !== null && apps !== undefined) {\n return apps.logo({ provider: channel, name: channel, size });\n }\n return <Mark icon={CALLS[step.call] ?? LightningIcon} size={size} />;\n}\n"],"mappings":";;;;;;AAwBA,MAAM,SAAS,QAAsC,QAAQ,QAAQ,eAAe,KAAK,GAAG;;;;;AAM5F,SAAgB,QAAQ,EACtB,MACA,MACA,MACA,QAMC;CACD,IAAI,SAAS,KAAA,GAAW,OAAO,oBAAA,UAAA,EAAA,UAAG,KAAK,KAAK;EAAE,UAAU;EAAI;EAAM;EAAM;CAAK,CAAC,EAAI,CAAA;CAClF,IAAI,MAAM,IAAI,GAAG,OAAO,oBAAC,MAAD;EAAM,KAAK;EAAY;CAAO,CAAA;CACtD,OACE,oBAAC,MAAD;EAAY;EACV,UAAA,oBAAC,QAAD,EAAA,UAAO,KAAK,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,KAAK,IAAU,CAAA;CACpD,CAAA;AAEV;AAEA,MAAM,QAAmE;CACvE,UAAU;CACV,SAAS;CACT,UAAU;AACZ;;;AAIA,SAAgB,YAAY,EAC1B,SACA,MACA,QAKC;CACD,IAAI,QAAQ,SAAS,SAAS,OAAO,oBAAC,MAAD;EAAM,MAAM,MAAM,QAAQ;EAAa;CAAO,CAAA;CACnF,MAAM,OAAO,QAAQ,aAAa,QAAQ;CAC1C,IAAI,SAAS,MAAM,OAAO,oBAAC,MAAD;EAAM,MAAM;EAAqB;CAAO,CAAA;CAClE,OAAO,oBAAC,SAAD;EAAe;EAAM,MAAM,QAAQ;EAAY;EAAY;CAAO,CAAA;AAC3E;;;AAIA,MAAM,KAAsD;CAC1D,UAAU;CACV,SAAS;CACT,UAAU;CACV,OAAO;CACP,MAAM;AACR;AAEA,SAAgB,OAAO,EACrB,IACA,SACA,MACA,QAOC;CACD,IAAI,OAAO,SAAS,OAAO,oBAAC,MAAD;EAAM,MAAM,GAAG;EAAW;CAAO,CAAA;CAC5D,OAAO,YAAY,OACjB,oBAAC,MAAD;EAAM,MAAM;EAAqB;CAAO,CAAA,IAExC,oBAAC,aAAD;EAAsB;EAAe;EAAY;CAAO,CAAA;AAE5D;;;;;AAMA,MAAa,OAA4D;CACvE,IAAI;CACJ,OAAO;CACP,QAAQ;CACR,MAAM;CACN,MAAM;CACN,MAAM;AACR;;AAGA,MAAM,QAAwC;CAC5C,IAAI;CACJ,QAAQ;CACR,MAAM;CACN,eAAe;CACf,aAAa;CACb,YAAY;CACZ,eAAe;CACf,MAAM;CACN,gBAAgB;CAChB,cAAc;CACd,eAAe;CACf,cAAc;CACd,mBAAmB;CACnB,iBAAiB;AACnB;AAEA,SAAgB,SACd,MACA,KACA,MACA,MACW;CACX,IAAI,QAAQ,MAAM,OAAO,oBAAC,SAAD;EAAS,MAAM,IAAI;EAAM,MAAM,IAAI;EAAY;EAAY;CAAO,CAAA;CAE3F,MAAM,UAAU,KAAK,SAAS,SAAS,OAAO,IAAI,IAAI;CACtD,IAAI,YAAY,QAAQ,SAAS,KAAA,GAC/B,OAAO,KAAK,KAAK;EAAE,UAAU;EAAS,MAAM;EAAS;CAAK,CAAC;CAE7D,OAAO,oBAAC,MAAD;EAAM,MAAM,MAAM,KAAK,SAAS;EAAqB;CAAO,CAAA;AACrE"}
1
+ {"version":3,"file":"marks.js","names":[],"sources":["../../src/automation/marks.tsx"],"sourcesContent":["import {\n ArrowBendDownRightIcon,\n BellIcon,\n BrainIcon,\n ChatCircleIcon,\n ChatTextIcon,\n ChatsCircleIcon,\n ClockIcon,\n FileTextIcon,\n FlaskIcon,\n GlobeIcon,\n HandTapIcon,\n LightningIcon,\n PlugsConnectedIcon,\n SparkleIcon,\n TableIcon,\n WebhooksLogoIcon,\n type Icon,\n} from \"@phosphor-icons/react\";\nimport type { ReactNode } from \"react\";\n\nimport type { AppDirectory } from \"../AgentAbout\";\nimport { Mark } from \"../kit/Mark\";\nimport { sentOn } from \"./record\";\nimport type { RunBy, RunStep, Trigger, Use } from \"./types\";\n\n/** Only an https logo is loaded: never a script, never a local file. */\nconst https = (url: string | null): url is string => url !== null && /^https:\\/\\//i.test(url);\n\n/**\n * An app's mark: the host's logo when it draws apps (`apps`), else the logo the API gives, else\n * the app's initial; each bare, as `Mark` draws it.\n */\nexport function AppMark({\n name,\n icon,\n size,\n apps,\n}: {\n name: string;\n icon: string | null;\n size: number;\n apps: AppDirectory | undefined;\n}) {\n if (apps !== undefined) return <>{apps.logo({ provider: \"\", name, icon, size })}</>;\n if (https(icon)) return <Mark src={icon} size={size} />;\n return (\n <Mark size={size}>\n <span>{name.trim().charAt(0).toUpperCase() || \"?\"}</span>\n </Mark>\n );\n}\n\nconst KINDS: Readonly<Record<Exclude<Trigger[\"kind\"], \"event\">, Icon>> = {\n schedule: ClockIcon,\n webhook: WebhooksLogoIcon,\n platform: ChatCircleIcon,\n manual: HandTapIcon,\n after: ArrowBendDownRightIcon,\n};\n\n/** What starts an automation, as a mark: a clock, a webhook, a message, the app whose event it\n * waits for. */\nexport function TriggerMark({\n trigger,\n size,\n apps,\n}: {\n trigger: Trigger;\n size: number;\n apps: AppDirectory | undefined;\n}) {\n if (trigger.kind !== \"event\") return <Mark icon={KINDS[trigger.kind]} size={size} />;\n const name = trigger.app_title ?? trigger.app;\n if (name === null) return <Mark icon={LightningIcon} size={size} />;\n return <AppMark name={name} icon={trigger.icon} size={size} apps={apps} />;\n}\n\n/** What started a run: its time, a call, its app's event, a message, a chat with the agent, a\n * test. */\nconst BY: Readonly<Record<Exclude<RunBy, \"event\">, Icon>> = {\n schedule: ClockIcon,\n webhook: WebhooksLogoIcon,\n platform: ChatCircleIcon,\n agent: ChatCircleIcon,\n test: FlaskIcon,\n api: PlugsConnectedIcon,\n after: ArrowBendDownRightIcon,\n};\n\nexport function ByMark({\n by,\n trigger,\n size,\n apps,\n}: {\n by: RunBy;\n /** The automation's, for an event's app; none, a plain mark. */\n trigger: Trigger | null;\n size: number;\n apps: AppDirectory | undefined;\n}) {\n if (by !== \"event\") return <Mark icon={BY[by]} size={size} />;\n return trigger === null ? (\n <Mark icon={LightningIcon} size={size} />\n ) : (\n <TriggerMark trigger={trigger} size={size} apps={apps} />\n );\n}\n\n/**\n * What a step uses, other than an app: the model, the agent, the inbox, a message on a channel, a\n * table, the web.\n */\nexport const USES: Readonly<Record<Exclude<Use[\"kind\"], \"app\">, Icon>> = {\n ai: SparkleIcon,\n agent: ChatsCircleIcon,\n notify: BellIcon,\n send: ChatTextIcon,\n data: TableIcon,\n http: GlobeIcon,\n};\n\n/** A call of a run's record, by what it reached; an app's tool by the app's mark. */\nconst CALLS: Readonly<Record<string, Icon>> = {\n ai: SparkleIcon,\n notify: BellIcon,\n send: ChatTextIcon,\n \"agent.start\": ChatsCircleIcon,\n \"data.find\": TableIcon,\n \"data.add\": TableIcon,\n \"data.update\": TableIcon,\n http: GlobeIcon,\n \"files.search\": FileTextIcon,\n \"files.read\": FileTextIcon,\n \"files.write\": FileTextIcon,\n \"files.list\": FileTextIcon,\n \"memory.remember\": BrainIcon,\n \"memory.recall\": BrainIcon,\n};\n\nexport function callMark(\n step: RunStep,\n app: { name: string; icon: string | null } | null,\n apps: AppDirectory | undefined,\n size: number,\n): ReactNode {\n if (app !== null) return <AppMark name={app.name} icon={app.icon} size={size} apps={apps} />;\n // A message sent on a channel: the channel's brand, when the host draws brands and it is known.\n const channel = step.call === \"send\" ? sentOn(step) : null;\n if (channel !== null && apps !== undefined) {\n return apps.logo({ provider: channel, name: channel, size });\n }\n return <Mark icon={CALLS[step.call] ?? LightningIcon} size={size} />;\n}\n"],"mappings":";;;;;;AA2BA,MAAM,SAAS,QAAsC,QAAQ,QAAQ,eAAe,KAAK,GAAG;;;;;AAM5F,SAAgB,QAAQ,EACtB,MACA,MACA,MACA,QAMC;CACD,IAAI,SAAS,KAAA,GAAW,OAAO,oBAAA,UAAA,EAAA,UAAG,KAAK,KAAK;EAAE,UAAU;EAAI;EAAM;EAAM;CAAK,CAAC,EAAI,CAAA;CAClF,IAAI,MAAM,IAAI,GAAG,OAAO,oBAAC,MAAD;EAAM,KAAK;EAAY;CAAO,CAAA;CACtD,OACE,oBAAC,MAAD;EAAY;EACV,UAAA,oBAAC,QAAD,EAAA,UAAO,KAAK,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,KAAK,IAAU,CAAA;CACpD,CAAA;AAEV;AAEA,MAAM,QAAmE;CACvE,UAAU;CACV,SAAS;CACT,UAAU;CACV,QAAQ;CACR,OAAO;AACT;;;AAIA,SAAgB,YAAY,EAC1B,SACA,MACA,QAKC;CACD,IAAI,QAAQ,SAAS,SAAS,OAAO,oBAAC,MAAD;EAAM,MAAM,MAAM,QAAQ;EAAa;CAAO,CAAA;CACnF,MAAM,OAAO,QAAQ,aAAa,QAAQ;CAC1C,IAAI,SAAS,MAAM,OAAO,oBAAC,MAAD;EAAM,MAAM;EAAqB;CAAO,CAAA;CAClE,OAAO,oBAAC,SAAD;EAAe;EAAM,MAAM,QAAQ;EAAY;EAAY;CAAO,CAAA;AAC3E;;;AAIA,MAAM,KAAsD;CAC1D,UAAU;CACV,SAAS;CACT,UAAU;CACV,OAAO;CACP,MAAM;CACN,KAAK;CACL,OAAO;AACT;AAEA,SAAgB,OAAO,EACrB,IACA,SACA,MACA,QAOC;CACD,IAAI,OAAO,SAAS,OAAO,oBAAC,MAAD;EAAM,MAAM,GAAG;EAAW;CAAO,CAAA;CAC5D,OAAO,YAAY,OACjB,oBAAC,MAAD;EAAM,MAAM;EAAqB;CAAO,CAAA,IAExC,oBAAC,aAAD;EAAsB;EAAe;EAAY;CAAO,CAAA;AAE5D;;;;;AAMA,MAAa,OAA4D;CACvE,IAAI;CACJ,OAAO;CACP,QAAQ;CACR,MAAM;CACN,MAAM;CACN,MAAM;AACR;;AAGA,MAAM,QAAwC;CAC5C,IAAI;CACJ,QAAQ;CACR,MAAM;CACN,eAAe;CACf,aAAa;CACb,YAAY;CACZ,eAAe;CACf,MAAM;CACN,gBAAgB;CAChB,cAAc;CACd,eAAe;CACf,cAAc;CACd,mBAAmB;CACnB,iBAAiB;AACnB;AAEA,SAAgB,SACd,MACA,KACA,MACA,MACW;CACX,IAAI,QAAQ,MAAM,OAAO,oBAAC,SAAD;EAAS,MAAM,IAAI;EAAM,MAAM,IAAI;EAAY;EAAY;CAAO,CAAA;CAE3F,MAAM,UAAU,KAAK,SAAS,SAAS,OAAO,IAAI,IAAI;CACtD,IAAI,YAAY,QAAQ,SAAS,KAAA,GAC/B,OAAO,KAAK,KAAK;EAAE,UAAU;EAAS,MAAM;EAAS;CAAK,CAAC;CAE7D,OAAO,oBAAC,MAAD;EAAM,MAAM,MAAM,KAAK,SAAS;EAAqB;CAAO,CAAA;AACrE"}
@@ -69,6 +69,10 @@ export interface AutomationMessages {
69
69
  event: (app: string | null, event: string | null) => string;
70
70
  /** One of the platform's events: a contact's message stored on a channel. */
71
71
  platform: string;
72
+ /** Only started by hand: the agent, the business's app, a button. */
73
+ manual: string;
74
+ /** When another of the agent's automations ends well, by its name. */
75
+ after: (automation: string | null) => string;
72
76
  /** When it runs next, after when it runs: the day, and the time when the line doesn't say it. */
73
77
  next: (day: DayRef, time: string | null) => string;
74
78
  /** How it stands, on its pill. */
@@ -14,6 +14,8 @@ const AUTOMATION_MESSAGES = {
14
14
  custom: "On a schedule of its own",
15
15
  webhook: "When another system calls it",
16
16
  platform: "When a message arrives",
17
+ manual: "When someone starts it",
18
+ after: (automation) => automation ? `After “${automation}”` : "After another automation",
17
19
  event: (app, event) => event ? `When ${event} in ${app ?? "an app"}` : `On an event in ${app ?? "an app"}`,
18
20
  next: (day, time) => {
19
21
  const at = time === null ? "" : ` at ${time}`;
@@ -57,7 +59,9 @@ const AUTOMATION_MESSAGES = {
57
59
  event: app ? `${app} event` : "event",
58
60
  platform: "a message",
59
61
  agent: "you asked in the chat",
60
- test: "test"
62
+ test: "test",
63
+ api: "your app",
64
+ after: "after another"
61
65
  })[by],
62
66
  statuses: {
63
67
  running: "Running",
@@ -1 +1 @@
1
- {"version":3,"file":"messages.js","names":[],"sources":["../../src/automation/messages.ts"],"sourcesContent":["import type { AutomationState, RunBy, RunStatus, Use } from \"./types\";\n\n/**\n * A day as a person says it beside a time: today, yesterday, tomorrow, a weekday within the\n * week (`name` as the locale names it, short or long), or a date further away.\n */\nexport type DayRef =\n { kind: \"today\" | \"yesterday\" | \"tomorrow\" } | { kind: \"weekday\" | \"date\"; name: string };\n\n/** A tool's call by what it does, read from its name: what a run's record says it did. */\nexport type Verb = \"search\" | \"send\" | \"create\" | \"update\" | \"remove\" | \"use\";\n\n/** What a run did, call by call, in words; `would` is a test's write, recorded and not done. */\nexport interface CallMessages {\n /** An app's tool, by what its name says it does, with the app's name. */\n tool: (verb: Verb, app: string, would: boolean) => string;\n /** One model call: it reads and writes for real in a test too. */\n ai: string;\n /** A line in the owner's inbox. */\n notify: (would: boolean) => string;\n /**\n * A message to a contact on one of the agent's channels: the channel's kind when the record\n * says it (`whatsapp`, `instagram`, `sms`), and who to (a number; \"\" for a contact by its id).\n */\n send: (channel: string | null, to: string, would: boolean) => string;\n /** A conversation of the agent opened for what needs judgement or a person. */\n start: (would: boolean) => string;\n /** A table's rows looked up, added, changed, by the table's title. */\n find: (table: string) => string;\n add: (table: string, would: boolean) => string;\n change: (table: string, would: boolean) => string;\n /** A call to another system, by its host. */\n http: (host: string, would: boolean) => string;\n /** The organization's files searched. */\n files: string;\n /** One of its files read, kept, or its files listed, by the file's name. */\n read: (file: string) => string;\n wrote: (file: string, would: boolean) => string;\n listed: string;\n /** A line kept in, and one looked up from, what the agent learned. */\n remember: (would: boolean) => string;\n recall: string;\n}\n\n/** The words of an automation and its runs: the trigger in words, its state, its runs. */\nexport interface AutomationMessages {\n /** When it runs, from its cron line (`whenOf`): the days, then the times. */\n daily: string;\n /** Monday to Friday. */\n weekdays: string;\n /** Saturday and Sunday. */\n weekends: string;\n /** Some days of the week, by their names. */\n weekly: (days: string[]) => string;\n /** Some days of the month. */\n monthly: (days: number[]) => string;\n /** The times it runs at, after the days (\"9:00\", as the locale writes it). */\n at: (times: string[]) => string;\n /** Every few minutes or hours, all day or `between` two times. */\n every: (count: number, unit: \"minute\" | \"hour\") => string;\n between: (from: string, to: string) => string;\n /** A frequency on some days only: \"every 15 minutes\" and \"on weekdays\". */\n on: (often: string, days: string) => string;\n /** A schedule the sentence can't say. */\n custom: string;\n /** What another kind of trigger waits for. */\n webhook: string;\n event: (app: string | null, event: string | null) => string;\n /** One of the platform's events: a contact's message stored on a channel. */\n platform: string;\n /** When it runs next, after when it runs: the day, and the time when the line doesn't say it. */\n next: (day: DayRef, time: string | null) => string;\n /** How it stands, on its pill. */\n states: Readonly<Record<AutomationState, string>>;\n /** Its switch, for screen readers. */\n switchLabel: string;\n /** Over its steps, the table it moves, its runs. */\n steps: string;\n runs: string;\n /** In the place of its runs, when there are none yet. */\n noRuns: string;\n /** What a step uses, other than an app (which goes by its name). */\n uses: Readonly<Record<Exclude<Use[\"kind\"], \"app\">, string>>;\n /** The values of the table past the ones shown: \"+3\". */\n more: (count: number) => string;\n /** An automation the agent named that is not there (removed, renamed). */\n gone: string;\n /** When a run started, beside what started it. */\n moment: (day: DayRef, time: string) => string;\n /** What started a run; an event's by its app's name, when known. */\n by: (by: RunBy, app: string | null) => string;\n /** How a run stands. */\n statuses: Readonly<Record<RunStatus, string>>;\n /** A test's line: what the run returned, though it changed nothing. */\n dry: (summary: string) => string;\n /** How long a run took. */\n took: (ms: number) => string;\n /** Over the conversations a run opened, and each one's link. */\n opened: string;\n conversation: (index: number) => string;\n /** The conversation the agent was asked in, for a run it started. */\n askedIn: string;\n /** A run that called nothing. */\n noCalls: string;\n /** Over the calls made outside any step. */\n outside: string;\n /** A call made several times in a row: \"×12\". */\n times: (count: number) => string;\n calls: CallMessages;\n}\n\nconst plural = (count: number, one: string, many: string) =>\n count === 1 ? one : `${count} ${many}`;\n\n/** English: what the package says until a host gives its own words. */\nexport const AUTOMATION_MESSAGES: AutomationMessages = {\n daily: \"Every day\",\n weekdays: \"Every weekday\",\n weekends: \"Every weekend\",\n weekly: (days) =>\n days.length === 1 ? `Every ${days[0] ?? \"\"}` : `On ${new Intl.ListFormat(\"en\").format(days)}`,\n monthly: (days) => `On day ${new Intl.ListFormat(\"en\").format(days.map(String))} of every month`,\n at: (times) => `at ${new Intl.ListFormat(\"en\").format(times)}`,\n every: (count, unit) => `Every ${plural(count, unit, `${unit}s`)}`,\n between: (from, to) => `from ${from} to ${to}`,\n on: (often, days) => `${often}, ${days.charAt(0).toLowerCase()}${days.slice(1)}`,\n custom: \"On a schedule of its own\",\n webhook: \"When another system calls it\",\n platform: \"When a message arrives\",\n event: (app, event) =>\n event ? `When ${event} in ${app ?? \"an app\"}` : `On an event in ${app ?? \"an app\"}`,\n next: (day, time) => {\n const at = time === null ? \"\" : ` at ${time}`;\n switch (day.kind) {\n case \"today\":\n return `next today${at}`;\n case \"tomorrow\":\n return `next tomorrow${at}`;\n case \"yesterday\":\n return `next${at}`;\n default:\n return `next on ${day.name}${at}`;\n }\n },\n states: { active: \"On\", paused: \"Paused\", failing: \"Last run failed\" },\n switchLabel: \"On\",\n steps: \"Steps\",\n runs: \"Runs\",\n noRuns: \"It hasn't run yet.\",\n uses: {\n ai: \"AI\",\n agent: \"Agent\",\n notify: \"Inbox\",\n send: \"Message\",\n data: \"Table\",\n http: \"Web\",\n },\n more: (count) => `+${count}`,\n gone: \"This automation is no longer there.\",\n moment: (day, time) => {\n switch (day.kind) {\n case \"today\":\n return `Today ${time}`;\n case \"yesterday\":\n return `Yesterday ${time}`;\n case \"tomorrow\":\n return `Tomorrow ${time}`;\n default:\n return `${day.name} ${time}`;\n }\n },\n by: (by, app) =>\n ({\n schedule: \"scheduled\",\n webhook: \"webhook\",\n event: app ? `${app} event` : \"event\",\n platform: \"a message\",\n agent: \"you asked in the chat\",\n test: \"test\",\n })[by],\n statuses: {\n running: \"Running\",\n waiting: \"Waiting\",\n done: \"Done\",\n failed: \"Failed\",\n skipped: \"Skipped\",\n },\n dry: (summary) => `Test: ${summary}`,\n took: (ms) =>\n ms < 60_000 ? `${Math.max(1, Math.round(ms / 1000))} s` : `${Math.round(ms / 60_000)} min`,\n opened: \"Conversations it opened\",\n conversation: (index) => `Conversation ${index}`,\n askedIn: \"The conversation you asked in\",\n noCalls: \"It didn't call anything.\",\n outside: \"Outside the steps\",\n times: (count) => `×${count}`,\n calls: {\n tool: (verb, app, would) => {\n const done = {\n search: `Searched ${app}`,\n send: `Sent with ${app}`,\n create: `Created in ${app}`,\n update: `Changed in ${app}`,\n remove: `Removed in ${app}`,\n use: `Used ${app}`,\n }[verb];\n return would && verb !== \"search\"\n ? `Would have ${done.charAt(0).toLowerCase()}${done.slice(1)}`\n : done;\n },\n ai: \"Used AI\",\n notify: (would) => (would ? \"Would have told you in the inbox\" : \"Told you in the inbox\"),\n send: (channel, to, would) => {\n const what =\n { whatsapp: \"a WhatsApp\", sms: \"an SMS\", instagram: \"an Instagram message\" }[\n channel ?? \"\"\n ] ?? \"a message\";\n const sent = `${what} to ${to || \"a contact\"}`;\n return would ? `Would have sent ${sent}` : `Sent ${sent}`;\n },\n start: (would) => (would ? \"Would have opened a conversation\" : \"Opened a conversation\"),\n find: (table) => `Looked up ${table}`,\n add: (table, would) =>\n would ? `Would have added a row to ${table}` : `Added a row to ${table}`,\n change: (table, would) =>\n would ? `Would have changed a row of ${table}` : `Changed a row of ${table}`,\n http: (host, would) => (would ? `Would have called ${host}` : `Called ${host}`),\n files: \"Searched the files\",\n read: (file) => `Read ${file}`,\n wrote: (file, would) => (would ? `Would have saved ${file}` : `Saved ${file}`),\n listed: \"Looked through its files\",\n remember: (would) => (would ? \"Would have kept a memory\" : \"Kept a memory\"),\n recall: \"Searched what it learned\",\n },\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"messages.js","names":[],"sources":["../../src/automation/messages.ts"],"sourcesContent":["import type { AutomationState, RunBy, RunStatus, Use } from \"./types\";\n\n/**\n * A day as a person says it beside a time: today, yesterday, tomorrow, a weekday within the\n * week (`name` as the locale names it, short or long), or a date further away.\n */\nexport type DayRef =\n { kind: \"today\" | \"yesterday\" | \"tomorrow\" } | { kind: \"weekday\" | \"date\"; name: string };\n\n/** A tool's call by what it does, read from its name: what a run's record says it did. */\nexport type Verb = \"search\" | \"send\" | \"create\" | \"update\" | \"remove\" | \"use\";\n\n/** What a run did, call by call, in words; `would` is a test's write, recorded and not done. */\nexport interface CallMessages {\n /** An app's tool, by what its name says it does, with the app's name. */\n tool: (verb: Verb, app: string, would: boolean) => string;\n /** One model call: it reads and writes for real in a test too. */\n ai: string;\n /** A line in the owner's inbox. */\n notify: (would: boolean) => string;\n /**\n * A message to a contact on one of the agent's channels: the channel's kind when the record\n * says it (`whatsapp`, `instagram`, `sms`), and who to (a number; \"\" for a contact by its id).\n */\n send: (channel: string | null, to: string, would: boolean) => string;\n /** A conversation of the agent opened for what needs judgement or a person. */\n start: (would: boolean) => string;\n /** A table's rows looked up, added, changed, by the table's title. */\n find: (table: string) => string;\n add: (table: string, would: boolean) => string;\n change: (table: string, would: boolean) => string;\n /** A call to another system, by its host. */\n http: (host: string, would: boolean) => string;\n /** The organization's files searched. */\n files: string;\n /** One of its files read, kept, or its files listed, by the file's name. */\n read: (file: string) => string;\n wrote: (file: string, would: boolean) => string;\n listed: string;\n /** A line kept in, and one looked up from, what the agent learned. */\n remember: (would: boolean) => string;\n recall: string;\n}\n\n/** The words of an automation and its runs: the trigger in words, its state, its runs. */\nexport interface AutomationMessages {\n /** When it runs, from its cron line (`whenOf`): the days, then the times. */\n daily: string;\n /** Monday to Friday. */\n weekdays: string;\n /** Saturday and Sunday. */\n weekends: string;\n /** Some days of the week, by their names. */\n weekly: (days: string[]) => string;\n /** Some days of the month. */\n monthly: (days: number[]) => string;\n /** The times it runs at, after the days (\"9:00\", as the locale writes it). */\n at: (times: string[]) => string;\n /** Every few minutes or hours, all day or `between` two times. */\n every: (count: number, unit: \"minute\" | \"hour\") => string;\n between: (from: string, to: string) => string;\n /** A frequency on some days only: \"every 15 minutes\" and \"on weekdays\". */\n on: (often: string, days: string) => string;\n /** A schedule the sentence can't say. */\n custom: string;\n /** What another kind of trigger waits for. */\n webhook: string;\n event: (app: string | null, event: string | null) => string;\n /** One of the platform's events: a contact's message stored on a channel. */\n platform: string;\n /** Only started by hand: the agent, the business's app, a button. */\n manual: string;\n /** When another of the agent's automations ends well, by its name. */\n after: (automation: string | null) => string;\n /** When it runs next, after when it runs: the day, and the time when the line doesn't say it. */\n next: (day: DayRef, time: string | null) => string;\n /** How it stands, on its pill. */\n states: Readonly<Record<AutomationState, string>>;\n /** Its switch, for screen readers. */\n switchLabel: string;\n /** Over its steps, the table it moves, its runs. */\n steps: string;\n runs: string;\n /** In the place of its runs, when there are none yet. */\n noRuns: string;\n /** What a step uses, other than an app (which goes by its name). */\n uses: Readonly<Record<Exclude<Use[\"kind\"], \"app\">, string>>;\n /** The values of the table past the ones shown: \"+3\". */\n more: (count: number) => string;\n /** An automation the agent named that is not there (removed, renamed). */\n gone: string;\n /** When a run started, beside what started it. */\n moment: (day: DayRef, time: string) => string;\n /** What started a run; an event's by its app's name, when known. */\n by: (by: RunBy, app: string | null) => string;\n /** How a run stands. */\n statuses: Readonly<Record<RunStatus, string>>;\n /** A test's line: what the run returned, though it changed nothing. */\n dry: (summary: string) => string;\n /** How long a run took. */\n took: (ms: number) => string;\n /** Over the conversations a run opened, and each one's link. */\n opened: string;\n conversation: (index: number) => string;\n /** The conversation the agent was asked in, for a run it started. */\n askedIn: string;\n /** A run that called nothing. */\n noCalls: string;\n /** Over the calls made outside any step. */\n outside: string;\n /** A call made several times in a row: \"×12\". */\n times: (count: number) => string;\n calls: CallMessages;\n}\n\nconst plural = (count: number, one: string, many: string) =>\n count === 1 ? one : `${count} ${many}`;\n\n/** English: what the package says until a host gives its own words. */\nexport const AUTOMATION_MESSAGES: AutomationMessages = {\n daily: \"Every day\",\n weekdays: \"Every weekday\",\n weekends: \"Every weekend\",\n weekly: (days) =>\n days.length === 1 ? `Every ${days[0] ?? \"\"}` : `On ${new Intl.ListFormat(\"en\").format(days)}`,\n monthly: (days) => `On day ${new Intl.ListFormat(\"en\").format(days.map(String))} of every month`,\n at: (times) => `at ${new Intl.ListFormat(\"en\").format(times)}`,\n every: (count, unit) => `Every ${plural(count, unit, `${unit}s`)}`,\n between: (from, to) => `from ${from} to ${to}`,\n on: (often, days) => `${often}, ${days.charAt(0).toLowerCase()}${days.slice(1)}`,\n custom: \"On a schedule of its own\",\n webhook: \"When another system calls it\",\n platform: \"When a message arrives\",\n manual: \"When someone starts it\",\n after: (automation) => (automation ? `After “${automation}”` : \"After another automation\"),\n event: (app, event) =>\n event ? `When ${event} in ${app ?? \"an app\"}` : `On an event in ${app ?? \"an app\"}`,\n next: (day, time) => {\n const at = time === null ? \"\" : ` at ${time}`;\n switch (day.kind) {\n case \"today\":\n return `next today${at}`;\n case \"tomorrow\":\n return `next tomorrow${at}`;\n case \"yesterday\":\n return `next${at}`;\n default:\n return `next on ${day.name}${at}`;\n }\n },\n states: { active: \"On\", paused: \"Paused\", failing: \"Last run failed\" },\n switchLabel: \"On\",\n steps: \"Steps\",\n runs: \"Runs\",\n noRuns: \"It hasn't run yet.\",\n uses: {\n ai: \"AI\",\n agent: \"Agent\",\n notify: \"Inbox\",\n send: \"Message\",\n data: \"Table\",\n http: \"Web\",\n },\n more: (count) => `+${count}`,\n gone: \"This automation is no longer there.\",\n moment: (day, time) => {\n switch (day.kind) {\n case \"today\":\n return `Today ${time}`;\n case \"yesterday\":\n return `Yesterday ${time}`;\n case \"tomorrow\":\n return `Tomorrow ${time}`;\n default:\n return `${day.name} ${time}`;\n }\n },\n by: (by, app) =>\n ({\n schedule: \"scheduled\",\n webhook: \"webhook\",\n event: app ? `${app} event` : \"event\",\n platform: \"a message\",\n agent: \"you asked in the chat\",\n test: \"test\",\n api: \"your app\",\n after: \"after another\",\n })[by],\n statuses: {\n running: \"Running\",\n waiting: \"Waiting\",\n done: \"Done\",\n failed: \"Failed\",\n skipped: \"Skipped\",\n },\n dry: (summary) => `Test: ${summary}`,\n took: (ms) =>\n ms < 60_000 ? `${Math.max(1, Math.round(ms / 1000))} s` : `${Math.round(ms / 60_000)} min`,\n opened: \"Conversations it opened\",\n conversation: (index) => `Conversation ${index}`,\n askedIn: \"The conversation you asked in\",\n noCalls: \"It didn't call anything.\",\n outside: \"Outside the steps\",\n times: (count) => `×${count}`,\n calls: {\n tool: (verb, app, would) => {\n const done = {\n search: `Searched ${app}`,\n send: `Sent with ${app}`,\n create: `Created in ${app}`,\n update: `Changed in ${app}`,\n remove: `Removed in ${app}`,\n use: `Used ${app}`,\n }[verb];\n return would && verb !== \"search\"\n ? `Would have ${done.charAt(0).toLowerCase()}${done.slice(1)}`\n : done;\n },\n ai: \"Used AI\",\n notify: (would) => (would ? \"Would have told you in the inbox\" : \"Told you in the inbox\"),\n send: (channel, to, would) => {\n const what =\n { whatsapp: \"a WhatsApp\", sms: \"an SMS\", instagram: \"an Instagram message\" }[\n channel ?? \"\"\n ] ?? \"a message\";\n const sent = `${what} to ${to || \"a contact\"}`;\n return would ? `Would have sent ${sent}` : `Sent ${sent}`;\n },\n start: (would) => (would ? \"Would have opened a conversation\" : \"Opened a conversation\"),\n find: (table) => `Looked up ${table}`,\n add: (table, would) =>\n would ? `Would have added a row to ${table}` : `Added a row to ${table}`,\n change: (table, would) =>\n would ? `Would have changed a row of ${table}` : `Changed a row of ${table}`,\n http: (host, would) => (would ? `Would have called ${host}` : `Called ${host}`),\n files: \"Searched the files\",\n read: (file) => `Read ${file}`,\n wrote: (file, would) => (would ? `Would have saved ${file}` : `Saved ${file}`),\n listed: \"Looked through its files\",\n remember: (would) => (would ? \"Would have kept a memory\" : \"Kept a memory\"),\n recall: \"Searched what it learned\",\n },\n};\n"],"mappings":"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"}
@@ -29,6 +29,14 @@ function whenOf(trigger, words, code) {
29
29
  text: words.platform,
30
30
  timed: false
31
31
  };
32
+ case "manual": return {
33
+ text: words.manual,
34
+ timed: false
35
+ };
36
+ case "after": return {
37
+ text: words.after(trigger.after ?? null),
38
+ timed: false
39
+ };
32
40
  case "event": return {
33
41
  text: words.event(trigger.app_title ?? trigger.app, trigger.event_title),
34
42
  timed: false
@@ -1 +1 @@
1
- {"version":3,"file":"when.js","names":[],"sources":["../../src/automation/when.ts"],"sourcesContent":["/**\n * When an automation runs, and when one ran, as a person says it: its trigger in a sentence\n * (\"Cada día laborable a las 9:00\"), its next run (\"próxima mañana\"), the moment a run started\n * (\"Ayer 16:12\"), how long it took. The words are the host's (`AutomationMessages`); the locale's\n * code writes the times, the days and the lists. Pure, so it is tested without a page.\n */\n\nimport { parseCron, type Cron } from \"./cron\";\nimport type { AutomationMessages, DayRef } from \"./messages\";\nimport type { Automation, Trigger } from \"./types\";\n\n/** A time of day as the locale writes it (\"9:00\", \"9:00 AM\"), from a cron line's hour and minute. */\nexport function clockOf(hour: number, minute: number, code: string): string {\n return new Intl.DateTimeFormat(code, {\n hour: \"numeric\",\n minute: \"2-digit\",\n timeZone: \"UTC\",\n }).format(new Date(Date.UTC(2026, 0, 1, hour, minute)));\n}\n\n/** A trigger in words, and whether it names the one time of day it runs (so the next needs none). */\nexport interface When {\n text: string;\n timed: boolean;\n}\n\n/** A trigger in words: its schedule as a sentence, a webhook's call, an app's event, a message. */\nexport function whenOf(trigger: Trigger, words: AutomationMessages, code: string): When {\n switch (trigger.kind) {\n case \"schedule\":\n return trigger.schedule === null\n ? { text: words.custom, timed: false }\n : scheduleOf(trigger.schedule, words, code);\n case \"webhook\":\n return { text: words.webhook, timed: false };\n case \"platform\":\n return { text: words.platform, timed: false };\n case \"event\":\n return {\n text: words.event(trigger.app_title ?? trigger.app, trigger.event_title),\n timed: false,\n };\n }\n}\n\nconst sorted = (values: ReadonlySet<number>): number[] => [...values].sort((a, b) => a - b);\n\n/** Whether the values are every `step`-th one from `from` up to `to`: 0, 15, 30, 45. */\nfunction stepOf(values: number[]): number | null {\n if (values.length < 2) return null;\n const step = (values[1] ?? 0) - (values[0] ?? 0);\n return values.every((value, at) => value === (values[0] ?? 0) + at * step) ? step : null;\n}\n\n/** Monday first, as a week is read; Sunday is 0 in cron. */\nconst weekOrder = (day: number) => (day + 6) % 7;\n\nfunction weekdayName(day: number, code: string): string {\n // 4 January 2026 is a Sunday.\n return new Intl.DateTimeFormat(code, { weekday: \"long\", timeZone: \"UTC\" }).format(\n new Date(Date.UTC(2026, 0, 4 + day)),\n );\n}\n\n/** The days a line runs on, in words; null for what a sentence can't say. */\nfunction daysOf(cron: Cron, words: AutomationMessages, code: string): string | null {\n if (cron.months.size !== 12) return null;\n if (cron.anyDay && cron.anyWeekday) return words.daily;\n if (cron.anyDay) {\n const days = [...cron.weekdays].sort((a, b) => weekOrder(a) - weekOrder(b));\n const key = days.join(\",\");\n if (days.length === 7) return words.daily;\n if (key === \"1,2,3,4,5\") return words.weekdays;\n if (key === \"6,0\") return words.weekends;\n return words.weekly(days.map((day) => weekdayName(day, code)));\n }\n if (cron.anyWeekday) return words.monthly(sorted(cron.days));\n return null;\n}\n\n/** At most this many times are listed one by one; more read as a frequency. */\nconst LISTED = 4;\n\n/** The times a line runs at in a day: listed (\"a las 9:00\"), or how often (\"cada hora\"). */\nfunction timesOf(\n cron: Cron,\n words: AutomationMessages,\n code: string,\n): { text: string; listed: number } | null {\n const hours = sorted(cron.hours);\n const minutes = sorted(cron.minutes);\n const first = hours[0] ?? 0;\n const last = hours[hours.length - 1] ?? 0;\n if (hours.length * minutes.length <= LISTED) {\n const times = hours.flatMap((hour) => minutes.map((minute) => clockOf(hour, minute, code)));\n return { text: words.at(times), listed: times.length };\n }\n // Once in each of its hours, every few hours; or every few minutes through its hours.\n const minuteStep = minutes.length === 60 ? 1 : stepOf(minutes);\n const hourStep = hours.length === 24 || hours.length === 1 ? 1 : stepOf(hours);\n let often: string;\n if (minutes.length === 1 && hourStep !== null) {\n often = words.every(hourStep, \"hour\");\n } else if (minuteStep !== null && minutes[0] === 0 && 60 % minuteStep === 0 && hourStep === 1) {\n often = words.every(minuteStep, \"minute\");\n } else {\n return null;\n }\n // The whole day (\"cada 2 horas\"), or a part of it (\"cada hora de 8:00 a 18:00\").\n const whole = hourStep !== null && hours.length * hourStep === 24 && first < hourStep;\n if (whole) return { text: often, listed: 0 };\n const from = clockOf(first, minutes[0] ?? 0, code);\n const to = clockOf(last, minutes[minutes.length - 1] ?? 0, code);\n return { text: `${often} ${words.between(from, to)}`, listed: 0 };\n}\n\n/** A cron line as a sentence: its days, then its times or how often. */\nexport function scheduleOf(line: string, words: AutomationMessages, code: string): When {\n const cron = parseCron(line);\n const days = cron && daysOf(cron, words, code);\n const times = cron && timesOf(cron, words, code);\n if (!days || !times) return { text: words.custom, timed: false };\n if (times.listed > 0) return { text: `${days} ${times.text}`, timed: times.listed === 1 };\n return { text: days === words.daily ? times.text : words.on(times.text, days), timed: false };\n}\n\n/** A calendar day where the person is, as a count of days from the epoch's. */\nconst dayNumber = (moment: Date): number =>\n Math.round(\n (new Date(moment.getFullYear(), moment.getMonth(), moment.getDate()).getTime() -\n new Date(1970, 0, 1).getTime()) /\n 86_400_000,\n );\n\n/**\n * A moment's day beside `now`, where the person is: today, yesterday, tomorrow, a weekday within\n * the week, else its date (with its year when not this one).\n */\nexport function dayRefOf(\n moment: Date,\n now: Date,\n code: string,\n weekday: \"short\" | \"long\" = \"short\",\n): DayRef {\n const days = dayNumber(moment) - dayNumber(now);\n if (days === 0) return { kind: \"today\" };\n if (days === -1) return { kind: \"yesterday\" };\n if (days === 1) return { kind: \"tomorrow\" };\n if (Math.abs(days) < 7) {\n return { kind: \"weekday\", name: new Intl.DateTimeFormat(code, { weekday }).format(moment) };\n }\n return {\n kind: \"date\",\n name: new Intl.DateTimeFormat(code, {\n day: \"numeric\",\n month: \"short\",\n ...(moment.getFullYear() === now.getFullYear() ? {} : { year: \"numeric\" }),\n }).format(moment),\n };\n}\n\n/** A moment's time of day where the person is: \"9:00\". */\nexport const timeOf = (moment: Date, code: string): string =>\n new Intl.DateTimeFormat(code, { hour: \"numeric\", minute: \"2-digit\" }).format(moment);\n\n/** When a run started: \"Hoy 9:00\", \"Ayer 16:12\", \"Lun 9:00\", \"3 oct 9:00\". */\nexport function momentOf(at: Date, now: Date, words: AutomationMessages, code: string): string {\n return words.moment(dayRefOf(at, now, code), timeOf(at, code));\n}\n\n/**\n * The line under an automation's title: when it runs and, while it is on and has a next run,\n * when that is (\"Cada día laborable a las 9:00, próxima mañana\"). The next run's time is said\n * when the sentence doesn't already say it, and always when it is today.\n */\nexport function lineOf(\n automation: Pick<Automation, \"trigger\" | \"on\" | \"next_run_at\">,\n now: Date,\n words: AutomationMessages,\n code: string,\n): string {\n const when = whenOf(automation.trigger, words, code);\n if (!automation.on || automation.next_run_at === null) return when.text;\n const next = new Date(automation.next_run_at);\n const day = dayRefOf(next, now, code, \"long\");\n const time = when.timed && day.kind !== \"today\" ? null : timeOf(next, code);\n return `${when.text}, ${words.next(day, time)}`;\n}\n\n/** How long a run took, once it ended. */\nexport function tookOf(\n run: { started_at: string; ended_at: string | null },\n words: AutomationMessages,\n): string | null {\n if (run.ended_at === null) return null;\n return words.took(Math.max(0, Date.parse(run.ended_at) - Date.parse(run.started_at)));\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"when.js","names":[],"sources":["../../src/automation/when.ts"],"sourcesContent":["/**\n * When an automation runs, and when one ran, as a person says it: its trigger in a sentence\n * (\"Cada día laborable a las 9:00\"), its next run (\"próxima mañana\"), the moment a run started\n * (\"Ayer 16:12\"), how long it took. The words are the host's (`AutomationMessages`); the locale's\n * code writes the times, the days and the lists. Pure, so it is tested without a page.\n */\n\nimport { parseCron, type Cron } from \"./cron\";\nimport type { AutomationMessages, DayRef } from \"./messages\";\nimport type { Automation, Trigger } from \"./types\";\n\n/** A time of day as the locale writes it (\"9:00\", \"9:00 AM\"), from a cron line's hour and minute. */\nexport function clockOf(hour: number, minute: number, code: string): string {\n return new Intl.DateTimeFormat(code, {\n hour: \"numeric\",\n minute: \"2-digit\",\n timeZone: \"UTC\",\n }).format(new Date(Date.UTC(2026, 0, 1, hour, minute)));\n}\n\n/** A trigger in words, and whether it names the one time of day it runs (so the next needs none). */\nexport interface When {\n text: string;\n timed: boolean;\n}\n\n/** A trigger in words: its schedule as a sentence, a webhook's call, an app's event, a message. */\nexport function whenOf(trigger: Trigger, words: AutomationMessages, code: string): When {\n switch (trigger.kind) {\n case \"schedule\":\n return trigger.schedule === null\n ? { text: words.custom, timed: false }\n : scheduleOf(trigger.schedule, words, code);\n case \"webhook\":\n return { text: words.webhook, timed: false };\n case \"platform\":\n return { text: words.platform, timed: false };\n case \"manual\":\n return { text: words.manual, timed: false };\n case \"after\":\n return { text: words.after(trigger.after ?? null), timed: false };\n case \"event\":\n return {\n text: words.event(trigger.app_title ?? trigger.app, trigger.event_title),\n timed: false,\n };\n }\n}\n\nconst sorted = (values: ReadonlySet<number>): number[] => [...values].sort((a, b) => a - b);\n\n/** Whether the values are every `step`-th one from `from` up to `to`: 0, 15, 30, 45. */\nfunction stepOf(values: number[]): number | null {\n if (values.length < 2) return null;\n const step = (values[1] ?? 0) - (values[0] ?? 0);\n return values.every((value, at) => value === (values[0] ?? 0) + at * step) ? step : null;\n}\n\n/** Monday first, as a week is read; Sunday is 0 in cron. */\nconst weekOrder = (day: number) => (day + 6) % 7;\n\nfunction weekdayName(day: number, code: string): string {\n // 4 January 2026 is a Sunday.\n return new Intl.DateTimeFormat(code, { weekday: \"long\", timeZone: \"UTC\" }).format(\n new Date(Date.UTC(2026, 0, 4 + day)),\n );\n}\n\n/** The days a line runs on, in words; null for what a sentence can't say. */\nfunction daysOf(cron: Cron, words: AutomationMessages, code: string): string | null {\n if (cron.months.size !== 12) return null;\n if (cron.anyDay && cron.anyWeekday) return words.daily;\n if (cron.anyDay) {\n const days = [...cron.weekdays].sort((a, b) => weekOrder(a) - weekOrder(b));\n const key = days.join(\",\");\n if (days.length === 7) return words.daily;\n if (key === \"1,2,3,4,5\") return words.weekdays;\n if (key === \"6,0\") return words.weekends;\n return words.weekly(days.map((day) => weekdayName(day, code)));\n }\n if (cron.anyWeekday) return words.monthly(sorted(cron.days));\n return null;\n}\n\n/** At most this many times are listed one by one; more read as a frequency. */\nconst LISTED = 4;\n\n/** The times a line runs at in a day: listed (\"a las 9:00\"), or how often (\"cada hora\"). */\nfunction timesOf(\n cron: Cron,\n words: AutomationMessages,\n code: string,\n): { text: string; listed: number } | null {\n const hours = sorted(cron.hours);\n const minutes = sorted(cron.minutes);\n const first = hours[0] ?? 0;\n const last = hours[hours.length - 1] ?? 0;\n if (hours.length * minutes.length <= LISTED) {\n const times = hours.flatMap((hour) => minutes.map((minute) => clockOf(hour, minute, code)));\n return { text: words.at(times), listed: times.length };\n }\n // Once in each of its hours, every few hours; or every few minutes through its hours.\n const minuteStep = minutes.length === 60 ? 1 : stepOf(minutes);\n const hourStep = hours.length === 24 || hours.length === 1 ? 1 : stepOf(hours);\n let often: string;\n if (minutes.length === 1 && hourStep !== null) {\n often = words.every(hourStep, \"hour\");\n } else if (minuteStep !== null && minutes[0] === 0 && 60 % minuteStep === 0 && hourStep === 1) {\n often = words.every(minuteStep, \"minute\");\n } else {\n return null;\n }\n // The whole day (\"cada 2 horas\"), or a part of it (\"cada hora de 8:00 a 18:00\").\n const whole = hourStep !== null && hours.length * hourStep === 24 && first < hourStep;\n if (whole) return { text: often, listed: 0 };\n const from = clockOf(first, minutes[0] ?? 0, code);\n const to = clockOf(last, minutes[minutes.length - 1] ?? 0, code);\n return { text: `${often} ${words.between(from, to)}`, listed: 0 };\n}\n\n/** A cron line as a sentence: its days, then its times or how often. */\nexport function scheduleOf(line: string, words: AutomationMessages, code: string): When {\n const cron = parseCron(line);\n const days = cron && daysOf(cron, words, code);\n const times = cron && timesOf(cron, words, code);\n if (!days || !times) return { text: words.custom, timed: false };\n if (times.listed > 0) return { text: `${days} ${times.text}`, timed: times.listed === 1 };\n return { text: days === words.daily ? times.text : words.on(times.text, days), timed: false };\n}\n\n/** A calendar day where the person is, as a count of days from the epoch's. */\nconst dayNumber = (moment: Date): number =>\n Math.round(\n (new Date(moment.getFullYear(), moment.getMonth(), moment.getDate()).getTime() -\n new Date(1970, 0, 1).getTime()) /\n 86_400_000,\n );\n\n/**\n * A moment's day beside `now`, where the person is: today, yesterday, tomorrow, a weekday within\n * the week, else its date (with its year when not this one).\n */\nexport function dayRefOf(\n moment: Date,\n now: Date,\n code: string,\n weekday: \"short\" | \"long\" = \"short\",\n): DayRef {\n const days = dayNumber(moment) - dayNumber(now);\n if (days === 0) return { kind: \"today\" };\n if (days === -1) return { kind: \"yesterday\" };\n if (days === 1) return { kind: \"tomorrow\" };\n if (Math.abs(days) < 7) {\n return { kind: \"weekday\", name: new Intl.DateTimeFormat(code, { weekday }).format(moment) };\n }\n return {\n kind: \"date\",\n name: new Intl.DateTimeFormat(code, {\n day: \"numeric\",\n month: \"short\",\n ...(moment.getFullYear() === now.getFullYear() ? {} : { year: \"numeric\" }),\n }).format(moment),\n };\n}\n\n/** A moment's time of day where the person is: \"9:00\". */\nexport const timeOf = (moment: Date, code: string): string =>\n new Intl.DateTimeFormat(code, { hour: \"numeric\", minute: \"2-digit\" }).format(moment);\n\n/** When a run started: \"Hoy 9:00\", \"Ayer 16:12\", \"Lun 9:00\", \"3 oct 9:00\". */\nexport function momentOf(at: Date, now: Date, words: AutomationMessages, code: string): string {\n return words.moment(dayRefOf(at, now, code), timeOf(at, code));\n}\n\n/**\n * The line under an automation's title: when it runs and, while it is on and has a next run,\n * when that is (\"Cada día laborable a las 9:00, próxima mañana\"). The next run's time is said\n * when the sentence doesn't already say it, and always when it is today.\n */\nexport function lineOf(\n automation: Pick<Automation, \"trigger\" | \"on\" | \"next_run_at\">,\n now: Date,\n words: AutomationMessages,\n code: string,\n): string {\n const when = whenOf(automation.trigger, words, code);\n if (!automation.on || automation.next_run_at === null) return when.text;\n const next = new Date(automation.next_run_at);\n const day = dayRefOf(next, now, code, \"long\");\n const time = when.timed && day.kind !== \"today\" ? null : timeOf(next, code);\n return `${when.text}, ${words.next(day, time)}`;\n}\n\n/** How long a run took, once it ended. */\nexport function tookOf(\n run: { started_at: string; ended_at: string | null },\n words: AutomationMessages,\n): string | null {\n if (run.ended_at === null) return null;\n return words.took(Math.max(0, Date.parse(run.ended_at) - Date.parse(run.started_at)));\n}\n"],"mappings":"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"}
package/dist/blocks.d.ts CHANGED
@@ -103,6 +103,76 @@ export interface ConnectBlock {
103
103
  purpose: string;
104
104
  connection: ConnectView | null;
105
105
  }
106
+ /**
107
+ * One thing on a calendar: its title at its time ("YYYY-MM-DDTHH:MM" in the person's time; a day
108
+ * alone, "YYYY-MM-DD", is all of it), until `end`, with a line of detail and a page to open.
109
+ */
110
+ export interface CalendarEvent {
111
+ id: string;
112
+ title: string;
113
+ start: string;
114
+ end?: string;
115
+ detail?: string;
116
+ /** https only. */
117
+ url?: string;
118
+ }
119
+ /**
120
+ * A day's events, or a week's from `start` ("YYYY-MM-DD"), in their order. With `action`,
121
+ * pressing one sends it with `{event: id}`; one with `url` opens its page instead.
122
+ */
123
+ export interface CalendarBlock {
124
+ type: "calendar";
125
+ span: "day" | "week";
126
+ start: string;
127
+ events: CalendarEvent[];
128
+ action?: string;
129
+ }
130
+ /** One thing on a timeline: when, what, a line about it, where it stands in a word, its mark. */
131
+ export interface TimelineItem {
132
+ id?: string;
133
+ at: string;
134
+ title: string;
135
+ text?: string;
136
+ status?: string;
137
+ tone?: Tone;
138
+ }
139
+ /** Dated things, one under another, the newest first. */
140
+ export interface TimelineBlock {
141
+ type: "timeline";
142
+ items: TimelineItem[];
143
+ }
144
+ export interface BoardCard {
145
+ id: string;
146
+ title: string;
147
+ subtitle?: string;
148
+ /** At its foot: a price, a day, who. */
149
+ meta?: string;
150
+ }
151
+ export interface BoardColumn {
152
+ id: string;
153
+ title: string;
154
+ cards: BoardCard[];
155
+ }
156
+ /**
157
+ * Cards in columns, a column per stage. With `action`, the person moves a card to another column,
158
+ * which sends `action` with `{card, column}`.
159
+ */
160
+ export interface BoardBlock {
161
+ type: "board";
162
+ columns: BoardColumn[];
163
+ action?: string;
164
+ }
165
+ /**
166
+ * One of the host's own blocks, by the name it declared (`PUT /v1/blocks/{name}`): its `props`,
167
+ * checked by the API against the schema the host declared, and `text`, the same in words, which a
168
+ * page that doesn't draw it shows instead.
169
+ */
170
+ export interface HostBlock {
171
+ type: "host";
172
+ name: string;
173
+ props: Record<string, unknown>;
174
+ text: string;
175
+ }
106
176
  /** One thing the person can do: pressing it sends `action`, or, with `url`, opens that page. */
107
177
  export interface ButtonBlock {
108
178
  type: "button";
@@ -197,11 +267,22 @@ export interface StackBlock {
197
267
  direction: "column" | "row";
198
268
  children: Block[];
199
269
  }
200
- export type Block = CardBlock | StackBlock | DividerBlock | TextBlock | FieldsBlock | TableBlock | ListBlock | StatBlock | ChartBlock | NoteBlock | ImageBlock | AutomationBlock | FileBlock | ConnectBlock | ButtonBlock | ChoiceBlock | FormBlock;
201
- /** What the agent shows: blocks, and the words a channel without a screen gets instead. */
270
+ export type Block = CardBlock | StackBlock | DividerBlock | TextBlock | FieldsBlock | TableBlock | ListBlock | StatBlock | ChartBlock | NoteBlock | ImageBlock | AutomationBlock | FileBlock | ConnectBlock | ButtonBlock | ChoiceBlock | FormBlock | CalendarBlock | TimelineBlock | BoardBlock | HostBlock;
271
+ /**
272
+ * Where a surface shows in a page: in the conversation, in a panel beside it, or in a modal over
273
+ * it. A page that doesn't allow the place shows it in the conversation.
274
+ */
275
+ export type Place = "chat" | "panel" | "modal";
276
+ /**
277
+ * What the agent shows: blocks, and the words a channel without a screen gets instead; where it
278
+ * shows and, in a panel or a modal, its title, which the conversation's line to open it says.
279
+ */
202
280
  export interface Surface {
203
281
  blocks: Block[];
204
282
  text: string;
283
+ /** None: the conversation. */
284
+ where?: Place;
285
+ title?: string;
205
286
  }
206
287
  //#endregion
207
288
  //# sourceMappingURL=blocks.d.ts.map
package/dist/client.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { AboutOut, MemoryFact } from "./about.js";
2
2
  import { Automation, AutomationView, RunRecord, Task } from "./automation/types.js";
3
3
  import { Credentials, Speaker } from "./credentials.js";
4
- import { AgentFace, CallToken, Channel, Conversation, ConversationDetail, ConversationFile, FileAt, FilePreview, StoredFile, WidgetConfig } from "./types.js";
4
+ import { AgentFace, CallToken, Channel, Conversation, ConversationDetail, ConversationFile, FileAt, FilePreview, StoredFile, ViewRendered, WidgetConfig } from "./types.js";
5
5
  //#region src/client.d.ts
6
6
  /**
7
7
  * What the API answered when it was not a success: its status, its sentence (`detail`, written
@@ -74,8 +74,10 @@ export interface Client {
74
74
  /** Takes one memory from it. */
75
75
  forget: (id: string, memoryId: string) => Promise<void>;
76
76
  /**
77
- * The owner's call with the agent, from the browser (v3/VOICE.md): the call and its
78
- * conversation (a test) made now, and where to join it (`VoiceCall`'s `connect`).
77
+ * The owner's call with the agent, from our own app (v3/VOICE.md): the call and its
78
+ * conversation (a test) made now, and where to join it (`VoiceCall`'s `connect`). App only:
79
+ * it is `/api`'s, signed in by the owner's cookie, and 404s on `/v1`; a host's page calls with
80
+ * `conversations.call(agentId)`.
79
81
  */
80
82
  call: (id: string) => Promise<CallToken>;
81
83
  };
@@ -98,6 +100,14 @@ export interface Client {
98
100
  /** One run of an automation with its record: every call it made, step by step. */
99
101
  get: (id: string) => Promise<RunRecord>;
100
102
  };
103
+ /** What an agent keeps on show for each person it serves (`compose_view`, `/agents/{id}/views`). */
104
+ views: {
105
+ /**
106
+ * A view rendered from the rows now: for a person, their own; for the business, for the
107
+ * contact it names (`contact`, their id).
108
+ */
109
+ render: (name: string, contact?: string) => Promise<ViewRendered>;
110
+ };
101
111
  /**
102
112
  * The tasks an agent runs for contacts, each with its newest runs: a person's own only (what
103
113
  * they asked the agent for in their conversations); the business's, every one.
@@ -116,8 +126,16 @@ export interface Client {
116
126
  conversations: {
117
127
  /** One conversation with its messages. */
118
128
  get: (id: string) => Promise<ConversationDetail>;
119
- /** A new conversation with an agent. */
120
- start: (agentId: string) => Promise<Conversation>;
129
+ /** The agent's conversations, newest first: a person's own only, each with what is unread. */
130
+ list: (agentId: string) => Promise<Conversation[]>;
131
+ /** A new conversation with an agent, titled when given. */
132
+ start: (agentId: string, options?: {
133
+ title?: string | undefined;
134
+ }) => Promise<Conversation>;
135
+ /** How far the person has read it: all of it, now. */
136
+ read: (id: string) => Promise<Conversation>;
137
+ /** Its title, as the person gives it. */
138
+ rename: (id: string, title: string) => Promise<Conversation>;
121
139
  /** The files that arrived in a conversation: attached, sent through a form. */
122
140
  files: (id: string) => Promise<ConversationFile[]>;
123
141
  /**
package/dist/client.js CHANGED
@@ -42,6 +42,8 @@ function readJson(text) {
42
42
  }
43
43
  }
44
44
  const seg = encodeURIComponent;
45
+ /** A request's JSON body. */
46
+ const body = (value) => new Blob([JSON.stringify(value)], { type: "application/json" });
45
47
  /** A file's path in a URL: each name of it encoded, the slashes between them kept. */
46
48
  const filePath = (path) => path.split("/").map(seg).join("/");
47
49
  /** Where a file's preview is read: under its conversation, its agent or the organization. */
@@ -173,15 +175,19 @@ function createClient(options) {
173
175
  get: (agentId, name) => json("GET", `/agents/${seg(agentId)}/automations/${seg(name)}`)
174
176
  },
175
177
  runs: { get: (id) => json("GET", `/runs/${seg(id)}`) },
178
+ views: { render: (name, contact) => json("GET", `/views/${seg(name)}/render${contact === void 0 ? "" : `?contact=${seg(contact)}`}`) },
176
179
  tasks: {
177
180
  list: (agentId) => json("GET", `/agents/${seg(agentId)}/tasks`),
178
- switch: (agentId, taskId, on) => json("PATCH", `/agents/${seg(agentId)}/tasks/${seg(taskId)}`, new Blob([JSON.stringify({ on })], { type: "application/json" })),
181
+ switch: (agentId, taskId, on) => json("PATCH", `/agents/${seg(agentId)}/tasks/${seg(taskId)}`, body({ on })),
179
182
  remove: (agentId, taskId) => run("DELETE", `/agents/${seg(agentId)}/tasks/${seg(taskId)}`)
180
183
  },
181
184
  channels: { list: (agentId) => json("GET", agentId === void 0 ? "/channels" : `/channels?agent=${seg(agentId)}`) },
182
185
  conversations: {
183
186
  get: (id) => json("GET", `/conversations/${seg(id)}`),
184
- start: (agentId) => json("POST", `/agents/${seg(agentId)}/conversations`),
187
+ list: (agentId) => json("GET", `/agents/${seg(agentId)}/conversations`),
188
+ start: (agentId, { title } = {}) => json("POST", `/agents/${seg(agentId)}/conversations`, title === void 0 ? void 0 : body({ title })),
189
+ read: (id) => json("POST", `/conversations/${seg(id)}/read`, body({})),
190
+ rename: (id, title) => json("PATCH", `/conversations/${seg(id)}`, body({ title })),
185
191
  files: (id) => json("GET", `/conversations/${seg(id)}/files`),
186
192
  upload: (id, file, options) => sendFile(`/conversations/${seg(id)}/files`, file, options),
187
193
  play: async (id, fileId) => {
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["import type { AboutOut, MemoryFact } from \"./about\";\nimport type { Automation, AutomationView, RunRecord, Task } from \"./automation/types\";\nimport { signerOf, type Credentials, type Speaker } from \"./credentials\";\nimport type {\n AgentFace,\n WidgetConfig,\n CallToken,\n Channel,\n Conversation,\n ConversationDetail,\n ConversationFile,\n FileAt,\n FileLink,\n FilePreview,\n StoredFile,\n} from \"./types\";\n\n/**\n * What the API answered when it was not a success: its status, its sentence (`detail`, written\n * to be shown) and its `code`, for a host to word in its own language.\n */\nexport class ApiError extends Error {\n readonly status: number;\n readonly detail: string;\n /** What went wrong, by name (`not_found`, `no_calls`); null when the answer named nothing. */\n readonly code: string | null;\n\n constructor(status: number, detail: string, code: string | null = null) {\n super(detail);\n this.name = \"ApiError\";\n this.status = status;\n this.detail = detail;\n this.code = code;\n }\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null;\n\nconst isIssue = (value: unknown): value is { msg: string } =>\n isRecord(value) && typeof value.msg === \"string\";\n\n/**\n * Reads an error body: `{code, detail: \"message\"}`, or FastAPI's 422 `{detail: [{msg, …}, …]}`\n * (the first issue speaks for all). A body with neither is named by its status alone.\n */\nexport function parseError(status: number, body: unknown): ApiError {\n const detail = isRecord(body) ? body.detail : undefined;\n const code = isRecord(body) && typeof body.code === \"string\" ? body.code : null;\n if (typeof detail === \"string\" && detail !== \"\") return new ApiError(status, detail, code);\n if (Array.isArray(detail)) {\n const issue = detail.find(isIssue);\n if (issue) return new ApiError(status, issue.msg);\n }\n return new ApiError(status, `HTTP ${status}`);\n}\n\n/** A body read as JSON, or null when it is not. */\nfunction readJson(text: string): unknown {\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return null;\n }\n}\n\n/**\n * Where the API is, and who calls it (`Credentials`: the cookie, a person's token, a token asked\n * of the host's backend, or a publishable key and its visitor).\n */\nexport type ClientOptions = Credentials & {\n /**\n * The API's origin and prefix: `/api` in our own app (the cookie's, on the page's origin), the\n * public `https://…/v1` from a host's page.\n */\n baseUrl: string;\n /** A host's own `fetch`, when the global one is not the right one. */\n fetch?: typeof fetch | undefined;\n};\n\n/** The files under one owner: the organization's, or an agent's own. */\nexport interface Files {\n /** Every file, by path. */\n list: () => Promise<StoredFile[]>;\n /**\n * Stores the bytes under the path (replacing a file of the same name), with the blob's type\n * as the file's; `searchable` makes it knowledge `search_files` reads (text, PDF).\n */\n put: (path: string, blob: Blob, searchable: boolean) => Promise<StoredFile>;\n delete: (path: string) => Promise<void>;\n}\n\n/** How an upload goes: how much of it has gone, and how to let it go. */\nexport interface UploadOptions {\n /** Hears the share sent so far, from 0 to 1, as it goes. */\n onProgress?: ((share: number) => void) | undefined;\n /** Aborting it stops the upload, and the promise rejects with an `AbortError`. */\n signal?: AbortSignal | undefined;\n}\n\n/** The API, as far as this package reaches it. */\nexport interface Client {\n /**\n * The widget's agent and look, by the publishable key it speaks with (a web channel's key).\n */\n widget: () => Promise<WidgetConfig>;\n /**\n * Who the page speaks for, by its credentials: the business (our own app's cookie) or a person\n * it serves (a token, a publishable key's visitor). What waits for the business's yes is\n * answered by the business alone.\n */\n who: Speaker;\n agents: {\n /** What a person may know of the agent: its name, role, mark, and whether it takes calls. */\n face: (id: string) => Promise<AgentFace>;\n /** What the agent says about itself (null until it has written it) and the facts it rests on. */\n about: (id: string) => Promise<AboutOut>;\n /** Has the agent write about itself anew (a model call, a few seconds); answers like `about`. */\n describe: (id: string) => Promise<AboutOut>;\n /** The agent's own files, by path. */\n files: (id: string) => Promise<StoredFile[]>;\n /** Stores a file of the agent's own; see `Files.put`. */\n putFile: (id: string, path: string, blob: Blob, searchable: boolean) => Promise<StoredFile>;\n deleteFile: (id: string, path: string) => Promise<void>;\n /** What the agent remembered, newest first. */\n memories: (id: string) => Promise<MemoryFact[]>;\n /** Takes one memory from it. */\n forget: (id: string, memoryId: string) => Promise<void>;\n /**\n * The owner's call with the agent, from the browser (v3/VOICE.md): the call and its\n * conversation (a test) made now, and where to join it (`VoiceCall`'s `connect`).\n */\n call: (id: string) => Promise<CallToken>;\n };\n /**\n * The organization's files, shared by every agent; and the preview of any file the caller may\n * read, the organization's, an agent's own or a conversation's (`preview`).\n */\n files: Files & {\n /** What a viewer draws of the file, by its `kind` (v3/FILES.md, Preview). */\n preview: (at: FileAt) => Promise<FilePreview>;\n };\n /** What an agent does on its own (v3/AUTOMATIONS.md). */\n automations: {\n /** The agent's automations, each as it stands now with its newest run. */\n list: (agentId: string) => Promise<Automation[]>;\n /** One of them by its name, with its newest runs. */\n get: (agentId: string, name: string) => Promise<AutomationView>;\n };\n runs: {\n /** One run of an automation with its record: every call it made, step by step. */\n get: (id: string) => Promise<RunRecord>;\n };\n /**\n * The tasks an agent runs for contacts, each with its newest runs: a person's own only (what\n * they asked the agent for in their conversations); the business's, every one.\n */\n tasks: {\n list: (agentId: string) => Promise<Task[]>;\n /** Pauses it, or turns it on again; answers it as it stands. */\n switch: (agentId: string, taskId: string, on: boolean) => Promise<Task>;\n remove: (agentId: string, taskId: string) => Promise<void>;\n };\n /** Where people write to the organization's agents (v3/CHANNELS.md). */\n channels: {\n /** The organization's channels, or one agent's: the disconnected ones too. */\n list: (agentId?: string) => Promise<Channel[]>;\n };\n conversations: {\n /** One conversation with its messages. */\n get: (id: string) => Promise<ConversationDetail>;\n /** A new conversation with an agent. */\n start: (agentId: string) => Promise<Conversation>;\n /** The files that arrived in a conversation: attached, sent through a form. */\n files: (id: string) => Promise<ConversationFile[]>;\n /**\n * Stores a file in the conversation as the person picks it, to say it or send it through a\n * form by its id: its bytes as they are, under its type and name.\n */\n upload: (id: string, file: File, options?: UploadOptions) => Promise<ConversationFile>;\n /**\n * Where a picture, a video or a sound of the conversation plays from in the page's own\n * player: an address that needs no credential, for an hour, streamed as the player seeks.\n */\n play: (id: string, fileId: string) => Promise<string>;\n /**\n * A call with the agent from the browser, for whoever may talk to it (a person, as theirs):\n * the call and its own conversation made now, where what is said lands, and where to join it\n * (`VoiceCall`'s `connect`).\n */\n call: (agentId: string) => Promise<CallToken>;\n };\n /**\n * The conversation's socket address, signed in: `ws:` beside `http:`, `wss:` beside `https:`,\n * with the token (and the visitor) in its query, since a browser's socket takes no header.\n * `fresh` asks the host's backend for a new token first (`getToken`), for a socket that could\n * not open with the last one.\n */\n liveUrl: (conversationId: string, options?: { fresh?: boolean | undefined }) => Promise<string>;\n}\n\nconst seg = encodeURIComponent;\n\n/** A file's path in a URL: each name of it encoded, the slashes between them kept. */\nconst filePath = (path: string): string => path.split(\"/\").map(seg).join(\"/\");\n\n/** Where a file's preview is read: under its conversation, its agent or the organization. */\nconst previewPath = (at: FileAt): string => {\n if (\"conversationId\" in at) {\n return `/conversations/${seg(at.conversationId)}/files/${seg(at.fileId)}/preview`;\n }\n const owner = \"agentId\" in at ? `/agents/${seg(at.agentId)}` : \"\";\n return `${owner}/files/${filePath(at.path)}/preview`;\n};\n\ntype Method = \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n\n/**\n * The API under `baseUrl`: JSON in and out, an `ApiError` for anything but a success, signed in\n * the one way `options` gives. A token asked of the host's backend (`getToken`) that the API\n * refuses (401: it expired) is asked for once more, and the request sent again with it.\n */\nexport function createClient(options: ClientOptions): Client {\n const { baseUrl, fetch: call = fetch } = options;\n const signer = signerOf(options);\n\n /** One request with the cookie or the credential's headers; a body goes as it is, typed. */\n async function attempt(method: Method, path: string, body: Blob | undefined) {\n const signed = await signer.sign();\n const response = await call(`${baseUrl}${path}`, {\n method,\n credentials: signer.credentials,\n headers:\n body === undefined ? signed.headers : { ...signed.headers, \"Content-Type\": body.type },\n ...(body === undefined ? {} : { body }),\n });\n return { response, signed };\n }\n\n async function send(method: Method, path: string, body?: Blob): Promise<Response> {\n let { response, signed } = await attempt(method, path, body);\n if (response.status === 401 && signed.renew !== null) {\n signed.renew();\n ({ response } = await attempt(method, path, body));\n }\n if (!response.ok) {\n const answer: unknown = await response.json().catch(() => null);\n throw parseError(response.status, answer);\n }\n return response;\n }\n\n async function json<T>(method: Method, path: string, body?: Blob): Promise<T> {\n return (await (await send(method, path, body)).json()) as T;\n }\n\n /** A request whose answer has no body (204). */\n async function run(method: Method, path: string): Promise<void> {\n await send(method, path);\n }\n\n /** The four of `Files` under a prefix: `/files`, or `/agents/{id}/files`. */\n const filesUnder = (prefix: string): Files => ({\n list: () => json(\"GET\", prefix),\n put: (path, blob, searchable) =>\n json(\"PUT\", `${prefix}/${filePath(path)}?searchable=${searchable}`, blob),\n delete: (path) => run(\"DELETE\", `${prefix}/${filePath(path)}`),\n });\n\n const own = (id: string) => filesUnder(`/agents/${seg(id)}/files`);\n\n /**\n * A file sent by XMLHttpRequest, the one way a browser tells how much of a body has gone; the\n * cookie goes as with `fetch` on the same origin, any other credential as its headers. Refused\n * with a token asked of the host's backend, it is sent once more with a new one.\n */\n async function sendFile(\n path: string,\n file: File,\n options: UploadOptions = {},\n ): Promise<ConversationFile> {\n const signed = await signer.sign();\n try {\n return await post(path, file, signed.headers, options);\n } catch (error) {\n const expired = error instanceof ApiError && error.status === 401;\n if (!expired || signed.renew === null) throw error;\n signed.renew();\n return post(path, file, (await signer.sign()).headers, options);\n }\n }\n\n function post(\n path: string,\n file: File,\n headers: Readonly<Record<string, string>>,\n { onProgress, signal }: UploadOptions,\n ): Promise<ConversationFile> {\n return new Promise((resolve, reject) => {\n const aborted = () => new DOMException(\"The upload was aborted\", \"AbortError\");\n if (signal?.aborted) {\n reject(aborted());\n return;\n }\n const request = new XMLHttpRequest();\n const abort = () => {\n request.abort();\n };\n request.open(\"POST\", `${baseUrl}${path}`);\n for (const [name, value] of Object.entries(headers)) request.setRequestHeader(name, value);\n request.setRequestHeader(\"Content-Type\", file.type || \"application/octet-stream\");\n request.setRequestHeader(\"X-File-Name\", encodeURIComponent(file.name));\n request.upload.addEventListener(\"progress\", (event) => {\n if (event.lengthComputable && event.total > 0) onProgress?.(event.loaded / event.total);\n });\n request.addEventListener(\"loadend\", () => {\n signal?.removeEventListener(\"abort\", abort);\n });\n request.addEventListener(\"load\", () => {\n const body = readJson(request.responseText);\n if (request.status >= 200 && request.status < 300) resolve(body as ConversationFile);\n else reject(parseError(request.status, body));\n });\n request.addEventListener(\"error\", () => {\n reject(new ApiError(0, \"The upload could not reach the server\"));\n });\n request.addEventListener(\"abort\", () => {\n reject(aborted());\n });\n signal?.addEventListener(\"abort\", abort, { once: true });\n request.send(file);\n });\n }\n\n return {\n widget: () => json(\"GET\", \"/channels/web/widget\"),\n who: signer.who,\n agents: {\n face: (id) => json(\"GET\", `/agents/${seg(id)}/face`),\n about: (id) => json(\"GET\", `/agents/${seg(id)}/about`),\n describe: (id) => json(\"POST\", `/agents/${seg(id)}/describe`),\n files: (id) => own(id).list(),\n putFile: (id, path, blob, searchable) => own(id).put(path, blob, searchable),\n deleteFile: (id, path) => own(id).delete(path),\n memories: (id) => json(\"GET\", `/agents/${seg(id)}/memories`),\n forget: (id, memoryId) => run(\"DELETE\", `/agents/${seg(id)}/memories/${seg(memoryId)}`),\n call: (id) => json(\"POST\", `/agents/${seg(id)}/voice/token`),\n },\n files: { ...filesUnder(\"/files\"), preview: (at) => json(\"GET\", previewPath(at)) },\n automations: {\n list: (agentId) => json(\"GET\", `/agents/${seg(agentId)}/automations`),\n get: (agentId, name) => json(\"GET\", `/agents/${seg(agentId)}/automations/${seg(name)}`),\n },\n runs: {\n get: (id) => json(\"GET\", `/runs/${seg(id)}`),\n },\n tasks: {\n list: (agentId) => json(\"GET\", `/agents/${seg(agentId)}/tasks`),\n switch: (agentId, taskId, on) =>\n json(\n \"PATCH\",\n `/agents/${seg(agentId)}/tasks/${seg(taskId)}`,\n new Blob([JSON.stringify({ on })], { type: \"application/json\" }),\n ),\n remove: (agentId, taskId) => run(\"DELETE\", `/agents/${seg(agentId)}/tasks/${seg(taskId)}`),\n },\n channels: {\n list: (agentId) =>\n json(\"GET\", agentId === undefined ? \"/channels\" : `/channels?agent=${seg(agentId)}`),\n },\n conversations: {\n get: (id) => json(\"GET\", `/conversations/${seg(id)}`),\n start: (agentId) => json(\"POST\", `/agents/${seg(agentId)}/conversations`),\n files: (id) => json(\"GET\", `/conversations/${seg(id)}/files`),\n upload: (id, file, options) => sendFile(`/conversations/${seg(id)}/files`, file, options),\n play: async (id, fileId) => {\n const link = await json<FileLink>(\n \"GET\",\n `/conversations/${seg(id)}/files/${seg(fileId)}/link`,\n );\n return new URL(`${baseUrl}${link.path}`, window.location.href).href;\n },\n call: (agentId) => json(\"POST\", `/agents/${seg(agentId)}/calls`),\n },\n liveUrl: async (conversationId, { fresh = false } = {}) => {\n const url = new URL(\n `${baseUrl}/conversations/${seg(conversationId)}/live`,\n window.location.href,\n );\n url.protocol = url.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n for (const [name, value] of Object.entries(await signer.query(fresh))) {\n url.searchParams.set(name, value);\n }\n return url.href;\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["import type { AboutOut, MemoryFact } from \"./about\";\nimport type { Automation, AutomationView, RunRecord, Task } from \"./automation/types\";\nimport { signerOf, type Credentials, type Speaker } from \"./credentials\";\nimport type {\n AgentFace,\n WidgetConfig,\n CallToken,\n Channel,\n Conversation,\n ConversationDetail,\n ConversationFile,\n FileAt,\n FileLink,\n FilePreview,\n StoredFile,\n ViewRendered,\n} from \"./types\";\n\n/**\n * What the API answered when it was not a success: its status, its sentence (`detail`, written\n * to be shown) and its `code`, for a host to word in its own language.\n */\nexport class ApiError extends Error {\n readonly status: number;\n readonly detail: string;\n /** What went wrong, by name (`not_found`, `no_calls`); null when the answer named nothing. */\n readonly code: string | null;\n\n constructor(status: number, detail: string, code: string | null = null) {\n super(detail);\n this.name = \"ApiError\";\n this.status = status;\n this.detail = detail;\n this.code = code;\n }\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null;\n\nconst isIssue = (value: unknown): value is { msg: string } =>\n isRecord(value) && typeof value.msg === \"string\";\n\n/**\n * Reads an error body: `{code, detail: \"message\"}`, or FastAPI's 422 `{detail: [{msg, …}, …]}`\n * (the first issue speaks for all). A body with neither is named by its status alone.\n */\nexport function parseError(status: number, body: unknown): ApiError {\n const detail = isRecord(body) ? body.detail : undefined;\n const code = isRecord(body) && typeof body.code === \"string\" ? body.code : null;\n if (typeof detail === \"string\" && detail !== \"\") return new ApiError(status, detail, code);\n if (Array.isArray(detail)) {\n const issue = detail.find(isIssue);\n if (issue) return new ApiError(status, issue.msg);\n }\n return new ApiError(status, `HTTP ${status}`);\n}\n\n/** A body read as JSON, or null when it is not. */\nfunction readJson(text: string): unknown {\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return null;\n }\n}\n\n/**\n * Where the API is, and who calls it (`Credentials`: the cookie, a person's token, a token asked\n * of the host's backend, or a publishable key and its visitor).\n */\nexport type ClientOptions = Credentials & {\n /**\n * The API's origin and prefix: `/api` in our own app (the cookie's, on the page's origin), the\n * public `https://…/v1` from a host's page.\n */\n baseUrl: string;\n /** A host's own `fetch`, when the global one is not the right one. */\n fetch?: typeof fetch | undefined;\n};\n\n/** The files under one owner: the organization's, or an agent's own. */\nexport interface Files {\n /** Every file, by path. */\n list: () => Promise<StoredFile[]>;\n /**\n * Stores the bytes under the path (replacing a file of the same name), with the blob's type\n * as the file's; `searchable` makes it knowledge `search_files` reads (text, PDF).\n */\n put: (path: string, blob: Blob, searchable: boolean) => Promise<StoredFile>;\n delete: (path: string) => Promise<void>;\n}\n\n/** How an upload goes: how much of it has gone, and how to let it go. */\nexport interface UploadOptions {\n /** Hears the share sent so far, from 0 to 1, as it goes. */\n onProgress?: ((share: number) => void) | undefined;\n /** Aborting it stops the upload, and the promise rejects with an `AbortError`. */\n signal?: AbortSignal | undefined;\n}\n\n/** The API, as far as this package reaches it. */\nexport interface Client {\n /**\n * The widget's agent and look, by the publishable key it speaks with (a web channel's key).\n */\n widget: () => Promise<WidgetConfig>;\n /**\n * Who the page speaks for, by its credentials: the business (our own app's cookie) or a person\n * it serves (a token, a publishable key's visitor). What waits for the business's yes is\n * answered by the business alone.\n */\n who: Speaker;\n agents: {\n /** What a person may know of the agent: its name, role, mark, and whether it takes calls. */\n face: (id: string) => Promise<AgentFace>;\n /** What the agent says about itself (null until it has written it) and the facts it rests on. */\n about: (id: string) => Promise<AboutOut>;\n /** Has the agent write about itself anew (a model call, a few seconds); answers like `about`. */\n describe: (id: string) => Promise<AboutOut>;\n /** The agent's own files, by path. */\n files: (id: string) => Promise<StoredFile[]>;\n /** Stores a file of the agent's own; see `Files.put`. */\n putFile: (id: string, path: string, blob: Blob, searchable: boolean) => Promise<StoredFile>;\n deleteFile: (id: string, path: string) => Promise<void>;\n /** What the agent remembered, newest first. */\n memories: (id: string) => Promise<MemoryFact[]>;\n /** Takes one memory from it. */\n forget: (id: string, memoryId: string) => Promise<void>;\n /**\n * The owner's call with the agent, from our own app (v3/VOICE.md): the call and its\n * conversation (a test) made now, and where to join it (`VoiceCall`'s `connect`). App only:\n * it is `/api`'s, signed in by the owner's cookie, and 404s on `/v1`; a host's page calls with\n * `conversations.call(agentId)`.\n */\n call: (id: string) => Promise<CallToken>;\n };\n /**\n * The organization's files, shared by every agent; and the preview of any file the caller may\n * read, the organization's, an agent's own or a conversation's (`preview`).\n */\n files: Files & {\n /** What a viewer draws of the file, by its `kind` (v3/FILES.md, Preview). */\n preview: (at: FileAt) => Promise<FilePreview>;\n };\n /** What an agent does on its own (v3/AUTOMATIONS.md). */\n automations: {\n /** The agent's automations, each as it stands now with its newest run. */\n list: (agentId: string) => Promise<Automation[]>;\n /** One of them by its name, with its newest runs. */\n get: (agentId: string, name: string) => Promise<AutomationView>;\n };\n runs: {\n /** One run of an automation with its record: every call it made, step by step. */\n get: (id: string) => Promise<RunRecord>;\n };\n /** What an agent keeps on show for each person it serves (`compose_view`, `/agents/{id}/views`). */\n views: {\n /**\n * A view rendered from the rows now: for a person, their own; for the business, for the\n * contact it names (`contact`, their id).\n */\n render: (name: string, contact?: string) => Promise<ViewRendered>;\n };\n /**\n * The tasks an agent runs for contacts, each with its newest runs: a person's own only (what\n * they asked the agent for in their conversations); the business's, every one.\n */\n tasks: {\n list: (agentId: string) => Promise<Task[]>;\n /** Pauses it, or turns it on again; answers it as it stands. */\n switch: (agentId: string, taskId: string, on: boolean) => Promise<Task>;\n remove: (agentId: string, taskId: string) => Promise<void>;\n };\n /** Where people write to the organization's agents (v3/CHANNELS.md). */\n channels: {\n /** The organization's channels, or one agent's: the disconnected ones too. */\n list: (agentId?: string) => Promise<Channel[]>;\n };\n conversations: {\n /** One conversation with its messages. */\n get: (id: string) => Promise<ConversationDetail>;\n /** The agent's conversations, newest first: a person's own only, each with what is unread. */\n list: (agentId: string) => Promise<Conversation[]>;\n /** A new conversation with an agent, titled when given. */\n start: (agentId: string, options?: { title?: string | undefined }) => Promise<Conversation>;\n /** How far the person has read it: all of it, now. */\n read: (id: string) => Promise<Conversation>;\n /** Its title, as the person gives it. */\n rename: (id: string, title: string) => Promise<Conversation>;\n /** The files that arrived in a conversation: attached, sent through a form. */\n files: (id: string) => Promise<ConversationFile[]>;\n /**\n * Stores a file in the conversation as the person picks it, to say it or send it through a\n * form by its id: its bytes as they are, under its type and name.\n */\n upload: (id: string, file: File, options?: UploadOptions) => Promise<ConversationFile>;\n /**\n * Where a picture, a video or a sound of the conversation plays from in the page's own\n * player: an address that needs no credential, for an hour, streamed as the player seeks.\n */\n play: (id: string, fileId: string) => Promise<string>;\n /**\n * A call with the agent from the browser, for whoever may talk to it (a person, as theirs):\n * the call and its own conversation made now, where what is said lands, and where to join it\n * (`VoiceCall`'s `connect`).\n */\n call: (agentId: string) => Promise<CallToken>;\n };\n /**\n * The conversation's socket address, signed in: `ws:` beside `http:`, `wss:` beside `https:`,\n * with the token (and the visitor) in its query, since a browser's socket takes no header.\n * `fresh` asks the host's backend for a new token first (`getToken`), for a socket that could\n * not open with the last one.\n */\n liveUrl: (conversationId: string, options?: { fresh?: boolean | undefined }) => Promise<string>;\n}\n\nconst seg = encodeURIComponent;\n\n/** A request's JSON body. */\nconst body = (value: unknown): Blob =>\n new Blob([JSON.stringify(value)], { type: \"application/json\" });\n\n/** A file's path in a URL: each name of it encoded, the slashes between them kept. */\nconst filePath = (path: string): string => path.split(\"/\").map(seg).join(\"/\");\n\n/** Where a file's preview is read: under its conversation, its agent or the organization. */\nconst previewPath = (at: FileAt): string => {\n if (\"conversationId\" in at) {\n return `/conversations/${seg(at.conversationId)}/files/${seg(at.fileId)}/preview`;\n }\n const owner = \"agentId\" in at ? `/agents/${seg(at.agentId)}` : \"\";\n return `${owner}/files/${filePath(at.path)}/preview`;\n};\n\ntype Method = \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n\n/**\n * The API under `baseUrl`: JSON in and out, an `ApiError` for anything but a success, signed in\n * the one way `options` gives. A token asked of the host's backend (`getToken`) that the API\n * refuses (401: it expired) is asked for once more, and the request sent again with it.\n */\nexport function createClient(options: ClientOptions): Client {\n const { baseUrl, fetch: call = fetch } = options;\n const signer = signerOf(options);\n\n /** One request with the cookie or the credential's headers; a body goes as it is, typed. */\n async function attempt(method: Method, path: string, body: Blob | undefined) {\n const signed = await signer.sign();\n const response = await call(`${baseUrl}${path}`, {\n method,\n credentials: signer.credentials,\n headers:\n body === undefined ? signed.headers : { ...signed.headers, \"Content-Type\": body.type },\n ...(body === undefined ? {} : { body }),\n });\n return { response, signed };\n }\n\n async function send(method: Method, path: string, body?: Blob): Promise<Response> {\n let { response, signed } = await attempt(method, path, body);\n if (response.status === 401 && signed.renew !== null) {\n signed.renew();\n ({ response } = await attempt(method, path, body));\n }\n if (!response.ok) {\n const answer: unknown = await response.json().catch(() => null);\n throw parseError(response.status, answer);\n }\n return response;\n }\n\n async function json<T>(method: Method, path: string, body?: Blob): Promise<T> {\n return (await (await send(method, path, body)).json()) as T;\n }\n\n /** A request whose answer has no body (204). */\n async function run(method: Method, path: string): Promise<void> {\n await send(method, path);\n }\n\n /** The four of `Files` under a prefix: `/files`, or `/agents/{id}/files`. */\n const filesUnder = (prefix: string): Files => ({\n list: () => json(\"GET\", prefix),\n put: (path, blob, searchable) =>\n json(\"PUT\", `${prefix}/${filePath(path)}?searchable=${searchable}`, blob),\n delete: (path) => run(\"DELETE\", `${prefix}/${filePath(path)}`),\n });\n\n const own = (id: string) => filesUnder(`/agents/${seg(id)}/files`);\n\n /**\n * A file sent by XMLHttpRequest, the one way a browser tells how much of a body has gone; the\n * cookie goes as with `fetch` on the same origin, any other credential as its headers. Refused\n * with a token asked of the host's backend, it is sent once more with a new one.\n */\n async function sendFile(\n path: string,\n file: File,\n options: UploadOptions = {},\n ): Promise<ConversationFile> {\n const signed = await signer.sign();\n try {\n return await post(path, file, signed.headers, options);\n } catch (error) {\n const expired = error instanceof ApiError && error.status === 401;\n if (!expired || signed.renew === null) throw error;\n signed.renew();\n return post(path, file, (await signer.sign()).headers, options);\n }\n }\n\n function post(\n path: string,\n file: File,\n headers: Readonly<Record<string, string>>,\n { onProgress, signal }: UploadOptions,\n ): Promise<ConversationFile> {\n return new Promise((resolve, reject) => {\n const aborted = () => new DOMException(\"The upload was aborted\", \"AbortError\");\n if (signal?.aborted) {\n reject(aborted());\n return;\n }\n const request = new XMLHttpRequest();\n const abort = () => {\n request.abort();\n };\n request.open(\"POST\", `${baseUrl}${path}`);\n for (const [name, value] of Object.entries(headers)) request.setRequestHeader(name, value);\n request.setRequestHeader(\"Content-Type\", file.type || \"application/octet-stream\");\n request.setRequestHeader(\"X-File-Name\", encodeURIComponent(file.name));\n request.upload.addEventListener(\"progress\", (event) => {\n if (event.lengthComputable && event.total > 0) onProgress?.(event.loaded / event.total);\n });\n request.addEventListener(\"loadend\", () => {\n signal?.removeEventListener(\"abort\", abort);\n });\n request.addEventListener(\"load\", () => {\n const body = readJson(request.responseText);\n if (request.status >= 200 && request.status < 300) resolve(body as ConversationFile);\n else reject(parseError(request.status, body));\n });\n request.addEventListener(\"error\", () => {\n reject(new ApiError(0, \"The upload could not reach the server\"));\n });\n request.addEventListener(\"abort\", () => {\n reject(aborted());\n });\n signal?.addEventListener(\"abort\", abort, { once: true });\n request.send(file);\n });\n }\n\n return {\n widget: () => json(\"GET\", \"/channels/web/widget\"),\n who: signer.who,\n agents: {\n face: (id) => json(\"GET\", `/agents/${seg(id)}/face`),\n about: (id) => json(\"GET\", `/agents/${seg(id)}/about`),\n describe: (id) => json(\"POST\", `/agents/${seg(id)}/describe`),\n files: (id) => own(id).list(),\n putFile: (id, path, blob, searchable) => own(id).put(path, blob, searchable),\n deleteFile: (id, path) => own(id).delete(path),\n memories: (id) => json(\"GET\", `/agents/${seg(id)}/memories`),\n forget: (id, memoryId) => run(\"DELETE\", `/agents/${seg(id)}/memories/${seg(memoryId)}`),\n call: (id) => json(\"POST\", `/agents/${seg(id)}/voice/token`),\n },\n files: { ...filesUnder(\"/files\"), preview: (at) => json(\"GET\", previewPath(at)) },\n automations: {\n list: (agentId) => json(\"GET\", `/agents/${seg(agentId)}/automations`),\n get: (agentId, name) => json(\"GET\", `/agents/${seg(agentId)}/automations/${seg(name)}`),\n },\n runs: {\n get: (id) => json(\"GET\", `/runs/${seg(id)}`),\n },\n views: {\n render: (name, contact) =>\n json(\n \"GET\",\n `/views/${seg(name)}/render${contact === undefined ? \"\" : `?contact=${seg(contact)}`}`,\n ),\n },\n tasks: {\n list: (agentId) => json(\"GET\", `/agents/${seg(agentId)}/tasks`),\n switch: (agentId, taskId, on) =>\n json(\"PATCH\", `/agents/${seg(agentId)}/tasks/${seg(taskId)}`, body({ on })),\n remove: (agentId, taskId) => run(\"DELETE\", `/agents/${seg(agentId)}/tasks/${seg(taskId)}`),\n },\n channels: {\n list: (agentId) =>\n json(\"GET\", agentId === undefined ? \"/channels\" : `/channels?agent=${seg(agentId)}`),\n },\n conversations: {\n get: (id) => json(\"GET\", `/conversations/${seg(id)}`),\n list: (agentId) => json(\"GET\", `/agents/${seg(agentId)}/conversations`),\n start: (agentId, { title } = {}) =>\n json(\n \"POST\",\n `/agents/${seg(agentId)}/conversations`,\n title === undefined ? undefined : body({ title }),\n ),\n read: (id) => json(\"POST\", `/conversations/${seg(id)}/read`, body({})),\n rename: (id, title) => json(\"PATCH\", `/conversations/${seg(id)}`, body({ title })),\n files: (id) => json(\"GET\", `/conversations/${seg(id)}/files`),\n upload: (id, file, options) => sendFile(`/conversations/${seg(id)}/files`, file, options),\n play: async (id, fileId) => {\n const link = await json<FileLink>(\n \"GET\",\n `/conversations/${seg(id)}/files/${seg(fileId)}/link`,\n );\n return new URL(`${baseUrl}${link.path}`, window.location.href).href;\n },\n call: (agentId) => json(\"POST\", `/agents/${seg(agentId)}/calls`),\n },\n liveUrl: async (conversationId, { fresh = false } = {}) => {\n const url = new URL(\n `${baseUrl}/conversations/${seg(conversationId)}/live`,\n window.location.href,\n );\n url.protocol = url.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n for (const [name, value] of Object.entries(await signer.query(fresh))) {\n url.searchParams.set(name, value);\n }\n return url.href;\n },\n };\n}\n"],"mappings":"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"}