@graph8/sdk 0.361.0 → 0.370.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 +14 -10
- package/blocks/_foundation/README.md +172 -1
- package/blocks/_foundation/block.json +234 -13
- package/blocks/_foundation/components/api-error.tsx +71 -25
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- 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 +39 -7
- package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
- package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
- package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
- package/blocks/_foundation/components/screen/index.ts +22 -0
- package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
- package/blocks/_foundation/components/screen/section-card.tsx +47 -0
- package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
- package/blocks/_foundation/components/screen/split-view.tsx +125 -0
- package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
- package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +32 -0
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/components/screen/toolbar.tsx +144 -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-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/hooks/use-url-state.ts +74 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/plural.ts +23 -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 +14 -0
- package/blocks/agents/block.json +33 -1
- package/blocks/agents/components/agent-builder.tsx +13 -8
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -1
- package/blocks/agents/components/agent-run-detail.tsx +102 -0
- package/blocks/agents/components/agent-runs.tsx +16 -7
- package/blocks/agents/components/agents-screen.tsx +184 -0
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +209 -0
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +23 -9
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +21 -1
- package/blocks/appointments/block.json +43 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +77 -0
- package/blocks/appointments/components/meeting-list.tsx +14 -6
- package/blocks/appointments/components/meetings-screen.tsx +157 -0
- package/blocks/appointments/components/scheduling-screen.tsx +122 -0
- package/blocks/appointments/hooks/use-event-types.ts +17 -0
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +26 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +16 -5
- package/blocks/campaigns/components/campaign-launch-step.tsx +102 -19
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-wizard.tsx +43 -10
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +154 -0
- package/blocks/campaigns/components/icp-editor.tsx +15 -15
- package/blocks/campaigns/components/persona-detail.tsx +172 -0
- package/blocks/campaigns/components/persona-editor.tsx +14 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +134 -1
- 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/coach-screen.tsx +222 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- 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 +7 -5
- package/blocks/crm/README.md +28 -0
- package/blocks/crm/block.json +58 -2
- package/blocks/crm/components/companies-screen.tsx +345 -0
- package/blocks/crm/components/company-detail.tsx +8 -6
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +6 -3
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +359 -0
- package/blocks/crm/components/dedupe-review.tsx +7 -5
- package/blocks/crm/components/import-wizard.tsx +4 -2
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-form.tsx +5 -3
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +379 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +7 -1
- package/blocks/crm/lib/crm-types.ts +12 -2
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +34 -3
- package/blocks/deals/block.json +45 -4
- package/blocks/deals/components/deal-detail.tsx +27 -13
- package/blocks/deals/components/deal-related.tsx +87 -0
- package/blocks/deals/components/deals-screen.tsx +298 -0
- package/blocks/deals/components/pipeline-board.tsx +4 -2
- package/blocks/deals/components/stage-editor.tsx +4 -4
- package/blocks/deals/hooks/use-default-owner.ts +60 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +5 -1
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deals-types.ts +14 -0
- package/blocks/dialer/README.md +34 -2
- package/blocks/dialer/block.json +67 -1
- package/blocks/dialer/components/call-contact-button.tsx +252 -0
- package/blocks/dialer/components/call-log-screen.tsx +205 -0
- package/blocks/dialer/components/call-log.tsx +7 -3
- package/blocks/dialer/components/call-queue.tsx +1 -1
- package/blocks/dialer/components/dialer-screen.tsx +319 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/session-start-form.tsx +4 -2
- package/blocks/dialer/components/softphone.tsx +3 -1
- package/blocks/enrichment/README.md +14 -2
- package/blocks/enrichment/block.json +32 -4
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +10 -5
- package/blocks/enrichment/components/email-verify-badge.tsx +28 -8
- 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 +288 -0
- package/blocks/enrichment/components/waterfall-config.tsx +39 -10
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +12 -0
- package/blocks/forms/block.json +39 -17
- package/blocks/forms/components/form-builder.tsx +3 -1
- package/blocks/forms/components/form-embed.tsx +3 -1
- package/blocks/forms/components/forms-screen.tsx +353 -0
- package/blocks/forms/components/submissions-table.tsx +16 -6
- package/blocks/inbox/README.md +27 -3
- package/blocks/inbox/block.json +14 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- 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 +9 -0
- package/blocks/integrations/block.json +46 -1
- package/blocks/integrations/components/crm-detail.tsx +100 -0
- package/blocks/integrations/components/integration-list.tsx +9 -4
- package/blocks/integrations/components/integrations-screen.tsx +135 -0
- package/blocks/integrations/components/sync-status.tsx +5 -3
- package/blocks/integrations/components/webhook-detail.tsx +160 -0
- package/blocks/integrations/components/webhook-manager.tsx +13 -8
- package/blocks/integrations/lib/integrations-utils.ts +6 -1
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/intent-feed.tsx +5 -2
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -4
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/lifecycle/README.md +16 -0
- package/blocks/lifecycle/block.json +34 -7
- package/blocks/lifecycle/components/account-related.tsx +66 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +149 -0
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +13 -0
- package/blocks/lists/block.json +39 -1
- package/blocks/lists/components/add-to-list-button.tsx +3 -2
- package/blocks/lists/components/list-picker.tsx +5 -1
- package/blocks/lists/components/list-table.tsx +11 -5
- package/blocks/lists/components/lists-screen.tsx +344 -0
- package/blocks/lists/components/suppression-manager.tsx +6 -2
- package/blocks/lists/components/suppressions-screen.tsx +255 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +65 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +23 -13
- package/blocks/mailboxes/components/domain-detail.tsx +142 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +6 -3
- package/blocks/mailboxes/components/mailbox-detail.tsx +156 -0
- package/blocks/mailboxes/components/mailboxes-screen.tsx +223 -0
- package/blocks/mailboxes/components/warmup-health.tsx +4 -2
- package/blocks/mailboxes/lib/mailboxes-utils.ts +22 -1
- package/blocks/newsletter/README.md +13 -0
- package/blocks/newsletter/block.json +47 -23
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -1
- package/blocks/newsletter/components/newsletter-screen.tsx +404 -0
- package/blocks/newsletter/components/subscriber-table.tsx +12 -6
- package/blocks/nurture/README.md +12 -0
- package/blocks/nurture/block.json +35 -13
- package/blocks/nurture/components/nurture-builder.tsx +3 -1
- package/blocks/nurture/components/nurture-enroll.tsx +6 -3
- package/blocks/nurture/components/nurture-form.tsx +23 -3
- package/blocks/nurture/components/nurture-screen.tsx +283 -0
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +17 -0
- package/blocks/objects/block.json +29 -16
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +2 -1
- package/blocks/objects/components/object-schema-editor.tsx +3 -1
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +12 -0
- package/blocks/pages/block.json +35 -22
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +46 -11
- package/blocks/pages/components/page-list.tsx +6 -3
- package/blocks/pages/components/pages-screen.tsx +154 -0
- package/blocks/quotes/README.md +17 -0
- package/blocks/quotes/block.json +38 -9
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-list.tsx +7 -3
- package/blocks/quotes/components/quote-related.tsx +123 -0
- package/blocks/quotes/components/quotes-screen.tsx +169 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- 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 +49 -9
- package/blocks/sequencer/block.json +99 -19
- package/blocks/sequencer/components/enroll-dialog.tsx +9 -3
- package/blocks/sequencer/components/sequence-builder.tsx +211 -46
- package/blocks/sequencer/components/sequence-detail.tsx +259 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +114 -0
- package/blocks/sequencer/components/sequence-list.tsx +33 -8
- package/blocks/sequencer/components/sequence-stats.tsx +3 -2
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +159 -0
- package/blocks/sequencer/components/step-editor.tsx +3 -1
- package/blocks/sequencer/lib/sequencer-utils.ts +128 -5
- 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 +11 -0
- package/blocks/social-ads/block.json +101 -2
- 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 +188 -0
- package/blocks/social-ads/components/audience-sync.tsx +11 -7
- package/blocks/social-ads/components/campaign-detail.tsx +133 -0
- package/blocks/social-ads/components/creative-detail.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +15 -5
- package/blocks/social-ads/components/post-composer.tsx +3 -1
- package/blocks/social-ads/components/post-detail.tsx +160 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +160 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +3 -1
- package/blocks/visitors/components/visitor-company-card.tsx +2 -2
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +11 -2
- 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 +7 -4
- package/blocks/webchat/components/conversation-list.tsx +7 -3
- 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 +4 -2
- package/blocks/workbench/components/workbench-grid.tsx +9 -3
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +192 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- 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 +3 -1
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +2785 -1637
- 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 +113 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +25 -8
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +20 -0
- package/dist/types/blocks/navfile.d.ts +35 -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 +32 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +2 -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.json +41 -16
- package/stacks/apollo/pages/companies.tsx.tmpl +50 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +117 -0
- package/stacks/apollo.json +118 -36
- package/stacks/calendly.json +60 -0
- package/stacks/clay/pages/companies.tsx.tmpl +15 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +19 -0
- package/stacks/clay.json +59 -26
- package/stacks/gong.json +81 -35
- package/stacks/hootsuite.json +99 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +47 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +111 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +92 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot.json +167 -66
- package/stacks/intercom.json +48 -20
- package/stacks/mailchimp.json +52 -33
- package/stacks/outreach/pages/contacts.tsx.tmpl +85 -0
- package/stacks/outreach.json +88 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +47 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +92 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive.json +115 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +47 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce.json +81 -31
- package/stacks/stack.schema.json +6 -1
- package/stacks/zoominfo/pages/companies.tsx.tmpl +15 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +102 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +55 -8
- package/templates/next-app/app/globals.css.tmpl +105 -0
- package/templates/next-app/app/layout.tsx.tmpl +18 -4
- package/templates/next-app/components/app-shell.tsx.tmpl +248 -23
- package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
- package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
- package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/lib/nav.ts.tmpl +1 -1
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WebchatScreen: the finished web chat page with three tabs.
|
|
5
|
+
* Visitors: the people who used the widget (search, identified-only, paging), engagement funnel as stats, and a detail
|
|
6
|
+
* panel for the selected visitor (deep link `?id=`). Message threads live in the inbox block: pass
|
|
7
|
+
* `renderDetailActions` to link a visitor to their conversation.
|
|
8
|
+
* Widget: ChatWidgetConfig (pick or create a chat bot, style the widget, starter questions, delete a question with confirm).
|
|
9
|
+
* Install: the tracking snippet to paste on your pages, plus what must be set up in graph8 first.
|
|
10
|
+
*
|
|
11
|
+
* SDK operations (all through the composed parts): listWebchatVisitors, getWebchatVisitorFunnel, getOrgSnippet (read);
|
|
12
|
+
* ChatWidgetConfig's bot, settings and question calls (write); deleteWebchatQuestion (destructive, ConfirmSpendDialog).
|
|
13
|
+
* Saves in the Widget tab are live for the next visitor. Mapping an answering agent to a bot is done in the graph8 app.
|
|
14
|
+
*/
|
|
15
|
+
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
|
16
|
+
import { Copy, MessageSquare } from "lucide-react";
|
|
17
|
+
import { Button, Skeleton, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@graph8/sdk/ui";
|
|
18
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
19
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
20
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
21
|
+
import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
|
|
22
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
23
|
+
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
24
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
25
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
26
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
27
|
+
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
28
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
29
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
30
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
31
|
+
import { pickItems, visitorCompany, visitorEmail, visitorLastSeen, visitorName } from "../lib/webchat-types";
|
|
32
|
+
import type { Visitor } from "../lib/webchat-types";
|
|
33
|
+
import { ChatWidgetConfig } from "./chat-widget-config";
|
|
34
|
+
import { rowActivationProps } from "@/g8/hooks/use-row-activation";
|
|
35
|
+
import { RecordLink } from "@/g8/components/screen/record-link";
|
|
36
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
37
|
+
|
|
38
|
+
export interface WebchatScreenProps {
|
|
39
|
+
/** Limit the funnel and open the Widget tab on this bot. */
|
|
40
|
+
botId?: string;
|
|
41
|
+
pageSize?: number;
|
|
42
|
+
/** Extra buttons in the header. */
|
|
43
|
+
toolbarActions?: ReactNode;
|
|
44
|
+
/** Buttons in the visitor detail header (for example "Open in inbox"). */
|
|
45
|
+
renderDetailActions?: (visitor: Visitor) => ReactNode;
|
|
46
|
+
/** Extra tabs in the visitor detail panel. */
|
|
47
|
+
extraDetailTabs?: Array<{ id: string; label: string; render: (visitor: Visitor) => ReactNode }>;
|
|
48
|
+
renderRowActions?: (visitor: Visitor) => ReactNode;
|
|
49
|
+
className?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const TABS = [
|
|
53
|
+
{ id: "visitors", label: "Visitors" },
|
|
54
|
+
{ id: "widget", label: "Widget" },
|
|
55
|
+
{ id: "install", label: "Install" },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
function useDebounced<T>(value: T, ms = 300): T {
|
|
59
|
+
const [v, setV] = useState(value);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const t = setTimeout(() => setV(value), ms);
|
|
62
|
+
return () => clearTimeout(t);
|
|
63
|
+
}, [value, ms]);
|
|
64
|
+
return v;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A stable, deep-linkable key: the visitor id, else the email. A row with neither is selectable but not linkable (never the row index, which points at different people per page). */
|
|
68
|
+
const idOf = (v: Visitor): string | null => {
|
|
69
|
+
if (v.id != null && String(v.id) !== "") return String(v.id);
|
|
70
|
+
const email = visitorEmail(v);
|
|
71
|
+
return email ? `email:${email}` : null;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function flatFields(v: Visitor): Array<{ label: string; value: ReactNode }> {
|
|
75
|
+
return Object.entries(v)
|
|
76
|
+
.filter(([, x]) => ["string", "number", "boolean"].includes(typeof x) && String(x) !== "")
|
|
77
|
+
.map(([k, x]) => ({ label: k.replace(/_/g, " "), value: String(x) }));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function VisitorDetail({ visitor, onClose, onExpand, onCollapse, props }: { visitor: Visitor; onClose: () => void; onExpand?: () => void; onCollapse?: () => void; props: WebchatScreenProps }) {
|
|
81
|
+
const name = visitorName(visitor);
|
|
82
|
+
const identified = Boolean(visitorEmail(visitor));
|
|
83
|
+
return (
|
|
84
|
+
<DetailPanel
|
|
85
|
+
title={name}
|
|
86
|
+
subtitle={[visitorEmail(visitor), visitorCompany(visitor)].filter(Boolean).join(" at ") || "Anonymous visitor"}
|
|
87
|
+
avatar={<EntityAvatar name={name} kind="person" />}
|
|
88
|
+
badges={<StatusPill tone={identified ? "positive" : "neutral"}>{identified ? "Identified" : "Anonymous"}</StatusPill>}
|
|
89
|
+
actions={props.renderDetailActions?.(visitor)}
|
|
90
|
+
onClose={onClose}
|
|
91
|
+
onExpand={onExpand}
|
|
92
|
+
onCollapse={onCollapse}
|
|
93
|
+
tabs={[
|
|
94
|
+
{ id: "overview", label: "Overview", content: <KeyValueList columns={1} items={[{ label: "Last seen", value: visitorLastSeen(visitor) || "Not known" }, ...flatFields(visitor)]} /> },
|
|
95
|
+
...(props.extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(visitor) })),
|
|
96
|
+
]}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function VisitorsTab({ props }: { props: WebchatScreenProps }) {
|
|
102
|
+
const pageSize = props.pageSize ?? 25;
|
|
103
|
+
const [page, setPage] = useState(1);
|
|
104
|
+
const [identifiedOnly, setIdentifiedOnly] = useState(false);
|
|
105
|
+
const [text, setText] = useState("");
|
|
106
|
+
const search = useDebounced(text.trim());
|
|
107
|
+
const [openId, setOpenId] = useUrlState("id");
|
|
108
|
+
const [view, setView] = useUrlState("view");
|
|
109
|
+
const [localSel, setLocalSel] = useState<Visitor | null>(null);
|
|
110
|
+
const visitors = useG8Query(
|
|
111
|
+
(signal) =>
|
|
112
|
+
g8Get<unknown>("/webchat/visitors", { page, page_size: pageSize, identified_only: identifiedOnly || undefined, search: search || undefined }, { signal }).then((r) => ({
|
|
113
|
+
items: pickItems<Visitor>(r.data),
|
|
114
|
+
total: (r.data as { total?: number } | undefined)?.total,
|
|
115
|
+
})),
|
|
116
|
+
[page, pageSize, identifiedOnly, search],
|
|
117
|
+
);
|
|
118
|
+
const items = visitors.data?.items ?? [];
|
|
119
|
+
const selected = (openId ? items.find((v) => idOf(v) === openId) : undefined) ?? (localSel && items.includes(localSel) ? localSel : undefined);
|
|
120
|
+
const reset = () => { setPage(1); setOpenId(null); setView(null); setLocalSel(null); };
|
|
121
|
+
const pick = (v: Visitor) => { const id = idOf(v); setLocalSel(id ? null : v); setOpenId(id); };
|
|
122
|
+
const expanded = view === "full" && Boolean(selected);
|
|
123
|
+
|
|
124
|
+
const list = (
|
|
125
|
+
<div className="space-y-3">
|
|
126
|
+
<Toolbar
|
|
127
|
+
search={{ value: text, onChange: (v) => { setText(v); reset(); }, placeholder: "Search by name or email" }}
|
|
128
|
+
filters={<label className="flex items-center gap-2 text-sm"><input type="checkbox" checked={identifiedOnly} onChange={(e) => { setIdentifiedOnly(e.target.checked); reset(); }} />Identified only</label>}
|
|
129
|
+
/>
|
|
130
|
+
{visitors.error ? (
|
|
131
|
+
<ApiError error={visitors.error} title="Could not load visitors" onRetry={() => void visitors.refetch()} />
|
|
132
|
+
) : visitors.loading && !visitors.data ? (
|
|
133
|
+
<div aria-busy="true" className="grid gap-2">{Array.from({ length: 4 }, (_, i) => <Skeleton key={i} className="h-9 w-full" />)}</div>
|
|
134
|
+
) : items.length === 0 ? (
|
|
135
|
+
search || identifiedOnly ? (
|
|
136
|
+
<EmptyState title="No visitors match" description="Try a different search, or include anonymous visitors." action={<Button size="sm" variant="outline" onClick={() => { setText(""); setIdentifiedOnly(false); reset(); }}>Clear filters</Button>} />
|
|
137
|
+
) : (
|
|
138
|
+
<EmptyState icon={<MessageSquare className="size-6" />} title="No chat visitors yet" description="People appear here after they use the widget on your site. Install the snippet first." action={<span className="text-sm text-muted-foreground">See the Install tab.</span>} />
|
|
139
|
+
)
|
|
140
|
+
) : (
|
|
141
|
+
<Table>
|
|
142
|
+
<TableHeader><TableRow><TableHead>Visitor</TableHead><TableHead>Email</TableHead><TableHead>Company</TableHead><TableHead>Last seen</TableHead>{props.renderRowActions ? <TableHead /> : null}</TableRow></TableHeader>
|
|
143
|
+
<TableBody>
|
|
144
|
+
{items.map((v, i) => (
|
|
145
|
+
<TableRow key={idOf(v) ?? `row-${i}`} data-state={selected === v ? "selected" : undefined} className="cursor-pointer" {...rowActivationProps(() => pick(v))}>
|
|
146
|
+
<TableCell><span className="flex items-center gap-2"><EntityAvatar name={visitorName(v)} kind="person" size="sm" />{idOf(v) ? <RecordLink param="id" id={idOf(v) as string} onOpen={() => pick(v)}>{visitorName(v)}</RecordLink> : visitorName(v)}</span></TableCell>
|
|
147
|
+
<TableCell>{visitorEmail(v)}</TableCell>
|
|
148
|
+
<TableCell>{visitorCompany(v)}</TableCell>
|
|
149
|
+
<TableCell>{visitorLastSeen(v)}</TableCell>
|
|
150
|
+
{props.renderRowActions ? <TableCell onClick={(e) => e.stopPropagation()}>{props.renderRowActions(v)}</TableCell> : null}
|
|
151
|
+
</TableRow>
|
|
152
|
+
))}
|
|
153
|
+
</TableBody>
|
|
154
|
+
</Table>
|
|
155
|
+
)}
|
|
156
|
+
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
157
|
+
<span>{visitors.data?.total != null ? `${visitors.data.total.toLocaleString()} visitors` : `Page ${page}`}</span>
|
|
158
|
+
<div className="flex gap-2">
|
|
159
|
+
<Button size="sm" variant="outline" disabled={page <= 1} onClick={() => { setPage(page - 1); setOpenId(null); setLocalSel(null); }}>Previous</Button>
|
|
160
|
+
<Button size="sm" variant="outline" disabled={items.length < pageSize} onClick={() => { setPage(page + 1); setOpenId(null); setLocalSel(null); }}>Next</Button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
const close = () => { setOpenId(null); setView(null); setLocalSel(null); };
|
|
166
|
+
return (
|
|
167
|
+
<SplitView
|
|
168
|
+
list={list}
|
|
169
|
+
detailOpen={Boolean(selected)}
|
|
170
|
+
onCloseDetail={close}
|
|
171
|
+
expanded={expanded}
|
|
172
|
+
onExpandedChange={(next) => setView(next ? "full" : null)}
|
|
173
|
+
detail={selected ? <VisitorDetail visitor={selected} onClose={close} onExpand={expanded ? undefined : () => setView("full")} onCollapse={expanded ? () => setView(null) : undefined} props={props} /> : null}
|
|
174
|
+
/>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function InstallTab() {
|
|
179
|
+
const brand = useBrand();
|
|
180
|
+
const snippet = useG8Query((signal) => g8Get<{ script_tag?: string }>("/snippet", {}, { signal }).then((r) => r.data), []);
|
|
181
|
+
const [copied, setCopied] = useState(false);
|
|
182
|
+
const [copyFailed, setCopyFailed] = useState(false);
|
|
183
|
+
const tag = snippet.data?.script_tag;
|
|
184
|
+
const copy = () => {
|
|
185
|
+
setCopyFailed(false);
|
|
186
|
+
const write = navigator.clipboard?.writeText(tag ?? "");
|
|
187
|
+
if (!write) { setCopyFailed(true); return; }
|
|
188
|
+
write.then(() => setCopied(true)).catch(() => { setCopied(false); setCopyFailed(true); });
|
|
189
|
+
};
|
|
190
|
+
return (
|
|
191
|
+
<div className="grid gap-4">
|
|
192
|
+
<SectionCard title="Embed code" description={`The widget loads through the ${brand.linkLabel} tracking snippet. Add it to every page that should show the chat.`}>
|
|
193
|
+
{snippet.error ? (
|
|
194
|
+
<ApiError error={snippet.error} title="Could not load the snippet" onRetry={() => void snippet.refetch()} />
|
|
195
|
+
) : tag ? (
|
|
196
|
+
<div className="space-y-2">
|
|
197
|
+
<pre aria-label="Snippet" className="overflow-auto rounded-md bg-muted p-3 text-xs">{tag}</pre>
|
|
198
|
+
<Button size="sm" variant="outline" onClick={copy}><Copy className="mr-1 size-4" />{copied ? "Copied" : "Copy snippet"}</Button>
|
|
199
|
+
{copyFailed ? <p role="alert" className="text-sm text-error-foreground">Copy failed. Select the code above and copy it.</p> : null}
|
|
200
|
+
</div>
|
|
201
|
+
) : (
|
|
202
|
+
<Skeleton className="h-12 w-full" />
|
|
203
|
+
)}
|
|
204
|
+
</SectionCard>
|
|
205
|
+
<SectionCard title="Before it works">
|
|
206
|
+
<ul className="list-disc space-y-1 pl-5 text-sm">
|
|
207
|
+
<li>The snippet is on the pages that should show the chat.</li>
|
|
208
|
+
<li>An answering agent is mapped to the bot. A bot with none answers nothing. Set it up in {brand.linkLabel}.</li>
|
|
209
|
+
<li>Style the widget in the Widget tab. Saves are live for the next visitor.</li>
|
|
210
|
+
</ul>
|
|
211
|
+
<a className="mt-2 inline-block text-sm underline" href={brand.appUrl} target="_blank" rel="noreferrer">Open {brand.linkLabel}</a>
|
|
212
|
+
</SectionCard>
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function WebchatScreen(props: WebchatScreenProps) {
|
|
218
|
+
const { botId, toolbarActions, className } = props;
|
|
219
|
+
const [tab, setTab] = useUrlState("tab", "visitors");
|
|
220
|
+
const funnel = useG8Query((signal) => g8Get<Record<string, unknown>>("/webchat/visitors/funnel", { bot_id: botId }, { signal }).then((r) => r.data), [botId]);
|
|
221
|
+
const stats = useMemo(() => {
|
|
222
|
+
const counts = Object.entries((funnel.data?.counts ?? funnel.data?.events ?? {}) as Record<string, unknown>).filter(([, v]) => typeof v === "number") as Array<[string, number]>;
|
|
223
|
+
return counts.slice(0, 4).map(([k, v]) => ({ label: k.replace(/_/g, " "), value: v.toLocaleString() }));
|
|
224
|
+
}, [funnel.data]);
|
|
225
|
+
const active = TABS.some((t) => t.id === tab) ? tab : "visitors";
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<ScreenLayout
|
|
229
|
+
className={className}
|
|
230
|
+
title="Web chat"
|
|
231
|
+
description="Who talks to your site's AI chat, how it looks, and how to put it on your pages."
|
|
232
|
+
icon={<MessageSquare className="size-5" />}
|
|
233
|
+
actions={toolbarActions}
|
|
234
|
+
tabs={TABS}
|
|
235
|
+
activeTab={active}
|
|
236
|
+
onTabChange={(id) => setTab(id)}
|
|
237
|
+
stats={stats.length > 0 ? stats : undefined}
|
|
238
|
+
>
|
|
239
|
+
{funnel.error ? <p role="alert" className="mb-3 text-sm text-muted-foreground">Could not load the engagement numbers. <button type="button" className="underline" onClick={() => void funnel.refetch()}>Retry</button></p> : null}
|
|
240
|
+
{active === "visitors" ? <VisitorsTab props={props} /> : null}
|
|
241
|
+
{active === "widget" ? <ChatWidgetConfig initialBotId={botId} /> : null}
|
|
242
|
+
{active === "install" ? <InstallTab /> : null}
|
|
243
|
+
</ScreenLayout>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
@@ -7,6 +7,17 @@ price, run it, and watch it finish. Finished rows go to a list. Builds on `enric
|
|
|
7
7
|
|
|
8
8
|
Install: `npx @graph8/sdk add workbench` (installs `foundation`, `crm`, `enrichment` and `lists` first).
|
|
9
9
|
|
|
10
|
+
## Screens
|
|
11
|
+
|
|
12
|
+
`WorkbenchScreen` is the finished Workbench page; mount it with no props. The list view shows every workbench with row and
|
|
13
|
+
column counts, search, **New workbench** (People or Companies) and a confirmed delete. `?id=<table>` opens a table: a
|
|
14
|
+
header with row, column and recipe-column counts and the full `WorkbenchGrid` (edit cells, paste or add rows, add AI,
|
|
15
|
+
waterfall and formula columns, quote then run, progress, unlock locked rows, send rows to a list, delete rows).
|
|
16
|
+
**Import people** (free preview and match count, then a confirmed import; rows land locked) and **Scrape the web** (credit estimate and balance, confirmed run, polled progress) are in the table header (`SearchImportPanel`, `ScraperPanel`). Open any row (`?row=`) for `WorkbenchRowDetail`: every cell, per-cell edit, unlock, send to a list, delete. `?view=full` opens a row full-page.
|
|
17
|
+
Slots: `toolbarActions`, `renderRowActions(table)`, `renderDetailActions(table)`.
|
|
18
|
+
|
|
19
|
+
Prerequisites: `enrichment` and `lists` installed, proxy allowlist `workbench`, `enrichment`, `lists`; provider keys and credits as below.
|
|
20
|
+
|
|
10
21
|
## What you get
|
|
11
22
|
|
|
12
23
|
| Component | Use it for | Props |
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"add rows by typing or pasting a spreadsheet; locked rows from a search import show masked cells",
|
|
9
9
|
"column recipes: AI prompt with {{column}} placeholders, enrichment waterfall presets, formulas, plain columns, target-field mapping",
|
|
10
10
|
"runs are quoted for free, confirmed with the exact credits, started with the quote as the cap and followed to a final receipt",
|
|
11
|
-
"unlock locked rows (1 credit each), send rows to a list, delete rows"
|
|
11
|
+
"unlock locked rows (1 credit each), send rows to a list, delete rows",
|
|
12
|
+
"finished WorkbenchScreen page"
|
|
12
13
|
],
|
|
13
14
|
"dependsOn": [
|
|
14
15
|
"enrichment",
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
"export_workbench_table_workbench_tables__table_id__export_post",
|
|
26
27
|
"get_workbench_analytics_workbench_tables__table_id__analytics_get",
|
|
27
28
|
"get_workbench_history_workbench_tables__table_id__history_get",
|
|
29
|
+
"get_workbench_row_workbench_tables__table_id__rows__row_id__get",
|
|
28
30
|
"get_workbench_scraper_run_workbench_tables__table_id__scraper_runs__run_id__get",
|
|
29
31
|
"get_workbench_table_workbench_tables__table_id__get",
|
|
30
32
|
"import_workbench_from_search_workbench_tables__table_id__import_search_post",
|
|
@@ -143,6 +145,50 @@
|
|
|
143
145
|
"billable",
|
|
144
146
|
"destructive"
|
|
145
147
|
]
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"name": "WorkbenchScreen",
|
|
151
|
+
"file": "components/workbench-screen.tsx",
|
|
152
|
+
"purpose": "Finished Workbench page: workbench list with create/delete and the full table (WorkbenchGrid) with a stats header.",
|
|
153
|
+
"operations": [
|
|
154
|
+
"create_workbench_table_workbench_tables_post",
|
|
155
|
+
"delete_workbench_table_workbench_tables__table_id__delete",
|
|
156
|
+
"list_workbench_tables_workbench_tables_get"
|
|
157
|
+
],
|
|
158
|
+
"tiers": [
|
|
159
|
+
"read",
|
|
160
|
+
"write",
|
|
161
|
+
"destructive"
|
|
162
|
+
]
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"name": "SearchImportPanel",
|
|
166
|
+
"file": "components/workbench-sources.tsx",
|
|
167
|
+
"purpose": "Import people from a search into a table: free preview and match count, then a confirmed billable import (rows land locked). The same file exports ScraperPanel: web scraper with credit estimate, confirmed billable run and polled progress.",
|
|
168
|
+
"operations": [
|
|
169
|
+
"estimate_workbench_scraper_credits_workbench_scraper_credit_estimate_get",
|
|
170
|
+
"get_workbench_scraper_run_workbench_tables__table_id__scraper_runs__run_id__get",
|
|
171
|
+
"import_workbench_from_search_workbench_tables__table_id__import_search_post",
|
|
172
|
+
"preview_workbench_search_import_workbench_tables__table_id__import_search_preview_post",
|
|
173
|
+
"run_workbench_scraper_workbench_tables__table_id__scraper_runs_post"
|
|
174
|
+
],
|
|
175
|
+
"tiers": [
|
|
176
|
+
"read",
|
|
177
|
+
"billable"
|
|
178
|
+
]
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"name": "WorkbenchRowDetail",
|
|
182
|
+
"file": "components/workbench-row-detail.tsx",
|
|
183
|
+
"purpose": "One workbench row in full: every cell, status, per-cell edit, unlock, send to a list and delete.",
|
|
184
|
+
"operations": [
|
|
185
|
+
"get_workbench_row_workbench_tables__table_id__rows__row_id__get",
|
|
186
|
+
"update_workbench_row_workbench_tables__table_id__rows__row_id__patch"
|
|
187
|
+
],
|
|
188
|
+
"tiers": [
|
|
189
|
+
"read",
|
|
190
|
+
"write"
|
|
191
|
+
]
|
|
146
192
|
}
|
|
147
193
|
],
|
|
148
194
|
"hooks": [
|
|
@@ -254,6 +300,7 @@
|
|
|
254
300
|
}
|
|
255
301
|
],
|
|
256
302
|
"peerDependencies": {
|
|
303
|
+
"lucide-react": ">=0.400.0",
|
|
257
304
|
"react": ">=18"
|
|
258
305
|
}
|
|
259
306
|
}
|
|
@@ -30,6 +30,7 @@ import { useProviders } from "@/g8/enrichment/hooks/use-providers";
|
|
|
30
30
|
import { WATERFALL_PRESETS, buildWaterfallConfig, requiredTargets } from "../lib/waterfall-presets";
|
|
31
31
|
import { pageItems, toMachineName } from "../lib/workbench-types";
|
|
32
32
|
import type { WorkbenchColumn } from "../lib/workbench-types";
|
|
33
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
33
34
|
|
|
34
35
|
export interface ColumnRecipeEditorProps {
|
|
35
36
|
tableId: string;
|
|
@@ -52,6 +53,7 @@ const TABS: Array<{ id: Tab; label: string }> = [
|
|
|
52
53
|
const newId = () => globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
53
54
|
|
|
54
55
|
export function ColumnRecipeEditor({ tableId, columns, entityType = "contact", onSaved, className }: ColumnRecipeEditorProps) {
|
|
56
|
+
const brand = useBrand();
|
|
55
57
|
const [tab, setTab] = useState<Tab>("ai");
|
|
56
58
|
const [name, setName] = useState("");
|
|
57
59
|
const [prompt, setPrompt] = useState("");
|
|
@@ -132,7 +134,7 @@ export function ColumnRecipeEditor({ tableId, columns, entityType = "contact", o
|
|
|
132
134
|
<label key={s.provider} className="flex items-center gap-2 text-sm">
|
|
133
135
|
<input type="checkbox" aria-label={`Use ${s.provider}`} checked={!skipped.includes(s.provider)} onChange={() => setSkipped(skipped.includes(s.provider) ? skipped.filter((x) => x !== s.provider) : [...skipped, s.provider])} />
|
|
134
136
|
<span className="w-6 text-muted-foreground">{i + 1}.</span><span className="font-medium">{s.provider}</span>
|
|
135
|
-
<span className="text-xs text-muted-foreground">{info?.byok_configured ? "your key" : info?.system_funded ?
|
|
137
|
+
<span className="text-xs text-muted-foreground">{info?.byok_configured ? "your key" : info?.system_funded ? `${brand.linkLabel} key, uses credits when run` : "needs your key"}</span>
|
|
136
138
|
</label>
|
|
137
139
|
);
|
|
138
140
|
})}
|
|
@@ -163,7 +165,7 @@ export function ColumnRecipeEditor({ tableId, columns, entityType = "contact", o
|
|
|
163
165
|
|
|
164
166
|
{tab === "map" ? (
|
|
165
167
|
<div className="flex flex-col gap-2">
|
|
166
|
-
<p className="text-sm text-muted-foreground">Tell
|
|
168
|
+
<p className="text-sm text-muted-foreground">Tell {brand.linkLabel} what each of your columns holds, so enrichment steps know where the first name, last name and website are.</p>
|
|
167
169
|
{targets.error ? <ApiError error={targets.error} title="Could not load the fields" onRetry={() => void targets.refetch()} /> : null}
|
|
168
170
|
{map.error ? <ApiError error={map.error} title="Could not save the mapping" /> : null}
|
|
169
171
|
{columns.filter((c) => (c.column_type ?? "data") === "data" && !c.enrichment_type).map((c) => (
|
|
@@ -39,6 +39,10 @@ export interface WorkbenchGridProps {
|
|
|
39
39
|
tableId: string;
|
|
40
40
|
/** Rows per window. Default 100. */
|
|
41
41
|
pageSize?: number;
|
|
42
|
+
/** Adds an "Open" button on every row (a row detail screen). Omit it and the grid is unchanged. */
|
|
43
|
+
onOpenRow?: (row: WorkbenchRow) => void;
|
|
44
|
+
/** Highlights the open row. */
|
|
45
|
+
activeRowId?: number | null;
|
|
42
46
|
className?: string;
|
|
43
47
|
}
|
|
44
48
|
|
|
@@ -50,7 +54,7 @@ function runTarget(column: WorkbenchColumn): RunTarget | null {
|
|
|
50
54
|
return null;
|
|
51
55
|
}
|
|
52
56
|
|
|
53
|
-
export function WorkbenchGrid({ tableId, pageSize = 100, className }: WorkbenchGridProps) {
|
|
57
|
+
export function WorkbenchGrid({ tableId, pageSize = 100, onOpenRow, activeRowId, className }: WorkbenchGridProps) {
|
|
54
58
|
const [end, setEnd] = useState(pageSize);
|
|
55
59
|
const [panel, setPanel] = useState<Panel>(null);
|
|
56
60
|
const [selected, setSelected] = useState<number[]>([]);
|
|
@@ -114,6 +118,7 @@ export function WorkbenchGrid({ tableId, pageSize = 100, className }: WorkbenchG
|
|
|
114
118
|
<TableHeader>
|
|
115
119
|
<TableRow>
|
|
116
120
|
<TableHead className="w-8"><input type="checkbox" aria-label="Select all rows" checked={allOn} onChange={() => setSelected(allOn ? [] : items.map((r) => r.id))} /></TableHead>
|
|
121
|
+
{onOpenRow ? <TableHead className="w-14"><span className="sr-only">Open</span></TableHead> : null}
|
|
117
122
|
{columns.map((c) => {
|
|
118
123
|
const target = runTarget(c);
|
|
119
124
|
return (
|
|
@@ -130,8 +135,9 @@ export function WorkbenchGrid({ tableId, pageSize = 100, className }: WorkbenchG
|
|
|
130
135
|
</TableHeader>
|
|
131
136
|
<TableBody>
|
|
132
137
|
{items.map((row) => (
|
|
133
|
-
<TableRow key={row.id} data-state={selected.includes(row.id) ? "selected" : undefined}>
|
|
138
|
+
<TableRow key={row.id} data-state={selected.includes(row.id) || activeRowId === row.id ? "selected" : undefined}>
|
|
134
139
|
<TableCell><input type="checkbox" aria-label={`Select row ${row.id}`} checked={selected.includes(row.id)} onChange={() => toggle(row.id)} /></TableCell>
|
|
140
|
+
{onOpenRow ? <TableCell><Button size="sm" variant="ghost" aria-label={`Open row ${row.id}`} onClick={() => onOpenRow(row)}>Open</Button></TableCell> : null}
|
|
135
141
|
{columns.map((c) => {
|
|
136
142
|
const value = cellValue(row, c);
|
|
137
143
|
const isEdit = editing?.rowId === row.id && editing.column === c.name;
|
|
@@ -140,7 +146,7 @@ export function WorkbenchGrid({ tableId, pageSize = 100, className }: WorkbenchG
|
|
|
140
146
|
<TableCell key={c.id} onDoubleClick={() => editable && setEditing({ rowId: row.id, column: c.name, value: formatCell(value) })}>
|
|
141
147
|
{isEdit && editing ? (
|
|
142
148
|
<Input autoFocus aria-label={`Edit ${c.name}`} value={editing.value} onChange={(e) => setEditing({ ...editing, value: e.target.value })}
|
|
143
|
-
onKeyDown={(e) => { if (e.key === "Enter" && !save.loading) void save.mutate(editing); if (e.key === "Escape") setEditing(null); }} />
|
|
149
|
+
onKeyDown={(e) => { if (e.key === "Enter" && !save.loading) { if (editing.value === formatCell(value)) setEditing(null); else void save.mutate(editing); } if (e.key === "Escape") setEditing(null); }} />
|
|
144
150
|
) : (
|
|
145
151
|
<span className={isMasked(value) || (isLocked(row) && value == null) ? "text-muted-foreground" : ""} title={isLocked(row) ? "Locked: unlock the row to see this" : undefined}>{formatCell(value) || (isLocked(row) ? "locked" : "")}</span>
|
|
146
152
|
)}
|
|
@@ -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
|
+
}
|