@graph8/sdk 0.362.0 → 0.373.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 +33 -10
- package/blocks/_foundation/README.md +78 -1
- package/blocks/_foundation/block.json +123 -6
- package/blocks/_foundation/components/api-error.tsx +9 -7
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +2 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +93 -2
- package/blocks/agents/block.json +343 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +29 -10
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +119 -0
- package/blocks/agents/components/agent-runs.tsx +19 -10
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +226 -0
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +222 -0
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +38 -11
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +33 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +4 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +38 -18
- package/blocks/appointments/components/meetings-screen.tsx +7 -4
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/appointments/components/scheduling-screen.tsx +2 -1
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +17 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +17 -6
- package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +163 -0
- package/blocks/campaigns/components/icp-editor.tsx +22 -17
- package/blocks/campaigns/components/persona-detail.tsx +181 -0
- package/blocks/campaigns/components/persona-editor.tsx +19 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +161 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
- package/blocks/crm/README.md +34 -0
- package/blocks/crm/block.json +107 -2
- package/blocks/crm/components/companies-screen.tsx +359 -0
- package/blocks/crm/components/company-detail.tsx +10 -8
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +10 -7
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +377 -0
- package/blocks/crm/components/dedupe-review.tsx +13 -6
- package/blocks/crm/components/import-wizard.tsx +7 -9
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +6 -4
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +399 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +9 -1
- package/blocks/crm/lib/crm-types.ts +26 -2
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +160 -5
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +25 -13
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +31 -10
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +66 -26
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +5 -5
- package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +45 -4
- package/blocks/dialer/block.json +84 -3
- package/blocks/dialer/components/call-contact-button.tsx +269 -0
- package/blocks/dialer/components/call-log-screen.tsx +208 -0
- package/blocks/dialer/components/call-log.tsx +23 -7
- package/blocks/dialer/components/call-queue.tsx +4 -4
- package/blocks/dialer/components/dialer-screen.tsx +361 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +61 -12
- package/blocks/dialer/components/softphone.tsx +4 -2
- package/blocks/enrichment/README.md +29 -7
- package/blocks/enrichment/block.json +49 -9
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
- package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
- package/blocks/enrichment/components/waterfall-config.tsx +66 -17
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +139 -18
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +4 -2
- package/blocks/forms/components/form-embed.tsx +4 -2
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +56 -19
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +17 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +32 -3
- package/blocks/inbox/block.json +29 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +63 -1
- package/blocks/integrations/block.json +234 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +119 -0
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +13 -8
- package/blocks/integrations/components/integrations-screen.tsx +152 -0
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +11 -9
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +179 -0
- package/blocks/integrations/components/webhook-manager.tsx +43 -25
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +11 -1
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +13 -10
- package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/block.json +13 -8
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +20 -1
- package/blocks/lists/block.json +69 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +20 -9
- package/blocks/lists/components/list-picker.tsx +17 -3
- package/blocks/lists/components/list-table.tsx +12 -6
- package/blocks/lists/components/lists-screen.tsx +346 -0
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +33 -8
- package/blocks/lists/components/suppressions-screen.tsx +264 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +69 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
- package/blocks/mailboxes/components/domain-detail.tsx +148 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
- package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
- package/blocks/mailboxes/components/warmup-health.tsx +24 -22
- package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +157 -24
- package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/block.json +28 -14
- package/blocks/nurture/components/nurture-builder.tsx +13 -3
- package/blocks/nurture/components/nurture-enroll.tsx +12 -10
- package/blocks/nurture/components/nurture-form.tsx +27 -10
- package/blocks/nurture/components/nurture-screen.tsx +26 -11
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +18 -1
- package/blocks/objects/block.json +42 -21
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +10 -2
- package/blocks/objects/components/object-schema-editor.tsx +4 -2
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +140 -23
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +9 -4
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-list.tsx +2 -1
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/quotes/README.md +13 -0
- package/blocks/quotes/block.json +65 -13
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +5 -3
- package/blocks/quotes/components/quote-related.tsx +8 -4
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +55 -9
- package/blocks/sequencer/block.json +126 -19
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +224 -60
- package/blocks/sequencer/components/sequence-detail.tsx +261 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
- package/blocks/sequencer/components/sequence-list.tsx +34 -9
- package/blocks/sequencer/components/sequence-stats.tsx +11 -10
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +162 -0
- package/blocks/sequencer/components/step-editor.tsx +14 -12
- package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +19 -0
- package/blocks/social-ads/block.json +233 -1
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
- package/blocks/social-ads/components/audience-sync.tsx +17 -9
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +138 -0
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +155 -0
- package/blocks/social-ads/components/creative-gallery.tsx +35 -7
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +7 -3
- package/blocks/social-ads/components/post-detail.tsx +163 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +187 -0
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +21 -10
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +26 -16
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +16 -7
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +8 -5
- package/blocks/webchat/components/conversation-list.tsx +8 -4
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +10 -4
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +202 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +4 -2
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +3109 -1977
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +122 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +27 -20
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +17 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +55 -17
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +147 -37
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +64 -0
- package/stacks/clay/pages/companies.tsx.tmpl +31 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +73 -27
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +98 -35
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +104 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +181 -64
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +52 -20
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +68 -34
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +106 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +117 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +85 -31
- package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +115 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +40 -9
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/app/layout.tsx.tmpl +7 -2
- package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WorkbenchRowDetail: one workbench row in full. Every column with its value (a recipe's `extracted_data` before the
|
|
5
|
+
* imported `raw_data`), the row's status, error and enrichment time, an edit field per plain data column, and the row
|
|
6
|
+
* actions (unlock, send to a list, delete) for this one row.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. getWorkbenchRow GET /workbench/tables/{table_id}/rows/{row_id} (read)
|
|
10
|
+
* 2. updateWorkbenchRow PATCH /workbench/tables/{table_id}/rows/{row_id} { field, value } (write; one field per call)
|
|
11
|
+
* 3. via WorkbenchRowActions: quoteWorkbenchUnlock, unlockWorkbenchRows (billable, confirmed), exportWorkbenchTable,
|
|
12
|
+
* bulkDeleteWorkbenchRows (destructive, confirmed)
|
|
13
|
+
* A locked row (credit_charged false) shows masked cells and cannot be edited until it is unlocked (1 credit per row).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { useState } from "react";
|
|
17
|
+
import { Button, Input, Skeleton } from "@graph8/sdk/ui";
|
|
18
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
19
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
20
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
21
|
+
import { StatusPill, humanizeStatus, toneForStatus } from "@/g8/components/screen/status-pill";
|
|
22
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
23
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
24
|
+
import { g8Get, g8Patch } from "@/g8/lib/g8-client";
|
|
25
|
+
import { cellValue, formatCell, isLocked, isMasked } from "../lib/workbench-types";
|
|
26
|
+
import type { WorkbenchColumn, WorkbenchRow } from "../lib/workbench-types";
|
|
27
|
+
import { WorkbenchRowActions } from "./workbench-row-actions";
|
|
28
|
+
|
|
29
|
+
export interface WorkbenchRowDetailProps {
|
|
30
|
+
tableId: string;
|
|
31
|
+
rowId: number;
|
|
32
|
+
columns: WorkbenchColumn[];
|
|
33
|
+
entityType?: "contact" | "company";
|
|
34
|
+
/** Called after an edit, unlock or export so the grid can reload; `deleted` also means this row is gone. */
|
|
35
|
+
onChanged?: (what: "edited" | "unlocked" | "exported" | "deleted") => void;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The endpoint answers the row itself; tolerate a `{ row }` wrapper too. */
|
|
40
|
+
function asRow(data: unknown): WorkbenchRow | null {
|
|
41
|
+
const d = data as (WorkbenchRow & { row?: WorkbenchRow }) | null | undefined;
|
|
42
|
+
return d?.row ?? (d && typeof d.id === "number" ? d : null);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function WorkbenchRowDetail({ tableId, rowId, columns, entityType = "contact", onChanged, className }: WorkbenchRowDetailProps) {
|
|
46
|
+
const [draft, setDraft] = useState<{ column: string; value: string } | null>(null);
|
|
47
|
+
const row = useG8Query(async (signal) => asRow((await g8Get(`/workbench/tables/${tableId}/rows/${rowId}`, undefined, { signal })).data), [tableId, rowId]);
|
|
48
|
+
const save = useG8Mutation((v: { column: string; value: string }) => g8Patch(`/workbench/tables/${tableId}/rows/${rowId}`, { field: v.column, value: v.value }), {
|
|
49
|
+
onSuccess: () => { setDraft(null); void row.refetch(); onChanged?.("edited"); },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (row.error) return <ApiError error={row.error} title="Could not load the row" onRetry={() => void row.refetch()} />;
|
|
53
|
+
const r = row.data;
|
|
54
|
+
if (!r) return <Skeleton className="h-32 w-full" />;
|
|
55
|
+
const locked = isLocked(r);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className={`flex flex-col gap-4 ${className ?? ""}`}>
|
|
59
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
60
|
+
{locked ? <StatusPill tone="warning" dot>Locked</StatusPill> : <StatusPill tone="positive" dot>Unlocked</StatusPill>}
|
|
61
|
+
{r.status ? <StatusPill tone={toneForStatus(r.status)} dot>{humanizeStatus(r.status)}</StatusPill> : null}
|
|
62
|
+
</div>
|
|
63
|
+
{r.error_message ? <p role="alert" className="text-sm text-destructive">{r.error_message}</p> : null}
|
|
64
|
+
{save.error ? <ApiError error={save.error} title="Could not save the cell" /> : null}
|
|
65
|
+
<SectionCard title="Fields" description={locked ? "Unlock this row to see and edit real values." : "Edit a plain data field and save it."}>
|
|
66
|
+
<ul className="flex flex-col gap-3">
|
|
67
|
+
{columns.map((c) => {
|
|
68
|
+
const value = cellValue(r, c);
|
|
69
|
+
const editable = !locked && (c.column_type ?? "data") === "data" && !c.enrichment_type;
|
|
70
|
+
const open = draft?.column === c.name;
|
|
71
|
+
return (
|
|
72
|
+
<li key={c.id} className="flex flex-col gap-1">
|
|
73
|
+
<span className="text-xs text-muted-foreground">{c.display_name || c.name}{c.enrichment_type ? ` (${c.enrichment_type})` : ""}</span>
|
|
74
|
+
{open ? (
|
|
75
|
+
<div className="flex gap-2">
|
|
76
|
+
<Input aria-label={`Edit ${c.name}`} value={draft.value} onChange={(e) => setDraft({ column: c.name, value: e.target.value })} />
|
|
77
|
+
<Button size="sm" loading={save.loading} onClick={() => void save.mutate(draft)}>Save</Button>
|
|
78
|
+
<Button size="sm" variant="ghost" onClick={() => setDraft(null)}>Cancel</Button>
|
|
79
|
+
</div>
|
|
80
|
+
) : (
|
|
81
|
+
<div className="flex items-center justify-between gap-2">
|
|
82
|
+
<span className={`min-w-0 break-words text-sm ${isMasked(value) ? "text-muted-foreground" : ""}`}>{isMasked(value) ? "Locked" : formatCell(value) || <span className="text-muted-foreground">-</span>}</span>
|
|
83
|
+
{editable ? <Button size="sm" variant="ghost" aria-label={`Edit ${c.name} value`} onClick={() => setDraft({ column: c.name, value: formatCell(value) })}>Edit</Button> : null}
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
</li>
|
|
87
|
+
);
|
|
88
|
+
})}
|
|
89
|
+
</ul>
|
|
90
|
+
</SectionCard>
|
|
91
|
+
<SectionCard title="About this row">
|
|
92
|
+
<KeyValueList columns={2} items={[
|
|
93
|
+
{ label: "Row id", value: String(r.id), mono: true },
|
|
94
|
+
{ label: "Source", value: r.source_type ? humanizeStatus(r.source_type) : "" },
|
|
95
|
+
{ label: "Enriched", value: r.enriched_at ? new Date(r.enriched_at).toLocaleString() : "" },
|
|
96
|
+
{ label: "Credit charged", value: r.credit_charged == null ? "" : r.credit_charged ? "Yes" : "No, locked" },
|
|
97
|
+
]} />
|
|
98
|
+
</SectionCard>
|
|
99
|
+
<SectionCard title="Actions" description="Unlock real emails and phones, send this row to a list, or delete it.">
|
|
100
|
+
<WorkbenchRowActions tableId={tableId} rowIds={[rowId]} entityType={entityType} onChanged={(what) => { void row.refetch(); onChanged?.(what); }} />
|
|
101
|
+
</SectionCard>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WorkbenchScreen: the finished Workbench page. Without `?id=` it is the list of workbenches (search, row and column
|
|
5
|
+
* counts, People or Companies, New workbench in a side sheet, confirmed delete). With `?id=<table>` it opens that table:
|
|
6
|
+
* a header with its facts and the full WorkbenchGrid (edit cells, paste or add rows, add AI / waterfall / formula
|
|
7
|
+
* columns, quote then run a column, watch progress, unlock locked rows, send rows to a list, delete rows).
|
|
8
|
+
*
|
|
9
|
+
* Composes: ScreenLayout, StatStrip, Toolbar, FormSheet, StatusPill, EntityAvatar (screen kit), WorkbenchGrid and the
|
|
10
|
+
* parts it composes (WorkbenchAddRows, ColumnRecipeEditor, RunProgress, WorkbenchRowActions), ConfirmSpendDialog.
|
|
11
|
+
*
|
|
12
|
+
* SDK operations, in order:
|
|
13
|
+
* 1. listWorkbenchTables GET /workbench/tables?page&limit&filter_name (read)
|
|
14
|
+
* 2. createWorkbenchTable POST /workbench/tables { name, entity_type } (write); the new table opens at once
|
|
15
|
+
* 3. deleteWorkbenchTable DELETE /workbench/tables/{table_id} (destructive, ConfirmSpendDialog)
|
|
16
|
+
* 4. getWorkbenchTable GET /workbench/tables/{table_id} (read, via useWorkbenchTable)
|
|
17
|
+
* 5. Import people / Scrape the web: previewWorkbenchSearchImport, importWorkbenchFromSearch (billable, confirmed),
|
|
18
|
+
* estimateWorkbenchScraperCredits, runWorkbenchScraper (billable, confirmed), getWorkbenchScraperRun (see workbench-sources.tsx)
|
|
19
|
+
* 6. everything inside the grid: see WorkbenchGrid (runs and unlocks are billable and confirmed)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { useEffect, useRef, useState } from "react";
|
|
23
|
+
import type { ReactNode } from "react";
|
|
24
|
+
import { ArrowLeft, Globe, Plus, Table2, Trash2, UsersRound } from "lucide-react";
|
|
25
|
+
import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
26
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
27
|
+
import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
|
|
28
|
+
import { byPage } from "@/g8/lib/paging";
|
|
29
|
+
import { LoadMore } from "@/g8/components/paging-notices";
|
|
30
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
31
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
32
|
+
import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
|
|
33
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
34
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
35
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
36
|
+
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
37
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
38
|
+
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
39
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
40
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
41
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
42
|
+
import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
43
|
+
import { useWorkbenchTable } from "../hooks/use-workbench-table";
|
|
44
|
+
import { pageItems, pageTotal } from "../lib/workbench-types";
|
|
45
|
+
import type { WorkbenchTable } from "../lib/workbench-types";
|
|
46
|
+
import { WorkbenchGrid } from "./workbench-grid";
|
|
47
|
+
import { WorkbenchRowDetail } from "./workbench-row-detail";
|
|
48
|
+
import { ScraperPanel, SearchImportPanel } from "./workbench-sources";
|
|
49
|
+
|
|
50
|
+
export interface WorkbenchScreenProps {
|
|
51
|
+
toolbarActions?: ReactNode;
|
|
52
|
+
renderRowActions?: (table: WorkbenchTable) => ReactNode;
|
|
53
|
+
/** Extra buttons in an opened table's header. */
|
|
54
|
+
renderDetailActions?: (table: WorkbenchTable) => ReactNode;
|
|
55
|
+
className?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const noun = (t: { entity_type?: string | null }) => (t.entity_type === "company" ? "Companies" : "People");
|
|
59
|
+
|
|
60
|
+
function OpenTable({ tableId, onBack, renderDetailActions, className }: { tableId: string; onBack: () => void; renderDetailActions?: (t: WorkbenchTable) => ReactNode; className?: string }) {
|
|
61
|
+
const { table, columns, loading, error, refetch } = useWorkbenchTable(tableId);
|
|
62
|
+
const [source, setSource] = useState<"import" | "scrape" | null>(null);
|
|
63
|
+
const [rowParam, setRowParam] = useUrlState("row");
|
|
64
|
+
const [view, setView] = useUrlState("view");
|
|
65
|
+
const full = view === "full";
|
|
66
|
+
const rowId = rowParam ? Number(rowParam) : null;
|
|
67
|
+
// New rows arrive from outside the grid (import, scraper): remount it so it reloads, and re-read the counts.
|
|
68
|
+
const [gridKey, setGridKey] = useState(0);
|
|
69
|
+
const changed = () => { setGridKey((k) => k + 1); void refetch(); };
|
|
70
|
+
return (
|
|
71
|
+
<ScreenLayout
|
|
72
|
+
className={className}
|
|
73
|
+
eyebrow={<Button variant="ghost" size="sm" onClick={onBack}><ArrowLeft className="mr-1 size-4" aria-hidden />Back to workbenches</Button>}
|
|
74
|
+
title={table?.name ?? "Workbench"}
|
|
75
|
+
description={table ? `${noun(table)} table. Add columns that look things up or write with AI, price a run, then send finished rows to a list.` : undefined}
|
|
76
|
+
icon={<Table2 className="size-5" aria-hidden />}
|
|
77
|
+
actions={table ? (<>
|
|
78
|
+
<Button size="sm" variant="outline" onClick={() => setSource("import")}><UsersRound className="mr-1 size-4" aria-hidden />Import people</Button>
|
|
79
|
+
<Button size="sm" variant="outline" onClick={() => setSource("scrape")}><Globe className="mr-1 size-4" aria-hidden />Scrape the web</Button>
|
|
80
|
+
{renderDetailActions?.(table)}
|
|
81
|
+
</>) : undefined}
|
|
82
|
+
stats={table ? [
|
|
83
|
+
{ label: "Rows", value: (table.row_count ?? 0).toLocaleString() },
|
|
84
|
+
{ label: "Columns", value: columns.length.toLocaleString() },
|
|
85
|
+
{ label: "Recipe columns", value: columns.filter((c) => c.enrichment_type).length },
|
|
86
|
+
] : undefined}
|
|
87
|
+
>
|
|
88
|
+
{error ? <ApiError error={error} title="Could not open the workbench" onRetry={() => void refetch()} /> : null}
|
|
89
|
+
{loading && !table ? <Skeleton className="h-8 w-64" /> : null}
|
|
90
|
+
<SplitView
|
|
91
|
+
expanded={full} onExpandedChange={(e) => setView(e ? "full" : null)}
|
|
92
|
+
list={<WorkbenchGrid key={gridKey} tableId={tableId} activeRowId={rowId} onOpenRow={(r) => setRowParam(String(r.id))} />}
|
|
93
|
+
detailOpen={rowId != null && Number.isFinite(rowId)}
|
|
94
|
+
onCloseDetail={() => setRowParam(null)}
|
|
95
|
+
detailWidth="lg"
|
|
96
|
+
detailTitle="Row"
|
|
97
|
+
detail={rowId != null ? (
|
|
98
|
+
<DetailPanel key={rowId} onExpand={full ? undefined : () => setView("full")} onCollapse={full ? () => setView(null) : undefined} title={`Row ${rowId}`} subtitle={table?.name} onClose={() => setRowParam(null)}>
|
|
99
|
+
<WorkbenchRowDetail tableId={tableId} rowId={rowId} columns={columns} entityType={table?.entity_type === "company" ? "company" : "contact"}
|
|
100
|
+
onChanged={(what) => { if (what === "deleted") setRowParam(null); changed(); }} />
|
|
101
|
+
</DetailPanel>
|
|
102
|
+
) : null}
|
|
103
|
+
/>
|
|
104
|
+
<FormSheet open={source === "import"} onOpenChange={(o) => !o && setSource(null)} width="lg" title="Import people from a search" description="Preview for free, then import. Imported rows are locked until you unlock them.">
|
|
105
|
+
<SearchImportPanel tableId={tableId} onChanged={changed} />
|
|
106
|
+
</FormSheet>
|
|
107
|
+
<FormSheet open={source === "scrape"} onOpenChange={(o) => !o && setSource(null)} width="lg" title="Scrape the web into this table" description="Describe what to collect. You see the credit estimate before anything runs.">
|
|
108
|
+
<ScraperPanel tableId={tableId} onChanged={changed} />
|
|
109
|
+
</FormSheet>
|
|
110
|
+
</ScreenLayout>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Workbenches per read; "Load more" appends the next page. */
|
|
115
|
+
const TABLE_PAGE = 100;
|
|
116
|
+
|
|
117
|
+
export function WorkbenchScreen({ toolbarActions, renderRowActions, renderDetailActions, className }: WorkbenchScreenProps) {
|
|
118
|
+
const [openId, setOpenId] = useUrlState("id");
|
|
119
|
+
const [search, setSearch] = useState("");
|
|
120
|
+
const [term, setTerm] = useState("");
|
|
121
|
+
useEffect(() => { const t = setTimeout(() => setTerm(search.trim()), 250); return () => clearTimeout(t); }, [search]);
|
|
122
|
+
const [creating, setCreating] = useState(false);
|
|
123
|
+
const [name, setName] = useState("");
|
|
124
|
+
const [entity, setEntity] = useState<"contact" | "company">("contact");
|
|
125
|
+
const [deleting, setDeleting] = useState<WorkbenchTable | null>(null);
|
|
126
|
+
|
|
127
|
+
const apiTotal = useRef<number | null>(null);
|
|
128
|
+
const tableRows = useLoadMoreList(byPage<WorkbenchTable>(TABLE_PAGE, async (page, signal) => {
|
|
129
|
+
const data = (await g8Get("/workbench/tables", { page, limit: TABLE_PAGE, filter_name: term || undefined }, { signal })).data;
|
|
130
|
+
const rows = pageItems<WorkbenchTable>(data);
|
|
131
|
+
apiTotal.current = pageTotal(data, -1) >= 0 ? pageTotal(data, -1) : null;
|
|
132
|
+
return { rows, hasNext: apiTotal.current != null ? page * TABLE_PAGE < apiTotal.current : rows.length >= TABLE_PAGE };
|
|
133
|
+
}), [term], { enabled: !openId });
|
|
134
|
+
const tables = { ...tableRows, data: tableRows.data ? { rows: tableRows.data, total: apiTotal.current } : undefined };
|
|
135
|
+
const create = useG8Mutation(
|
|
136
|
+
async () => (await g8Post<WorkbenchTable>("/workbench/tables", { name: name.trim(), entity_type: entity })).data,
|
|
137
|
+
{ onSuccess: (t) => { setCreating(false); setName(""); setOpenId(t.id); } },
|
|
138
|
+
);
|
|
139
|
+
const remove = useG8Mutation((id: string) => g8Delete(`/workbench/tables/${id}`), { onSuccess: () => { setDeleting(null); void tables.refetch(); } });
|
|
140
|
+
|
|
141
|
+
if (openId) return <OpenTable key={openId} tableId={openId} onBack={() => setOpenId(null)} renderDetailActions={renderDetailActions} className={className} />;
|
|
142
|
+
|
|
143
|
+
const items = tables.data?.rows ?? [];
|
|
144
|
+
const partial = tables.data?.total == null || tables.data.total > items.length;
|
|
145
|
+
const hint = partial ? "On this page" : undefined;
|
|
146
|
+
return (
|
|
147
|
+
<ScreenLayout
|
|
148
|
+
className={className}
|
|
149
|
+
title="Workbench"
|
|
150
|
+
description="Spreadsheets that fill themselves in: paste or import records, add AI and enrichment columns, then send the finished rows to a list."
|
|
151
|
+
icon={<Table2 className="size-5" aria-hidden />}
|
|
152
|
+
actions={<>{toolbarActions}<Button size="sm" onClick={() => setCreating(true)}><Plus className="mr-1 size-4" aria-hidden />New workbench</Button></>}
|
|
153
|
+
stats={[
|
|
154
|
+
{ label: "Workbenches", value: (tables.data?.total ?? items.length).toLocaleString(), hint: tables.data?.total == null ? "On this page" : undefined },
|
|
155
|
+
{ label: "Rows", value: items.reduce((n, t) => n + (t.row_count ?? 0), 0).toLocaleString(), hint },
|
|
156
|
+
{ label: "People tables", value: items.filter((t) => t.entity_type !== "company").length, hint },
|
|
157
|
+
{ label: "Company tables", value: items.filter((t) => t.entity_type === "company").length, hint },
|
|
158
|
+
]}
|
|
159
|
+
toolbar={<Toolbar search={{ value: search, onChange: setSearch, placeholder: "Search workbenches" }} />}
|
|
160
|
+
>
|
|
161
|
+
{tables.error ? <ApiError error={tables.error} title="Could not load workbenches" onRetry={() => void tables.refetch()} /> : null}
|
|
162
|
+
{remove.error ? <ApiError error={remove.error} title="Could not delete the workbench" /> : null}
|
|
163
|
+
{tables.loading && !tables.data ? <Skeleton className="h-24 w-full" /> : null}
|
|
164
|
+
{!tables.loading && !tables.error && items.length === 0 ? (
|
|
165
|
+
<EmptyState icon={<Table2 className="size-6" aria-hidden />} title={term ? "No workbench matches" : "No workbenches yet"} description={term ? "Try another name." : "Create one, then paste rows or import people."}
|
|
166
|
+
action={!term ? <Button size="sm" onClick={() => setCreating(true)}>Create your first workbench</Button> : undefined} />
|
|
167
|
+
) : null}
|
|
168
|
+
<ul className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
169
|
+
{items.map((t) => (
|
|
170
|
+
<li key={t.id} data-testid={`workbench-${t.id}`} className="flex items-center gap-3 rounded-md border p-3">
|
|
171
|
+
<EntityAvatar name={t.name} kind={t.entity_type === "company" ? "company" : "person"} />
|
|
172
|
+
<button type="button" className="flex min-w-0 flex-1 flex-col text-left" onClick={() => setOpenId(t.id)}>
|
|
173
|
+
<span className="truncate text-sm font-medium">{t.name}</span>
|
|
174
|
+
<span className="text-xs text-muted-foreground">{(t.row_count ?? 0).toLocaleString()} rows</span>
|
|
175
|
+
</button>
|
|
176
|
+
<StatusPill tone="neutral">{noun(t)}</StatusPill>
|
|
177
|
+
{renderRowActions?.(t)}
|
|
178
|
+
<Button size="sm" variant="ghost" aria-label={`Delete ${t.name}`} onClick={() => setDeleting(t)}><Trash2 className="size-4" aria-hidden /></Button>
|
|
179
|
+
</li>
|
|
180
|
+
))}
|
|
181
|
+
</ul>
|
|
182
|
+
<LoadMore shown={items.length} noun="workbenches" hasMore={tableRows.hasMore} loading={tableRows.loadingMore} onLoadMore={tableRows.loadMore} />
|
|
183
|
+
|
|
184
|
+
<FormSheet open={creating} onOpenChange={setCreating} title="New workbench" description="Choose what each row is. You can paste rows or import people next."
|
|
185
|
+
footer={(<><Button variant="ghost" onClick={() => setCreating(false)}>Cancel</Button><Button loading={create.loading} disabled={!name.trim()} onClick={() => void create.mutate(undefined)}>Create workbench</Button></>)}>
|
|
186
|
+
<div className="flex flex-col gap-3">
|
|
187
|
+
<div className="flex flex-col gap-1"><Label htmlFor="wb-new-name">Name</Label><Input id="wb-new-name" placeholder="Series B fintech accounts" value={name} onChange={(e) => setName(e.target.value)} /></div>
|
|
188
|
+
<div className="flex flex-col gap-1"><Label htmlFor="wb-new-kind">Rows are</Label>
|
|
189
|
+
<select id="wb-new-kind" className="h-9 rounded-md border bg-background px-2 text-sm" value={entity} onChange={(e) => setEntity(e.target.value as "contact" | "company")}>
|
|
190
|
+
<option value="contact">People</option><option value="company">Companies</option>
|
|
191
|
+
</select>
|
|
192
|
+
</div>
|
|
193
|
+
{create.error ? <ApiError error={create.error} title="Could not create the workbench" /> : null}
|
|
194
|
+
</div>
|
|
195
|
+
</FormSheet>
|
|
196
|
+
|
|
197
|
+
<ConfirmSpendDialog open={deleting != null} tier="destructive" title={`Delete "${deleting?.name ?? ""}"`} description="The table, its columns and all of its rows are removed."
|
|
198
|
+
affected={deleting?.row_count ?? undefined} confirmLabel="Delete workbench" confirming={remove.loading}
|
|
199
|
+
onConfirm={() => deleting && void remove.mutate(deleting.id)} onCancel={() => setDeleting(null)} />
|
|
200
|
+
</ScreenLayout>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WorkbenchSourcePanels: two ways to fill a workbench table with records.
|
|
5
|
+
* - SearchImportPanel: describe the people you want (titles, industries, countries, seniority, company domains),
|
|
6
|
+
* preview a free sample and the match count, then import them. Imported rows land LOCKED (names and companies
|
|
7
|
+
* visible, emails and phones masked): unlock them from the table's row actions.
|
|
8
|
+
* - ScraperPanel: describe what to collect in words ("seed-stage fintechs in Berlin"), see the credit estimate and
|
|
9
|
+
* your available credits, run the built-in web scraper and follow its progress.
|
|
10
|
+
*
|
|
11
|
+
* SDK operations, in order:
|
|
12
|
+
* SearchImportPanel
|
|
13
|
+
* 1. previewWorkbenchSearchImport POST /workbench/tables/{table_id}/import/search/preview { filters, sample_size, max_result } (read, free)
|
|
14
|
+
* -> { contacts, count }
|
|
15
|
+
* 2. importWorkbenchFromSearch POST /workbench/tables/{table_id}/import/search { filters, max_result, skip_duplicates } (billable)
|
|
16
|
+
* Goes through ConfirmSpendDialog first. -> { inserted, duplicates, columns_created, processed }
|
|
17
|
+
* ScraperPanel
|
|
18
|
+
* 1. estimateWorkbenchScraperCredits GET /workbench/scraper/credit-estimate?table_id&max_results&auto_enrich (read, free)
|
|
19
|
+
* -> { total_estimated_credits, available_credits, ... }
|
|
20
|
+
* 2. runWorkbenchScraper POST /workbench/tables/{table_id}/scraper/runs { prompt, max_results, auto_enrich, approved_max_credits, quote_expires_at } (billable)
|
|
21
|
+
* Goes through ConfirmSpendDialog first with the estimate; the estimate is also the cap sent back. -> { run_id, status }
|
|
22
|
+
* 3. getWorkbenchScraperRun GET /workbench/tables/{table_id}/scraper/runs/{run_id} (read) polled with useJobPoll
|
|
23
|
+
* -> { status, results_count, progress_pct, progress_message, error_message }
|
|
24
|
+
*
|
|
25
|
+
* Nothing runs without the confirmation. There is no stream, so the scraper run is polled with backoff.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { useState } from "react";
|
|
29
|
+
import { Button, Input, Label } from "@graph8/sdk/ui";
|
|
30
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
31
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
32
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
33
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
34
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
35
|
+
|
|
36
|
+
export interface WorkbenchSourcePanelProps {
|
|
37
|
+
tableId: string;
|
|
38
|
+
/** Called after rows were added (import done, scraper run finished) so the grid can reload. */
|
|
39
|
+
onChanged?: () => void;
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const csv = (text: string): string[] => text.split(",").map((s) => s.trim()).filter(Boolean);
|
|
44
|
+
|
|
45
|
+
interface Preview { contacts?: Array<Record<string, unknown>>; count?: number }
|
|
46
|
+
interface ImportResult { inserted?: number; duplicates?: number; columns_created?: number; processed?: number }
|
|
47
|
+
interface Estimate { estimated_credits?: number; total_estimated_credits?: number; available_credits?: number; quote_expires_at?: string }
|
|
48
|
+
interface RunStarted { run_id?: string; status?: string }
|
|
49
|
+
interface ScraperRun { status?: string; results_count?: number; progress_pct?: number | null; progress_message?: string | null; error_message?: string | null }
|
|
50
|
+
|
|
51
|
+
const FIELDS = [
|
|
52
|
+
{ key: "job_title", label: "Job titles", placeholder: "VP Sales, Head of Sales" },
|
|
53
|
+
{ key: "seniority_level", label: "Seniority", placeholder: "vp, director" },
|
|
54
|
+
{ key: "company_industry", label: "Industries", placeholder: "computer software" },
|
|
55
|
+
{ key: "country", label: "Countries", placeholder: "United States" },
|
|
56
|
+
{ key: "company_domain", label: "Company domains", placeholder: "acme.com, globex.com" },
|
|
57
|
+
] as const;
|
|
58
|
+
|
|
59
|
+
/** Display name of a sampled person. Locked values arrive as "***" and are never shown. */
|
|
60
|
+
function sampleName(c: Record<string, unknown>): string {
|
|
61
|
+
const text = (v: unknown) => (typeof v === "string" && v !== "***" ? v : "");
|
|
62
|
+
return [text(c.first_name), text(c.last_name)].filter(Boolean).join(" ") || "Locked contact";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function SearchImportPanel({ tableId, onChanged, className }: WorkbenchSourcePanelProps) {
|
|
66
|
+
const [values, setValues] = useState<Record<string, string>>({});
|
|
67
|
+
const [max, setMax] = useState("100");
|
|
68
|
+
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
69
|
+
const filters = FIELDS.map((f) => ({ field: f.key, operator: "any_of", value: csv(values[f.key] ?? "") })).filter((f) => f.value.length > 0);
|
|
70
|
+
const key = JSON.stringify(filters);
|
|
71
|
+
const limit = Math.max(1, Math.min(Number(max) || 0, 10000));
|
|
72
|
+
|
|
73
|
+
const preview = useG8Mutation(async () => (await g8Post<Preview>(`/workbench/tables/${tableId}/import/search/preview`, { filters, sample_size: 10, max_result: limit })).data);
|
|
74
|
+
const run = useG8Mutation(
|
|
75
|
+
async () => (await g8Post<ImportResult>(`/workbench/tables/${tableId}/import/search`, { filters, max_result: limit, skip_duplicates: true })).data,
|
|
76
|
+
{ onSuccess: () => { setConfirmOpen(false); onChanged?.(); } },
|
|
77
|
+
);
|
|
78
|
+
// A preview describes one set of filters: forget it when they change.
|
|
79
|
+
const [previewKey, setPreviewKey] = useState("");
|
|
80
|
+
const shown = preview.data && previewKey === key ? preview.data : null;
|
|
81
|
+
const done = run.data;
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className={`flex flex-col gap-3 ${className ?? ""}`}>
|
|
85
|
+
<p className="text-sm text-muted-foreground">Describe the people you want. Separate several values with commas. All filters must match.</p>
|
|
86
|
+
{FIELDS.map((f) => (
|
|
87
|
+
<div key={f.key} className="flex flex-col gap-1">
|
|
88
|
+
<Label htmlFor={`imp-${f.key}`}>{f.label}</Label>
|
|
89
|
+
<Input id={`imp-${f.key}`} placeholder={f.placeholder} value={values[f.key] ?? ""} onChange={(e) => setValues({ ...values, [f.key]: e.target.value })} />
|
|
90
|
+
</div>
|
|
91
|
+
))}
|
|
92
|
+
<div className="flex flex-col gap-1">
|
|
93
|
+
<Label htmlFor="imp-max">Import at most</Label>
|
|
94
|
+
<Input id="imp-max" inputMode="numeric" className="w-32" value={max} onChange={(e) => setMax(e.target.value)} />
|
|
95
|
+
</div>
|
|
96
|
+
<div className="flex gap-2">
|
|
97
|
+
<Button variant="outline" size="sm" loading={preview.loading} disabled={filters.length === 0} onClick={async () => { setPreviewKey(key); await preview.mutate(undefined); }}>Preview sample</Button>
|
|
98
|
+
<Button size="sm" disabled={filters.length === 0 || !shown || (shown.count ?? 0) === 0} onClick={() => setConfirmOpen(true)}>Import {shown ? Math.min(shown.count ?? 0, limit).toLocaleString() : ""} people</Button>
|
|
99
|
+
</div>
|
|
100
|
+
{preview.error ? <ApiError error={preview.error} title="Could not preview the search" /> : null}
|
|
101
|
+
{shown ? (
|
|
102
|
+
<div role="status" className="rounded-md border p-3 text-sm">
|
|
103
|
+
<p><strong>{(shown.count ?? 0).toLocaleString()}</strong> people match. Sample:</p>
|
|
104
|
+
<ul className="mt-1 list-disc pl-5 text-muted-foreground">
|
|
105
|
+
{(shown.contacts ?? []).slice(0, 10).map((c, i) => <li key={i}>{sampleName(c)}{typeof c.job_title === "string" ? `, ${c.job_title}` : ""}{typeof c.company_name === "string" ? ` at ${c.company_name}` : ""}</li>)}
|
|
106
|
+
</ul>
|
|
107
|
+
<p className="mt-2 text-xs text-muted-foreground">Emails and phones stay locked until you unlock the rows (1 credit per row).</p>
|
|
108
|
+
</div>
|
|
109
|
+
) : null}
|
|
110
|
+
{run.error ? <ApiError error={run.error} title="Could not import" /> : null}
|
|
111
|
+
{done ? <p role="status" className="text-sm">Imported {(done.inserted ?? 0).toLocaleString()} people{done.duplicates ? `, skipped ${done.duplicates.toLocaleString()} duplicates` : ""}. Their contact details are locked: select the rows and unlock them.</p> : null}
|
|
112
|
+
<ConfirmSpendDialog open={confirmOpen} tier="billable" title={`Import up to ${shown ? Math.min(shown.count ?? 0, limit).toLocaleString() : limit.toLocaleString()} people`}
|
|
113
|
+
description="They are added to this table with names and companies visible and contact details locked. Unlocking costs 1 credit per row."
|
|
114
|
+
affected={shown ? Math.min(shown.count ?? 0, limit) : undefined} confirmLabel="Import" confirming={run.loading}
|
|
115
|
+
onConfirm={() => void run.mutate(undefined)} onCancel={() => setConfirmOpen(false)} />
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const RUNNING = new Set(["pending", "queued", "starting", "running", "in_progress", "processing"]);
|
|
121
|
+
|
|
122
|
+
export function ScraperPanel({ tableId, onChanged, className }: WorkbenchSourcePanelProps) {
|
|
123
|
+
const [prompt, setPrompt] = useState("");
|
|
124
|
+
const [maxResults, setMaxResults] = useState("100");
|
|
125
|
+
const [autoEnrich, setAutoEnrich] = useState(false);
|
|
126
|
+
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
127
|
+
const [runId, setRunId] = useState<string | null>(null);
|
|
128
|
+
const count = Math.max(10, Math.min(Number(maxResults) || 0, 2000));
|
|
129
|
+
const scope = `${count}:${autoEnrich}`;
|
|
130
|
+
|
|
131
|
+
const [estimateScope, setEstimateScope] = useState("");
|
|
132
|
+
const estimate = useG8Mutation(async () => (await g8Get<Estimate>("/workbench/scraper/credit-estimate", { table_id: tableId, max_results: count, auto_enrich: autoEnrich })).data);
|
|
133
|
+
const quote = estimate.data && estimateScope === scope ? estimate.data : null;
|
|
134
|
+
const credits = quote ? (quote.total_estimated_credits ?? quote.estimated_credits ?? 0) : 0;
|
|
135
|
+
|
|
136
|
+
const [stale, setStale] = useState(false);
|
|
137
|
+
const start = useG8Mutation(
|
|
138
|
+
async () => (await g8Post<RunStarted>(`/workbench/tables/${tableId}/scraper/runs`, {
|
|
139
|
+
prompt: prompt.trim(), max_results: count, auto_enrich: autoEnrich, approved_max_credits: credits, quote_expires_at: quote?.quote_expires_at,
|
|
140
|
+
})).data,
|
|
141
|
+
{ onSuccess: (r) => { setConfirmOpen(false); if (r.run_id) setRunId(r.run_id); } },
|
|
142
|
+
);
|
|
143
|
+
/** The server prices a run against the estimate's own expiry. Never invent one: an expired or missing quote is re-estimated. */
|
|
144
|
+
const confirmRun = () => {
|
|
145
|
+
const expires = quote?.quote_expires_at ? Date.parse(quote.quote_expires_at) : NaN;
|
|
146
|
+
if (!Number.isFinite(expires) || expires <= Date.now()) {
|
|
147
|
+
setConfirmOpen(false); setEstimateScope(""); estimate.reset(); setStale(true);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
void start.mutate(undefined);
|
|
151
|
+
};
|
|
152
|
+
const progress = useJobPoll<ScraperRun>(
|
|
153
|
+
async (signal) => (await g8Get<ScraperRun>(`/workbench/tables/${tableId}/scraper/runs/${runId}`, undefined, { signal })).data,
|
|
154
|
+
{ enabled: runId != null, intervalMs: 3000, maxIntervalMs: 20000, isTerminal: (r) => !RUNNING.has(String(r.status ?? "").toLowerCase()), onDone: () => onChanged?.() },
|
|
155
|
+
);
|
|
156
|
+
const run = progress.status;
|
|
157
|
+
|
|
158
|
+
if (runId) {
|
|
159
|
+
return (
|
|
160
|
+
<div className={`flex flex-col gap-3 ${className ?? ""}`} role="status">
|
|
161
|
+
<h3 className="text-sm font-medium">{progress.done ? `Scraper ${run?.status ?? "finished"}` : "Scraping the web..."}</h3>
|
|
162
|
+
<div className="h-2 overflow-hidden rounded bg-muted" aria-label="Progress"><div className="h-full bg-primary" style={{ width: `${progress.done ? 100 : (run?.progress_pct ?? 0)}%` }} /></div>
|
|
163
|
+
<p className="text-sm">{(run?.results_count ?? 0).toLocaleString()} results collected.{run?.progress_message ? ` ${run.progress_message}` : ""}</p>
|
|
164
|
+
{run?.error_message ? <p role="alert" className="text-sm text-destructive">{run.error_message}</p> : null}
|
|
165
|
+
{progress.error ? <ApiError error={progress.error} title="Could not read the run" onRetry={() => progress.start()} /> : null}
|
|
166
|
+
{progress.done ? <Button size="sm" variant="outline" onClick={() => { setRunId(null); progress.stop(); start.reset(); }}>Start another run</Button> : null}
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div className={`flex flex-col gap-3 ${className ?? ""}`}>
|
|
173
|
+
<div className="flex flex-col gap-1">
|
|
174
|
+
<Label htmlFor="scr-prompt">What should it collect?</Label>
|
|
175
|
+
<Input id="scr-prompt" placeholder="Seed-stage fintech companies in Berlin" value={prompt} onChange={(e) => setPrompt(e.target.value)} />
|
|
176
|
+
</div>
|
|
177
|
+
<div className="flex flex-col gap-1">
|
|
178
|
+
<Label htmlFor="scr-max">How many results (10 to 2,000)</Label>
|
|
179
|
+
<Input id="scr-max" inputMode="numeric" className="w-32" value={maxResults} onChange={(e) => setMaxResults(e.target.value)} />
|
|
180
|
+
</div>
|
|
181
|
+
<label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={autoEnrich} onChange={(e) => setAutoEnrich(e.target.checked)} /> Enrich every result with the table's enrichment columns (costs more)</label>
|
|
182
|
+
<div className="flex gap-2">
|
|
183
|
+
<Button variant="outline" size="sm" loading={estimate.loading} disabled={!prompt.trim()} onClick={async () => { setStale(false); setEstimateScope(scope); await estimate.mutate(undefined); }}>Estimate credits</Button>
|
|
184
|
+
<Button size="sm" disabled={!prompt.trim() || !quote} onClick={() => setConfirmOpen(true)}>Run scraper</Button>
|
|
185
|
+
</div>
|
|
186
|
+
{estimate.error ? <ApiError error={estimate.error} title="Could not estimate the cost" /> : null}
|
|
187
|
+
{quote ? <p role="status" className="text-sm">Up to <strong>{credits.toLocaleString()}</strong> credits{quote.available_credits != null ? `. You have ${quote.available_credits.toLocaleString()} available.` : "."}{quote.available_credits != null && credits > quote.available_credits ? " That is more than your balance, so the run would be refused." : ""}</p> : null}
|
|
188
|
+
{stale ? <p role="status" className="text-sm">That estimate has expired. Estimate the credits again before running the scraper.</p> : null}
|
|
189
|
+
{start.error ? <ApiError error={start.error} title="Could not start the scraper" /> : null}
|
|
190
|
+
<ConfirmSpendDialog open={confirmOpen} tier="billable" title={`Scrape up to ${count.toLocaleString()} results`} description={`"${prompt.trim()}". The scraper charges per result collected; the estimate is the most this run can charge.`}
|
|
191
|
+
estimatedCredits={credits} confirmLabel="Run scraper" confirming={start.loading} onConfirm={confirmRun} onCancel={() => setConfirmOpen(false)} />
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
@@ -14,13 +14,16 @@
|
|
|
14
14
|
import { useEffect, useState } from "react";
|
|
15
15
|
import { Button, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
16
16
|
import { ApiError } from "@/g8/components/api-error";
|
|
17
|
+
import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
|
|
18
|
+
import { byPage } from "@/g8/lib/paging";
|
|
19
|
+
import { LoadMore } from "@/g8/components/paging-notices";
|
|
17
20
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
18
21
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
19
22
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
20
23
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
21
24
|
import { g8Delete, g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
22
25
|
import { WorkbenchGrid } from "./workbench-grid";
|
|
23
|
-
import { pageItems } from "../lib/workbench-types";
|
|
26
|
+
import { pageItems, pageTotal } from "../lib/workbench-types";
|
|
24
27
|
import type { WorkbenchTable } from "../lib/workbench-types";
|
|
25
28
|
|
|
26
29
|
export interface WorkbenchTableListProps {
|
|
@@ -29,6 +32,9 @@ export interface WorkbenchTableListProps {
|
|
|
29
32
|
className?: string;
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
/** Workbenches per read; "Load more" appends the next page. */
|
|
36
|
+
const TABLE_PAGE = 100;
|
|
37
|
+
|
|
32
38
|
export function WorkbenchTableList({ onOpen, className }: WorkbenchTableListProps) {
|
|
33
39
|
const [name, setName] = useState("");
|
|
34
40
|
const [entity, setEntity] = useState<"contact" | "company">("contact");
|
|
@@ -39,7 +45,12 @@ export function WorkbenchTableList({ onOpen, className }: WorkbenchTableListProp
|
|
|
39
45
|
const [opened, setOpened] = useState<WorkbenchTable | null>(null);
|
|
40
46
|
const open = (t: WorkbenchTable) => { if (onOpen) onOpen(t); else setOpened(t); };
|
|
41
47
|
|
|
42
|
-
const tables =
|
|
48
|
+
const tables = useLoadMoreList(byPage<WorkbenchTable>(TABLE_PAGE, async (page, signal) => {
|
|
49
|
+
const data = (await g8Get("/workbench/tables", { page, limit: TABLE_PAGE, filter_name: term || undefined }, { signal })).data;
|
|
50
|
+
const rows = pageItems<WorkbenchTable>(data);
|
|
51
|
+
const total = pageTotal(data, -1);
|
|
52
|
+
return { rows, hasNext: total >= 0 ? page * TABLE_PAGE < total : rows.length >= TABLE_PAGE };
|
|
53
|
+
}), [term]);
|
|
43
54
|
const create = useG8Mutation(
|
|
44
55
|
async () => (await g8Post<WorkbenchTable>("/workbench/tables", { name: name.trim(), entity_type: entity })).data,
|
|
45
56
|
{ onSuccess: (t) => { setName(""); void tables.refetch(); open(t); } },
|
|
@@ -89,6 +100,7 @@ export function WorkbenchTableList({ onOpen, className }: WorkbenchTableListProp
|
|
|
89
100
|
</li>
|
|
90
101
|
))}
|
|
91
102
|
</ul>
|
|
103
|
+
<LoadMore shown={(tables.data ?? []).length} noun="workbenches" hasMore={tables.hasMore} loading={tables.loadingMore} onLoadMore={tables.loadMore} />
|
|
92
104
|
<ConfirmSpendDialog
|
|
93
105
|
open={deleting != null}
|
|
94
106
|
tier="destructive"
|
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
Editable React screens for automations: a workflow list, a step-list editor (not a canvas), trigger
|
|
4
4
|
configuration and run history. Install with `npx @graph8/sdk add workflows`.
|
|
5
5
|
|
|
6
|
+
## Screens
|
|
7
|
+
|
|
8
|
+
`WorkflowsScreen` (`components/workflows-screen.tsx`) is the finished automations workspace; mount it as the page.
|
|
9
|
+
|
|
10
|
+
- **Workflows tab**: stat strip (workflows, enabled, disabled, failed runs), search, status filter, the list with
|
|
11
|
+
Run (plan preview and run input first), Enable / Disable and Delete, and **New workflow**. Opening one (deep link
|
|
12
|
+
`?id=`) shows **Steps** (editor), **Trigger** (type, interval, cursor reset) and **Runs** for that workflow.
|
|
13
|
+
- **Runs tab**: every run as a list; opening one (`?run=`) shows `RunDetail` (status, timing, who started it, error, input, output) with pause / resume / stop / restart. `&view=full` opens any record as a full page.
|
|
14
|
+
- Create, run, enable and disable are `external`, delete is `destructive`: all go through `ConfirmSpendDialog`.
|
|
15
|
+
Needs `workflows:read|write|run|delete`.
|
|
16
|
+
- Slots: `renderRowActions`, `renderDetailActions`, `extraDetailTabs`, `toolbarActions`. `RunPlan` and `RunDetail` are now
|
|
17
|
+
exported; `RunHistory` gained `selectedId`, `onSelectRun`, `hideDetail`, `refreshKey`, `nameFor`.
|
|
18
|
+
|
|
6
19
|
| Component | What it is |
|
|
7
20
|
|---|---|
|
|
8
21
|
| `WorkflowList` | Workflows with run (plan preview + JSON input), enable/disable, create, delete. |
|
|
@@ -13,24 +13,42 @@
|
|
|
13
13
|
"foundation"
|
|
14
14
|
],
|
|
15
15
|
"operations": [
|
|
16
|
-
"list_workflows_workflows_get",
|
|
17
16
|
"create_workflow_workflows_post",
|
|
18
|
-
"plan_workflow_workflows__action_id__plan_post",
|
|
19
|
-
"execute_workflow_workflows__action_id__execute_post",
|
|
20
|
-
"update_workflow_workflows__action_id__put",
|
|
21
17
|
"delete_workflow_workflows__action_id__delete",
|
|
22
|
-
"
|
|
18
|
+
"execute_workflow_workflows__action_id__execute_post",
|
|
19
|
+
"get_execution_workflows_executions__execution_id__get",
|
|
23
20
|
"get_node_types_schema_workflows_node_types_schema_get",
|
|
24
21
|
"get_trigger_status_workflows__action_id__trigger_status_get",
|
|
25
|
-
"
|
|
22
|
+
"get_workflow_workflows__action_id__get",
|
|
26
23
|
"list_workflow_executions_workflows_executions_get",
|
|
27
|
-
"
|
|
24
|
+
"list_workflows_workflows_get",
|
|
28
25
|
"pause_execution_workflows_executions__execution_id__pause_post",
|
|
26
|
+
"plan_workflow_workflows__action_id__plan_post",
|
|
27
|
+
"reset_trigger_cursor_workflows__action_id__trigger_reset_post",
|
|
28
|
+
"restart_execution_workflows_executions__execution_id__restart_post",
|
|
29
29
|
"resume_execution_workflows_executions__execution_id__resume_post",
|
|
30
30
|
"stop_execution_workflows_executions__execution_id__stop_post",
|
|
31
|
-
"
|
|
31
|
+
"update_workflow_workflows__action_id__put"
|
|
32
32
|
],
|
|
33
33
|
"components": [
|
|
34
|
+
{
|
|
35
|
+
"name": "WorkflowsScreen",
|
|
36
|
+
"file": "components/workflows-screen.tsx",
|
|
37
|
+
"purpose": "Finished automations workspace: stats, search, list with run/enable/delete, create, and a detail panel with steps, trigger and runs, plus a workspace-wide Runs tab.",
|
|
38
|
+
"operations": [
|
|
39
|
+
"create_workflow_workflows_post",
|
|
40
|
+
"delete_workflow_workflows__action_id__delete",
|
|
41
|
+
"execute_workflow_workflows__action_id__execute_post",
|
|
42
|
+
"list_workflow_executions_workflows_executions_get",
|
|
43
|
+
"list_workflows_workflows_get",
|
|
44
|
+
"update_workflow_workflows__action_id__put"
|
|
45
|
+
],
|
|
46
|
+
"tiers": [
|
|
47
|
+
"read",
|
|
48
|
+
"external",
|
|
49
|
+
"destructive"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
34
52
|
{
|
|
35
53
|
"name": "WorkflowList",
|
|
36
54
|
"file": "components/workflow-list.tsx",
|
|
@@ -183,6 +201,7 @@
|
|
|
183
201
|
}
|
|
184
202
|
],
|
|
185
203
|
"peerDependencies": {
|
|
186
|
-
"react": ">=18"
|
|
204
|
+
"react": ">=18",
|
|
205
|
+
"lucide-react": ">=0.400.0"
|
|
187
206
|
}
|
|
188
207
|
}
|