@graph8/sdk 0.373.0 → 0.374.0

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 (279) hide show
  1. package/README.md +3 -3
  2. package/blocks/_foundation/README.md +47 -0
  3. package/blocks/_foundation/block.json +13 -0
  4. package/blocks/_foundation/components/auth-gate.tsx +363 -0
  5. package/blocks/_foundation/server/auth.ts +815 -0
  6. package/blocks/campaigns/README.md +1 -1
  7. package/blocks/campaigns/block.json +34 -1
  8. package/blocks/campaigns/components/campaign-links.tsx +180 -0
  9. package/blocks/company-intelligence/components/research-run.tsx +6 -2
  10. package/blocks/crm/README.md +19 -0
  11. package/blocks/crm/block.json +205 -6
  12. package/blocks/crm/components/bulk-update.tsx +119 -0
  13. package/blocks/crm/components/contact-documents-tab.tsx +144 -0
  14. package/blocks/crm/components/contact-insights-tab.tsx +118 -0
  15. package/blocks/crm/components/contact-lookup-link.tsx +47 -6
  16. package/blocks/crm/components/contact-row-actions.tsx +43 -0
  17. package/blocks/crm/components/contact-tag-chips.tsx +113 -0
  18. package/blocks/crm/components/contacts-screen.tsx +46 -5
  19. package/blocks/crm/components/owner-assign.tsx +132 -0
  20. package/blocks/crm/components/record-open-links.tsx +79 -0
  21. package/blocks/crm/components/tag-assign.tsx +112 -0
  22. package/blocks/crm/components/tag-manager.tsx +178 -0
  23. package/blocks/crm/components/unlock-info-button.tsx +82 -0
  24. package/blocks/crm/components/verify-email-button.tsx +93 -0
  25. package/blocks/crm/hooks/use-contact-tags.ts +29 -0
  26. package/blocks/crm/lib/record-actions.ts +102 -0
  27. package/blocks/deals/README.md +1 -0
  28. package/blocks/deals/block.json +17 -0
  29. package/blocks/deals/components/create-deal-button.tsx +109 -0
  30. package/blocks/deals/components/deal-detail.tsx +5 -3
  31. package/blocks/dialer/components/call-log-screen.tsx +6 -1
  32. package/blocks/documents/README.md +86 -0
  33. package/blocks/documents/block.json +378 -0
  34. package/blocks/documents/components/account-bar.tsx +48 -0
  35. package/blocks/documents/components/correspondence-form.tsx +95 -0
  36. package/blocks/documents/components/correspondence-import.tsx +106 -0
  37. package/blocks/documents/components/correspondence-panel.tsx +191 -0
  38. package/blocks/documents/components/documents-screen.tsx +76 -0
  39. package/blocks/documents/components/library-document-detail.tsx +175 -0
  40. package/blocks/documents/components/library-document-form.tsx +105 -0
  41. package/blocks/documents/components/library-folder-form.tsx +74 -0
  42. package/blocks/documents/components/library-panel.tsx +235 -0
  43. package/blocks/documents/components/library-upload.tsx +160 -0
  44. package/blocks/documents/components/question-row.tsx +130 -0
  45. package/blocks/documents/components/questionnaire-detail.tsx +197 -0
  46. package/blocks/documents/components/questionnaires-panel.tsx +173 -0
  47. package/blocks/documents/components/record-documents-tab.tsx +109 -0
  48. package/blocks/documents/lib/documents-types.ts +252 -0
  49. package/blocks/documents/lib/download.ts +64 -0
  50. package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
  51. package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
  52. package/blocks/documents/recipes/organize-the-library.ts +25 -0
  53. package/blocks/email-signatures/README.md +123 -0
  54. package/blocks/email-signatures/block.json +341 -0
  55. package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
  56. package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
  57. package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
  58. package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
  59. package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
  60. package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
  61. package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
  62. package/blocks/email-signatures/components/signature-policy.tsx +130 -0
  63. package/blocks/email-signatures/components/signature-preview.tsx +119 -0
  64. package/blocks/email-signatures/components/signature-report.tsx +77 -0
  65. package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
  66. package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
  67. package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
  68. package/blocks/email-signatures/lib/signatures-types.ts +392 -0
  69. package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
  70. package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
  71. package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
  72. package/blocks/enrichment/README.md +22 -4
  73. package/blocks/enrichment/block.json +145 -5
  74. package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
  75. package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
  76. package/blocks/enrichment/components/bulk-enrich-panel.tsx +7 -7
  77. package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
  78. package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
  79. package/blocks/enrichment/components/enrichment-screen.tsx +34 -3
  80. package/blocks/enrichment/components/list-pipelines.tsx +97 -0
  81. package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
  82. package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
  83. package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
  84. package/blocks/enrichment/lib/enrichment-types.ts +12 -0
  85. package/blocks/enrichment/lib/pipeline-types.ts +234 -0
  86. package/blocks/forms/README.md +2 -0
  87. package/blocks/forms/components/forms-screen.tsx +6 -3
  88. package/blocks/inbox/README.md +40 -4
  89. package/blocks/inbox/block.json +130 -8
  90. package/blocks/inbox/components/blocked-senders.tsx +103 -0
  91. package/blocks/inbox/components/compose-message.tsx +193 -0
  92. package/blocks/inbox/components/inbox-screen.tsx +54 -6
  93. package/blocks/inbox/components/inbox-search.tsx +106 -0
  94. package/blocks/inbox/components/tag-manager.tsx +136 -0
  95. package/blocks/inbox/components/thread-actions.tsx +133 -0
  96. package/blocks/inbox/components/thread-view.tsx +2 -0
  97. package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
  98. package/blocks/inbox/lib/inbox-manage.ts +227 -0
  99. package/blocks/newsletter/components/newsletter-screen.tsx +3 -1
  100. package/blocks/pages/block.json +10 -1
  101. package/blocks/pages/hooks/use-landing-page-paths.ts +60 -0
  102. package/blocks/programmatic-pages/README.md +82 -0
  103. package/blocks/programmatic-pages/block.json +292 -0
  104. package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
  105. package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
  106. package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
  107. package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
  108. package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
  109. package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
  110. package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
  111. package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
  112. package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
  113. package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
  114. package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
  115. package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
  116. package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
  117. package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
  118. package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
  119. package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
  120. package/blocks/quotes/README.md +25 -1
  121. package/blocks/quotes/block.json +123 -6
  122. package/blocks/quotes/components/manual-products.tsx +144 -0
  123. package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
  124. package/blocks/quotes/components/quote-documents.tsx +94 -0
  125. package/blocks/quotes/components/quote-evidence.tsx +95 -0
  126. package/blocks/quotes/components/quote-list.tsx +23 -5
  127. package/blocks/quotes/components/quote-settings.tsx +184 -0
  128. package/blocks/quotes/components/quote-templates.tsx +189 -0
  129. package/blocks/quotes/components/quote-viewer.tsx +16 -3
  130. package/blocks/quotes/components/quotes-screen.tsx +15 -5
  131. package/blocks/quotes/lib/quote-admin.ts +264 -0
  132. package/blocks/sequencer/README.md +55 -0
  133. package/blocks/sequencer/block.json +172 -5
  134. package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
  135. package/blocks/sequencer/components/sequence-builder.tsx +13 -3
  136. package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
  137. package/blocks/sequencer/components/sequence-detail.tsx +6 -0
  138. package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
  139. package/blocks/sequencer/components/sequence-enrollments.tsx +2 -1
  140. package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
  141. package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
  142. package/blocks/sequencer/components/sequence-templates.tsx +160 -0
  143. package/blocks/sequencer/components/sequences-screen.tsx +36 -6
  144. package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
  145. package/blocks/sequencer/components/step-editor.tsx +10 -1
  146. package/blocks/sequencer/components/step-templates.tsx +224 -0
  147. package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
  148. package/blocks/sequencer/lib/sequencer-utils.ts +4 -0
  149. package/blocks/signals/README.md +39 -0
  150. package/blocks/signals/block.json +287 -0
  151. package/blocks/signals/components/auto-seed-panel.tsx +111 -0
  152. package/blocks/signals/components/listener-form.tsx +184 -0
  153. package/blocks/signals/components/matched-posts.tsx +195 -0
  154. package/blocks/signals/components/post-engagements.tsx +114 -0
  155. package/blocks/signals/components/record-social-tab.tsx +49 -0
  156. package/blocks/signals/components/social-listeners-screen.tsx +250 -0
  157. package/blocks/signals/components/social-settings.tsx +128 -0
  158. package/blocks/signals/components/source-lists.tsx +83 -0
  159. package/blocks/signals/components/top-engagers.tsx +68 -0
  160. package/blocks/signals/components/x-integration.tsx +129 -0
  161. package/blocks/signals/lib/signals-types.ts +228 -0
  162. package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
  163. package/blocks/signals/recipes/spend-against-caps.ts +14 -0
  164. package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
  165. package/blocks/visitors/components/traffic-chart.tsx +3 -2
  166. package/blocks/web-analytics/README.md +102 -0
  167. package/blocks/web-analytics/block.json +292 -0
  168. package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
  169. package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
  170. package/blocks/web-analytics/components/audience-tab.tsx +135 -0
  171. package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
  172. package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
  173. package/blocks/web-analytics/components/overview-tab.tsx +102 -0
  174. package/blocks/web-analytics/components/pages-tab.tsx +148 -0
  175. package/blocks/web-analytics/components/ranked-table.tsx +103 -0
  176. package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
  177. package/blocks/web-analytics/components/report-card.tsx +71 -0
  178. package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
  179. package/blocks/web-analytics/hooks/use-report.ts +48 -0
  180. package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
  181. package/blocks/web-analytics/lib/page-path.ts +14 -0
  182. package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
  183. package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
  184. package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
  185. package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
  186. package/blocks/workbench/README.md +54 -2
  187. package/blocks/workbench/block.json +234 -5
  188. package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
  189. package/blocks/workbench/components/workbench-automation.tsx +36 -0
  190. package/blocks/workbench/components/workbench-connections.tsx +136 -0
  191. package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
  192. package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
  193. package/blocks/workbench/components/workbench-grid.tsx +70 -7
  194. package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
  195. package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
  196. package/blocks/workbench/components/workbench-schedule.tsx +191 -0
  197. package/blocks/workbench/components/workbench-screen.tsx +50 -17
  198. package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
  199. package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
  200. package/blocks/workbench/components/workbench-triggers.tsx +178 -0
  201. package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
  202. package/blocks/workbench/lib/api-status.ts +8 -0
  203. package/blocks/workbench/lib/automation-types.ts +257 -0
  204. package/dist/cli.js +257 -51
  205. package/dist/index.browser.js +1 -1
  206. package/dist/index.browser.js.map +1 -1
  207. package/dist/index.browser.mjs +1 -1
  208. package/dist/index.browser.mjs.map +1 -1
  209. package/dist/index.js +1 -1
  210. package/dist/index.js.map +1 -1
  211. package/dist/index.mjs +1 -1
  212. package/dist/index.mjs.map +1 -1
  213. package/dist/react.browser.js +1 -1
  214. package/dist/react.browser.js.map +1 -1
  215. package/dist/react.browser.mjs +1 -1
  216. package/dist/react.browser.mjs.map +1 -1
  217. package/dist/react.js +1 -1
  218. package/dist/react.js.map +1 -1
  219. package/dist/react.mjs +1 -1
  220. package/dist/react.mjs.map +1 -1
  221. package/dist/types/blocks/auth.d.ts +45 -0
  222. package/dist/types/blocks/create.d.ts +5 -2
  223. package/dist/types/version.d.ts +2 -2
  224. package/docs/api/forms.md +1 -1
  225. package/package.json +1 -1
  226. package/stacks/6sense/pages/contacts.tsx.tmpl +2 -0
  227. package/stacks/6sense.json +20 -0
  228. package/stacks/apollo/pages/calls.tsx.tmpl +9 -9
  229. package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
  230. package/stacks/apollo/pages/contacts.tsx.tmpl +6 -0
  231. package/stacks/apollo/pages/inbox-contact.tsx.tmpl +3 -3
  232. package/stacks/apollo/pages/lists.tsx.tmpl +14 -2
  233. package/stacks/apollo/pages/sequences.tsx.tmpl +8 -2
  234. package/stacks/apollo.json +33 -2
  235. package/stacks/clay/pages/contacts.tsx.tmpl +2 -0
  236. package/stacks/clay/pages/lists.tsx.tmpl +14 -2
  237. package/stacks/gong/pages/calls.tsx.tmpl +9 -9
  238. package/stacks/gong/pages/contacts.tsx.tmpl +2 -0
  239. package/stacks/hootsuite/pages/contacts.tsx.tmpl +2 -0
  240. package/stacks/hootsuite.json +10 -0
  241. package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
  242. package/stacks/hubspot/pages/companies.tsx.tmpl +2 -0
  243. package/stacks/hubspot/pages/contacts.tsx.tmpl +6 -0
  244. package/stacks/hubspot/pages/deals.tsx.tmpl +4 -0
  245. package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
  246. package/stacks/hubspot/pages/inbox.tsx.tmpl +2 -2
  247. package/stacks/hubspot/pages/sequences.tsx.tmpl +8 -2
  248. package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
  249. package/stacks/hubspot.json +45 -2
  250. package/stacks/intercom/pages/inbox-contact.tsx.tmpl +3 -3
  251. package/stacks/intercom.json +10 -0
  252. package/stacks/mailchimp/pages/contacts.tsx.tmpl +2 -0
  253. package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
  254. package/stacks/mailchimp.json +2 -1
  255. package/stacks/outreach/pages/calls.tsx.tmpl +9 -9
  256. package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
  257. package/stacks/outreach/pages/contacts.tsx.tmpl +2 -0
  258. package/stacks/outreach/pages/inbox-contact.tsx.tmpl +3 -3
  259. package/stacks/outreach/pages/sequences.tsx.tmpl +8 -2
  260. package/stacks/outreach.json +13 -2
  261. package/stacks/pipedrive/pages/contacts.tsx.tmpl +2 -0
  262. package/stacks/pipedrive/pages/inbox.tsx.tmpl +2 -2
  263. package/stacks/pipedrive.json +10 -0
  264. package/stacks/salesforce/pages/companies.tsx.tmpl +2 -0
  265. package/stacks/salesforce/pages/contacts.tsx.tmpl +6 -0
  266. package/stacks/salesforce/pages/deals.tsx.tmpl +4 -0
  267. package/stacks/salesforce.json +10 -0
  268. package/stacks/zoominfo/pages/contacts.tsx.tmpl +4 -0
  269. package/stacks/zoominfo/pages/lists.tsx.tmpl +14 -2
  270. package/stacks/zoominfo.json +20 -0
  271. package/styles/styles.css +1 -1
  272. package/templates/auth/auth-route.ts.tmpl +16 -0
  273. package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
  274. package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
  275. package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
  276. package/templates/auth/g8.auth.password.ts.tmpl +18 -0
  277. package/templates/auth/g8.auth.session.ts.tmpl +23 -0
  278. package/templates/auth/middleware.ts.tmpl +14 -0
  279. package/templates/next-app/README.md.tmpl +5 -0
