@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,228 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Types, vocabularies and pure helpers for the signals block's social listening part.
5
+ *
6
+ * Every response of the social-listeners routes is an OPEN object in the contract, so the row types below follow the
7
+ * app's own models (social_listener/interfaces/fastapi/listeners_router.py), which the dev API forwards unchanged.
8
+ * Enums are copied from the request schemas (generated/schemas.ts), not from memory: the API refuses anything else.
9
+ * A count the channel did not report is null and shown as a dash, never as 0. Calls nothing.
10
+ */
11
+
12
+ export type Platform = "x" | "linkedin" | "reddit" | "instagram" | "youtube";
13
+ export type ListenerType = "brand" | "competitor" | "keyword" | "engagement" | "user";
14
+ export type PostStatus = "new" | "reviewed" | "dismissed";
15
+ export type EngagementType = "comment" | "reaction";
16
+
17
+ export const PLATFORMS: { value: Platform; label: string }[] = [
18
+ { value: "x", label: "X" },
19
+ { value: "linkedin", label: "LinkedIn" },
20
+ { value: "reddit", label: "Reddit" },
21
+ { value: "instagram", label: "Instagram" },
22
+ { value: "youtube", label: "YouTube" },
23
+ ];
24
+ export const LISTENER_TYPES: { value: ListenerType; label: string; hint: string }[] = [
25
+ { value: "brand", label: "Brand", hint: "Posts that mention your brand or products." },
26
+ { value: "competitor", label: "Competitor", hint: "Posts that mention a competitor." },
27
+ { value: "keyword", label: "Keyword", hint: "Posts that match a search query." },
28
+ { value: "engagement", label: "Engagement", hint: "Posts from people who look like your buyers." },
29
+ { value: "user", label: "Account", hint: "Everything one account posts." },
30
+ ];
31
+ export const POST_STATUSES: { value: PostStatus; label: string }[] = [
32
+ { value: "new", label: "New" },
33
+ { value: "reviewed", label: "Reviewed" },
34
+ { value: "dismissed", label: "Dismissed" },
35
+ ];
36
+
37
+ /** Minimum the API accepts for a poll interval (seconds). */
38
+ export const MIN_POLL_SEC = 60;
39
+ /** The API's page-size cap for the post and engagement reads. */
40
+ export const PAGE_SIZE = 100;
41
+
42
+ /**
43
+ * Credits per language-model pass of an auto-seed or a generated query. The API publishes no price; this is the backend
44
+ * constant (credit_server `social_listener_brand_query_build`, mirrored in frontend/features/social-listener/social-listener.costs.ts).
45
+ */
46
+ export const AI_PASS_CREDITS = 5;
47
+
48
+ export interface Listener {
49
+ id: string;
50
+ platform: string;
51
+ listener_type: string;
52
+ name: string;
53
+ query?: string | null;
54
+ target_handle?: string | null;
55
+ filters?: Record<string, unknown> | null;
56
+ poll_interval_sec?: number | null;
57
+ monthly_credit_cap?: number | null;
58
+ is_active: boolean;
59
+ paused_reason?: string | null;
60
+ last_polled_at?: string | null;
61
+ source_list_id?: string | null;
62
+ created_at?: string | null;
63
+ updated_at?: string | null;
64
+ }
65
+ export interface ListenerDetailData { listener: Listener; match_count?: number | null; credit_burn?: number | null }
66
+
67
+ export interface MatchedPost {
68
+ post_external_id: string;
69
+ platform: string;
70
+ matched_listener_ids?: string[];
71
+ author_handle?: string | null;
72
+ author_name?: string | null;
73
+ author_followers?: number | null;
74
+ author_verified?: boolean | null;
75
+ author_profile_image_url?: string | null;
76
+ text?: string | null;
77
+ post_url?: string | null;
78
+ created_at?: string | null;
79
+ like_count?: number | null;
80
+ reply_count?: number | null;
81
+ repost_count?: number | null;
82
+ view_count?: number | null;
83
+ classifier_score?: number | null;
84
+ engagement_intent?: string | null;
85
+ resolved_mashup_contact_id?: string | null;
86
+ status?: string | null;
87
+ }
88
+ export interface PostEngagement {
89
+ engagement_external_id: string;
90
+ engagement_type: string;
91
+ reaction_type?: string | null;
92
+ author_handle?: string | null;
93
+ author_name?: string | null;
94
+ author_url?: string | null;
95
+ author_headline?: string | null;
96
+ text?: string | null;
97
+ created_at?: string | null;
98
+ }
99
+ export interface TopAccount {
100
+ author_external_id: string;
101
+ author_handle?: string | null;
102
+ author_name?: string | null;
103
+ author_url?: string | null;
104
+ author_headline?: string | null;
105
+ platform: string;
106
+ posts_engaged?: number | null;
107
+ comments?: number | null;
108
+ reactions?: number | null;
109
+ last_engaged_at?: string | null;
110
+ }
111
+ export interface SourceList {
112
+ audience_id: number;
113
+ list_title?: string | null;
114
+ audience_type?: string | null;
115
+ contact_count?: number | null;
116
+ is_active?: boolean | null;
117
+ is_dynamic?: boolean | null;
118
+ last_synced_at?: string | null;
119
+ total_listeners_created?: number | null;
120
+ channel_coverage?: Record<string, number> | null;
121
+ }
122
+ export interface SocialStats {
123
+ monthly_credit_burn?: number | null;
124
+ posts_matched?: number | null;
125
+ posts_reviewed?: number | null;
126
+ posts_dismissed?: number | null;
127
+ contacts_resolved?: number | null;
128
+ x_api_spend_usd?: number | null;
129
+ listeners_active?: number | null;
130
+ listeners_paused?: number | null;
131
+ }
132
+ export interface ChannelRule { enabled?: boolean; monthly_credit_cap?: number; per_poll_result_cap?: number; max_comments_per_post?: number; max_reactions_per_post?: number; fetch_engagements?: boolean }
133
+ export interface CaptureRule { enabled?: boolean; min_followers?: number; only_when_classified?: boolean }
134
+ export interface SocialSettings {
135
+ monthly_credit_cap?: number | null;
136
+ other_channels_monthly_credit_cap?: number | null;
137
+ alert_threshold_pct?: number | null;
138
+ auto_pause_on_cap?: boolean | null;
139
+ auto_archive_days?: number | null;
140
+ auto_create_contacts?: boolean | null;
141
+ classifier_model?: string | null;
142
+ channel_settings?: Record<string, ChannelRule> | null;
143
+ contact_capture?: Record<string, CaptureRule> | null;
144
+ }
145
+ export interface XStatus { connected?: boolean; mode?: string | null; x_app_username?: string | null; connected_at?: string | null; degraded_reason?: string | null }
146
+ export interface XMode { auth_mode?: string | null; byo_connected?: boolean | null; shared_available?: boolean | null; shared_cap_total?: number | null; shared_cap_remaining?: number | null }
147
+ export interface SeedJob { job_id?: string; status?: string; stage?: string; error?: string | null; result?: { brand_created?: number; competitors_created?: number; keywords_created?: number; engagement_created?: number; skipped?: string[] } | null }
148
+ export interface CostPreview { posts_per_day_estimate?: number | null; credits_per_month_estimate?: number | null; x_usd_per_month_estimate?: number | null; cached?: boolean; note?: string | null }
149
+
150
+ /** The rows of an open `{ items: [] }` payload (the dev API surfaces every list as `items`). */
151
+ export function itemsOf<T>(data: unknown): T[] {
152
+ const d = data as { items?: unknown } | T[] | null | undefined;
153
+ if (Array.isArray(d)) return d;
154
+ return Array.isArray(d?.items) ? (d.items as T[]) : [];
155
+ }
156
+ export function totalOf(data: unknown): number | undefined {
157
+ const t = (data as { total?: unknown } | null | undefined)?.total;
158
+ return typeof t === "number" ? t : undefined;
159
+ }
160
+
161
+ export const platformLabel = (p: string | null | undefined): string => PLATFORMS.find((x) => x.value === p)?.label ?? (p ? p : "");
162
+ export const typeLabel = (t: string | null | undefined): string => LISTENER_TYPES.find((x) => x.value === t)?.label ?? (t ? t : "");
163
+ /** A number, or null when the channel did not report one (unknown is not zero). */
164
+ export const num = (n: number | null | undefined): number | null => (typeof n === "number" && Number.isFinite(n) ? n : null);
165
+ export const fmt = (n: number | null | undefined): string => { const v = num(n); return v === null ? "-" : v.toLocaleString(); };
166
+ export const when = (iso: string | null | undefined): string => {
167
+ if (!iso) return "-";
168
+ const t = Date.parse(iso);
169
+ return Number.isNaN(t) ? "-" : new Date(t).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
170
+ };
171
+ export const cadence = (sec: number | null | undefined): string => {
172
+ const s = num(sec);
173
+ if (s === null) return "-";
174
+ if (s % 3600 === 0) return `every ${s / 3600} h`;
175
+ if (s % 60 === 0) return `every ${s / 60} min`;
176
+ return `every ${s} s`;
177
+ };
178
+ /** Credits per month a poll cadence cannot bound: only the cap does, so the cap is what a confirm states. */
179
+ export const capText = (cap: number | null | undefined): string => {
180
+ const c = num(cap);
181
+ return c === null ? "no cap reported" : c === 0 ? "no per-listener cap" : `capped at ${c.toLocaleString()} credits a month`;
182
+ };
183
+ export const statusOf = (l: Pick<Listener, "is_active">): "active" | "paused" => (l.is_active ? "active" : "paused");
184
+
185
+ /**
186
+ * Only the fields that changed. A cleared text or number is NOT sent: the dev API builds the inner body with
187
+ * `model_dump(exclude_none=True)`, so a null would be dropped and the save would report something that never happened.
188
+ */
189
+ export function listenerDiff(before: Listener, next: { name: string; query: string; target_handle: string; poll_interval_sec: number | null; monthly_credit_cap: number | null }): Record<string, unknown> {
190
+ const out: Record<string, unknown> = {};
191
+ if (next.name.trim() !== before.name) out.name = next.name.trim();
192
+ if (next.query.trim() && next.query.trim() !== (before.query ?? "")) out.query = next.query.trim();
193
+ if (next.target_handle.trim() && next.target_handle.trim() !== (before.target_handle ?? "")) out.target_handle = next.target_handle.trim();
194
+ if (next.poll_interval_sec !== null && next.poll_interval_sec !== num(before.poll_interval_sec)) out.poll_interval_sec = next.poll_interval_sec;
195
+ if (next.monthly_credit_cap !== null && next.monthly_credit_cap !== num(before.monthly_credit_cap)) out.monthly_credit_cap = next.monthly_credit_cap;
196
+ return out;
197
+ }
198
+
199
+ /** A page of a POST-search read: rows plus whether another page follows (from the reported total, else a full page). */
200
+ export function morePages(data: unknown, rows: number, page: number, size: number): boolean {
201
+ const t = totalOf(data);
202
+ return t !== undefined ? page * size < t : rows >= size;
203
+ }
204
+
205
+ /** The sequences a person can enrol into: live ones only. */
206
+ export interface SequenceRow { id: string; name?: string | null; status?: string | null }
207
+ export const isLiveSequence = (s: SequenceRow): boolean => ["live", "waiting", "running", "active"].includes(String(s.status ?? "").toLowerCase());
208
+
209
+ /** Top-level numeric settings: key, label, max (when the API bounds it). */
210
+ export const SETTING_NUMBERS: { key: "monthly_credit_cap" | "other_channels_monthly_credit_cap" | "alert_threshold_pct" | "auto_archive_days"; label: string; max?: number; hint: string }[] = [
211
+ { key: "monthly_credit_cap", label: "Monthly credit cap (X)", hint: "The org-wide ceiling on social listening credits." },
212
+ { key: "other_channels_monthly_credit_cap", label: "Monthly credit cap (other channels)", hint: "LinkedIn, Reddit, Instagram and YouTube together. 0 means no org-wide ceiling." },
213
+ { key: "alert_threshold_pct", label: "Alert at (% of cap)", max: 100, hint: "You are alerted when spend reaches this share of the cap." },
214
+ { key: "auto_archive_days", label: "Archive posts after (days)", hint: "Older matched posts are archived." },
215
+ ];
216
+ export const CHANNEL_NUMBERS: { key: "monthly_credit_cap" | "per_poll_result_cap" | "max_comments_per_post" | "max_reactions_per_post"; label: string }[] = [
217
+ { key: "monthly_credit_cap", label: "Monthly credit cap" },
218
+ { key: "per_poll_result_cap", label: "Posts per poll" },
219
+ { key: "max_comments_per_post", label: "Comments per post" },
220
+ { key: "max_reactions_per_post", label: "Reactions per post" },
221
+ ];
222
+
223
+ /** Whole non-negative integer from a text box, or null when empty / invalid. */
224
+ export function wholeNumber(text: string, max?: number): number | null {
225
+ if (text.trim() === "") return null;
226
+ const n = Number(text);
227
+ return Number.isInteger(n) && n >= 0 && (max === undefined || n <= max) ? n : null;
228
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @title Estimate a listener's cost, then create it with a monthly cap
3
+ * @when Starting to watch a keyword or competitor. Creating a listener makes it poll, and reading posts spends credits, so
4
+ * a person confirms the cap first. The estimate charges nothing.
5
+ */
6
+ import { g8 } from "@graph8/sdk";
7
+
8
+ g8.init({ apiKey: process.env.G8_API_KEY! });
9
+
10
+ export async function previewThenCreate(query: string, name: string, monthlyCreditCap: number, approved: boolean): Promise<{ estimateCreditsPerMonth: unknown; listenerId?: string }> {
11
+ const est = await g8.api.signals.previewListenerCost({ body: { listener_type: "keyword", platform: "x", query } });
12
+ const estimateCreditsPerMonth = (est.data as { credits_per_month_estimate?: unknown }).credits_per_month_estimate;
13
+ if (!approved) return { estimateCreditsPerMonth };
14
+ const made = await g8.api.signals.createListener({ body: { listener_type: "keyword", platform: "x", name, query, monthly_credit_cap: monthlyCreditCap } });
15
+ return { estimateCreditsPerMonth, listenerId: (made.data as { id?: string }).id };
16
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @title Compare this month's credit spend with the org-wide cap
3
+ * @when A budget alert for social listening. A cap of 0 means no org-wide ceiling, which is not "at the limit".
4
+ */
5
+ import { g8 } from "@graph8/sdk";
6
+
7
+ g8.init({ apiKey: process.env.G8_API_KEY! });
8
+
9
+ export async function spendVsCap(): Promise<{ spent: number | null; cap: number | null; share: number | null }> {
10
+ const [stats, settings] = await Promise.all([g8.api.signals.getSocialStats({}), g8.api.signals.getSocialSettings({})]);
11
+ const spent = (stats.data as { monthly_credit_burn?: number }).monthly_credit_burn ?? null;
12
+ const cap = (settings.data as { monthly_credit_cap?: number }).monthly_credit_cap ?? null;
13
+ return { spent, cap, share: spent !== null && cap ? spent / cap : null };
14
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @title Read new matched posts and mark them reviewed
3
+ * @when A daily triage queue for social signals. The read spends nothing (it reads what the worker stored); a post is
4
+ * identified by its channel AND its id.
5
+ */
6
+ import { g8 } from "@graph8/sdk";
7
+
8
+ g8.init({ apiKey: process.env.G8_API_KEY! });
9
+
10
+ export async function newPosts(limit = 50): Promise<Array<{ platform: string; id: string; text: string | null }>> {
11
+ const res = await g8.api.signals.searchMatchedPosts({ body: { status: "new", limit, page: 1 } });
12
+ const rows = ((res.data as { items?: Array<{ platform: string; post_external_id: string; text?: string | null }> }).items ?? []);
13
+ return rows.map((p) => ({ platform: p.platform, id: p.post_external_id, text: p.text ?? null }));
14
+ }
15
+
16
+ export async function markReviewed(platform: "x" | "linkedin" | "reddit" | "instagram" | "youtube", postId: string): Promise<void> {
17
+ await g8.api.signals.markPost({ path: { post_id: postId }, query: { platform }, body: { status: "reviewed" } });
18
+ }
@@ -75,7 +75,8 @@ export function LineChart({ points, metric }: { points: TrendPoint[]; metric: Me
75
75
 
76
76
  function activeNow(envelope: unknown): number | undefined {
77
77
  const data = (envelope as { data?: Record<string, unknown> } | null)?.data;
78
- const value = data?.active_users_5min ?? data?.active_5min ?? data?.active_users ?? data?.five_min ?? data?.active_now;
78
+ // The realtime report answers `active_5m` (reports/analytics/queries/realtime.py); the older names stay as fallbacks.
79
+ const value = data?.active_5m ?? data?.active_users_5min ?? data?.active_5min ?? data?.active_users ?? data?.five_min ?? data?.active_now;
79
80
  return typeof value === "number" ? value : undefined;
80
81
  }
81
82
 
@@ -114,7 +115,7 @@ export function TrafficChart({ days = 30, writeKey, realtimeSeconds = 10, classN
114
115
  ))}
115
116
  </div>
116
117
  <p className="text-sm text-muted-foreground" aria-live="polite">
117
- {now != null ? `${now} on the site now` : ""}
118
+ {now != null ? `${now} on the site now` : live.data || live.error ? "On the site now: -" : ""}
118
119
  </p>
119
120
  </div>
120
121
 
@@ -0,0 +1,102 @@
1
+ # web-analytics
2
+
3
+ Website analytics for your own sites: **traffic, acquisition, audience, pages, engagement, conversions and realtime**, read from the
4
+ tracking pipeline (the same reports as the graph8 Analytics pages). Every call is a free read. Nothing here is billable, external
5
+ or destructive, so there are no confirmations.
6
+
7
+ ## Screens
8
+
9
+ `WebAnalyticsScreen` (`components/web-analytics-screen.tsx`) is the finished page.
10
+
11
+ ```tsx
12
+ import { WebAnalyticsScreen } from "@/g8/web-analytics/components/web-analytics-screen";
13
+ export default function Page() { return <WebAnalyticsScreen />; }
14
+ ```
15
+
16
+ One **site picker** and one **window** drive every tab and live in the URL, so a link opens the same view:
17
+ `?site=pubKeyA,pubKeyB` (none = all sites), `?days=7|30|90|365` or `?from=YYYY-MM-DD&to=YYYY-MM-DD`, `?bots=1`, `?internal=1`, `?tab=`.
18
+
19
+ | Tab | What it shows |
20
+ |---|---|
21
+ | Overview | visitors, new visitors, sessions, page views; daily traffic; conversion rate; top pages (a row opens the page detail) |
22
+ | Acquisition | channels, channel trend, sources and mediums, UTM campaigns, referrals, social networks, search engines |
23
+ | Audience | top countries, cities and languages; every country or city; languages; browsers, operating systems, devices; new or returning |
24
+ | Pages | top, landing and exit pages; custom events; a page detail panel (`?page=/path`) with views, visitors, form submissions, bookings |
25
+ | Engagement | sessions, pages per session, time on site, bounce and engaged share against the previous period; trend; depth and time distributions; by channel, campaign, landing page |
26
+ | Conversions | conversion rate; funnel (visitors, viewed a form, submitted, booked); goals and their trend; forms, form trend, form analysis |
27
+ | Realtime | active visitors in the last 5 and 30 minutes, active pages, traffic sources, locations; re-read every 10 seconds while visible |
28
+
29
+ Optional props: `resolveLandingPage(path)` returns the id of the landing page at exactly that address (one match only), and `onOpenLandingPage(id)` opens it
30
+ (the hubspot stack wires both to the pages block). `refreshSeconds` (default 10; 0 turns the realtime poll off), `toolbarActions`, `className`.
31
+
32
+ ## Rules the screen keeps
33
+
34
+ - **Loading is visible.** Changing a site, the window or a switch dims every card (and a first load shows a skeleton). A failed or refused request clears the card and shows the error with Retry; the previous numbers never stay under an error.
35
+ - **Unavailable is not zero.** The reports swallow a failed ClickHouse query and answer `meta.stale: true` with empty data. A card in that state says it is unavailable (with Retry), and the tiles that belong to that card go with it.
36
+ - **Unknown rates are dashes.** A conversion rate the API sends as null (no visitors, no viewers) and any rate whose denominator is 0 shows a dash, never zero percent.
37
+ - **No request the API refuses.** `days` 1 to 365; a custom range needs both days, real dates, start not after end, at most 365 days (Apply is disabled with the reason otherwise); site keys are letters, digits, `-`, `_`; `limit` never exceeds each report's maximum (languages 100, form analysis 100, engagement by landing page 100, realtime pages and sources 50, the rest 200); a page path must start with `/` and use plain URL characters.
38
+ - **Sort, then cut.** Tables sort the whole list a read returned and only then show the first 10 ("Show all N"). A read that came back full says "Showing the first N".
39
+ - **No invented defaults.** The window defaults to the API's own 30 days; bots and internal traffic stay excluded (the flags are sent only when switched on).
40
+
41
+ ## Types
42
+
43
+ The contract answers every web analytics report with `AnalyticsResponse { success, data: object | array, meta }`: `data` is **untyped**. The row types in
44
+ `lib/web-analytics-types.ts` are typed from the backend instead (the column aliases of `reports/analytics/queries/{audience,acquisition,behavior,conversions,engagement,realtime}.py`).
45
+ Every field is optional and read defensively.
46
+
47
+ ## Install
48
+
49
+ ```
50
+ npx @graph8/sdk add web-analytics
51
+ ```
52
+
53
+ Installs `foundation`, `crm` and `visitors` first if missing. Everything lands under `g8/web-analytics/`. Your app's proxy must allow the `analytics` and `streams` prefixes.
54
+
55
+ ## Prerequisites (the human must do these)
56
+
57
+ | Where | What | How to verify |
58
+ |---|---|---|
59
+ | The website | The graph8 tracking snippet is pasted into the site. Without it every report is empty. | The visitors block's SnippetInstall reports events received. |
60
+ | graph8 key | `analytics:read`, and `integrations:read` for the site list | `g8.api.me.describeCurrentKey()` |
61
+
62
+ ## Operations (39)
63
+
64
+ - `get_acquisition_campaigns_analytics_acquisition_campaigns_get`
65
+ - `get_acquisition_channels_analytics_acquisition_channels_get`
66
+ - `get_acquisition_channels_trend_analytics_acquisition_channels_trend_get`
67
+ - `get_acquisition_referrals_analytics_acquisition_referrals_get`
68
+ - `get_acquisition_search_analytics_acquisition_search_get`
69
+ - `get_acquisition_social_analytics_acquisition_social_get`
70
+ - `get_acquisition_sources_analytics_acquisition_sources_get`
71
+ - `get_audience_behavior_analytics_audience_behavior_get`
72
+ - `get_audience_demographics_analytics_audience_demographics_get`
73
+ - `get_audience_languages_analytics_audience_languages_get`
74
+ - `get_audience_overview_analytics_audience_overview_get`
75
+ - `get_audience_trend_analytics_audience_trend_get`
76
+ - `get_conversion_rate_analytics_conversions_rate_get`
77
+ - `get_engagement_by_campaign_analytics_engagement_by_campaign_get`
78
+ - `get_engagement_by_channel_analytics_engagement_by_channel_get`
79
+ - `get_engagement_by_landing_page_analytics_engagement_by_landing_page_get`
80
+ - `get_engagement_overview_analytics_engagement_overview_get`
81
+ - `get_engagement_trend_analytics_engagement_trend_get`
82
+ - `get_events_analytics_behavior_events_get`
83
+ - `get_exit_pages_analytics_behavior_exit_pages_get`
84
+ - `get_form_analysis_analytics_conversions_forms_analysis_get`
85
+ - `get_forms_analytics_conversions_forms_get`
86
+ - `get_forms_trend_analytics_conversions_forms_trend_get`
87
+ - `get_funnel_analytics_conversions_funnel_get`
88
+ - `get_geo_cities_analytics_audience_geo_cities_get`
89
+ - `get_geo_countries_analytics_audience_geo_get`
90
+ - `get_goals_analytics_conversions_goals_get`
91
+ - `get_goals_trend_analytics_conversions_goals_trend_get`
92
+ - `get_landing_pages_analytics_behavior_landing_pages_get`
93
+ - `get_page_metrics_analytics_behavior_page_metrics_get`
94
+ - `get_realtime_analytics_realtime_get`
95
+ - `get_realtime_locations_analytics_realtime_locations_get`
96
+ - `get_realtime_pages_analytics_realtime_pages_get`
97
+ - `get_realtime_sources_analytics_realtime_sources_get`
98
+ - `get_session_depth_distribution_analytics_engagement_depth_distribution_get`
99
+ - `get_technology_overview_analytics_audience_technology_get`
100
+ - `get_time_on_site_distribution_analytics_engagement_time_distribution_get`
101
+ - `get_top_pages_analytics_behavior_pages_get`
102
+ - `list_org_streams_streams_get`