@graph8/sdk 0.308.1 → 0.357.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.
- package/README.md +81 -0
- package/blocks/README.md +32 -0
- package/blocks/_foundation/README.md +78 -0
- package/blocks/_foundation/block.json +79 -0
- package/blocks/_foundation/components/api-error.tsx +62 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
- package/blocks/_foundation/components/empty-state.tsx +24 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
- package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
- package/blocks/_foundation/hooks/use-paginated.ts +105 -0
- package/blocks/_foundation/lib/g8-client.ts +128 -0
- package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
- package/blocks/_foundation/server/proxy.ts +233 -0
- package/blocks/_foundation/server/route.ts +30 -0
- package/blocks/agents/README.md +133 -0
- package/blocks/agents/block.json +223 -0
- package/blocks/agents/components/agent-builder.tsx +199 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
- package/blocks/agents/components/agent-runs.tsx +175 -0
- package/blocks/agents/components/agent-skills-panel.tsx +126 -0
- package/blocks/agents/components/agent-test-console.tsx +175 -0
- package/blocks/agents/lib/agent-types.ts +114 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
- package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
- package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
- package/blocks/analytics/README.md +73 -0
- package/blocks/analytics/block.json +178 -0
- package/blocks/analytics/components/funnel-chart.tsx +79 -0
- package/blocks/analytics/components/kpi-tiles.tsx +74 -0
- package/blocks/analytics/components/leaderboard.tsx +86 -0
- package/blocks/analytics/components/report-builder.tsx +200 -0
- package/blocks/analytics/hooks/use-report.ts +26 -0
- package/blocks/analytics/lib/analytics-types.ts +108 -0
- package/blocks/analytics/lib/use-debounced.ts +16 -0
- package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
- package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
- package/blocks/analytics/recipes/03-saved-report.ts +30 -0
- package/blocks/appointments/README.md +80 -0
- package/blocks/appointments/block.json +218 -0
- package/blocks/appointments/components/availability-editor.tsx +192 -0
- package/blocks/appointments/components/booking-page.tsx +154 -0
- package/blocks/appointments/components/event-type-editor.tsx +214 -0
- package/blocks/appointments/components/meeting-detail.tsx +147 -0
- package/blocks/appointments/components/meeting-list.tsx +148 -0
- package/blocks/appointments/hooks/use-available-slots.ts +29 -0
- package/blocks/appointments/hooks/use-schedules.ts +18 -0
- package/blocks/appointments/lib/appointments-types.ts +159 -0
- package/blocks/appointments/lib/idempotency.ts +30 -0
- package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
- package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
- package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
- package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
- package/blocks/block.schema.json +130 -0
- package/blocks/block.types.ts +109 -0
- package/blocks/campaigns/README.md +118 -0
- package/blocks/campaigns/block.json +361 -0
- package/blocks/campaigns/components/campaign-assets.tsx +180 -0
- package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
- package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
- package/blocks/campaigns/components/campaign-list.tsx +120 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
- package/blocks/campaigns/components/campaign-select.tsx +52 -0
- package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
- package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
- package/blocks/campaigns/components/form-field.tsx +29 -0
- package/blocks/campaigns/components/icp-editor.tsx +230 -0
- package/blocks/campaigns/components/persona-editor.tsx +210 -0
- package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
- package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
- package/blocks/campaigns/recipes/campaign-report.ts +22 -0
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
- package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
- package/blocks/coach/README.md +93 -0
- package/blocks/coach/block.json +125 -0
- package/blocks/coach/components/call-scorecard.tsx +126 -0
- package/blocks/coach/components/coaching-feed.tsx +140 -0
- package/blocks/coach/lib/coach-types.ts +116 -0
- package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
- package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
- package/blocks/copilot/README.md +99 -0
- package/blocks/copilot/block.json +151 -0
- package/blocks/copilot/components/copilot-memory.tsx +87 -0
- package/blocks/copilot/components/copilot-panel.tsx +189 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
- package/blocks/copilot/lib/copilot-types.ts +111 -0
- package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
- package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
- package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
- package/blocks/crm/README.md +122 -0
- package/blocks/crm/block.json +378 -0
- package/blocks/crm/components/activity-timeline.tsx +99 -0
- package/blocks/crm/components/company-detail.tsx +118 -0
- package/blocks/crm/components/company-table.tsx +173 -0
- package/blocks/crm/components/contact-detail.tsx +135 -0
- package/blocks/crm/components/contact-table.tsx +217 -0
- package/blocks/crm/components/dedupe-review.tsx +164 -0
- package/blocks/crm/components/import-wizard.tsx +193 -0
- package/blocks/crm/components/notes-panel.tsx +112 -0
- package/blocks/crm/components/record-form.tsx +191 -0
- package/blocks/crm/components/task-list.tsx +107 -0
- package/blocks/crm/hooks/use-custom-fields.ts +18 -0
- package/blocks/crm/hooks/use-debounced.ts +13 -0
- package/blocks/crm/hooks/use-page-query.ts +52 -0
- package/blocks/crm/lib/crm-types.ts +136 -0
- package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
- package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
- package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
- package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
- package/blocks/deals/README.md +78 -0
- package/blocks/deals/block.json +198 -0
- package/blocks/deals/components/deal-detail.tsx +134 -0
- package/blocks/deals/components/forecast-view.tsx +112 -0
- package/blocks/deals/components/pipeline-board.tsx +142 -0
- package/blocks/deals/components/stage-editor.tsx +96 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
- package/blocks/deals/hooks/use-pipelines.ts +19 -0
- package/blocks/deals/lib/deals-types.ts +128 -0
- package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
- package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
- package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
- package/blocks/dialer/README.md +114 -0
- package/blocks/dialer/block.json +260 -0
- package/blocks/dialer/components/call-log.tsx +78 -0
- package/blocks/dialer/components/call-queue.tsx +188 -0
- package/blocks/dialer/components/dialer-workspace.tsx +47 -0
- package/blocks/dialer/components/disposition-panel.tsx +75 -0
- package/blocks/dialer/components/live-calls-table.tsx +107 -0
- package/blocks/dialer/components/session-start-form.tsx +111 -0
- package/blocks/dialer/components/softphone.tsx +122 -0
- package/blocks/dialer/components/transcript-viewer.tsx +82 -0
- package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
- package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
- package/blocks/dialer/hooks/use-dispositions.ts +29 -0
- package/blocks/dialer/lib/dialer-types.ts +158 -0
- package/blocks/dialer/lib/softphone-transport.ts +67 -0
- package/blocks/dialer/lib/twilio-transport.ts +74 -0
- package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
- package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
- package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
- package/blocks/enrichment/README.md +105 -0
- package/blocks/enrichment/block.json +204 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
- package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
- package/blocks/enrichment/components/enrich-button.tsx +136 -0
- package/blocks/enrichment/components/waterfall-config.tsx +127 -0
- package/blocks/enrichment/hooks/use-providers.ts +18 -0
- package/blocks/enrichment/lib/enrichment-types.ts +104 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
- package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
- package/blocks/forms/README.md +112 -0
- package/blocks/forms/block.json +200 -0
- package/blocks/forms/components/form-builder.tsx +150 -0
- package/blocks/forms/components/form-embed.tsx +103 -0
- package/blocks/forms/components/form-mapping-editor.tsx +125 -0
- package/blocks/forms/components/site-audit-panel.tsx +73 -0
- package/blocks/forms/components/submissions-table.tsx +158 -0
- package/blocks/forms/lib/form-types.ts +127 -0
- package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
- package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
- package/blocks/forms/recipes/route-form-to-list.ts +44 -0
- package/blocks/inbox/README.md +72 -0
- package/blocks/inbox/block.json +194 -0
- package/blocks/inbox/components/inbox-list.tsx +156 -0
- package/blocks/inbox/components/inbox-screen.tsx +49 -0
- package/blocks/inbox/components/reply-composer.tsx +183 -0
- package/blocks/inbox/components/tag-and-assign.tsx +189 -0
- package/blocks/inbox/components/thread-badge.tsx +17 -0
- package/blocks/inbox/components/thread-view.tsx +108 -0
- package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
- package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
- package/blocks/inbox/lib/idempotency.ts +30 -0
- package/blocks/inbox/lib/inbox-types.ts +100 -0
- package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
- package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
- package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
- package/blocks/integrations/README.md +75 -0
- package/blocks/integrations/block.json +172 -0
- package/blocks/integrations/components/integration-list.tsx +165 -0
- package/blocks/integrations/components/sync-status.tsx +223 -0
- package/blocks/integrations/components/webhook-manager.tsx +300 -0
- package/blocks/integrations/lib/integrations-utils.ts +62 -0
- package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
- package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
- package/blocks/integrations/recipes/register-webhook.ts +18 -0
- package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
- package/blocks/intent/README.md +57 -0
- package/blocks/intent/block.json +138 -0
- package/blocks/intent/components/account-intent-score.tsx +91 -0
- package/blocks/intent/components/intent-feed.tsx +215 -0
- package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
- package/blocks/intent/hooks/use-account-intent.ts +46 -0
- package/blocks/intent/lib/intent-utils.ts +95 -0
- package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
- package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
- package/blocks/intent/recipes/who-visited-url.ts +18 -0
- package/blocks/lifecycle/README.md +79 -0
- package/blocks/lifecycle/block.json +198 -0
- package/blocks/lifecycle/components/churn-list.tsx +92 -0
- package/blocks/lifecycle/components/customer-table.tsx +119 -0
- package/blocks/lifecycle/components/lead-queue.tsx +127 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
- package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
- package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
- package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
- package/blocks/lifecycle/lib/lifecycle.ts +99 -0
- package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
- package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
- package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
- package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
- package/blocks/lists/README.md +96 -0
- package/blocks/lists/block.json +166 -0
- package/blocks/lists/components/add-to-list-button.tsx +111 -0
- package/blocks/lists/components/list-picker.tsx +94 -0
- package/blocks/lists/components/list-table.tsx +160 -0
- package/blocks/lists/components/suppression-manager.tsx +153 -0
- package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
- package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
- package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
- package/blocks/mailboxes/README.md +71 -0
- package/blocks/mailboxes/block.json +187 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
- package/blocks/mailboxes/components/warmup-health.tsx +232 -0
- package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
- package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
- package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
- package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
- package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
- package/blocks/newsletter/README.md +122 -0
- package/blocks/newsletter/block.json +224 -0
- package/blocks/newsletter/components/issue-form.tsx +124 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
- package/blocks/newsletter/components/subscriber-table.tsx +173 -0
- package/blocks/newsletter/lib/newsletter-types.ts +97 -0
- package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
- package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
- package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
- package/blocks/nurture/README.md +110 -0
- package/blocks/nurture/block.json +191 -0
- package/blocks/nurture/components/nurture-builder.tsx +114 -0
- package/blocks/nurture/components/nurture-enroll.tsx +107 -0
- package/blocks/nurture/components/nurture-form.tsx +120 -0
- package/blocks/nurture/components/nurture-stats.tsx +81 -0
- package/blocks/nurture/lib/nurture-types.ts +109 -0
- package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
- package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
- package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
- package/blocks/objects/README.md +120 -0
- package/blocks/objects/block.json +243 -0
- package/blocks/objects/components/attribute-field-input.tsx +82 -0
- package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
- package/blocks/objects/components/object-not-found.tsx +34 -0
- package/blocks/objects/components/object-record-detail.tsx +143 -0
- package/blocks/objects/components/object-schema-editor.tsx +197 -0
- package/blocks/objects/components/object-table.tsx +130 -0
- package/blocks/objects/components/record-form-dialog.tsx +112 -0
- package/blocks/objects/components/record-roles-panel.tsx +106 -0
- package/blocks/objects/hooks/use-object-schema.ts +36 -0
- package/blocks/objects/lib/object-types.ts +214 -0
- package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
- package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
- package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
- package/blocks/pages/README.md +104 -0
- package/blocks/pages/block.json +223 -0
- package/blocks/pages/components/brand-kit-editor.tsx +152 -0
- package/blocks/pages/components/content-library.tsx +163 -0
- package/blocks/pages/components/page-editor.tsx +253 -0
- package/blocks/pages/components/page-list.tsx +159 -0
- package/blocks/pages/components/seo-report.tsx +159 -0
- package/blocks/pages/lib/studio.ts +77 -0
- package/blocks/pages/recipes/audit-site-seo.ts +28 -0
- package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
- package/blocks/pages/recipes/edit-page-section.ts +25 -0
- package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
- package/blocks/quotes/README.md +79 -0
- package/blocks/quotes/block.json +183 -0
- package/blocks/quotes/components/product-picker.tsx +80 -0
- package/blocks/quotes/components/quote-builder.tsx +195 -0
- package/blocks/quotes/components/quote-list.tsx +79 -0
- package/blocks/quotes/components/quote-viewer.tsx +135 -0
- package/blocks/quotes/components/signature-status.tsx +77 -0
- package/blocks/quotes/hooks/use-debounced.ts +13 -0
- package/blocks/quotes/lib/quotes.ts +142 -0
- package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
- package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
- package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
- package/blocks/radar/README.md +106 -0
- package/blocks/radar/block.json +189 -0
- package/blocks/radar/components/competitor-battle-card.tsx +53 -0
- package/blocks/radar/components/competitor-tracker.tsx +158 -0
- package/blocks/radar/components/radar-overview.tsx +67 -0
- package/blocks/radar/components/radar-report.tsx +86 -0
- package/blocks/radar/hooks/use-competitors.ts +23 -0
- package/blocks/radar/lib/radar-types.ts +124 -0
- package/blocks/radar/recipes/battle-card.ts +28 -0
- package/blocks/radar/recipes/deep-dive-report.ts +32 -0
- package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
- package/blocks/radar/recipes/track-and-scan.ts +39 -0
- package/blocks/search/README.md +129 -0
- package/blocks/search/block.json +185 -0
- package/blocks/search/components/filter-builder.tsx +149 -0
- package/blocks/search/components/prospect-results.tsx +175 -0
- package/blocks/search/components/save-to-list-dialog.tsx +135 -0
- package/blocks/search/components/saved-searches.tsx +95 -0
- package/blocks/search/hooks/use-filter-options.ts +41 -0
- package/blocks/search/lib/search-types.ts +149 -0
- package/blocks/search/recipes/explore-filter-values.ts +27 -0
- package/blocks/search/recipes/saved-searches.ts +34 -0
- package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
- package/blocks/sequencer/README.md +87 -0
- package/blocks/sequencer/block.json +214 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
- package/blocks/sequencer/components/sequence-builder.tsx +306 -0
- package/blocks/sequencer/components/sequence-list.tsx +200 -0
- package/blocks/sequencer/components/sequence-stats.tsx +256 -0
- package/blocks/sequencer/components/step-editor.tsx +213 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
- package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
- package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
- package/blocks/sequencer/recipes/sequence-report.ts +22 -0
- package/blocks/social-ads/README.md +128 -0
- package/blocks/social-ads/block.json +256 -0
- package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
- package/blocks/social-ads/components/audience-sync.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +121 -0
- package/blocks/social-ads/components/post-composer.tsx +122 -0
- package/blocks/social-ads/components/social-post-list.tsx +116 -0
- package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
- package/blocks/social-ads/lib/social-ads-types.ts +167 -0
- package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
- package/blocks/social-ads/recipes/schedule-post.ts +51 -0
- package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
- package/blocks/visitors/README.md +56 -0
- package/blocks/visitors/block.json +139 -0
- package/blocks/visitors/components/snippet-install.tsx +169 -0
- package/blocks/visitors/components/traffic-chart.tsx +156 -0
- package/blocks/visitors/components/visitor-company-card.tsx +92 -0
- package/blocks/visitors/components/visitor-feed.tsx +157 -0
- package/blocks/visitors/lib/visitors-utils.ts +78 -0
- package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
- package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
- package/blocks/visitors/recipes/traffic-summary.ts +24 -0
- package/blocks/webchat/README.md +87 -0
- package/blocks/webchat/block.json +134 -0
- package/blocks/webchat/components/chat-widget-config.tsx +160 -0
- package/blocks/webchat/components/conversation-list.tsx +74 -0
- package/blocks/webchat/lib/webchat-types.ts +83 -0
- package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
- package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
- package/blocks/workbench/README.md +135 -0
- package/blocks/workbench/block.json +259 -0
- package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
- package/blocks/workbench/components/run-progress.tsx +132 -0
- package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
- package/blocks/workbench/components/workbench-grid.tsx +159 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
- package/blocks/workbench/components/workbench-table-list.tsx +105 -0
- package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
- package/blocks/workbench/lib/waterfall-presets.ts +119 -0
- package/blocks/workbench/lib/workbench-types.ts +191 -0
- package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
- package/blocks/workbench/recipes/import-from-search.ts +42 -0
- package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
- package/blocks/workbench/recipes/waterfall-column.ts +55 -0
- package/blocks/workflows/README.md +100 -0
- package/blocks/workflows/block.json +188 -0
- package/blocks/workflows/components/run-history.tsx +136 -0
- package/blocks/workflows/components/trigger-config.tsx +134 -0
- package/blocks/workflows/components/workflow-editor.tsx +142 -0
- package/blocks/workflows/components/workflow-list.tsx +153 -0
- package/blocks/workflows/hooks/use-workflow.ts +51 -0
- package/blocks/workflows/lib/graph.ts +187 -0
- package/blocks/workflows/recipes/create-workflow.ts +35 -0
- package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
- package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
- package/blocks/workflows/recipes/set-trigger.ts +22 -0
- package/dist/cli.js +5330 -29
- package/dist/index.browser.js +7740 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.browser.mjs +7717 -0
- package/dist/index.browser.mjs.map +1 -0
- package/dist/index.d.mts +1440 -21
- package/dist/index.d.ts +1440 -21
- package/dist/index.js +42 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +42 -3
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +7598 -0
- package/dist/react.browser.js.map +1 -0
- package/dist/react.browser.mjs +7568 -0
- package/dist/react.browser.mjs.map +1 -0
- package/dist/react.d.mts +1340 -18
- package/dist/react.d.ts +1340 -18
- package/dist/react.js +41 -2
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +41 -2
- package/dist/react.mjs.map +1 -1
- package/dist/types/agentSetup.d.ts +6 -6
- package/dist/types/blocks/add.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +46 -0
- package/dist/types/blocks/doctor.d.ts +26 -0
- package/dist/types/blocks/registry.d.ts +134 -0
- package/dist/types/cli.d.ts +16 -0
- package/dist/types/core.d.ts +1 -1
- package/dist/types/crypto-browser.d.ts +2 -0
- package/dist/types/crypto-node.d.ts +6 -0
- package/dist/types/react.d.ts +1 -1
- package/dist/types/version.d.ts +2 -2
- package/dist/types/webhooks.d.ts +2 -2
- package/dist/ui/index.d.mts +54 -0
- package/dist/ui/index.d.ts +54 -0
- package/dist/ui/index.js +105 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/index.mjs +44 -0
- package/dist/ui/index.mjs.map +1 -0
- package/dist/ui/lib/compose-refs.d.mts +19 -0
- package/dist/ui/lib/compose-refs.d.ts +19 -0
- package/dist/ui/lib/compose-refs.js +76 -0
- package/dist/ui/lib/compose-refs.js.map +1 -0
- package/dist/ui/lib/compose-refs.mjs +41 -0
- package/dist/ui/lib/compose-refs.mjs.map +1 -0
- package/dist/ui/lib/use-mobile.d.mts +4 -0
- package/dist/ui/lib/use-mobile.d.ts +4 -0
- package/dist/ui/lib/use-mobile.js +57 -0
- package/dist/ui/lib/use-mobile.js.map +1 -0
- package/dist/ui/lib/use-mobile.mjs +22 -0
- package/dist/ui/lib/use-mobile.mjs.map +1 -0
- package/dist/ui/lib/utils.d.mts +10 -0
- package/dist/ui/lib/utils.d.ts +10 -0
- package/dist/ui/lib/utils.js +33 -0
- package/dist/ui/lib/utils.js.map +1 -0
- package/dist/ui/lib/utils.mjs +9 -0
- package/dist/ui/lib/utils.mjs.map +1 -0
- package/dist/ui/primitives/alert-dialog.d.mts +22 -0
- package/dist/ui/primitives/alert-dialog.d.ts +22 -0
- package/dist/ui/primitives/alert-dialog.js +163 -0
- package/dist/ui/primitives/alert-dialog.js.map +1 -0
- package/dist/ui/primitives/alert-dialog.mjs +119 -0
- package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
- package/dist/ui/primitives/avatar.d.mts +8 -0
- package/dist/ui/primitives/avatar.d.ts +8 -0
- package/dist/ui/primitives/avatar.js +91 -0
- package/dist/ui/primitives/avatar.js.map +1 -0
- package/dist/ui/primitives/avatar.mjs +55 -0
- package/dist/ui/primitives/avatar.mjs.map +1 -0
- package/dist/ui/primitives/breadcrumb.d.mts +13 -0
- package/dist/ui/primitives/breadcrumb.d.ts +13 -0
- package/dist/ui/primitives/breadcrumb.js +127 -0
- package/dist/ui/primitives/breadcrumb.js.map +1 -0
- package/dist/ui/primitives/breadcrumb.mjs +97 -0
- package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
- package/dist/ui/primitives/button.d.mts +15 -0
- package/dist/ui/primitives/button.d.ts +15 -0
- package/dist/ui/primitives/button.js +91 -0
- package/dist/ui/primitives/button.js.map +1 -0
- package/dist/ui/primitives/button.mjs +56 -0
- package/dist/ui/primitives/button.mjs.map +1 -0
- package/dist/ui/primitives/card.d.mts +10 -0
- package/dist/ui/primitives/card.d.ts +10 -0
- package/dist/ui/primitives/card.js +101 -0
- package/dist/ui/primitives/card.js.map +1 -0
- package/dist/ui/primitives/card.mjs +62 -0
- package/dist/ui/primitives/card.mjs.map +1 -0
- package/dist/ui/primitives/carousel.d.mts +45 -0
- package/dist/ui/primitives/carousel.d.ts +45 -0
- package/dist/ui/primitives/carousel.js +300 -0
- package/dist/ui/primitives/carousel.js.map +1 -0
- package/dist/ui/primitives/carousel.mjs +261 -0
- package/dist/ui/primitives/carousel.mjs.map +1 -0
- package/dist/ui/primitives/chart.d.mts +68 -0
- package/dist/ui/primitives/chart.d.ts +68 -0
- package/dist/ui/primitives/chart.js +280 -0
- package/dist/ui/primitives/chart.js.map +1 -0
- package/dist/ui/primitives/chart.mjs +241 -0
- package/dist/ui/primitives/chart.mjs.map +1 -0
- package/dist/ui/primitives/collapsible.d.mts +8 -0
- package/dist/ui/primitives/collapsible.d.ts +8 -0
- package/dist/ui/primitives/collapsible.js +72 -0
- package/dist/ui/primitives/collapsible.js.map +1 -0
- package/dist/ui/primitives/collapsible.mjs +36 -0
- package/dist/ui/primitives/collapsible.mjs.map +1 -0
- package/dist/ui/primitives/context-menu.d.mts +29 -0
- package/dist/ui/primitives/context-menu.d.ts +29 -0
- package/dist/ui/primitives/context-menu.js +203 -0
- package/dist/ui/primitives/context-menu.js.map +1 -0
- package/dist/ui/primitives/context-menu.mjs +155 -0
- package/dist/ui/primitives/context-menu.mjs.map +1 -0
- package/dist/ui/primitives/dialog.d.mts +26 -0
- package/dist/ui/primitives/dialog.d.ts +26 -0
- package/dist/ui/primitives/dialog.js +192 -0
- package/dist/ui/primitives/dialog.js.map +1 -0
- package/dist/ui/primitives/dialog.mjs +148 -0
- package/dist/ui/primitives/dialog.mjs.map +1 -0
- package/dist/ui/primitives/drawer.d.mts +26 -0
- package/dist/ui/primitives/drawer.d.ts +26 -0
- package/dist/ui/primitives/drawer.js +146 -0
- package/dist/ui/primitives/drawer.js.map +1 -0
- package/dist/ui/primitives/drawer.mjs +103 -0
- package/dist/ui/primitives/drawer.mjs.map +1 -0
- package/dist/ui/primitives/input.d.mts +5 -0
- package/dist/ui/primitives/input.d.ts +5 -0
- package/dist/ui/primitives/input.js +46 -0
- package/dist/ui/primitives/input.js.map +1 -0
- package/dist/ui/primitives/input.mjs +22 -0
- package/dist/ui/primitives/input.mjs.map +1 -0
- package/dist/ui/primitives/item.d.mts +27 -0
- package/dist/ui/primitives/item.d.ts +27 -0
- package/dist/ui/primitives/item.js +221 -0
- package/dist/ui/primitives/item.js.map +1 -0
- package/dist/ui/primitives/item.mjs +188 -0
- package/dist/ui/primitives/item.mjs.map +1 -0
- package/dist/ui/primitives/kbd.d.mts +15 -0
- package/dist/ui/primitives/kbd.d.ts +15 -0
- package/dist/ui/primitives/kbd.js +45 -0
- package/dist/ui/primitives/kbd.js.map +1 -0
- package/dist/ui/primitives/kbd.mjs +21 -0
- package/dist/ui/primitives/kbd.mjs.map +1 -0
- package/dist/ui/primitives/label.d.mts +6 -0
- package/dist/ui/primitives/label.d.ts +6 -0
- package/dist/ui/primitives/label.js +55 -0
- package/dist/ui/primitives/label.js.map +1 -0
- package/dist/ui/primitives/label.mjs +21 -0
- package/dist/ui/primitives/label.mjs.map +1 -0
- package/dist/ui/primitives/resizable.d.mts +10 -0
- package/dist/ui/primitives/resizable.d.ts +10 -0
- package/dist/ui/primitives/resizable.js +68 -0
- package/dist/ui/primitives/resizable.js.map +1 -0
- package/dist/ui/primitives/resizable.mjs +42 -0
- package/dist/ui/primitives/resizable.mjs.map +1 -0
- package/dist/ui/primitives/save-status.d.mts +31 -0
- package/dist/ui/primitives/save-status.d.ts +31 -0
- package/dist/ui/primitives/save-status.js +150 -0
- package/dist/ui/primitives/save-status.js.map +1 -0
- package/dist/ui/primitives/save-status.mjs +126 -0
- package/dist/ui/primitives/save-status.mjs.map +1 -0
- package/dist/ui/primitives/separator.d.mts +6 -0
- package/dist/ui/primitives/separator.d.ts +6 -0
- package/dist/ui/primitives/separator.js +62 -0
- package/dist/ui/primitives/separator.js.map +1 -0
- package/dist/ui/primitives/separator.mjs +28 -0
- package/dist/ui/primitives/separator.mjs.map +1 -0
- package/dist/ui/primitives/sheet.d.mts +16 -0
- package/dist/ui/primitives/sheet.d.ts +16 -0
- package/dist/ui/primitives/sheet.js +170 -0
- package/dist/ui/primitives/sheet.js.map +1 -0
- package/dist/ui/primitives/sheet.mjs +129 -0
- package/dist/ui/primitives/sheet.mjs.map +1 -0
- package/dist/ui/primitives/sidebar.d.mts +74 -0
- package/dist/ui/primitives/sidebar.d.ts +74 -0
- package/dist/ui/primitives/sidebar.js +697 -0
- package/dist/ui/primitives/sidebar.js.map +1 -0
- package/dist/ui/primitives/sidebar.mjs +651 -0
- package/dist/ui/primitives/sidebar.mjs.map +1 -0
- package/dist/ui/primitives/skeleton.d.mts +5 -0
- package/dist/ui/primitives/skeleton.d.ts +5 -0
- package/dist/ui/primitives/skeleton.js +40 -0
- package/dist/ui/primitives/skeleton.js.map +1 -0
- package/dist/ui/primitives/skeleton.mjs +16 -0
- package/dist/ui/primitives/skeleton.mjs.map +1 -0
- package/dist/ui/primitives/slider.d.mts +20 -0
- package/dist/ui/primitives/slider.d.ts +20 -0
- package/dist/ui/primitives/slider.js +112 -0
- package/dist/ui/primitives/slider.js.map +1 -0
- package/dist/ui/primitives/slider.mjs +78 -0
- package/dist/ui/primitives/slider.mjs.map +1 -0
- package/dist/ui/primitives/sortable.d.mts +50 -0
- package/dist/ui/primitives/sortable.d.ts +50 -0
- package/dist/ui/primitives/sortable.js +456 -0
- package/dist/ui/primitives/sortable.js.map +1 -0
- package/dist/ui/primitives/sortable.mjs +435 -0
- package/dist/ui/primitives/sortable.mjs.map +1 -0
- package/dist/ui/primitives/switch.d.mts +6 -0
- package/dist/ui/primitives/switch.d.ts +6 -0
- package/dist/ui/primitives/switch.js +67 -0
- package/dist/ui/primitives/switch.js.map +1 -0
- package/dist/ui/primitives/switch.mjs +33 -0
- package/dist/ui/primitives/switch.mjs.map +1 -0
- package/dist/ui/primitives/table.d.mts +12 -0
- package/dist/ui/primitives/table.d.ts +12 -0
- package/dist/ui/primitives/table.js +130 -0
- package/dist/ui/primitives/table.js.map +1 -0
- package/dist/ui/primitives/table.mjs +89 -0
- package/dist/ui/primitives/table.mjs.map +1 -0
- package/dist/ui/primitives/tabs.d.mts +9 -0
- package/dist/ui/primitives/tabs.d.ts +9 -0
- package/dist/ui/primitives/tabs.js +86 -0
- package/dist/ui/primitives/tabs.js.map +1 -0
- package/dist/ui/primitives/tabs.mjs +49 -0
- package/dist/ui/primitives/tabs.mjs.map +1 -0
- package/dist/ui/primitives/textarea.d.mts +5 -0
- package/dist/ui/primitives/textarea.d.ts +5 -0
- package/dist/ui/primitives/textarea.js +43 -0
- package/dist/ui/primitives/textarea.js.map +1 -0
- package/dist/ui/primitives/textarea.mjs +19 -0
- package/dist/ui/primitives/textarea.mjs.map +1 -0
- package/dist/ui/primitives/tooltip.d.mts +9 -0
- package/dist/ui/primitives/tooltip.d.ts +9 -0
- package/dist/ui/primitives/tooltip.js +94 -0
- package/dist/ui/primitives/tooltip.js.map +1 -0
- package/dist/ui/primitives/tooltip.mjs +57 -0
- package/dist/ui/primitives/tooltip.mjs.map +1 -0
- package/docs/agents.md +72 -29
- package/docs/api/README.md +6 -3
- package/docs/api/appObjects.d.ts +1 -1
- package/docs/api/appObjects.md +1 -1
- package/docs/api/by-path.md +29 -0
- package/docs/api/objects.d.ts +72 -5
- package/docs/api/objects.md +84 -7
- package/docs/api/processLists.d.ts +247 -0
- package/docs/api/processLists.md +409 -0
- package/docs/api/reports.d.ts +476 -0
- package/docs/api/reports.md +539 -0
- package/docs/api/scheduleBuy.d.ts +12 -8
- package/docs/api/scheduleBuy.md +13 -10
- package/docs/api/seo.d.ts +406 -3
- package/docs/api/seo.md +505 -1
- package/docs/api/workspaceBackup.d.ts +324 -0
- package/docs/api/workspaceBackup.md +427 -0
- package/package.json +174 -5
- package/stacks/6sense.json +46 -0
- package/stacks/README.md +20 -0
- package/stacks/apollo.json +128 -0
- package/stacks/clay.json +59 -0
- package/stacks/gong.json +72 -0
- package/stacks/hubspot.json +162 -0
- package/stacks/intercom.json +53 -0
- package/stacks/mailchimp.json +71 -0
- package/stacks/outreach.json +86 -0
- package/stacks/salesforce.json +94 -0
- package/stacks/stack.schema.json +32 -0
- package/stacks/starter.json +22 -0
- package/styles/styles.css +2 -0
- package/styles/theme.css +181 -0
- package/styles/tokens.css +284 -0
- package/templates/next-app/README.md.tmpl +35 -0
- package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
- package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
- package/templates/next-app/app/globals.css.tmpl +8 -0
- package/templates/next-app/app/layout.tsx.tmpl +19 -0
- package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
- package/templates/next-app/dot-env.local.example.tmpl +6 -0
- package/templates/next-app/dot-gitignore.tmpl +7 -0
- package/templates/next-app/lib/nav.ts.tmpl +2 -0
- package/templates/next-app/next.config.mjs.tmpl +4 -0
- package/templates/next-app/package.json.tmpl +25 -0
- package/templates/next-app/postcss.config.mjs.tmpl +4 -0
- package/templates/next-app/tsconfig.json.tmpl +21 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MeetingDetail: one booking (with confirm, cancel and internal notes) or one recorded meeting (summary,
|
|
5
|
+
* topics, action items, transcript).
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* booking: GET /appointments/bookings/{uid} (appointments.getBooking) read
|
|
9
|
+
* POST /appointments/bookings/{uid}/confirm (appointments.confirmBooking) EXTERNAL -> ConfirmSpendDialog (accept or decline a pending booking)
|
|
10
|
+
* POST /appointments/bookings/{uid}/cancel (appointments.cancelBooking) DESTRUCTIVE -> ConfirmSpendDialog (emails the attendees)
|
|
11
|
+
* POST /appointments/bookings/{uid}/notes (appointments.addBookingNote) EXTERNAL -> ConfirmSpendDialog
|
|
12
|
+
* recorded: GET /inbox/meetings/{meeting_id} (meetings.getMeeting) read
|
|
13
|
+
* Pass the selection MeetingList gives you: { kind, id }.
|
|
14
|
+
*/
|
|
15
|
+
import { useState, type ReactNode } from "react";
|
|
16
|
+
import { Button, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
17
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
18
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
19
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
20
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
21
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
22
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
23
|
+
import { formatDateTime, unwrap, type Booking, type RecordedMeeting } from "../lib/appointments-types";
|
|
24
|
+
import type { MeetingSelection } from "./meeting-list";
|
|
25
|
+
|
|
26
|
+
export interface MeetingDetailProps {
|
|
27
|
+
selection: MeetingSelection;
|
|
28
|
+
/** Called after a confirm, cancel or note so the list can refresh. */
|
|
29
|
+
onChanged?: () => void;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type Pending = { kind: "confirm"; accept: boolean } | { kind: "cancel" } | { kind: "note" } | null;
|
|
34
|
+
|
|
35
|
+
function Field({ label, children }: { label: string; children: ReactNode }) {
|
|
36
|
+
return (
|
|
37
|
+
<div>
|
|
38
|
+
<dt className="text-xs uppercase text-muted-foreground">{label}</dt>
|
|
39
|
+
<dd className="text-sm">{children}</dd>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function RecordedDetail({ id, className }: { id: string; className?: string }) {
|
|
45
|
+
const q = useG8Query(async (signal) => unwrap<RecordedMeeting>(await g8Get(`/inbox/meetings/${encodeURIComponent(id)}`, undefined, { signal })), [id]);
|
|
46
|
+
if (q.error) return <ApiError error={q.error} title="Could not load the meeting" onRetry={() => void q.refetch()} />;
|
|
47
|
+
if (!q.data) return <Skeleton className="h-40 w-full" aria-label="Loading meeting" />;
|
|
48
|
+
const m = q.data;
|
|
49
|
+
return (
|
|
50
|
+
<section className={`space-y-3 ${className ?? ""}`}>
|
|
51
|
+
<h2 className="text-base font-semibold">{m.subject || "Untitled meeting"}</h2>
|
|
52
|
+
<dl className="grid gap-3 sm:grid-cols-2">
|
|
53
|
+
<Field label="When">{formatDateTime(m.start_time)}</Field>
|
|
54
|
+
<Field label="Attendees">{(m.attendees ?? []).map((a) => a.name || a.email).join(", ") || "Unknown"}</Field>
|
|
55
|
+
</dl>
|
|
56
|
+
{m.transcript_summary ? <p className="whitespace-pre-wrap text-sm">{m.transcript_summary}</p> : <p className="text-sm text-muted-foreground">No summary yet ({m.transcript_status ?? "no transcript"}).</p>}
|
|
57
|
+
{m.key_topics?.length ? <p className="text-sm">Topics: {m.key_topics.join(", ")}</p> : null}
|
|
58
|
+
{m.action_items?.length ? (
|
|
59
|
+
<ul className="list-disc pl-5 text-sm">
|
|
60
|
+
{m.action_items.map((a, i) => (
|
|
61
|
+
<li key={i}>{String(a.text ?? a.title ?? a.description ?? JSON.stringify(a))}</li>
|
|
62
|
+
))}
|
|
63
|
+
</ul>
|
|
64
|
+
) : null}
|
|
65
|
+
{m.transcript_text ? (
|
|
66
|
+
<details className="text-sm">
|
|
67
|
+
<summary className="cursor-pointer font-medium">Transcript</summary>
|
|
68
|
+
<p className="mt-2 whitespace-pre-wrap">{m.transcript_text}</p>
|
|
69
|
+
</details>
|
|
70
|
+
) : null}
|
|
71
|
+
</section>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function BookingDetail({ uid, onChanged, className }: { uid: string; onChanged?: () => void; className?: string }) {
|
|
76
|
+
const q = useG8Query(async (signal) => unwrap<Booking>(await g8Get(`/appointments/bookings/${encodeURIComponent(uid)}`, undefined, { signal })), [uid]);
|
|
77
|
+
const [pending, setPending] = useState<Pending>(null);
|
|
78
|
+
// Each action has its own input: a note is never sent as a cancellation reason, or the reverse.
|
|
79
|
+
const [note, setNote] = useState("");
|
|
80
|
+
const [cancelReason, setCancelReason] = useState("");
|
|
81
|
+
const [declineReason, setDeclineReason] = useState("");
|
|
82
|
+
const base = `/appointments/bookings/${encodeURIComponent(uid)}`;
|
|
83
|
+
const done = () => { setPending(null); void q.refetch(); onChanged?.(); };
|
|
84
|
+
|
|
85
|
+
const confirmBooking = useG8Mutation((accept: boolean) => g8Post(`${base}/confirm`, { confirmed: accept, reason: accept ? undefined : declineReason || undefined }), { onSuccess: done, onError: () => setPending(null) });
|
|
86
|
+
const cancel = useG8Mutation(() => g8Post(`${base}/cancel`, { cancellation_reason: cancelReason || undefined }), { onSuccess: done, onError: () => setPending(null) });
|
|
87
|
+
const addNote = useG8Mutation(() => g8Post(`${base}/notes`, { text: note }), { onSuccess: () => { setNote(""); done(); }, onError: () => setPending(null) });
|
|
88
|
+
|
|
89
|
+
if (q.error) return <ApiError error={q.error} title="Could not load the booking" onRetry={() => void q.refetch()} />;
|
|
90
|
+
if (!q.data) return <Skeleton className="h-40 w-full" aria-label="Loading booking" />;
|
|
91
|
+
const b = q.data;
|
|
92
|
+
const active = b.status !== "cancelled" && b.status !== "rejected";
|
|
93
|
+
const failure = confirmBooking.error ?? cancel.error ?? addNote.error;
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<section className={`space-y-4 ${className ?? ""}`}>
|
|
97
|
+
<h2 className="text-base font-semibold">{b.title || b.event_type_title || "Meeting"}</h2>
|
|
98
|
+
<dl className="grid gap-3 sm:grid-cols-2">
|
|
99
|
+
<Field label="When">{formatDateTime(b.start_time)}</Field>
|
|
100
|
+
<Field label="Status"><span className="capitalize">{b.status}</span></Field>
|
|
101
|
+
<Field label="Host">{b.host_name || b.host_email || "Unassigned"}</Field>
|
|
102
|
+
<Field label="Attendees">{b.attendees.map((a) => `${a.name ?? ""} <${a.email}>`).join(", ") || "None"}</Field>
|
|
103
|
+
{b.meeting_url ? <Field label="Link"><a className="underline" href={b.meeting_url} target="_blank" rel="noreferrer">{b.meeting_url}</a></Field> : null}
|
|
104
|
+
{b.sequence_name ? <Field label="From sequence">{b.sequence_name}</Field> : null}
|
|
105
|
+
{b.cancellation_reason ? <Field label="Cancelled because">{b.cancellation_reason}</Field> : null}
|
|
106
|
+
</dl>
|
|
107
|
+
|
|
108
|
+
{(b.internal_notes ?? []).length > 0 ? (
|
|
109
|
+
<ul className="space-y-1 text-sm" aria-label="Notes">
|
|
110
|
+
{(b.internal_notes ?? []).map((n, i) => (
|
|
111
|
+
<li key={n.id ?? i} className="rounded border p-2">{n.text}</li>
|
|
112
|
+
))}
|
|
113
|
+
</ul>
|
|
114
|
+
) : null}
|
|
115
|
+
{failure ? <ApiError error={failure} title="That action failed" /> : null}
|
|
116
|
+
|
|
117
|
+
{active ? (
|
|
118
|
+
<div className="space-y-2">
|
|
119
|
+
<Textarea aria-label="Note" rows={2} placeholder="Internal note" value={note} onChange={(e) => setNote(e.target.value)} />
|
|
120
|
+
<div className="flex flex-wrap gap-2">
|
|
121
|
+
{b.status === "pending" ? (
|
|
122
|
+
<>
|
|
123
|
+
<Button size="sm" onClick={() => setPending({ kind: "confirm", accept: true })}>Accept</Button>
|
|
124
|
+
<Button size="sm" variant="outline" onClick={() => setPending({ kind: "confirm", accept: false })}>Decline</Button>
|
|
125
|
+
</>
|
|
126
|
+
) : null}
|
|
127
|
+
<Button size="sm" variant="outline" disabled={!note.trim()} onClick={() => setPending({ kind: "note" })}>Add note</Button>
|
|
128
|
+
<Button size="sm" variant="destructive" onClick={() => setPending({ kind: "cancel" })}>Cancel booking</Button>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
) : null}
|
|
132
|
+
|
|
133
|
+
<ConfirmSpendDialog open={pending?.kind === "confirm"} tier="external" title={pending?.kind === "confirm" && pending.accept ? "Accept this booking?" : "Decline this booking?"} description={pending?.kind === "confirm" && !pending.accept ? (<><p>The attendee is emailed the outcome.</p><Textarea aria-label="Decline reason" rows={2} placeholder="Reason (optional)" value={declineReason} onChange={(e) => setDeclineReason(e.target.value)} /></>) : "The attendee is emailed the outcome."} recipients={b.attendees.length || 1} confirming={confirmBooking.loading} onConfirm={() => pending?.kind === "confirm" && void confirmBooking.mutate(pending.accept)} onCancel={() => setPending(null)} />
|
|
134
|
+
<ConfirmSpendDialog open={pending?.kind === "cancel"} tier="destructive" title="Cancel this booking?" description={(<><p>The calendar event is removed and every attendee is emailed.</p><Textarea aria-label="Cancellation reason" rows={2} placeholder="Reason (optional)" value={cancelReason} onChange={(e) => setCancelReason(e.target.value)} /></>)} confirmLabel="Cancel booking" affected={1} confirming={cancel.loading} onConfirm={() => void cancel.mutate(undefined)} onCancel={() => setPending(null)} />
|
|
135
|
+
<ConfirmSpendDialog open={pending?.kind === "note"} tier="external" title="Add this note?" description="Notes can be shared with the attendees through booking workflows." confirmLabel="Add note" confirming={addNote.loading} onConfirm={() => void addNote.mutate(undefined)} onCancel={() => setPending(null)} />
|
|
136
|
+
</section>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function MeetingDetail({ selection, onChanged, className }: MeetingDetailProps) {
|
|
141
|
+
if (!selection.id) return <EmptyState title="Select a meeting" description="Pick one from the list to see its details." />;
|
|
142
|
+
return selection.kind === "booking" ? (
|
|
143
|
+
<BookingDetail key={selection.id} uid={selection.id} onChanged={onChanged} className={className} />
|
|
144
|
+
) : (
|
|
145
|
+
<RecordedDetail key={selection.id} id={selection.id} className={className} />
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MeetingList: booked meetings (Upcoming, Past, Cancelled) and recorded meetings with transcripts.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations, in order (one per view):
|
|
7
|
+
* Upcoming / Past / Cancelled: GET /appointments/bookings?after_start=|before_end=|status=&skip=&take=
|
|
8
|
+
* (appointments.listBookings) read
|
|
9
|
+
* Recorded: GET /inbox/meetings?search=&page=&page_size= (meetings.listMeetings) read
|
|
10
|
+
* Read-only: opening a row is MeetingDetail's job (cancel and notes live there, behind confirm dialogs).
|
|
11
|
+
*
|
|
12
|
+
* A booking is something scheduled through an event type; a recorded meeting is a call that was captured
|
|
13
|
+
* (calendar sync or bot) and may have a transcript and summary. They are different records with different ids.
|
|
14
|
+
*/
|
|
15
|
+
import { useEffect, useMemo, useState } from "react";
|
|
16
|
+
import { Button, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
17
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
18
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
19
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
20
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
21
|
+
import { formatDateTime, listOf, type Booking, type RecordedMeeting } from "../lib/appointments-types";
|
|
22
|
+
|
|
23
|
+
export type MeetingView = "upcoming" | "past" | "cancelled" | "recorded";
|
|
24
|
+
export type MeetingSelection = { kind: "booking"; id: string } | { kind: "meeting"; id: string };
|
|
25
|
+
|
|
26
|
+
export interface MeetingListProps {
|
|
27
|
+
onSelect?: (selection: MeetingSelection) => void;
|
|
28
|
+
initialView?: MeetingView;
|
|
29
|
+
pageSize?: number;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const VIEWS: ReadonlyArray<{ value: MeetingView; label: string }> = [
|
|
34
|
+
{ value: "upcoming", label: "Upcoming" },
|
|
35
|
+
{ value: "past", label: "Past" },
|
|
36
|
+
{ value: "cancelled", label: "Cancelled" },
|
|
37
|
+
{ value: "recorded", label: "Recorded" },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function bookingQuery(view: Exclude<MeetingView, "recorded">, now: string, page: number, take: number) {
|
|
41
|
+
const base = { skip: page * take, take };
|
|
42
|
+
if (view === "upcoming") return { ...base, after_start: now, sort_by: "start_time", sort_order: "asc" };
|
|
43
|
+
if (view === "past") return { ...base, before_end: now, sort_by: "start_time", sort_order: "desc" };
|
|
44
|
+
return { ...base, status: "cancelled", sort_by: "start_time", sort_order: "desc" };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function attendeeNames(b: Booking): string {
|
|
48
|
+
return b.attendees.map((a) => a.name || a.email).join(", ") || "No attendees";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function useDebouncedText(value: string, ms = 300): string {
|
|
52
|
+
const [v, setV] = useState(value);
|
|
53
|
+
useEffect(() => { const t = setTimeout(() => setV(value.trim()), ms); return () => clearTimeout(t); }, [value, ms]);
|
|
54
|
+
return v;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function MeetingList({ onSelect, initialView = "upcoming", pageSize = 20, className }: MeetingListProps) {
|
|
58
|
+
const [view, setView] = useState<MeetingView>(initialView);
|
|
59
|
+
const [page, setPage] = useState(0);
|
|
60
|
+
const [search, setSearch] = useState("");
|
|
61
|
+
const query = useDebouncedText(search);
|
|
62
|
+
const now = useMemo(() => new Date().toISOString(), [view]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
63
|
+
|
|
64
|
+
const rows = useG8Query(
|
|
65
|
+
async (signal) => {
|
|
66
|
+
if (view === "recorded") {
|
|
67
|
+
const env = await g8Get("/inbox/meetings", { search: query || undefined, page: page + 1, page_size: pageSize }, { signal });
|
|
68
|
+
return { kind: "meeting" as const, items: listOf<RecordedMeeting>(env), more: Boolean((env as { pagination?: { has_next?: boolean } }).pagination?.has_next) };
|
|
69
|
+
}
|
|
70
|
+
const env = await g8Get("/appointments/bookings", bookingQuery(view, now, page, pageSize), { signal });
|
|
71
|
+
const items = listOf<Booking>(env, "bookings");
|
|
72
|
+
return { kind: "booking" as const, items, more: items.length === pageSize };
|
|
73
|
+
},
|
|
74
|
+
[view, page, query, pageSize, now],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const switchView = (next: MeetingView) => {
|
|
78
|
+
setView(next);
|
|
79
|
+
setPage(0);
|
|
80
|
+
};
|
|
81
|
+
const result = rows.data;
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className={`flex flex-col gap-3 ${className ?? ""}`}>
|
|
85
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
86
|
+
<div role="group" aria-label="View" className="flex gap-1">
|
|
87
|
+
{VIEWS.map((v) => (
|
|
88
|
+
<Button key={v.value} size="sm" variant={view === v.value ? "default" : "outline"} onClick={() => switchView(v.value)}>
|
|
89
|
+
{v.label}
|
|
90
|
+
</Button>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
{view === "recorded" ? <Input aria-label="Search meetings" className="w-56" placeholder="Search recorded meetings" value={search} onChange={(e) => { setSearch(e.target.value); setPage(0); }} /> : null}
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{rows.error ? (
|
|
97
|
+
<ApiError error={rows.error} title="Could not load meetings" onRetry={() => void rows.refetch()} />
|
|
98
|
+
) : rows.loading && !result ? (
|
|
99
|
+
<div className="space-y-2" aria-busy="true" aria-label="Loading meetings">
|
|
100
|
+
{Array.from({ length: 5 }, (_, i) => (
|
|
101
|
+
<Skeleton key={i} className="h-10 w-full" />
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
) : !result || result.items.length === 0 ? (
|
|
105
|
+
<EmptyState title={`No ${view} meetings`} description={view === "recorded" ? "Meetings appear once a connected calendar or recorder captures them." : "Bookings made through your event types show up here."} />
|
|
106
|
+
) : (
|
|
107
|
+
<Table>
|
|
108
|
+
<TableHeader>
|
|
109
|
+
<TableRow>
|
|
110
|
+
<TableHead>When</TableHead>
|
|
111
|
+
<TableHead>{result.kind === "booking" ? "Meeting" : "Subject"}</TableHead>
|
|
112
|
+
<TableHead>{result.kind === "booking" ? "Attendees" : "Transcript"}</TableHead>
|
|
113
|
+
<TableHead>Status</TableHead>
|
|
114
|
+
</TableRow>
|
|
115
|
+
</TableHeader>
|
|
116
|
+
<TableBody>
|
|
117
|
+
{result.kind === "booking"
|
|
118
|
+
? (result.items as Booking[]).map((b) => (
|
|
119
|
+
<TableRow key={b.uid} className="cursor-pointer" onClick={() => onSelect?.({ kind: "booking", id: b.uid })}>
|
|
120
|
+
<TableCell className="whitespace-nowrap">{formatDateTime(b.start_time)}</TableCell>
|
|
121
|
+
<TableCell className="font-medium">{b.title || b.event_type_title || "Meeting"}</TableCell>
|
|
122
|
+
<TableCell className="max-w-64 truncate">{attendeeNames(b)}</TableCell>
|
|
123
|
+
<TableCell className="capitalize">{b.status}</TableCell>
|
|
124
|
+
</TableRow>
|
|
125
|
+
))
|
|
126
|
+
: (result.items as RecordedMeeting[]).map((m) => (
|
|
127
|
+
<TableRow key={m.id} className="cursor-pointer" onClick={() => onSelect?.({ kind: "meeting", id: m.id })}>
|
|
128
|
+
<TableCell className="whitespace-nowrap">{formatDateTime(m.start_time)}</TableCell>
|
|
129
|
+
<TableCell className="font-medium">{m.subject || "Untitled meeting"}</TableCell>
|
|
130
|
+
<TableCell className="max-w-64 truncate">{m.transcript_summary || "No summary yet"}</TableCell>
|
|
131
|
+
<TableCell className="capitalize">{m.transcript_status ?? "none"}</TableCell>
|
|
132
|
+
</TableRow>
|
|
133
|
+
))}
|
|
134
|
+
</TableBody>
|
|
135
|
+
</Table>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
<div className="flex justify-end gap-2">
|
|
139
|
+
<Button size="sm" variant="outline" disabled={page === 0 || rows.loading} onClick={() => setPage((p) => p - 1)}>
|
|
140
|
+
Previous
|
|
141
|
+
</Button>
|
|
142
|
+
<Button size="sm" variant="outline" disabled={!result?.more || rows.loading} onClick={() => setPage((p) => p + 1)}>
|
|
143
|
+
Next
|
|
144
|
+
</Button>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useAvailableSlots: bookable times for an event type, grouped by local date.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations: GET /appointments/slots?event_type_id=&start=&end=&time_zone= (appointments.getAvailableSlots), read.
|
|
7
|
+
*
|
|
8
|
+
* The window may be at most 60 days. The response is `{ slots: { "YYYY-MM-DD": [{ time, duration }] } }`
|
|
9
|
+
* keyed by date in the requested time zone; past times are never returned.
|
|
10
|
+
*/
|
|
11
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
12
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
13
|
+
import { unwrap, type Slot } from "../lib/appointments-types";
|
|
14
|
+
|
|
15
|
+
export function useAvailableSlots(eventTypeId: number | null | undefined, start: string, end: string, timeZone: string) {
|
|
16
|
+
const query = useG8Query(
|
|
17
|
+
async (signal) => {
|
|
18
|
+
const env = await g8Get(
|
|
19
|
+
"/appointments/slots",
|
|
20
|
+
{ event_type_id: eventTypeId as number, start, end, time_zone: timeZone },
|
|
21
|
+
{ signal },
|
|
22
|
+
);
|
|
23
|
+
return (unwrap<{ slots?: Record<string, Slot[]> }>(env).slots ?? {}) as Record<string, Slot[]>;
|
|
24
|
+
},
|
|
25
|
+
[eventTypeId, start, end, timeZone],
|
|
26
|
+
{ enabled: eventTypeId != null },
|
|
27
|
+
);
|
|
28
|
+
return { ...query, slotsByDate: query.data ?? {} };
|
|
29
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useSchedules: the org's availability schedules (working hours), default first.
|
|
5
|
+
*
|
|
6
|
+
* SDK operations: GET /appointments/schedules (appointments.listSchedules), read tier.
|
|
7
|
+
*/
|
|
8
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
9
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
10
|
+
import { listOf, type Schedule } from "../lib/appointments-types";
|
|
11
|
+
|
|
12
|
+
export function useSchedules() {
|
|
13
|
+
const query = useG8Query(async (signal) => {
|
|
14
|
+
const env = await g8Get("/appointments/schedules", undefined, { signal });
|
|
15
|
+
return listOf<Schedule>(env, "schedules", "items").sort((a, b) => Number(b.is_default) - Number(a.is_default));
|
|
16
|
+
}, []);
|
|
17
|
+
return { ...query, schedules: query.data ?? [] };
|
|
18
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shapes and helpers shared by the appointments components.
|
|
5
|
+
*
|
|
6
|
+
* The appointments endpoints are a thin pass-through of the scheduling engine: several of them return the
|
|
7
|
+
* object itself (`{ id, title, ... }`) rather than the `{ data }` envelope the rest of the API uses. Always read
|
|
8
|
+
* responses through `unwrap()` and lists through `listOf()` so both shapes work.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface EventType {
|
|
12
|
+
id: number;
|
|
13
|
+
title: string;
|
|
14
|
+
slug: string;
|
|
15
|
+
length: number;
|
|
16
|
+
description?: string | null;
|
|
17
|
+
hidden?: boolean;
|
|
18
|
+
requires_confirmation?: boolean;
|
|
19
|
+
minimum_booking_notice?: number;
|
|
20
|
+
schedule?: { id: number; name: string } | null;
|
|
21
|
+
scheduling_type?: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface TimeWindow {
|
|
25
|
+
start: string;
|
|
26
|
+
end: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Availability rule as the API stores it: ISO weekdays (1 = Monday ... 7 = Sunday) and "HH:MM" clock times. */
|
|
30
|
+
export interface AvailabilityRule {
|
|
31
|
+
id?: number;
|
|
32
|
+
days?: number[] | null;
|
|
33
|
+
start_time?: string | null;
|
|
34
|
+
end_time?: string | null;
|
|
35
|
+
/** Present on date overrides ("YYYY-MM-DD"). */
|
|
36
|
+
date?: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Schedule {
|
|
40
|
+
id: number;
|
|
41
|
+
name: string;
|
|
42
|
+
time_zone?: string | null;
|
|
43
|
+
is_default?: boolean;
|
|
44
|
+
availability?: AvailabilityRule[];
|
|
45
|
+
overrides?: AvailabilityRule[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface Attendee {
|
|
49
|
+
id?: number;
|
|
50
|
+
email: string;
|
|
51
|
+
name?: string | null;
|
|
52
|
+
time_zone?: string | null;
|
|
53
|
+
no_show?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface Booking {
|
|
57
|
+
id?: number;
|
|
58
|
+
uid: string;
|
|
59
|
+
title?: string | null;
|
|
60
|
+
start_time?: string | null;
|
|
61
|
+
end_time?: string | null;
|
|
62
|
+
status: string;
|
|
63
|
+
location?: string | null;
|
|
64
|
+
meeting_url?: string | null;
|
|
65
|
+
event_type_title?: string | null;
|
|
66
|
+
host_name?: string | null;
|
|
67
|
+
host_email?: string | null;
|
|
68
|
+
attendees: Attendee[];
|
|
69
|
+
sequence_name?: string | null;
|
|
70
|
+
description?: string | null;
|
|
71
|
+
cancellation_reason?: string | null;
|
|
72
|
+
internal_notes?: Array<{ id?: number; text?: string | null; created_at?: string | null }>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface Slot {
|
|
76
|
+
time: string;
|
|
77
|
+
duration: number;
|
|
78
|
+
attendees_remaining?: number | null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The recorded-meeting summary row from `GET /inbox/meetings`. */
|
|
82
|
+
export interface RecordedMeeting {
|
|
83
|
+
id: string;
|
|
84
|
+
subject?: string | null;
|
|
85
|
+
start_time?: string | null;
|
|
86
|
+
end_time?: string | null;
|
|
87
|
+
organizer_email?: string | null;
|
|
88
|
+
meeting_url?: string | null;
|
|
89
|
+
attendees?: Array<{ email?: string | null; name?: string | null }>;
|
|
90
|
+
transcript_status?: string | null;
|
|
91
|
+
transcript_summary?: string | null;
|
|
92
|
+
key_topics?: string[] | null;
|
|
93
|
+
action_items?: Array<Record<string, unknown>> | null;
|
|
94
|
+
transcript_text?: string | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const WEEKDAYS: ReadonlyArray<{ day: number; label: string }> = [
|
|
98
|
+
{ day: 1, label: "Monday" },
|
|
99
|
+
{ day: 2, label: "Tuesday" },
|
|
100
|
+
{ day: 3, label: "Wednesday" },
|
|
101
|
+
{ day: 4, label: "Thursday" },
|
|
102
|
+
{ day: 5, label: "Friday" },
|
|
103
|
+
{ day: 6, label: "Saturday" },
|
|
104
|
+
{ day: 7, label: "Sunday" },
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
/** The object inside `{ data }`, or the response itself when the endpoint returns the object directly. */
|
|
108
|
+
export function unwrap<T>(env: unknown): T {
|
|
109
|
+
const e = env as { data?: unknown } | null;
|
|
110
|
+
return (e && typeof e === "object" && "data" in e && e.data !== undefined && e.data !== null ? e.data : env) as T;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** An array from a bare array, `{ data: [...] }`, or `{ <key>: [...] }` under any of the given keys. */
|
|
114
|
+
export function listOf<T>(env: unknown, ...keys: string[]): T[] {
|
|
115
|
+
const inner = unwrap<unknown>(env);
|
|
116
|
+
if (Array.isArray(inner)) return inner as T[];
|
|
117
|
+
for (const key of keys) {
|
|
118
|
+
const v = (inner as Record<string, unknown> | null)?.[key];
|
|
119
|
+
if (Array.isArray(v)) return v as T[];
|
|
120
|
+
}
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function slugify(text: string): string {
|
|
125
|
+
return text
|
|
126
|
+
.toLowerCase()
|
|
127
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
128
|
+
.replace(/^-+|-+$/g, "")
|
|
129
|
+
.slice(0, 60);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** "09:00:00" and "9:00" both become "09:00" (the API accepts only HH:MM). */
|
|
133
|
+
export function toHHMM(value: string | null | undefined, fallback = "09:00"): string {
|
|
134
|
+
const m = /^(\d{1,2}):(\d{2})/.exec(value ?? "");
|
|
135
|
+
return m ? `${m[1].padStart(2, "0")}:${m[2]}` : fallback;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function formatDateTime(iso: string | null | undefined, timeZone?: string): string {
|
|
139
|
+
if (!iso) return "";
|
|
140
|
+
const d = new Date(iso);
|
|
141
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
142
|
+
return d.toLocaleString(undefined, { weekday: "short", month: "short", day: "numeric", hour: "numeric", minute: "2-digit", timeZone });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function formatTime(iso: string, timeZone?: string): string {
|
|
146
|
+
return new Date(iso).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit", timeZone });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function localTimeZone(): string {
|
|
150
|
+
try {
|
|
151
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
152
|
+
} catch {
|
|
153
|
+
return "UTC";
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function addMinutes(iso: string, minutes: number): string {
|
|
158
|
+
return new Date(new Date(iso).getTime() + minutes * 60_000).toISOString();
|
|
159
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useIdempotencyKey: one Idempotency-Key per distinct payload.
|
|
5
|
+
*
|
|
6
|
+
* The API remembers a key together with a fingerprint of the request body. Replaying the SAME body under the same
|
|
7
|
+
* key is a safe retry (a double click, a timeout). Reusing the key with a DIFFERENT body is a 409 for as long as
|
|
8
|
+
* the server holds it, and after a 5xx on a send/booking it holds the failure under the key for minutes. So the
|
|
9
|
+
* key must follow the payload: pass a string that changes whenever anything in the request body changes.
|
|
10
|
+
*
|
|
11
|
+
* const key = useIdempotencyKey(JSON.stringify([thread.id, body, to, subject]));
|
|
12
|
+
* g8Post(path, payload, { idempotencyKey: key.get() });
|
|
13
|
+
* // after a success: key.rotate()
|
|
14
|
+
*/
|
|
15
|
+
import { useRef } from "react";
|
|
16
|
+
|
|
17
|
+
export function useIdempotencyKey(fingerprint: string) {
|
|
18
|
+
const ref = useRef({ fingerprint, key: crypto.randomUUID() });
|
|
19
|
+
return {
|
|
20
|
+
/** The key for the current payload; a new one as soon as the payload differs from the last call's. */
|
|
21
|
+
get(): string {
|
|
22
|
+
if (ref.current.fingerprint !== fingerprint) ref.current = { fingerprint, key: crypto.randomUUID() };
|
|
23
|
+
return ref.current.key;
|
|
24
|
+
},
|
|
25
|
+
/** Start a new logical request (call after a success so the next identical payload is a new action). */
|
|
26
|
+
rotate(): void {
|
|
27
|
+
ref.current = { fingerprint, key: crypto.randomUUID() };
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Create a working-hours schedule and a bookable event type
|
|
3
|
+
* @when Setting up scheduling for an org or a new rep. Both writes are low-risk (tier `write`). Days are ISO
|
|
4
|
+
* weekdays (1 = Monday ... 7 = Sunday) and times are "HH:MM". The calendar that blocks busy time must be
|
|
5
|
+
* connected by a human in the graph8 app first (OAuth is not in the API).
|
|
6
|
+
*/
|
|
7
|
+
import { g8 } from "@graph8/sdk";
|
|
8
|
+
|
|
9
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
10
|
+
|
|
11
|
+
// 1. Working hours: Monday to Friday, 09:00 to 17:00.
|
|
12
|
+
const schedule = (await g8.api.appointments.createSchedule({
|
|
13
|
+
body: {
|
|
14
|
+
name: "Sales hours",
|
|
15
|
+
time_zone: "America/New_York",
|
|
16
|
+
is_default: true,
|
|
17
|
+
availability: [{ days: [1, 2, 3, 4, 5], start_time: "09:00", end_time: "17:00" }],
|
|
18
|
+
},
|
|
19
|
+
})) as { id?: number; data?: { id?: number } };
|
|
20
|
+
const scheduleId = schedule.id ?? schedule.data?.id;
|
|
21
|
+
if (!scheduleId) throw new Error("graph8 returned no schedule id");
|
|
22
|
+
|
|
23
|
+
// 2. A 30 minute meeting that uses those hours. Keep the returned id: there is no list-event-types call.
|
|
24
|
+
const eventType = (await g8.api.appointments.createEventType({
|
|
25
|
+
body: { title: "Intro call", slug: "intro-call", length: 30, schedule_id: scheduleId, minimum_booking_notice: 120 },
|
|
26
|
+
})) as { id?: number; data?: { id?: number } };
|
|
27
|
+
console.log("event type id", eventType.id ?? eventType.data?.id);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Find open times and book a meeting for a prospect
|
|
3
|
+
* @when An agent scheduling on behalf of a rep. Booking is tier `external`: it creates a calendar event and
|
|
4
|
+
* emails the attendee, so ask a human to approve the time and the attendee first. Slots come back grouped by
|
|
5
|
+
* date in the requested time zone, never in the past, and at most 60 days ahead.
|
|
6
|
+
*/
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { g8 } from "@graph8/sdk";
|
|
9
|
+
|
|
10
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
11
|
+
|
|
12
|
+
const eventTypeId = Number(process.argv[2]);
|
|
13
|
+
const start = new Date();
|
|
14
|
+
const end = new Date(start.getTime() + 14 * 86_400_000);
|
|
15
|
+
|
|
16
|
+
const res = (await g8.api.appointments.getAvailableSlots({
|
|
17
|
+
query: { event_type_id: eventTypeId, start: start.toISOString(), end: end.toISOString(), time_zone: "America/New_York" },
|
|
18
|
+
})) as { data?: { slots?: Record<string, Array<{ time: string; duration: number }>> } };
|
|
19
|
+
const byDate = res.data?.slots ?? {};
|
|
20
|
+
const firstDate = Object.keys(byDate).sort()[0];
|
|
21
|
+
const slot = firstDate ? byDate[firstDate][0] : undefined;
|
|
22
|
+
if (!slot) throw new Error("No open times in the next 14 days");
|
|
23
|
+
|
|
24
|
+
const booking = await g8.api.appointments.createBooking(
|
|
25
|
+
{
|
|
26
|
+
body: {
|
|
27
|
+
event_type_id: eventTypeId,
|
|
28
|
+
start_time: slot.time,
|
|
29
|
+
attendees: [{ name: "Ada Lovelace", email: "ada@example.com", time_zone: "America/New_York" }],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{ idempotencyKey: randomUUID() },
|
|
33
|
+
);
|
|
34
|
+
console.log(booking);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Upcoming bookings and pending approvals
|
|
3
|
+
* @when A daily digest for a rep or manager. Read-only. `listBookings` pages with `skip` and `take` (not a
|
|
4
|
+
* cursor); the body is `{ bookings: [...], total_count }`.
|
|
5
|
+
*/
|
|
6
|
+
import { g8 } from "@graph8/sdk";
|
|
7
|
+
|
|
8
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
9
|
+
|
|
10
|
+
const now = new Date().toISOString();
|
|
11
|
+
const upcoming = (await g8.api.appointments.listBookings({
|
|
12
|
+
query: { after_start: now, sort_by: "start_time", sort_order: "asc", skip: 0, take: 50 },
|
|
13
|
+
})) as { bookings?: Array<{ uid: string; title?: string | null; start_time?: string | null; status: string }> };
|
|
14
|
+
|
|
15
|
+
const bookings = upcoming.bookings ?? [];
|
|
16
|
+
const pending = bookings.filter((b) => b.status === "pending");
|
|
17
|
+
console.log(`${bookings.length} upcoming, ${pending.length} waiting for the host to confirm`);
|
|
18
|
+
for (const b of bookings) console.log(b.start_time, b.title ?? "Meeting", b.status);
|
|
19
|
+
|
|
20
|
+
// Recorded meetings (calendar-synced or bot-captured calls) are a different record with transcripts.
|
|
21
|
+
const recorded = await g8.api.meetings.listMeetings({ query: { page: 1, page_size: 10 } });
|
|
22
|
+
console.log(`${recorded.data.length} recent recorded meetings`);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Create a calendar invite for an ad-hoc meeting
|
|
3
|
+
* @when You need a shareable invite (title, start, duration, link) without an event type or a booking, for
|
|
4
|
+
* example to attach to an email an agent is drafting. Tier `write`: it creates the invite record; it does not
|
|
5
|
+
* email anyone. `tz` is an IANA zone.
|
|
6
|
+
*/
|
|
7
|
+
import { g8 } from "@graph8/sdk";
|
|
8
|
+
|
|
9
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
10
|
+
|
|
11
|
+
const invite = await g8.api.calendarInvites.createCalendarInvite({
|
|
12
|
+
body: {
|
|
13
|
+
title: "Kickoff with Acme",
|
|
14
|
+
start_at: "2026-11-02T15:00:00Z",
|
|
15
|
+
tz: "America/New_York",
|
|
16
|
+
duration_minutes: 45,
|
|
17
|
+
meeting_link: "https://meet.example.com/acme-kickoff",
|
|
18
|
+
description: "Agenda: goals, timeline, owners.",
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
console.log(invite.data);
|