@@ -0,0 +1,224 @@
1
+ "use client";
2
+
3
+ /**
4
+ * StepTemplates: reusable single steps (an email, a LinkedIn touch, a call). List, create, edit, archive and restore
5
+ * them, and have an agent draft a batch for you. StepTemplatePicker (exported here) inserts one into a sequence being
6
+ * built.
7
+ *
8
+ * SDK operations, in order:
9
+ * 1. GET /sequencer/templates/steps (sequences.listStepTemplates) read (archived ones included; filtered here)
10
+ * 2. POST /sequencer/templates/steps (sequences.createStepTemplate) write
11
+ * 3. PATCH /sequencer/templates/steps/{id} (sequences.updateStepTemplate) write - ONLY the changed fields
12
+ * 4. PATCH /sequencer/templates/steps/{id}/archive (sequences.archiveStepTemplate) DESTRUCTIVE to archive -> ConfirmSpendDialog; restoring is a plain write
13
+ * 5. GET /sequencer/agents/textual (sequences.listSequencerTextualAgents) read - the agents that can draft
14
+ * POST /sequencer/templates/steps/generate (sequences.generateStepTemplates) BILLABLE -> ConfirmSpendDialog first.
15
+ * Nothing is saved by generating: the drafts come back and each one is kept with "Save as template" or ignored.
16
+ * The edit form is seeded from a LOADED template (Save stays disabled until something changed) and never writes one that
17
+ * was not loaded. The step form is StepEditor, so the template obeys the same send-time rules as a step in a sequence.
18
+ */
19
+ import { useState } from "react";
20
+ import { Archive, ArchiveRestore, Pencil, Plus, Sparkles } from "lucide-react";
21
+ import { Button, Input, Skeleton } from "@graph8/sdk/ui";
22
+ import { ApiError } from "@/g8/components/api-error";
23
+ import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
24
+ import { EmptyState } from "@/g8/components/empty-state";
25
+ import { StatusPill } from "@/g8/components/screen/status-pill";
26
+ import { g8Get, g8Patch, g8Post } from "@/g8/lib/g8-client";
27
+ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
28
+ import { useG8Query } from "@/g8/hooks/use-g8-query";
29
+ import { agentOptions, stepTemplateBody, stepTemplateChanges, stepTemplates } from "../lib/sequencer-manage";
30
+ import type { StepTemplate } from "../lib/sequencer-manage";
31
+ import { KIND_LABEL, KINDS, newStep, stepIssues, stepSummary } from "../lib/sequencer-utils";
32
+ import type { DraftStep, StepKind } from "../lib/sequencer-utils";
33
+ import { StepEditor } from "./step-editor";
34
+
35
+ export interface StepTemplatesProps {
36
+ className?: string;
37
+ }
38
+
39
+ function TemplateForm({ loaded, onDone }: { loaded: StepTemplate | null; onDone: (changed: boolean) => void }) {
40
+ const [name, setName] = useState(loaded?.name ?? "");
41
+ const [step, setStep] = useState<DraftStep>(loaded?.step ?? newStep("email"));
42
+ const changes = loaded ? stepTemplateChanges(loaded, name, step) : {};
43
+ const save = useG8Mutation(async () => {
44
+ if (loaded) await g8Patch(`/sequencer/templates/steps/${encodeURIComponent(loaded.id)}`, changes);
45
+ else await g8Post("/sequencer/templates/steps", stepTemplateBody(name, step));
46
+ }, { invalidates: ["sequences"] });
47
+ const valid = name.trim().length > 0 && stepIssues(step).length === 0 && (loaded ? Object.keys(changes).length > 0 : true);
48
+ const submit = async () => {
49
+ try {
50
+ await save.mutateAsync(undefined);
51
+ onDone(true);
52
+ } catch {
53
+ /* shown below */
54
+ }
55
+ };
56
+ return (
57
+ <form className="space-y-3 rounded-md border border-border p-3" aria-label={loaded ? `Edit ${loaded.name}` : "New step template"} onSubmit={(e) => { e.preventDefault(); if (valid) void submit(); }}>
58
+ <Input aria-label="Template name" placeholder="Template name" value={name} onChange={(e) => setName(e.target.value)} />
59
+ {!loaded ? (
60
+ <label className="flex items-center gap-2 text-sm">
61
+ Kind
62
+ <select aria-label="Step kind" className="h-8 rounded border border-input px-1" value={step.kind} onChange={(e) => setStep(newStep(e.target.value as StepKind))}>
63
+ {KINDS.map((k) => <option key={k} value={k}>{KIND_LABEL[k]}</option>)}
64
+ </select>
65
+ </label>
66
+ ) : null}
67
+ <StepEditor step={step} index={1} onChange={setStep} lockInputType={loaded != null} />
68
+ {save.error ? <ApiError error={save.error} title="Could not save the template" /> : null}
69
+ <div className="flex justify-end gap-2">
70
+ <Button type="button" variant="outline" size="sm" onClick={() => onDone(false)}>Cancel</Button>
71
+ <Button type="submit" size="sm" disabled={!valid || save.loading}>{loaded ? "Save changes" : "Create template"}</Button>
72
+ </div>
73
+ </form>
74
+ );
75
+ }
76
+
77
+ function GeneratePanel({ onSaved }: { onSaved: () => void }) {
78
+ const [agent, setAgent] = useState("");
79
+ const [confirming, setConfirming] = useState(false);
80
+ const [drafts, setDrafts] = useState<StepTemplate[]>([]);
81
+ const agents = useG8Query(async (signal) => agentOptions(await g8Get("/sequencer/agents/textual", undefined, { signal })), []);
82
+ const generate = useG8Mutation(async () => {
83
+ const env = await g8Post("/sequencer/templates/steps/generate", { agent_name: agent });
84
+ setDrafts(stepTemplates(env, true));
85
+ setConfirming(false);
86
+ }, { onError: () => setConfirming(false) });
87
+ const keep = useG8Mutation(async (t: StepTemplate) => {
88
+ await g8Post("/sequencer/templates/steps", stepTemplateBody(t.name, t.step));
89
+ setDrafts((all) => all.filter((x) => x.id !== t.id));
90
+ onSaved();
91
+ }, { invalidates: ["sequences"] });
92
+ return (
93
+ <div className="space-y-3 rounded-md border border-border p-3" aria-label="Draft templates with AI">
94
+ <p className="text-sm text-muted-foreground">An agent drafts a set of step templates. You keep the ones you like; nothing is saved until you do.</p>
95
+ <div className="flex flex-wrap items-center gap-2">
96
+ <select aria-label="Agent" className="h-9 min-w-48 rounded-md border border-border bg-background px-2 text-sm" value={agent} disabled={agents.loading} onChange={(e) => setAgent(e.target.value)}>
97
+ <option value="">{agents.loading ? "Loading agents..." : "Choose the agent to draft"}</option>
98
+ {(agents.data ?? []).map((a) => <option key={a.value} value={a.value}>{a.label}</option>)}
99
+ </select>
100
+ <Button size="sm" disabled={!agent || generate.loading} onClick={() => setConfirming(true)}><Sparkles className="mr-1 size-4" aria-hidden />Draft with AI</Button>
101
+ </div>
102
+ {agents.error ? <ApiError error={agents.error} title="Could not load the agents" onRetry={() => void agents.refetch()} /> : null}
103
+ {agents.data && agents.data.length === 0 ? <p className="text-sm text-muted-foreground">No agent is set up yet, so there is nobody to draft. Create one in the Agents block.</p> : null}
104
+ {generate.error ? <ApiError error={generate.error} title="Could not draft the templates" /> : null}
105
+ {keep.error ? <ApiError error={keep.error} title="Could not save the template" /> : null}
106
+ {drafts.length > 0 ? (
107
+ <ul aria-label="Drafted templates" className="divide-y rounded-md border border-border text-sm">
108
+ {drafts.map((t) => (
109
+ <li key={t.id} className="flex items-center gap-2 p-2">
110
+ <span className="min-w-0 flex-1"><span className="font-medium">{t.name}</span><span className="block truncate text-xs text-muted-foreground">{stepSummary(t.step)}</span></span>
111
+ <Button size="sm" variant="outline" loading={keep.loading} onClick={() => void keep.mutate(t)}>Save as template</Button>
112
+ <Button size="sm" variant="ghost" onClick={() => setDrafts((all) => all.filter((x) => x.id !== t.id))}>Discard</Button>
113
+ </li>
114
+ ))}
115
+ </ul>
116
+ ) : null}
117
+ <ConfirmSpendDialog
118
+ open={confirming}
119
+ tier="billable"
120
+ title="Draft step templates with AI?"
121
+ description="The agent writes a set of templates. Each draft uses credits, even the ones you discard. Nothing is sent to anyone."
122
+ confirmLabel="Draft templates"
123
+ confirming={generate.loading}
124
+ onConfirm={() => void generate.mutate(undefined)}
125
+ onCancel={() => setConfirming(false)}
126
+ />
127
+ </div>
128
+ );
129
+ }
130
+
131
+ export function StepTemplates({ className }: StepTemplatesProps) {
132
+ const [editing, setEditing] = useState<StepTemplate | "new" | null>(null);
133
+ const [archiving, setArchiving] = useState<StepTemplate | null>(null);
134
+ const [showArchived, setShowArchived] = useState(false);
135
+ const [restoringId, setRestoringId] = useState<string | null>(null);
136
+ const [drafting, setDrafting] = useState(false);
137
+ const list = useG8Query(async (signal) => stepTemplates(await g8Get("/sequencer/templates/steps", undefined, { signal })), [], { key: ["sequences", "step-templates"] });
138
+ const archive = useG8Mutation(async (v: { template: StepTemplate; archived: boolean }) => {
139
+ await g8Patch(`/sequencer/templates/steps/${encodeURIComponent(v.template.id)}/archive`, { is_archived: v.archived });
140
+ setArchiving(null);
141
+ }, { invalidates: ["sequences"], onError: () => setArchiving(null) });
142
+
143
+ if (list.error) return <ApiError error={list.error} title="Could not load the step templates" onRetry={() => void list.refetch()} className={className} />;
144
+ if (list.loading && !list.data) return <div aria-busy="true" className={`space-y-2 ${className ?? ""}`}><Skeleton className="h-10 w-full" /><Skeleton className="h-10 w-full" /></div>;
145
+ const all = list.data ?? [];
146
+ const rows = all.filter((t) => showArchived || !t.archived);
147
+ const done = (changed: boolean) => { setEditing(null); if (changed) void list.refetch(); };
148
+
149
+ return (
150
+ <div className={`space-y-3 ${className ?? ""}`}>
151
+ <div className="flex flex-wrap items-center justify-between gap-2">
152
+ <h3 className="font-medium">Step templates</h3>
153
+ <div className="flex items-center gap-2">
154
+ <label className="flex items-center gap-1 text-xs"><input type="checkbox" checked={showArchived} onChange={(e) => setShowArchived(e.target.checked)} />Show archived</label>
155
+ <Button size="sm" variant="outline" onClick={() => setDrafting((d) => !d)}><Sparkles className="mr-1 size-4" aria-hidden />AI drafts</Button>
156
+ <Button size="sm" disabled={editing != null} onClick={() => setEditing("new")}><Plus className="mr-1 size-4" aria-hidden />New template</Button>
157
+ </div>
158
+ </div>
159
+ {drafting ? <GeneratePanel onSaved={() => void list.refetch()} /> : null}
160
+ {editing === "new" ? <TemplateForm loaded={null} onDone={done} /> : null}
161
+ {rows.length === 0 && editing !== "new" ? (
162
+ <EmptyState compact title="No step templates yet" description="Save an email or LinkedIn touch once, then drop it into any sequence." action={<Button size="sm" onClick={() => setEditing("new")}>Create a template</Button>} />
163
+ ) : (
164
+ <ul aria-label="Step templates" className="divide-y rounded-md border border-border">
165
+ {rows.map((t) => (
166
+ <li key={t.id} className="p-2 text-sm">
167
+ {editing !== "new" && editing?.id === t.id ? (
168
+ <TemplateForm loaded={t} onDone={done} />
169
+ ) : (
170
+ <div className="flex items-center gap-2">
171
+ <StatusPill tone="neutral">{KIND_LABEL[t.step.kind]}</StatusPill>
172
+ <span className="min-w-0 flex-1"><span className="font-medium">{t.name}</span><span className="block truncate text-xs text-muted-foreground">{stepSummary(t.step)}</span></span>
173
+ {t.archived ? <StatusPill tone="warning">archived</StatusPill> : null}
174
+ <Button size="sm" variant="ghost" aria-label={`Edit ${t.name}`} disabled={editing != null} onClick={() => setEditing(t)}><Pencil className="size-4" aria-hidden /></Button>
175
+ {t.archived ? (
176
+ <Button size="sm" variant="ghost" aria-label={`Restore ${t.name}`} loading={archive.loading && restoringId === t.id} onClick={() => { setRestoringId(t.id); void archive.mutate({ template: t, archived: false }); }}><ArchiveRestore className="size-4" aria-hidden /></Button>
177
+ ) : (
178
+ <Button size="sm" variant="ghost" aria-label={`Archive ${t.name}`} onClick={() => setArchiving(t)}><Archive className="size-4" aria-hidden /></Button>
179
+ )}
180
+ </div>
181
+ )}
182
+ </li>
183
+ ))}
184
+ </ul>
185
+ )}
186
+ {archive.error ? <ApiError error={archive.error} title="Could not update the template" /> : null}
187
+ <ConfirmSpendDialog
188
+ open={archiving != null}
189
+ tier="destructive"
190
+ title={`Archive "${archiving?.name ?? ""}"?`}
191
+ description="It leaves the template list. Sequences already built from it keep their steps. You can restore it from Show archived."
192
+ confirmLabel="Archive template"
193
+ affected={1}
194
+ confirming={archive.loading}
195
+ onConfirm={() => archiving && void archive.mutate({ template: archiving, archived: true })}
196
+ onCancel={() => setArchiving(null)}
197
+ />
198
+ </div>
199
+ );
200
+ }
201
+
202
+ export interface StepTemplatePickerProps {
203
+ /** Called with an unsaved copy of the template's step, ready to add to the sequence being built. */
204
+ onPick: (step: DraftStep, template: { id: string; name: string }) => void;
205
+ className?: string;
206
+ }
207
+
208
+ /** A select of the (non-archived) step templates and an Insert button. Loads the templates when it mounts. */
209
+ export function StepTemplatePicker({ onPick, className }: StepTemplatePickerProps) {
210
+ const [id, setId] = useState("");
211
+ const list = useG8Query(async (signal) => stepTemplates(await g8Get("/sequencer/templates/steps", undefined, { signal })), [], { key: ["sequences", "step-templates"] });
212
+ const options = (list.data ?? []).filter((t) => !t.archived);
213
+ const chosen = options.find((t) => t.id === id);
214
+ return (
215
+ <div className={`flex flex-wrap items-center gap-2 ${className ?? ""}`}>
216
+ <select aria-label="Step template" className="h-8 min-w-48 rounded-md border border-border bg-background px-2 text-sm" value={id} disabled={list.loading} onChange={(e) => setId(e.target.value)}>
217
+ <option value="">{list.loading ? "Loading templates..." : options.length === 0 ? "No step templates yet" : "Choose a step template"}</option>
218
+ {options.map((t) => <option key={t.id} value={t.id}>{t.name} ({KIND_LABEL[t.step.kind]})</option>)}
219
+ </select>
220
+ <Button size="sm" variant="outline" disabled={!chosen} onClick={() => chosen && onPick({ ...chosen.step, key: `tpl-${chosen.id}-${Date.now().toString(36)}` }, { id: chosen.id, name: chosen.name })}>Insert step</Button>
221
+ {list.error ? <ApiError error={list.error} compact onRetry={() => void list.refetch()} /> : null}
222
+ </div>
223
+ );
224
+ }
@@ -0,0 +1,235 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Pure helpers for managing sequences beyond building them: editing a sequence's own fields (only what changed),
5
+ * sequence and step templates, and AI drafting of step copy. No SDK calls here.
6
+ *
7
+ * Template and draft endpoints answer with open payloads (`{ items: [...] }`, or a bare object), so every reader is
8
+ * defensive: it takes a list from the usual places and skips rows it cannot use.
9
+ */
10
+ import { fromApiStep, KIND_LABEL, kindOf, newKey, STEP_TYPE, toApiStep } from "./sequencer-utils";
11
+ import type { ApiStep, DraftStep } from "./sequencer-utils";
12
+
13
+ const str = (v: unknown): string => (typeof v === "string" ? v : "");
14
+ const bool = (v: unknown): boolean => v === true;
15
+
16
+ /** Rows from `{ data: [...] }`, `{ data: { items: [...] } }` or a named list. */
17
+ export function templateRows<T = Record<string, unknown>>(envelope: unknown, ...names: string[]): T[] {
18
+ const data = (envelope as { data?: unknown } | null)?.data ?? envelope;
19
+ if (Array.isArray(data)) return data as T[];
20
+ const obj = (data ?? {}) as Record<string, unknown>;
21
+ for (const key of ["items", ...names]) if (Array.isArray(obj[key])) return obj[key] as T[];
22
+ return [];
23
+ }
24
+
25
+ // --- Editing a sequence ----------------------------------------------------------------------------------------
26
+
27
+ export interface EditableSequence {
28
+ name: string;
29
+ description: string;
30
+ finish_on_reply: boolean;
31
+ send_in_same_thread: boolean;
32
+ wait_for_new_contacts: boolean;
33
+ }
34
+
35
+ /** The editable fields of `GET /sequences/{id}`, with the API's defaults for anything it left out. */
36
+ export function editableSequence(record: Record<string, unknown>): EditableSequence {
37
+ return {
38
+ name: str(record.name),
39
+ description: str(record.description),
40
+ finish_on_reply: record.finish_on_reply !== false,
41
+ send_in_same_thread: bool(record.send_in_same_thread),
42
+ wait_for_new_contacts: bool(record.wait_for_new_contacts),
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Only the fields that differ from what was loaded, for `PATCH /sequences/{id}`. An empty object means nothing to
48
+ * save. A cleared description is sent as an empty string so it really clears.
49
+ */
50
+ export function sequenceChanges(loaded: EditableSequence, draft: EditableSequence): Partial<EditableSequence> {
51
+ const out: Partial<EditableSequence> = {};
52
+ if (draft.name.trim() !== loaded.name) out.name = draft.name.trim();
53
+ if (draft.description.trim() !== loaded.description) out.description = draft.description.trim();
54
+ if (draft.finish_on_reply !== loaded.finish_on_reply) out.finish_on_reply = draft.finish_on_reply;
55
+ if (draft.send_in_same_thread !== loaded.send_in_same_thread) out.send_in_same_thread = draft.send_in_same_thread;
56
+ if (draft.wait_for_new_contacts !== loaded.wait_for_new_contacts) out.wait_for_new_contacts = draft.wait_for_new_contacts;
57
+ return out;
58
+ }
59
+
60
+ /** What the change does to people, in words, for the confirm dialog. */
61
+ export function changeSummary(changes: Partial<EditableSequence>): string[] {
62
+ const out: string[] = [];
63
+ if ("name" in changes) out.push(`Rename to "${changes.name}"`);
64
+ if ("description" in changes) out.push(changes.description ? "Change the description" : "Clear the description");
65
+ if ("finish_on_reply" in changes) out.push(changes.finish_on_reply ? "Stop the sequence for a contact once they reply" : "Keep sending after a contact replies");
66
+ if ("send_in_same_thread" in changes) out.push(changes.send_in_same_thread ? "Send follow-ups in the same email thread" : "Send each email as a new thread");
67
+ if ("wait_for_new_contacts" in changes) out.push(changes.wait_for_new_contacts ? "Wait for new contacts added to the list" : "Stop picking up new contacts");
68
+ return out;
69
+ }
70
+
71
+ // --- Templates -------------------------------------------------------------------------------------------------
72
+
73
+ export interface SequenceTemplate {
74
+ id: string;
75
+ name: string;
76
+ description: string;
77
+ archived: boolean;
78
+ steps: ApiStep[];
79
+ }
80
+
81
+ export function sequenceTemplates(envelope: unknown): SequenceTemplate[] {
82
+ const out: SequenceTemplate[] = [];
83
+ for (const row of templateRows<Record<string, unknown>>(envelope, "templates")) {
84
+ const id = row?.id == null ? "" : String(row.id);
85
+ if (!id) continue;
86
+ const steps = (Array.isArray(row.steps) ? (row.steps as Array<Record<string, unknown>>) : []).map(
87
+ (s, i): ApiStep => ({
88
+ id: undefined,
89
+ name: str(s.name) || undefined,
90
+ step_order: typeof s.step_order === "number" ? s.step_order : i + 1,
91
+ step_type: str(s.step_type) || "EMAIL",
92
+ input_type: str(s.input_type) || "MANUAL_TEMPLATE",
93
+ time_interval: typeof s.time_interval === "number" ? s.time_interval : 0,
94
+ step_data: (s.step_data && typeof s.step_data === "object" ? s.step_data : {}) as Record<string, unknown>,
95
+ }),
96
+ );
97
+ out.push({ id, name: str(row.name) || id, description: str(row.description), archived: bool(row.is_archived), steps: steps.sort((a, b) => a.step_order - b.step_order) });
98
+ }
99
+ return out;
100
+ }
101
+
102
+ /** A template's steps as unsaved builder steps (no ids), ready to prefill a new sequence. */
103
+ export function templateToDraftSteps(template: Pick<SequenceTemplate, "steps">): DraftStep[] {
104
+ return template.steps.map((s) => ({ ...fromApiStep(s), key: newKey(), id: undefined, dirty: true }));
105
+ }
106
+
107
+ /** The body of `POST /sequencer/templates/sequences` from steps (each template step needs a name: its own when it has one, else "Step N: Kind"). */
108
+ export function templateBodyFromSteps(name: string, description: string, steps: ApiStep[]) {
109
+ return {
110
+ name: name.trim(),
111
+ ...(description.trim() ? { description: description.trim() } : {}),
112
+ steps: [...steps]
113
+ .sort((a, b) => a.step_order - b.step_order)
114
+ .map((s, i) => ({
115
+ name: s.name || `Step ${i + 1}: ${KIND_LABEL[kindOf(s.step_type)]}`,
116
+ step_type: s.step_type,
117
+ step_order: i + 1,
118
+ input_type: s.input_type ?? "MANUAL_TEMPLATE",
119
+ time_interval: s.time_interval ?? 0,
120
+ step_data: s.step_data ?? {},
121
+ })),
122
+ };
123
+ }
124
+
125
+ export interface StepTemplate {
126
+ id: string;
127
+ name: string;
128
+ archived: boolean;
129
+ step: DraftStep;
130
+ }
131
+
132
+ /** Step templates from a list answer. `drafts` accepts rows without an id (generated, not saved yet) and numbers them. */
133
+ export function stepTemplates(envelope: unknown, drafts = false): StepTemplate[] {
134
+ const out: StepTemplate[] = [];
135
+ templateRows<Record<string, unknown>>(envelope, "templates").forEach((row, i) => {
136
+ const id = row?.id == null ? (drafts ? `draft-${i}` : "") : String(row.id);
137
+ if (!id) return;
138
+ const step: DraftStep = {
139
+ ...fromApiStep({ step_order: 1, step_type: str(row.step_type) || "EMAIL", input_type: str(row.input_type) || "MANUAL_TEMPLATE", time_interval: 2 * 86400, step_data: (row.step_data ?? {}) as Record<string, unknown> }),
140
+ key: newKey(),
141
+ id: undefined,
142
+ dirty: true,
143
+ };
144
+ out.push({ id, name: str(row.name) || id, archived: bool(row.is_archived), step });
145
+ });
146
+ return out;
147
+ }
148
+
149
+ /** Body of `POST /sequencer/templates/steps` for a step being saved as a template. */
150
+ export function stepTemplateBody(name: string, step: DraftStep) {
151
+ const api = toApiStep(step, 1);
152
+ return { name: name.trim(), step_type: api.step_type, input_type: api.input_type, step_data: api.step_data };
153
+ }
154
+
155
+ /** Only what changed on a step template, for `PATCH /sequencer/templates/steps/{id}`. */
156
+ export function stepTemplateChanges(loaded: StepTemplate, name: string, step: DraftStep): Record<string, unknown> {
157
+ const out: Record<string, unknown> = {};
158
+ if (name.trim() !== loaded.name) out.name = name.trim();
159
+ const a = toApiStep(step, 1);
160
+ const b = toApiStep(loaded.step, 1);
161
+ if (JSON.stringify(a.step_data) !== JSON.stringify(b.step_data)) out.step_data = a.step_data;
162
+ if (a.step_type !== b.step_type) out.step_type = a.step_type;
163
+ return out;
164
+ }
165
+
166
+ export const STEP_TYPES_FOR_TEMPLATES = Object.values(STEP_TYPE);
167
+
168
+ // --- AI drafting -----------------------------------------------------------------------------------------------
169
+
170
+ export interface DraftContact {
171
+ id: number;
172
+ first_name: string;
173
+ last_name: string;
174
+ email: string;
175
+ label: string;
176
+ /** JSON of what is known about them; the drafting service writes from it. */
177
+ lead_info: string;
178
+ }
179
+
180
+ /** A contact row from `GET /contacts` as drafting input. Null only when there is no usable id; a contact with no email has `email: ""` (an email draft then cannot be requested). */
181
+ export function draftContact(row: Record<string, unknown>): DraftContact | null {
182
+ const id = typeof row.id === "number" ? row.id : Number(row.id);
183
+ if (!Number.isFinite(id)) return null;
184
+ const first = str(row.first_name);
185
+ const last = str(row.last_name);
186
+ const full = str(row.full_name) || str(row.name) || `${first} ${last}`.trim();
187
+ const email = str(row.work_email) || str(row.email);
188
+ const info: Record<string, unknown> = {};
189
+ for (const k of ["job_title", "company_name", "company_domain", "company_industry", "city", "country", "seniority", "department"]) if (row[k]) info[k] = row[k];
190
+ return { id, first_name: first || full.split(" ")[0] || "", last_name: last || full.split(" ").slice(1).join(" "), email, label: [full || email || `Contact ${id}`, str(row.company_name)].filter(Boolean).join(", "), lead_info: JSON.stringify(info) };
191
+ }
192
+
193
+ /** `{ subject, body }` from a draft answer, or null when the answer has neither. */
194
+ export function readEmailDraft(envelope: unknown): { subject: string; body: string } | null {
195
+ const data = ((envelope as { data?: unknown } | null)?.data ?? envelope) as Record<string, unknown> | null;
196
+ const subject = str(data?.subject);
197
+ const body = str(data?.body);
198
+ return subject || body ? { subject, body } : null;
199
+ }
200
+
201
+ /** `{ message, subject }` from a LinkedIn draft answer. */
202
+ export function readLinkedinDraft(envelope: unknown): { message: string; subject: string } | null {
203
+ const data = ((envelope as { data?: unknown } | null)?.data ?? envelope) as Record<string, unknown> | null;
204
+ const message = str(data?.message);
205
+ return message ? { message, subject: str(data?.subject) } : null;
206
+ }
207
+
208
+ /** The improved instruction from `POST /sequencer/content/instructions/improve` (`{ prompt }`). */
209
+ export function readImprovedInstruction(envelope: unknown): string {
210
+ const data = ((envelope as { data?: unknown } | null)?.data ?? envelope) as Record<string, unknown> | string | null;
211
+ if (typeof data === "string") return data;
212
+ return str(data?.prompt) || str(data?.improved_prompt) || str(data?.instruction) || str(data?.text);
213
+ }
214
+
215
+ /** Credits from `POST /sequencer/content/email/estimate`; `cached` means the draft is served free. */
216
+ export function readCreditEstimate(envelope: unknown): { credits: number; cached: boolean } | null {
217
+ const data = ((envelope as { data?: unknown } | null)?.data ?? envelope) as Record<string, unknown> | null;
218
+ const credits = typeof data?.estimated_credits === "number" ? data.estimated_credits : null;
219
+ return credits == null ? null : { credits, cached: data?.cached === true };
220
+ }
221
+
222
+ export interface AgentOption {
223
+ value: string;
224
+ label: string;
225
+ }
226
+
227
+ /** Textual agents from `GET /sequencer/agents/textual` (untyped: names or objects with a name). */
228
+ export function agentOptions(envelope: unknown): AgentOption[] {
229
+ const out: AgentOption[] = [];
230
+ for (const row of templateRows<unknown>(envelope, "agents")) {
231
+ const name = typeof row === "string" ? row : str((row as Record<string, unknown>)?.name) || str((row as Record<string, unknown>)?.agent_name);
232
+ if (name) out.push({ value: name, label: name });
233
+ }
234
+ return out;
235
+ }
@@ -42,11 +42,15 @@ export interface SequenceRow {
42
42
  contact_count?: number | null;
43
43
  sequence_kind?: string | null;
44
44
  associated_list_id?: number | null;
45
+ /** The Studio campaign this sequence belongs to, when linked. */
46
+ campaign_builder_campaign_id?: string | null;
45
47
  updated_at?: string | null;
46
48
  }
47
49
 
48
50
  export interface ApiStep {
49
51
  id?: string;
52
+ /** A sequence template step's own name (templates only). */
53
+ name?: string;
50
54
  step_order: number;
51
55
  step_type: string;
52
56
  input_type?: string;
@@ -0,0 +1,39 @@
1
+ # signals
2
+
3
+ Social listening for the intent family: watch X, LinkedIn, Reddit, Instagram and YouTube for posts about your brand,
4
+ competitors and keywords, triage what matched, and keep the spend bounded. Depends on `intent` (and through it `crm`).
5
+
6
+ ## Screens
7
+
8
+ - **SocialListenersScreen** (`/social-listening`): tabs Listeners, Posts, Top engagers, Sources and Settings, with totals at
9
+ the top. A listener opens as a deep-linkable detail (`?id=`, `&view=full` for a page) with Overview, Matches and Edit.
10
+ - **RecordSocialTab**: a contact's matched posts, for `ContactsScreen` `extraDetailTabs`.
11
+
12
+ ## Flows
13
+
14
+ Create a listener (cost estimate, optional generated query, monthly cap), pause or resume, edit changed fields only, delete.
15
+ Triage posts (reviewed or dismissed), save an author as a contact, add an author to a live sequence, turn a post's
16
+ engagers into a list. See the accounts that engage most. Watch every profile on a list. Seed starter listeners from your
17
+ keywords, brand, competitors and buyer profile. Change the org-wide caps and channel ceilings (admin). Test, connect and
18
+ disconnect your own X developer app.
19
+
20
+ Every billable, external or destructive call is behind `ConfirmSpendDialog`, with quantities taken from the data on screen.
21
+ Nothing connects, sends or spends on its own.
22
+
23
+ ## Prerequisites and scopes
24
+
25
+ `intent:read` (reads), `intent:run` (create and change listeners, posts, seeds, settings, X), `intent:delete` (delete a
26
+ listener), plus `lists:read` and `sequences:read` for the pickers. Settings changes also need an org admin.
27
+
28
+ ## Defaults
29
+
30
+ Poll interval and monthly cap are not sent unless typed. Price per language-model pass is 5 credits (backend constant, no
31
+ price endpoint). A person always chooses the sequence an author joins.
32
+
33
+ ## API gaps
34
+
35
+ - Every response is an open object; shapes follow the app's models and may gain fields.
36
+ - `PATCH /social-listeners/{id}` and `PATCH /settings` both drop nulls (`exclude_none`), so a query, a handle or a number cannot be unset: the form refuses to clear a query rather than report a save that did not happen. Settings PATCH: a channel or capture rule is replaced whole.
37
+ - No price read: the language-model pass price is a constant.
38
+ - No company-level post read; only a contact's posts (`listPostsByContact`).
39
+ - `markPost`, `savePostAuthorAsContact` and `addEngagersToList` are tiered billable but spend no credits (documented by the API).