@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,159 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SeoReport: site audits (score and issues) and rank tracking for a domain.
|
|
5
|
+
*
|
|
6
|
+
* Starting an audit RESERVES credits sized by the page limit and runs in the background, so the screen polls the
|
|
7
|
+
* audit until its state is terminal. Reading audits, issues and rankings is free.
|
|
8
|
+
*
|
|
9
|
+
* SDK operations, in the order the screen uses them:
|
|
10
|
+
* 1. listSeoAudits GET /seo/audits (read)
|
|
11
|
+
* 2. startSeoAudit POST /seo/audits {domain, max_pages} (billable; reserves credits by max_pages)
|
|
12
|
+
* 3. getSeoAudit GET /seo/audits/{id} (read; polled until state.is_terminal)
|
|
13
|
+
* 4. getSeoAuditIssues GET /seo/audits/{id}/issues (read)
|
|
14
|
+
* 5. listSeoTrackedKeywords GET /seo/rank-tracking?domain (read)
|
|
15
|
+
* 6. saveSeoTrackedKeywords POST /seo/rank-tracking/keywords (write)
|
|
16
|
+
* 7. removeSeoTrackedKeywords POST /seo/rank-tracking/keywords/remove (write)
|
|
17
|
+
*
|
|
18
|
+
* Starting an audit goes through ConfirmSpendDialog. The API has no credit-balance read, so the dialog explains
|
|
19
|
+
* the reservation instead of quoting a number.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { useState } from "react";
|
|
23
|
+
import { Button, Card, CardContent, CardHeader, CardTitle, Input, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
24
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
25
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
26
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
27
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
28
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
29
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
30
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
31
|
+
import { asRec, num, rowsOf, shortDate, text } from "../lib/studio";
|
|
32
|
+
import type { Rec } from "../lib/studio";
|
|
33
|
+
|
|
34
|
+
export interface SeoReportProps {
|
|
35
|
+
/** Pre-fill the domain, for example the customer's own site. */
|
|
36
|
+
defaultDomain?: string;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const SEVERITY_ORDER = ["critical", "high", "warning", "medium", "low", "info"];
|
|
41
|
+
const rank = (s: string) => (SEVERITY_ORDER.indexOf(s.toLowerCase()) + 1 || 99);
|
|
42
|
+
|
|
43
|
+
function AuditDetail({ auditId }: { auditId: string }) {
|
|
44
|
+
const poll = useJobPoll<Rec>(async (signal) => asRec((await g8Get(`/seo/audits/${encodeURIComponent(auditId)}`, undefined, { signal })).data), {
|
|
45
|
+
isTerminal: (a) => asRec(a.state).is_terminal === true,
|
|
46
|
+
});
|
|
47
|
+
const issues = useG8Query((signal) => g8Get(`/seo/audits/${encodeURIComponent(auditId)}/issues`, undefined, { signal }), [auditId, poll.done], { enabled: poll.done });
|
|
48
|
+
const audit = poll.status ?? {};
|
|
49
|
+
const state = asRec(audit.state);
|
|
50
|
+
const list = rowsOf<Rec>(asRec(issues.data?.data), "issues").sort((a, b) => rank(text(a.severity)) - rank(text(b.severity)));
|
|
51
|
+
return (
|
|
52
|
+
<Card>
|
|
53
|
+
<CardHeader><CardTitle className="text-sm">{text(audit.domain, "Audit")}</CardTitle></CardHeader>
|
|
54
|
+
<CardContent className="space-y-3">
|
|
55
|
+
{poll.error ? <ApiError error={poll.error} title="Could not read the audit" onRetry={poll.start} /> : null}
|
|
56
|
+
<p className="text-sm" data-testid="audit-state">{text(state.headline, poll.polling ? "Running" : "")}{state.detail ? ` ${text(state.detail)}` : ""}</p>
|
|
57
|
+
{poll.done ? (
|
|
58
|
+
<p className="text-sm">On-page score <strong>{num(audit.onpage_score) ?? "-"}</strong> across {num(audit.pages_crawled) ?? 0} pages.</p>
|
|
59
|
+
) : <Skeleton className="h-6 w-48" />}
|
|
60
|
+
{issues.error ? <ApiError error={issues.error} onRetry={() => void issues.refetch()} /> : null}
|
|
61
|
+
{poll.done && !issues.loading && list.length === 0 && !issues.error ? <p className="text-muted-foreground text-sm">No issues found.</p> : null}
|
|
62
|
+
{list.length ? (
|
|
63
|
+
<Table>
|
|
64
|
+
<TableHeader><TableRow><TableHead>Issue</TableHead><TableHead>Severity</TableHead><TableHead className="text-right">Affected pages</TableHead></TableRow></TableHeader>
|
|
65
|
+
<TableBody>
|
|
66
|
+
{list.map((i) => (
|
|
67
|
+
<TableRow key={text(i.issue_type)}><TableCell>{text(i.issue_type).replace(/_/g, " ")}</TableCell><TableCell className="capitalize">{text(i.severity)}</TableCell><TableCell className="text-right">{num(i.affected_count) ?? 0}</TableCell></TableRow>
|
|
68
|
+
))}
|
|
69
|
+
</TableBody>
|
|
70
|
+
</Table>
|
|
71
|
+
) : null}
|
|
72
|
+
</CardContent>
|
|
73
|
+
</Card>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function Keywords({ domain }: { domain: string }) {
|
|
78
|
+
const [kw, setKw] = useState("");
|
|
79
|
+
const list = useG8Query((signal) => g8Get("/seo/rank-tracking", { domain: domain || undefined }, { signal }), [domain]);
|
|
80
|
+
const add = useG8Mutation(() => g8Post("/seo/rank-tracking/keywords", { domain, keywords: kw.split(",").map((k) => k.trim()).filter(Boolean).map((keyword) => ({ keyword })) }), { onSuccess: () => { setKw(""); void list.refetch(); } });
|
|
81
|
+
const remove = useG8Mutation((id: string) => g8Post("/seo/rank-tracking/keywords/remove", { keyword_ids: [id] }), { onSuccess: () => void list.refetch() });
|
|
82
|
+
const rows = rowsOf<Rec>(list.data?.data);
|
|
83
|
+
return (
|
|
84
|
+
<Card>
|
|
85
|
+
<CardHeader><CardTitle className="text-sm">Tracked keywords</CardTitle></CardHeader>
|
|
86
|
+
<CardContent className="space-y-3">
|
|
87
|
+
<div className="flex gap-2">
|
|
88
|
+
<Input aria-label="Keywords" placeholder="Keywords, comma separated" value={kw} onChange={(e) => setKw(e.target.value)} />
|
|
89
|
+
<Button disabled={!domain || !kw.trim()} loading={add.loading} onClick={() => void add.mutate(undefined)}>Track</Button>
|
|
90
|
+
</div>
|
|
91
|
+
{list.error ? <ApiError error={list.error} onRetry={() => void list.refetch()} /> : null}
|
|
92
|
+
{add.error ? <ApiError error={add.error} title="Could not track those keywords" /> : null}
|
|
93
|
+
{remove.error ? <ApiError error={remove.error} title="Could not stop tracking" /> : null}
|
|
94
|
+
{!list.loading && !list.error && rows.length === 0 ? <EmptyState title="No keywords tracked" description="Add keywords to see where this domain ranks." /> : null}
|
|
95
|
+
{rows.length ? (
|
|
96
|
+
<Table>
|
|
97
|
+
<TableHeader><TableRow><TableHead>Keyword</TableHead><TableHead>Rank</TableHead><TableHead>Volume</TableHead><TableHead>Checked</TableHead><TableHead /></TableRow></TableHeader>
|
|
98
|
+
<TableBody>
|
|
99
|
+
{rows.map((r) => (
|
|
100
|
+
<TableRow key={text(r.id)}>
|
|
101
|
+
<TableCell>{text(r.keyword)}</TableCell><TableCell>{num(r.latest_rank) ?? "-"}</TableCell><TableCell>{num(r.search_volume) ?? "-"}</TableCell><TableCell>{shortDate(r.latest_checked_at)}</TableCell>
|
|
102
|
+
<TableCell className="text-right"><Button size="sm" variant="ghost" aria-label={`Stop tracking ${text(r.keyword)}`} onClick={() => void remove.mutate(text(r.id))}>Remove</Button></TableCell>
|
|
103
|
+
</TableRow>
|
|
104
|
+
))}
|
|
105
|
+
</TableBody>
|
|
106
|
+
</Table>
|
|
107
|
+
) : null}
|
|
108
|
+
</CardContent>
|
|
109
|
+
</Card>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function SeoReport({ defaultDomain = "", className }: SeoReportProps) {
|
|
114
|
+
const [domain, setDomain] = useState(defaultDomain);
|
|
115
|
+
const [maxPages, setMaxPages] = useState(100);
|
|
116
|
+
const [confirm, setConfirm] = useState(false);
|
|
117
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
118
|
+
const audits = useG8Query((signal) => g8Get("/seo/audits", { limit: 10 }, { signal }), []);
|
|
119
|
+
const start = useG8Mutation(() => g8Post("/seo/audits", { domain: domain.trim(), max_pages: maxPages }), {
|
|
120
|
+
onSuccess: (res) => {
|
|
121
|
+
setConfirm(false);
|
|
122
|
+
setSelected(text(asRec(res.data).id) || null);
|
|
123
|
+
void audits.refetch();
|
|
124
|
+
},
|
|
125
|
+
onError: () => setConfirm(false),
|
|
126
|
+
});
|
|
127
|
+
const rows = rowsOf<Rec>(audits.data?.data);
|
|
128
|
+
return (
|
|
129
|
+
<div className={className}>
|
|
130
|
+
<div className="mb-4 flex flex-wrap items-end gap-2">
|
|
131
|
+
<Input aria-label="Domain" placeholder="example.com" value={domain} onChange={(e) => setDomain(e.target.value)} className="w-64" />
|
|
132
|
+
<Input aria-label="Max pages" type="number" min={1} value={maxPages} onChange={(e) => setMaxPages(Math.max(1, Number(e.target.value) || 1))} className="w-28" />
|
|
133
|
+
<Button disabled={!domain.trim()} onClick={() => setConfirm(true)}>Run audit</Button>
|
|
134
|
+
</div>
|
|
135
|
+
{start.error ? <ApiError error={start.error} title="Could not start the audit" /> : null}
|
|
136
|
+
{audits.error ? <ApiError error={audits.error} onRetry={() => void audits.refetch()} /> : null}
|
|
137
|
+
{audits.loading && !audits.data ? <Skeleton className="mb-4 h-16 w-full" /> : null}
|
|
138
|
+
{!audits.loading && !audits.error && rows.length === 0 ? <EmptyState title="No audits yet" description="Run an audit to see your on-page score and what to fix first." /> : null}
|
|
139
|
+
{rows.length ? (
|
|
140
|
+
<div className="mb-4 flex flex-wrap gap-2">
|
|
141
|
+
{rows.map((a) => (
|
|
142
|
+
<Button key={text(a.id)} size="sm" variant={selected === text(a.id) ? "default" : "outline"} onClick={() => setSelected(text(a.id))}>
|
|
143
|
+
{text(a.domain)} · {shortDate(a.created_at)}
|
|
144
|
+
</Button>
|
|
145
|
+
))}
|
|
146
|
+
</div>
|
|
147
|
+
) : null}
|
|
148
|
+
<div className="space-y-4">
|
|
149
|
+
{selected ? <AuditDetail key={selected} auditId={selected} /> : null}
|
|
150
|
+
<Keywords domain={domain.trim()} />
|
|
151
|
+
</div>
|
|
152
|
+
{confirm ? (
|
|
153
|
+
<ConfirmSpendDialog open tier="billable" title={`Audit ${domain.trim()} (up to ${maxPages} pages)`}
|
|
154
|
+
description="Credits are reserved up front, sized by the page limit, and the unused part is released when the crawl completes."
|
|
155
|
+
confirmLabel="Run audit" confirming={start.loading} onConfirm={() => void start.mutate(undefined)} onCancel={() => setConfirm(false)} />
|
|
156
|
+
) : null}
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Small helpers for the Studio payloads the pages block reads.
|
|
5
|
+
*
|
|
6
|
+
* Studio responses are open-shaped: a list comes back as `{ items, <named key>, total }` (the original key,
|
|
7
|
+
* for example `landing_pages`, is kept AND mirrored onto `items`), a single record as a plain object.
|
|
8
|
+
* Nothing here knows more than that, so a field the API adds later never breaks a screen.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type Rec = Record<string, unknown>;
|
|
12
|
+
|
|
13
|
+
export function asRec(value: unknown): Rec {
|
|
14
|
+
return value && typeof value === "object" && !Array.isArray(value) ? (value as Rec) : {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The rows of a list payload: a bare array, or the first of `keys` (then `items`) that holds one. */
|
|
18
|
+
export function rowsOf<T = Rec>(data: unknown, ...keys: string[]): T[] {
|
|
19
|
+
if (Array.isArray(data)) return data as T[];
|
|
20
|
+
const rec = asRec(data);
|
|
21
|
+
for (const key of [...keys, "items"]) {
|
|
22
|
+
if (Array.isArray(rec[key])) return rec[key] as T[];
|
|
23
|
+
}
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function text(value: unknown, fallback = ""): string {
|
|
28
|
+
return typeof value === "string" ? value : value == null ? fallback : String(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function num(value: unknown): number | undefined {
|
|
32
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function shortDate(value: unknown): string {
|
|
36
|
+
const raw = text(value);
|
|
37
|
+
if (!raw) return "";
|
|
38
|
+
const date = new Date(raw);
|
|
39
|
+
return Number.isNaN(date.getTime()) ? raw : date.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface LandingPageRow {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
slug?: string | null;
|
|
46
|
+
status: string;
|
|
47
|
+
generation_mode?: string | null;
|
|
48
|
+
published_url?: string | null;
|
|
49
|
+
published_at?: string | null;
|
|
50
|
+
section_count?: number;
|
|
51
|
+
updated_at?: string | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface LandingSection {
|
|
55
|
+
type?: string;
|
|
56
|
+
content?: Rec;
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ContentPageRow {
|
|
61
|
+
id: string;
|
|
62
|
+
url: string;
|
|
63
|
+
domain?: string;
|
|
64
|
+
title?: string | null;
|
|
65
|
+
content_type?: string | null;
|
|
66
|
+
content_status?: string | null;
|
|
67
|
+
primary_keyword?: string | null;
|
|
68
|
+
seo_score?: number | null;
|
|
69
|
+
word_count?: number | null;
|
|
70
|
+
is_owned?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Where a publish/unpublish response keeps the live URL (the API names it differently per route). */
|
|
74
|
+
export function liveUrl(data: unknown): string {
|
|
75
|
+
const rec = asRec(data);
|
|
76
|
+
return text(rec.published_url) || text(rec.url) || text(asRec(rec.landing_page).published_url);
|
|
77
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Run an SEO site audit and list what to fix first
|
|
3
|
+
* @when The customer wants an on-page score for a domain. Starting an audit RESERVES credits sized by `max_pages`
|
|
4
|
+
* (billable tier), so confirm the page limit with the person. The crawl is asynchronous: poll until the audit state
|
|
5
|
+
* is terminal, then read the issues.
|
|
6
|
+
*/
|
|
7
|
+
import { g8 } from "@graph8/sdk";
|
|
8
|
+
|
|
9
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
10
|
+
|
|
11
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
|
|
13
|
+
export async function auditSite(domain: string, maxPages = 100): Promise<Array<{ issue: string; severity: string; pages: number }>> {
|
|
14
|
+
const started = await g8.api.seo.startSeoAudit({ body: { domain, max_pages: maxPages } });
|
|
15
|
+
const auditId = started.data.id;
|
|
16
|
+
|
|
17
|
+
for (let attempt = 0; attempt < 60; attempt++) {
|
|
18
|
+
const audit = await g8.api.seo.getSeoAudit({ path: { audit_id: auditId } });
|
|
19
|
+
if (audit.data.state.is_terminal) {
|
|
20
|
+
if (audit.data.state.category !== "complete") throw new Error(`${audit.data.state.headline}: ${audit.data.state.detail}`);
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
await sleep(Math.min(30_000, 2_000 * 1.5 ** attempt));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const issues = await g8.api.seo.getSeoAuditIssues({ path: { audit_id: auditId } });
|
|
27
|
+
return issues.data.issues.map((i) => ({ issue: i.issue_type, severity: i.severity, pages: i.affected_count }));
|
|
28
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Create a landing page, edit it, and publish it
|
|
3
|
+
* @when The customer asks for a new page. Creating with `generate: false` is free and gives an empty page; with
|
|
4
|
+
* `generate: true` (the API default) the AI writes the whole page and SPENDS credits. Publishing puts the page on a
|
|
5
|
+
* public URL (external tier): confirm with the person first, or let them click Publish in PageEditor.
|
|
6
|
+
*/
|
|
7
|
+
import { g8 } from "@graph8/sdk";
|
|
8
|
+
|
|
9
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
10
|
+
|
|
11
|
+
export async function createAndPublishPage(name: string, opts: { generate: boolean; publish: boolean }): Promise<string | undefined> {
|
|
12
|
+
// 1. Create. The new page's id is in the response; every later call needs it.
|
|
13
|
+
const created = await g8.api.landingPages.createLandingPage({ body: { name, generate: opts.generate } });
|
|
14
|
+
const pageId = String((created.data as Record<string, unknown>).id);
|
|
15
|
+
|
|
16
|
+
// 2. Read it back. `sections` is an array for sections-mode pages and null for freeform pages.
|
|
17
|
+
const page = await g8.api.landingPages.getLandingPage({ path: { landing_page_id: pageId } });
|
|
18
|
+
const sections = (page.data as Record<string, unknown>).sections;
|
|
19
|
+
console.log(`page ${pageId} has ${Array.isArray(sections) ? sections.length : 0} sections`);
|
|
20
|
+
|
|
21
|
+
if (!opts.publish) return undefined;
|
|
22
|
+
|
|
23
|
+
// 3. Publish. Only after the person agreed: the page is public as soon as this returns.
|
|
24
|
+
const live = await g8.api.landingPages.publishLandingPage({ path: { landing_page_id: pageId } });
|
|
25
|
+
return String((live.data as Record<string, unknown>).published_url ?? "");
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Change the text of one section of a landing page
|
|
3
|
+
* @when A person (or the agent) wants a headline or button label changed without re-running AI. The section
|
|
4
|
+
* content is REPLACED, so read the section first and send back the whole object with your change merged in.
|
|
5
|
+
*/
|
|
6
|
+
import { g8 } from "@graph8/sdk";
|
|
7
|
+
|
|
8
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
9
|
+
|
|
10
|
+
export async function editSectionText(pageId: string, sectionIdx: number, field: string, value: string): Promise<void> {
|
|
11
|
+
const page = await g8.api.landingPages.getLandingPage({ path: { landing_page_id: pageId } });
|
|
12
|
+
const sections = (page.data as { sections?: Array<{ content?: Record<string, unknown> }> | null }).sections;
|
|
13
|
+
if (!sections) throw new Error("Freeform page: it has no sections. Use the HTML edit routes instead.");
|
|
14
|
+
const current = sections[sectionIdx]?.content;
|
|
15
|
+
if (!current) throw new Error(`Page ${pageId} has no section ${sectionIdx}`);
|
|
16
|
+
|
|
17
|
+
await g8.api.landingPages.updateLandingPageSection({
|
|
18
|
+
path: { landing_page_id: pageId, section_idx: sectionIdx },
|
|
19
|
+
body: { content: { ...current, [field]: value } },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Every edit is snapshotted; this is how to find the version to restore if the change was wrong.
|
|
23
|
+
const versions = await g8.api.landingPages.listLandingPageVersions({ path: { landing_page_id: pageId } });
|
|
24
|
+
console.log(JSON.stringify(versions.data).slice(0, 400));
|
|
25
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Re-scan the brand kit from the customer's website
|
|
3
|
+
* @when Brand colors, fonts or logo look stale, or the customer changed their site. The scan is asynchronous: the
|
|
4
|
+
* overview reports `status: "generating"` until it finishes. Passing a different `website_url` REPLACES the stored
|
|
5
|
+
* design tokens, so only do that when the person asked to switch sites.
|
|
6
|
+
*/
|
|
7
|
+
import { g8 } from "@graph8/sdk";
|
|
8
|
+
|
|
9
|
+
g8.init({ apiKey: process.env.G8_API_KEY! });
|
|
10
|
+
|
|
11
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
|
|
13
|
+
export async function refreshBrandKit(websiteUrl?: string): Promise<Record<string, unknown>> {
|
|
14
|
+
await g8.api.brandKit.triggerBrandExtraction({ body: websiteUrl ? { website_url: websiteUrl } : undefined });
|
|
15
|
+
|
|
16
|
+
for (let attempt = 0; attempt < 40; attempt++) {
|
|
17
|
+
const overview = (await g8.api.brandKit.getBrandKitOverview({})).data as Record<string, unknown>;
|
|
18
|
+
if (overview.status !== "generating") return overview;
|
|
19
|
+
await sleep(Math.min(15_000, 2_000 * 1.4 ** attempt));
|
|
20
|
+
}
|
|
21
|
+
throw new Error("Brand scan still running after about 5 minutes; check again later.");
|
|
22
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# quotes: quotes & e-sign
|
|
2
|
+
|
|
3
|
+
Quote list, a draft builder, a quotable-product picker, a quote viewer that sends for signature, and signature status.
|
|
4
|
+
Install with `npx @graph8/sdk add quotes`.
|
|
5
|
+
|
|
6
|
+
| Component | What it is |
|
|
7
|
+
|---|---|
|
|
8
|
+
| `QuoteList` | Quotes with status filter and paging; scope with `dealId` or `companyId`. |
|
|
9
|
+
| `QuoteBuilder` | Create or edit a draft: details, signer search, line items, live totals, lists what is missing. |
|
|
10
|
+
| `ProductPicker` | Quotable products (Stripe + manual) and their prices; returns a ready line item. |
|
|
11
|
+
| `QuoteViewer` | One quote with totals and status-based actions: send/resend, edit as draft, duplicate, delete. |
|
|
12
|
+
| `SignatureStatus` | Envelope state, signers, activity, reminder. |
|
|
13
|
+
|
|
14
|
+
## Flow, in order (and the ids passed between steps)
|
|
15
|
+
|
|
16
|
+
1. `GET /quotable-products` -> items with `prices[]`. A line item needs `stripe_product_id` + `stripe_price_id`, or `manual_product_id`
|
|
17
|
+
copied from here. The API refuses non-quotable products. Tiered Stripe prices are not offered.
|
|
18
|
+
2. `GET /quote-contacts?q&company_id&deal_id` -> contacts with `id`, `email`, `name` (a picker LABEL, may be an email) and `person_name` (the real name or null). Use `person_name` as the signer name, never `name`. Or give `signer_email`.
|
|
19
|
+
3. `POST /quotes` with `deal_id` (from the **deals** block), `title`, `contract_start_date`, a signer, and `line_items` (amounts in **cents**) -> `data.id` = quote id.
|
|
20
|
+
Required fields are checked in the form before the call, so the agent does not hit a 422.
|
|
21
|
+
4. `PATCH /quotes/{id}` edits a **draft** only. A sent quote is changed with `POST /quotes/{id}/edit-as-draft` (`sent` status only; voids the sent copy and returns `new_quote_id`).
|
|
22
|
+
5. `POST /quotes/{id}/send` emails the signer a signing link (**external**, confirmed). Sending a `sent`/`viewed` quote resends the same link.
|
|
23
|
+
6. `GET /quotes/{id}` carries `envelope` (`sent | viewed | signed | declined`), `signers`, audit events. `POST /envelopes/{envelope.id}/resend` is the reminder (**external**, confirmed).
|
|
24
|
+
|
|
25
|
+
## Mounting
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
<QuoteList dealId={dealId} onOpen={(id) => router.push(`/quotes/${id}`)} onNew={() => router.push(`/quotes/new?deal=${dealId}`)} />
|
|
29
|
+
<QuoteBuilder dealId={dealId} onSaved={(id) => router.push(`/quotes/${id}`)} />
|
|
30
|
+
<QuoteViewer quoteId={id} onEdit={(q) => router.push(`/quotes/${q}/edit`)} onOpenQuote={(q) => router.push(`/quotes/${q}`)} />
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Prerequisites
|
|
34
|
+
|
|
35
|
+
- `foundation` installed (and `deals`, which supplies the `dealId`); proxy `allowedPrefixes` include `quotes`, `quotable-products`, `quote-contacts`, `envelopes`.
|
|
36
|
+
- `where: "api"`: at least one quotable product.
|
|
37
|
+
- `where: "app"`: quote settings (company legal name, terms, sender) filled in; Stripe connected for Stripe products and subscriptions.
|
|
38
|
+
|
|
39
|
+
## Tiers and what is confirmed
|
|
40
|
+
|
|
41
|
+
Quote writes (`createQuote`, `updateQuotePatch`, `duplicateQuote`) are tiered `external` in the contract because the same family sends quotes, but they only store a
|
|
42
|
+
draft. The builder therefore saves in one click, and the confirmation sits on the steps that reach the customer or destroy data: **send / resend, edit as draft, reminder** (external)
|
|
43
|
+
and **delete** (destructive). From code, never call `sendQuote` without the person's agreement.
|
|
44
|
+
|
|
45
|
+
## Defaults the agent should use without asking
|
|
46
|
+
|
|
47
|
+
- Net 30, USD, 25 rows per page, the first flat price of a product.
|
|
48
|
+
|
|
49
|
+
## Ask the person first
|
|
50
|
+
|
|
51
|
+
- **Sending or resending** any quote, and **editing a sent quote** (it voids the signing link).
|
|
52
|
+
|
|
53
|
+
## Known limits
|
|
54
|
+
|
|
55
|
+
- Accept, expire and revoke (`markQuoteAccepted`, `expireQuote`, `revokeQuote`), quote templates, quote settings writes and manual-product CRUD are not wired into screens.
|
|
56
|
+
- The `signatures.*` operations are EMAIL signatures, unrelated to e-signature of quotes; this block uses `eSignature.*`.
|
|
57
|
+
- There is no webhook for "quote signed" here; poll the envelope (see `track-quote-signature.ts`).
|
|
58
|
+
|
|
59
|
+
## Operations
|
|
60
|
+
|
|
61
|
+
| SDK method (`g8.api.*`) | Route | Tier | Scope |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| `listQuotes` | GET `/api/v1/quotes` | read | `quotes:read` |
|
|
64
|
+
| `getQuote` | GET `/api/v1/quotes/{quote_id}` | read | `quotes:read` |
|
|
65
|
+
| `searchQuoteContacts` | GET `/api/v1/quote-contacts` | read | `quotes:read` |
|
|
66
|
+
| `createQuote` | POST `/api/v1/quotes` | external | `quotes:run` |
|
|
67
|
+
| `updateQuotePatch` | PATCH `/api/v1/quotes/{quote_id}` | external | `quotes:run` |
|
|
68
|
+
| `listQuotableProducts` | GET `/api/v1/quotable-products` | read | `quotes:read` |
|
|
69
|
+
| `sendQuote` | POST `/api/v1/quotes/{quote_id}/send` | external | `quotes:run` |
|
|
70
|
+
| `editQuoteAsDraft` | POST `/api/v1/quotes/{quote_id}/edit-as-draft` | external | `quotes:run` |
|
|
71
|
+
| `duplicateQuote` | POST `/api/v1/quotes/{quote_id}/duplicate` | external | `quotes:run` |
|
|
72
|
+
| `deleteQuote` | DELETE `/api/v1/quotes/{quote_id}` | destructive | `quotes:delete` |
|
|
73
|
+
| `resendEnvelope` | POST `/api/v1/envelopes/{envelope_id}/resend` | external | `quotes:run` |
|
|
74
|
+
|
|
75
|
+
## Recipes
|
|
76
|
+
|
|
77
|
+
- `recipes/create-and-send-quote.ts`: Create a quote from quotable products and send it for signature
|
|
78
|
+
- `recipes/track-quote-signature.ts`: Wait for a quote to be signed (or declined)
|
|
79
|
+
- `recipes/revise-sent-quote.ts`: Change a quote that was already sent
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../block.schema.json",
|
|
3
|
+
"id": "quotes",
|
|
4
|
+
"title": "Quotes & e-sign",
|
|
5
|
+
"summary": "Quote list, a draft builder with a quotable-product picker, a quote viewer that sends for signature, and signature status.",
|
|
6
|
+
"features": [
|
|
7
|
+
"quotes list with status filter",
|
|
8
|
+
"draft builder with live totals and signer search",
|
|
9
|
+
"quotable product picker (Stripe and manual products)",
|
|
10
|
+
"send, resend and edit-as-draft behind confirms",
|
|
11
|
+
"e-signature status with reminder"
|
|
12
|
+
],
|
|
13
|
+
"dependsOn": [
|
|
14
|
+
"deals"
|
|
15
|
+
],
|
|
16
|
+
"operations": [
|
|
17
|
+
"list_quotes_quotes_get",
|
|
18
|
+
"get_quote_quotes__quote_id__get",
|
|
19
|
+
"search_quote_contacts_quote_contacts_get",
|
|
20
|
+
"create_quote_quotes_post",
|
|
21
|
+
"update_quote_quotes__quote_id__patch",
|
|
22
|
+
"list_quotable_products_quotable_products_get",
|
|
23
|
+
"send_quote_quotes__quote_id__send_post",
|
|
24
|
+
"edit_quote_as_draft_quotes__quote_id__edit_as_draft_post",
|
|
25
|
+
"duplicate_quote_quotes__quote_id__duplicate_post",
|
|
26
|
+
"delete_quote_quotes__quote_id__delete",
|
|
27
|
+
"resend_envelope_envelopes__envelope_id__resend_post"
|
|
28
|
+
],
|
|
29
|
+
"components": [
|
|
30
|
+
{
|
|
31
|
+
"name": "QuoteList",
|
|
32
|
+
"file": "components/quote-list.tsx",
|
|
33
|
+
"purpose": "Quotes with a status filter, totals in the quote currency, and paging; optionally scoped to a deal or company.",
|
|
34
|
+
"operations": [
|
|
35
|
+
"list_quotes_quotes_get"
|
|
36
|
+
],
|
|
37
|
+
"tiers": [
|
|
38
|
+
"read"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "QuoteBuilder",
|
|
43
|
+
"file": "components/quote-builder.tsx",
|
|
44
|
+
"purpose": "Create or edit a draft quote: details, signer search, line items from the product picker, live totals; lists what is still missing.",
|
|
45
|
+
"operations": [
|
|
46
|
+
"get_quote_quotes__quote_id__get",
|
|
47
|
+
"search_quote_contacts_quote_contacts_get",
|
|
48
|
+
"create_quote_quotes_post",
|
|
49
|
+
"update_quote_quotes__quote_id__patch"
|
|
50
|
+
],
|
|
51
|
+
"tiers": [
|
|
52
|
+
"read",
|
|
53
|
+
"external"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "ProductPicker",
|
|
58
|
+
"file": "components/product-picker.tsx",
|
|
59
|
+
"purpose": "Pick a quotable product and price (Stripe or manual) and get back a ready line item.",
|
|
60
|
+
"operations": [
|
|
61
|
+
"list_quotable_products_quotable_products_get"
|
|
62
|
+
],
|
|
63
|
+
"tiers": [
|
|
64
|
+
"read"
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"name": "QuoteViewer",
|
|
69
|
+
"file": "components/quote-viewer.tsx",
|
|
70
|
+
"purpose": "Read a quote with its totals and actions by status: send or resend for signature, edit as draft, duplicate, delete.",
|
|
71
|
+
"operations": [
|
|
72
|
+
"get_quote_quotes__quote_id__get",
|
|
73
|
+
"send_quote_quotes__quote_id__send_post",
|
|
74
|
+
"edit_quote_as_draft_quotes__quote_id__edit_as_draft_post",
|
|
75
|
+
"duplicate_quote_quotes__quote_id__duplicate_post",
|
|
76
|
+
"delete_quote_quotes__quote_id__delete"
|
|
77
|
+
],
|
|
78
|
+
"tiers": [
|
|
79
|
+
"read",
|
|
80
|
+
"external",
|
|
81
|
+
"destructive"
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "SignatureStatus",
|
|
86
|
+
"file": "components/signature-status.tsx",
|
|
87
|
+
"purpose": "E-signature state of a quote: sent, viewed, signed or declined, who still has to sign, activity, and a reminder behind a confirm.",
|
|
88
|
+
"operations": [
|
|
89
|
+
"get_quote_quotes__quote_id__get",
|
|
90
|
+
"resend_envelope_envelopes__envelope_id__resend_post"
|
|
91
|
+
],
|
|
92
|
+
"tiers": [
|
|
93
|
+
"read",
|
|
94
|
+
"external"
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
"hooks": [
|
|
99
|
+
{
|
|
100
|
+
"name": "useDebounced",
|
|
101
|
+
"file": "hooks/use-debounced.ts",
|
|
102
|
+
"purpose": "Debounce a fast-changing value (the signer search box)."
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
"lib": [
|
|
106
|
+
{
|
|
107
|
+
"file": "lib/quotes.ts",
|
|
108
|
+
"purpose": "Line-item math in cents, product and line types, request line mapping, status labels."
|
|
109
|
+
}
|
|
110
|
+
],
|
|
111
|
+
"recipes": [
|
|
112
|
+
{
|
|
113
|
+
"file": "recipes/create-and-send-quote.ts",
|
|
114
|
+
"title": "Create a quote from quotable products and send it for signature",
|
|
115
|
+
"when": "A deal is ready for paper; products must be quotable, amounts are cents, sending emails the signer (external)."
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"file": "recipes/track-quote-signature.ts",
|
|
119
|
+
"title": "Wait for a quote to be signed (or declined)",
|
|
120
|
+
"when": "After sending, when the next step depends on the signature; poll the envelope status."
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"file": "recipes/revise-sent-quote.ts",
|
|
124
|
+
"title": "Change a quote that was already sent",
|
|
125
|
+
"when": "Terms changed after sending; edit-as-draft voids the sent copy, then edit and resend (both external)."
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
"prerequisites": [
|
|
129
|
+
{
|
|
130
|
+
"id": "quotable-products",
|
|
131
|
+
"description": "At least one product is marked quotable (a Stripe product opted in, or a manual product); quotes can only contain quotable products.",
|
|
132
|
+
"where": "api",
|
|
133
|
+
"verify": "GET /quotable-products returns at least one item with a flat price."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"id": "quote-settings",
|
|
137
|
+
"description": "Quote settings (company legal details, terms, sender identity) are filled in, so sent quotes carry the right branding and terms.",
|
|
138
|
+
"where": "app",
|
|
139
|
+
"verify": "GET /quote-settings returns your company name and terms."
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"id": "stripe-connected",
|
|
143
|
+
"description": "Stripe is connected if quotes should carry Stripe products or turn into subscriptions when accepted.",
|
|
144
|
+
"where": "app",
|
|
145
|
+
"verify": "GET /stripe/integration-status reports connected."
|
|
146
|
+
}
|
|
147
|
+
],
|
|
148
|
+
"scopes": [
|
|
149
|
+
"quotes:delete",
|
|
150
|
+
"quotes:read",
|
|
151
|
+
"quotes:run"
|
|
152
|
+
],
|
|
153
|
+
"defaults": [
|
|
154
|
+
{
|
|
155
|
+
"decision": "Saving a draft",
|
|
156
|
+
"default": "One click, no confirmation: a draft emails nobody",
|
|
157
|
+
"askHuman": false
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"decision": "Sending or resending a quote",
|
|
161
|
+
"default": "Always confirmed in the UI; from code, only after the person agreed",
|
|
162
|
+
"askHuman": true
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"decision": "Payment terms",
|
|
166
|
+
"default": "net 30",
|
|
167
|
+
"askHuman": false
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"decision": "Currency",
|
|
171
|
+
"default": "USD unless the deal or products say otherwise",
|
|
172
|
+
"askHuman": false
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"decision": "Where the deal id comes from",
|
|
176
|
+
"default": "The deals block (PipelineBoard / DealDetail pass dealId); never typed by hand",
|
|
177
|
+
"askHuman": false
|
|
178
|
+
}
|
|
179
|
+
],
|
|
180
|
+
"peerDependencies": {
|
|
181
|
+
"react": ">=18"
|
|
182
|
+
}
|
|
183
|
+
}
|