@graph8/sdk 0.308.1 → 0.357.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -0
- package/blocks/README.md +32 -0
- package/blocks/_foundation/README.md +78 -0
- package/blocks/_foundation/block.json +79 -0
- package/blocks/_foundation/components/api-error.tsx +62 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +142 -0
- package/blocks/_foundation/components/empty-state.tsx +24 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +88 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +87 -0
- package/blocks/_foundation/hooks/use-job-poll.ts +130 -0
- package/blocks/_foundation/hooks/use-paginated.ts +105 -0
- package/blocks/_foundation/lib/g8-client.ts +128 -0
- package/blocks/_foundation/recipes/verify-proxy-key.ts +18 -0
- package/blocks/_foundation/server/proxy.ts +233 -0
- package/blocks/_foundation/server/route.ts +30 -0
- package/blocks/agents/README.md +133 -0
- package/blocks/agents/block.json +223 -0
- package/blocks/agents/components/agent-builder.tsx +199 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +144 -0
- package/blocks/agents/components/agent-runs.tsx +175 -0
- package/blocks/agents/components/agent-skills-panel.tsx +126 -0
- package/blocks/agents/components/agent-test-console.tsx +175 -0
- package/blocks/agents/lib/agent-types.ts +114 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +50 -0
- package/blocks/agents/recipes/run-and-follow-agent-run.ts +46 -0
- package/blocks/agents/recipes/test-agent-before-activating.ts +36 -0
- package/blocks/analytics/README.md +73 -0
- package/blocks/analytics/block.json +178 -0
- package/blocks/analytics/components/funnel-chart.tsx +79 -0
- package/blocks/analytics/components/kpi-tiles.tsx +74 -0
- package/blocks/analytics/components/leaderboard.tsx +86 -0
- package/blocks/analytics/components/report-builder.tsx +200 -0
- package/blocks/analytics/hooks/use-report.ts +26 -0
- package/blocks/analytics/lib/analytics-types.ts +108 -0
- package/blocks/analytics/lib/use-debounced.ts +16 -0
- package/blocks/analytics/recipes/01-weekly-outbound-report.ts +32 -0
- package/blocks/analytics/recipes/02-revenue-snapshot.ts +22 -0
- package/blocks/analytics/recipes/03-saved-report.ts +30 -0
- package/blocks/appointments/README.md +80 -0
- package/blocks/appointments/block.json +218 -0
- package/blocks/appointments/components/availability-editor.tsx +192 -0
- package/blocks/appointments/components/booking-page.tsx +154 -0
- package/blocks/appointments/components/event-type-editor.tsx +214 -0
- package/blocks/appointments/components/meeting-detail.tsx +147 -0
- package/blocks/appointments/components/meeting-list.tsx +148 -0
- package/blocks/appointments/hooks/use-available-slots.ts +29 -0
- package/blocks/appointments/hooks/use-schedules.ts +18 -0
- package/blocks/appointments/lib/appointments-types.ts +159 -0
- package/blocks/appointments/lib/idempotency.ts +30 -0
- package/blocks/appointments/recipes/01-create-event-type-and-hours.ts +27 -0
- package/blocks/appointments/recipes/02-find-slots-and-book.ts +34 -0
- package/blocks/appointments/recipes/03-bookings-digest.ts +22 -0
- package/blocks/appointments/recipes/04-calendar-invite.ts +21 -0
- package/blocks/block.schema.json +130 -0
- package/blocks/block.types.ts +109 -0
- package/blocks/campaigns/README.md +118 -0
- package/blocks/campaigns/block.json +361 -0
- package/blocks/campaigns/components/campaign-assets.tsx +180 -0
- package/blocks/campaigns/components/campaign-audience-step.tsx +91 -0
- package/blocks/campaigns/components/campaign-launch-step.tsx +147 -0
- package/blocks/campaigns/components/campaign-list.tsx +120 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +93 -0
- package/blocks/campaigns/components/campaign-select.tsx +52 -0
- package/blocks/campaigns/components/campaign-sequence-step.tsx +158 -0
- package/blocks/campaigns/components/campaign-wizard.tsx +238 -0
- package/blocks/campaigns/components/form-field.tsx +29 -0
- package/blocks/campaigns/components/icp-editor.tsx +230 -0
- package/blocks/campaigns/components/persona-editor.tsx +210 -0
- package/blocks/campaigns/hooks/use-company-profile.ts +34 -0
- package/blocks/campaigns/lib/campaigns-utils.ts +155 -0
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +33 -0
- package/blocks/campaigns/recipes/campaign-report.ts +22 -0
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +51 -0
- package/blocks/campaigns/recipes/set-up-icp-and-personas.ts +37 -0
- package/blocks/coach/README.md +93 -0
- package/blocks/coach/block.json +125 -0
- package/blocks/coach/components/call-scorecard.tsx +126 -0
- package/blocks/coach/components/coaching-feed.tsx +140 -0
- package/blocks/coach/lib/coach-types.ts +116 -0
- package/blocks/coach/recipes/read-a-scorecard.ts +27 -0
- package/blocks/coach/recipes/weekly-coaching-digest.ts +53 -0
- package/blocks/copilot/README.md +99 -0
- package/blocks/copilot/block.json +151 -0
- package/blocks/copilot/components/copilot-memory.tsx +87 -0
- package/blocks/copilot/components/copilot-panel.tsx +189 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +111 -0
- package/blocks/copilot/lib/copilot-types.ts +111 -0
- package/blocks/copilot/recipes/ask-the-copilot.ts +35 -0
- package/blocks/copilot/recipes/post-in-a-work-channel.ts +33 -0
- package/blocks/copilot/recipes/remember-and-recall.ts +19 -0
- package/blocks/crm/README.md +122 -0
- package/blocks/crm/block.json +378 -0
- package/blocks/crm/components/activity-timeline.tsx +99 -0
- package/blocks/crm/components/company-detail.tsx +118 -0
- package/blocks/crm/components/company-table.tsx +173 -0
- package/blocks/crm/components/contact-detail.tsx +135 -0
- package/blocks/crm/components/contact-table.tsx +217 -0
- package/blocks/crm/components/dedupe-review.tsx +164 -0
- package/blocks/crm/components/import-wizard.tsx +193 -0
- package/blocks/crm/components/notes-panel.tsx +112 -0
- package/blocks/crm/components/record-form.tsx +191 -0
- package/blocks/crm/components/task-list.tsx +107 -0
- package/blocks/crm/hooks/use-custom-fields.ts +18 -0
- package/blocks/crm/hooks/use-debounced.ts +13 -0
- package/blocks/crm/hooks/use-page-query.ts +52 -0
- package/blocks/crm/lib/crm-types.ts +136 -0
- package/blocks/crm/recipes/custom-fields-and-values.ts +28 -0
- package/blocks/crm/recipes/dedupe-and-merge.ts +30 -0
- package/blocks/crm/recipes/notes-tasks-and-activity.ts +23 -0
- package/blocks/crm/recipes/upsert-contact-with-company.ts +33 -0
- package/blocks/deals/README.md +78 -0
- package/blocks/deals/block.json +198 -0
- package/blocks/deals/components/deal-detail.tsx +134 -0
- package/blocks/deals/components/forecast-view.tsx +112 -0
- package/blocks/deals/components/pipeline-board.tsx +142 -0
- package/blocks/deals/components/stage-editor.tsx +96 -0
- package/blocks/deals/hooks/use-pipeline-deals.ts +67 -0
- package/blocks/deals/hooks/use-pipelines.ts +19 -0
- package/blocks/deals/lib/deals-types.ts +128 -0
- package/blocks/deals/recipes/01-create-deal-and-advance.ts +32 -0
- package/blocks/deals/recipes/02-pipeline-report.ts +35 -0
- package/blocks/deals/recipes/03-stale-deals-sweep.ts +20 -0
- package/blocks/dialer/README.md +114 -0
- package/blocks/dialer/block.json +260 -0
- package/blocks/dialer/components/call-log.tsx +78 -0
- package/blocks/dialer/components/call-queue.tsx +188 -0
- package/blocks/dialer/components/dialer-workspace.tsx +47 -0
- package/blocks/dialer/components/disposition-panel.tsx +75 -0
- package/blocks/dialer/components/live-calls-table.tsx +107 -0
- package/blocks/dialer/components/session-start-form.tsx +111 -0
- package/blocks/dialer/components/softphone.tsx +122 -0
- package/blocks/dialer/components/transcript-viewer.tsx +82 -0
- package/blocks/dialer/hooks/use-dialer-numbers.ts +19 -0
- package/blocks/dialer/hooks/use-dialer-session.ts +68 -0
- package/blocks/dialer/hooks/use-dispositions.ts +29 -0
- package/blocks/dialer/lib/dialer-types.ts +158 -0
- package/blocks/dialer/lib/softphone-transport.ts +67 -0
- package/blocks/dialer/lib/twilio-transport.ts +74 -0
- package/blocks/dialer/recipes/01-create-session-and-dial.ts +39 -0
- package/blocks/dialer/recipes/02-disposition-calls.ts +26 -0
- package/blocks/dialer/recipes/03-review-a-call.ts +23 -0
- package/blocks/enrichment/README.md +105 -0
- package/blocks/enrichment/block.json +204 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +205 -0
- package/blocks/enrichment/components/email-verify-badge.tsx +82 -0
- package/blocks/enrichment/components/enrich-button.tsx +136 -0
- package/blocks/enrichment/components/waterfall-config.tsx +127 -0
- package/blocks/enrichment/hooks/use-providers.ts +18 -0
- package/blocks/enrichment/lib/enrichment-types.ts +104 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +41 -0
- package/blocks/enrichment/recipes/lookup-and-verify.ts +25 -0
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +26 -0
- package/blocks/forms/README.md +112 -0
- package/blocks/forms/block.json +200 -0
- package/blocks/forms/components/form-builder.tsx +150 -0
- package/blocks/forms/components/form-embed.tsx +103 -0
- package/blocks/forms/components/form-mapping-editor.tsx +125 -0
- package/blocks/forms/components/site-audit-panel.tsx +73 -0
- package/blocks/forms/components/submissions-table.tsx +158 -0
- package/blocks/forms/lib/form-types.ts +127 -0
- package/blocks/forms/recipes/install-snippet-and-form.ts +21 -0
- package/blocks/forms/recipes/retry-failed-deliveries.ts +26 -0
- package/blocks/forms/recipes/route-form-to-list.ts +44 -0
- package/blocks/inbox/README.md +72 -0
- package/blocks/inbox/block.json +194 -0
- package/blocks/inbox/components/inbox-list.tsx +156 -0
- package/blocks/inbox/components/inbox-screen.tsx +49 -0
- package/blocks/inbox/components/reply-composer.tsx +183 -0
- package/blocks/inbox/components/tag-and-assign.tsx +189 -0
- package/blocks/inbox/components/thread-badge.tsx +17 -0
- package/blocks/inbox/components/thread-view.tsx +108 -0
- package/blocks/inbox/hooks/use-inbox-thread.ts +24 -0
- package/blocks/inbox/hooks/use-inbox-threads.ts +40 -0
- package/blocks/inbox/lib/idempotency.ts +30 -0
- package/blocks/inbox/lib/inbox-types.ts +100 -0
- package/blocks/inbox/recipes/01-triage-and-reply.ts +43 -0
- package/blocks/inbox/recipes/02-tag-and-assign.ts +31 -0
- package/blocks/inbox/recipes/03-hot-contacts-digest.ts +16 -0
- package/blocks/integrations/README.md +75 -0
- package/blocks/integrations/block.json +172 -0
- package/blocks/integrations/components/integration-list.tsx +165 -0
- package/blocks/integrations/components/sync-status.tsx +223 -0
- package/blocks/integrations/components/webhook-manager.tsx +300 -0
- package/blocks/integrations/lib/integrations-utils.ts +62 -0
- package/blocks/integrations/recipes/crm-sync-health.ts +19 -0
- package/blocks/integrations/recipes/push-contacts-to-crm.ts +20 -0
- package/blocks/integrations/recipes/register-webhook.ts +18 -0
- package/blocks/integrations/recipes/verify-webhook-route.ts +33 -0
- package/blocks/intent/README.md +57 -0
- package/blocks/intent/block.json +138 -0
- package/blocks/intent/components/account-intent-score.tsx +91 -0
- package/blocks/intent/components/intent-feed.tsx +215 -0
- package/blocks/intent/components/intent-keyword-manager.tsx +331 -0
- package/blocks/intent/hooks/use-account-intent.ts +46 -0
- package/blocks/intent/lib/intent-utils.ts +95 -0
- package/blocks/intent/recipes/rank-intent-accounts.ts +36 -0
- package/blocks/intent/recipes/track-competitor-domain.ts +23 -0
- package/blocks/intent/recipes/who-visited-url.ts +18 -0
- package/blocks/lifecycle/README.md +79 -0
- package/blocks/lifecycle/block.json +198 -0
- package/blocks/lifecycle/components/churn-list.tsx +92 -0
- package/blocks/lifecycle/components/customer-table.tsx +119 -0
- package/blocks/lifecycle/components/lead-queue.tsx +127 -0
- package/blocks/lifecycle/components/lifecycle-board.tsx +102 -0
- package/blocks/lifecycle/components/stage-move-dialog.tsx +75 -0
- package/blocks/lifecycle/hooks/use-debounced.ts +13 -0
- package/blocks/lifecycle/hooks/use-lifecycle-stages.ts +27 -0
- package/blocks/lifecycle/lib/lifecycle.ts +99 -0
- package/blocks/lifecycle/recipes/customer-brief.ts +23 -0
- package/blocks/lifecycle/recipes/move-company-stage.ts +17 -0
- package/blocks/lifecycle/recipes/read-lifecycle-board.ts +21 -0
- package/blocks/lifecycle/recipes/unlock-stage-contacts.ts +25 -0
- package/blocks/lists/README.md +96 -0
- package/blocks/lists/block.json +166 -0
- package/blocks/lists/components/add-to-list-button.tsx +111 -0
- package/blocks/lists/components/list-picker.tsx +94 -0
- package/blocks/lists/components/list-table.tsx +160 -0
- package/blocks/lists/components/suppression-manager.tsx +153 -0
- package/blocks/lists/recipes/create-list-and-add-contacts.ts +35 -0
- package/blocks/lists/recipes/refresh-and-check-lists.ts +21 -0
- package/blocks/lists/recipes/suppress-and-reinstate.ts +30 -0
- package/blocks/mailboxes/README.md +71 -0
- package/blocks/mailboxes/block.json +187 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +284 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +271 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +185 -0
- package/blocks/mailboxes/components/warmup-health.tsx +232 -0
- package/blocks/mailboxes/lib/mailboxes-utils.ts +69 -0
- package/blocks/mailboxes/recipes/buy-domain-and-mailboxes.ts +64 -0
- package/blocks/mailboxes/recipes/connect-imap-mailbox.ts +38 -0
- package/blocks/mailboxes/recipes/is-it-safe-to-send.ts +17 -0
- package/blocks/mailboxes/recipes/warmup-report.ts +31 -0
- package/blocks/newsletter/README.md +122 -0
- package/blocks/newsletter/block.json +224 -0
- package/blocks/newsletter/components/issue-form.tsx +124 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +152 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +131 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +93 -0
- package/blocks/newsletter/components/subscriber-table.tsx +173 -0
- package/blocks/newsletter/lib/newsletter-types.ts +97 -0
- package/blocks/newsletter/recipes/create-newsletter-and-draft-issue.ts +27 -0
- package/blocks/newsletter/recipes/import-audience-and-send.ts +43 -0
- package/blocks/newsletter/recipes/issue-performance-report.ts +26 -0
- package/blocks/nurture/README.md +110 -0
- package/blocks/nurture/block.json +191 -0
- package/blocks/nurture/components/nurture-builder.tsx +114 -0
- package/blocks/nurture/components/nurture-enroll.tsx +107 -0
- package/blocks/nurture/components/nurture-form.tsx +120 -0
- package/blocks/nurture/components/nurture-stats.tsx +81 -0
- package/blocks/nurture/lib/nurture-types.ts +109 -0
- package/blocks/nurture/recipes/create-a-nurture.ts +34 -0
- package/blocks/nurture/recipes/enroll-contacts-and-launch.ts +24 -0
- package/blocks/nurture/recipes/nurture-health-check.ts +28 -0
- package/blocks/objects/README.md +120 -0
- package/blocks/objects/block.json +243 -0
- package/blocks/objects/components/attribute-field-input.tsx +82 -0
- package/blocks/objects/components/attribute-form-dialog.tsx +159 -0
- package/blocks/objects/components/object-not-found.tsx +34 -0
- package/blocks/objects/components/object-record-detail.tsx +143 -0
- package/blocks/objects/components/object-schema-editor.tsx +197 -0
- package/blocks/objects/components/object-table.tsx +130 -0
- package/blocks/objects/components/record-form-dialog.tsx +112 -0
- package/blocks/objects/components/record-roles-panel.tsx +106 -0
- package/blocks/objects/hooks/use-object-schema.ts +36 -0
- package/blocks/objects/lib/object-types.ts +214 -0
- package/blocks/objects/recipes/define-object-and-add-records.ts +24 -0
- package/blocks/objects/recipes/update-audit-and-archive-records.ts +39 -0
- package/blocks/objects/recipes/upsert-records-from-your-system.ts +30 -0
- package/blocks/pages/README.md +104 -0
- package/blocks/pages/block.json +223 -0
- package/blocks/pages/components/brand-kit-editor.tsx +152 -0
- package/blocks/pages/components/content-library.tsx +163 -0
- package/blocks/pages/components/page-editor.tsx +253 -0
- package/blocks/pages/components/page-list.tsx +159 -0
- package/blocks/pages/components/seo-report.tsx +159 -0
- package/blocks/pages/lib/studio.ts +77 -0
- package/blocks/pages/recipes/audit-site-seo.ts +28 -0
- package/blocks/pages/recipes/create-and-publish-page.ts +26 -0
- package/blocks/pages/recipes/edit-page-section.ts +25 -0
- package/blocks/pages/recipes/refresh-brand-kit.ts +22 -0
- package/blocks/quotes/README.md +79 -0
- package/blocks/quotes/block.json +183 -0
- package/blocks/quotes/components/product-picker.tsx +80 -0
- package/blocks/quotes/components/quote-builder.tsx +195 -0
- package/blocks/quotes/components/quote-list.tsx +79 -0
- package/blocks/quotes/components/quote-viewer.tsx +135 -0
- package/blocks/quotes/components/signature-status.tsx +77 -0
- package/blocks/quotes/hooks/use-debounced.ts +13 -0
- package/blocks/quotes/lib/quotes.ts +142 -0
- package/blocks/quotes/recipes/create-and-send-quote.ts +44 -0
- package/blocks/quotes/recipes/revise-sent-quote.ts +18 -0
- package/blocks/quotes/recipes/track-quote-signature.ts +22 -0
- package/blocks/radar/README.md +106 -0
- package/blocks/radar/block.json +189 -0
- package/blocks/radar/components/competitor-battle-card.tsx +53 -0
- package/blocks/radar/components/competitor-tracker.tsx +158 -0
- package/blocks/radar/components/radar-overview.tsx +67 -0
- package/blocks/radar/components/radar-report.tsx +86 -0
- package/blocks/radar/hooks/use-competitors.ts +23 -0
- package/blocks/radar/lib/radar-types.ts +124 -0
- package/blocks/radar/recipes/battle-card.ts +28 -0
- package/blocks/radar/recipes/deep-dive-report.ts +32 -0
- package/blocks/radar/recipes/marketing-snapshot.ts +18 -0
- package/blocks/radar/recipes/track-and-scan.ts +39 -0
- package/blocks/search/README.md +129 -0
- package/blocks/search/block.json +185 -0
- package/blocks/search/components/filter-builder.tsx +149 -0
- package/blocks/search/components/prospect-results.tsx +175 -0
- package/blocks/search/components/save-to-list-dialog.tsx +135 -0
- package/blocks/search/components/saved-searches.tsx +95 -0
- package/blocks/search/hooks/use-filter-options.ts +41 -0
- package/blocks/search/lib/search-types.ts +149 -0
- package/blocks/search/recipes/explore-filter-values.ts +27 -0
- package/blocks/search/recipes/saved-searches.ts +34 -0
- package/blocks/search/recipes/search-and-save-to-list.ts +37 -0
- package/blocks/sequencer/README.md +87 -0
- package/blocks/sequencer/block.json +214 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +93 -0
- package/blocks/sequencer/components/sequence-builder.tsx +306 -0
- package/blocks/sequencer/components/sequence-list.tsx +200 -0
- package/blocks/sequencer/components/sequence-stats.tsx +256 -0
- package/blocks/sequencer/components/step-editor.tsx +213 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +168 -0
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +51 -0
- package/blocks/sequencer/recipes/launch-and-enroll.ts +44 -0
- package/blocks/sequencer/recipes/pause-when-unsafe.ts +30 -0
- package/blocks/sequencer/recipes/sequence-report.ts +22 -0
- package/blocks/social-ads/README.md +128 -0
- package/blocks/social-ads/block.json +256 -0
- package/blocks/social-ads/components/ad-campaign-list.tsx +136 -0
- package/blocks/social-ads/components/audience-sync.tsx +150 -0
- package/blocks/social-ads/components/creative-gallery.tsx +121 -0
- package/blocks/social-ads/components/post-composer.tsx +122 -0
- package/blocks/social-ads/components/social-post-list.tsx +116 -0
- package/blocks/social-ads/hooks/use-social-accounts.ts +16 -0
- package/blocks/social-ads/lib/social-ads-types.ts +167 -0
- package/blocks/social-ads/recipes/creative-to-gallery.ts +41 -0
- package/blocks/social-ads/recipes/schedule-post.ts +51 -0
- package/blocks/social-ads/recipes/sync-audience-and-launch.ts +41 -0
- package/blocks/visitors/README.md +56 -0
- package/blocks/visitors/block.json +139 -0
- package/blocks/visitors/components/snippet-install.tsx +169 -0
- package/blocks/visitors/components/traffic-chart.tsx +156 -0
- package/blocks/visitors/components/visitor-company-card.tsx +92 -0
- package/blocks/visitors/components/visitor-feed.tsx +157 -0
- package/blocks/visitors/lib/visitors-utils.ts +78 -0
- package/blocks/visitors/recipes/check-snippet-installed.ts +24 -0
- package/blocks/visitors/recipes/recent-company-visits.ts +27 -0
- package/blocks/visitors/recipes/traffic-summary.ts +24 -0
- package/blocks/webchat/README.md +87 -0
- package/blocks/webchat/block.json +134 -0
- package/blocks/webchat/components/chat-widget-config.tsx +160 -0
- package/blocks/webchat/components/conversation-list.tsx +74 -0
- package/blocks/webchat/lib/webchat-types.ts +83 -0
- package/blocks/webchat/recipes/create-bot-and-style-widget.ts +24 -0
- package/blocks/webchat/recipes/list-chat-visitors.ts +24 -0
- package/blocks/workbench/README.md +135 -0
- package/blocks/workbench/block.json +259 -0
- package/blocks/workbench/components/column-recipe-editor.tsx +183 -0
- package/blocks/workbench/components/run-progress.tsx +132 -0
- package/blocks/workbench/components/workbench-add-rows.tsx +100 -0
- package/blocks/workbench/components/workbench-grid.tsx +159 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +102 -0
- package/blocks/workbench/components/workbench-table-list.tsx +105 -0
- package/blocks/workbench/hooks/use-workbench-table.ts +30 -0
- package/blocks/workbench/lib/waterfall-presets.ts +119 -0
- package/blocks/workbench/lib/workbench-types.ts +191 -0
- package/blocks/workbench/recipes/build-enriched-table.ts +65 -0
- package/blocks/workbench/recipes/import-from-search.ts +42 -0
- package/blocks/workbench/recipes/scrape-into-table.ts +36 -0
- package/blocks/workbench/recipes/waterfall-column.ts +55 -0
- package/blocks/workflows/README.md +100 -0
- package/blocks/workflows/block.json +188 -0
- package/blocks/workflows/components/run-history.tsx +136 -0
- package/blocks/workflows/components/trigger-config.tsx +134 -0
- package/blocks/workflows/components/workflow-editor.tsx +142 -0
- package/blocks/workflows/components/workflow-list.tsx +153 -0
- package/blocks/workflows/hooks/use-workflow.ts +51 -0
- package/blocks/workflows/lib/graph.ts +187 -0
- package/blocks/workflows/recipes/create-workflow.ts +35 -0
- package/blocks/workflows/recipes/find-failed-runs.ts +15 -0
- package/blocks/workflows/recipes/run-workflow-and-wait.ts +32 -0
- package/blocks/workflows/recipes/set-trigger.ts +22 -0
- package/dist/cli.js +5330 -29
- package/dist/index.browser.js +7740 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.browser.mjs +7717 -0
- package/dist/index.browser.mjs.map +1 -0
- package/dist/index.d.mts +1440 -21
- package/dist/index.d.ts +1440 -21
- package/dist/index.js +42 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +42 -3
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +7598 -0
- package/dist/react.browser.js.map +1 -0
- package/dist/react.browser.mjs +7568 -0
- package/dist/react.browser.mjs.map +1 -0
- package/dist/react.d.mts +1340 -18
- package/dist/react.d.ts +1340 -18
- package/dist/react.js +41 -2
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +41 -2
- package/dist/react.mjs.map +1 -1
- package/dist/types/agentSetup.d.ts +6 -6
- package/dist/types/blocks/add.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +46 -0
- package/dist/types/blocks/doctor.d.ts +26 -0
- package/dist/types/blocks/registry.d.ts +134 -0
- package/dist/types/cli.d.ts +16 -0
- package/dist/types/core.d.ts +1 -1
- package/dist/types/crypto-browser.d.ts +2 -0
- package/dist/types/crypto-node.d.ts +6 -0
- package/dist/types/react.d.ts +1 -1
- package/dist/types/version.d.ts +2 -2
- package/dist/types/webhooks.d.ts +2 -2
- package/dist/ui/index.d.mts +54 -0
- package/dist/ui/index.d.ts +54 -0
- package/dist/ui/index.js +105 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/index.mjs +44 -0
- package/dist/ui/index.mjs.map +1 -0
- package/dist/ui/lib/compose-refs.d.mts +19 -0
- package/dist/ui/lib/compose-refs.d.ts +19 -0
- package/dist/ui/lib/compose-refs.js +76 -0
- package/dist/ui/lib/compose-refs.js.map +1 -0
- package/dist/ui/lib/compose-refs.mjs +41 -0
- package/dist/ui/lib/compose-refs.mjs.map +1 -0
- package/dist/ui/lib/use-mobile.d.mts +4 -0
- package/dist/ui/lib/use-mobile.d.ts +4 -0
- package/dist/ui/lib/use-mobile.js +57 -0
- package/dist/ui/lib/use-mobile.js.map +1 -0
- package/dist/ui/lib/use-mobile.mjs +22 -0
- package/dist/ui/lib/use-mobile.mjs.map +1 -0
- package/dist/ui/lib/utils.d.mts +10 -0
- package/dist/ui/lib/utils.d.ts +10 -0
- package/dist/ui/lib/utils.js +33 -0
- package/dist/ui/lib/utils.js.map +1 -0
- package/dist/ui/lib/utils.mjs +9 -0
- package/dist/ui/lib/utils.mjs.map +1 -0
- package/dist/ui/primitives/alert-dialog.d.mts +22 -0
- package/dist/ui/primitives/alert-dialog.d.ts +22 -0
- package/dist/ui/primitives/alert-dialog.js +163 -0
- package/dist/ui/primitives/alert-dialog.js.map +1 -0
- package/dist/ui/primitives/alert-dialog.mjs +119 -0
- package/dist/ui/primitives/alert-dialog.mjs.map +1 -0
- package/dist/ui/primitives/avatar.d.mts +8 -0
- package/dist/ui/primitives/avatar.d.ts +8 -0
- package/dist/ui/primitives/avatar.js +91 -0
- package/dist/ui/primitives/avatar.js.map +1 -0
- package/dist/ui/primitives/avatar.mjs +55 -0
- package/dist/ui/primitives/avatar.mjs.map +1 -0
- package/dist/ui/primitives/breadcrumb.d.mts +13 -0
- package/dist/ui/primitives/breadcrumb.d.ts +13 -0
- package/dist/ui/primitives/breadcrumb.js +127 -0
- package/dist/ui/primitives/breadcrumb.js.map +1 -0
- package/dist/ui/primitives/breadcrumb.mjs +97 -0
- package/dist/ui/primitives/breadcrumb.mjs.map +1 -0
- package/dist/ui/primitives/button.d.mts +15 -0
- package/dist/ui/primitives/button.d.ts +15 -0
- package/dist/ui/primitives/button.js +91 -0
- package/dist/ui/primitives/button.js.map +1 -0
- package/dist/ui/primitives/button.mjs +56 -0
- package/dist/ui/primitives/button.mjs.map +1 -0
- package/dist/ui/primitives/card.d.mts +10 -0
- package/dist/ui/primitives/card.d.ts +10 -0
- package/dist/ui/primitives/card.js +101 -0
- package/dist/ui/primitives/card.js.map +1 -0
- package/dist/ui/primitives/card.mjs +62 -0
- package/dist/ui/primitives/card.mjs.map +1 -0
- package/dist/ui/primitives/carousel.d.mts +45 -0
- package/dist/ui/primitives/carousel.d.ts +45 -0
- package/dist/ui/primitives/carousel.js +300 -0
- package/dist/ui/primitives/carousel.js.map +1 -0
- package/dist/ui/primitives/carousel.mjs +261 -0
- package/dist/ui/primitives/carousel.mjs.map +1 -0
- package/dist/ui/primitives/chart.d.mts +68 -0
- package/dist/ui/primitives/chart.d.ts +68 -0
- package/dist/ui/primitives/chart.js +280 -0
- package/dist/ui/primitives/chart.js.map +1 -0
- package/dist/ui/primitives/chart.mjs +241 -0
- package/dist/ui/primitives/chart.mjs.map +1 -0
- package/dist/ui/primitives/collapsible.d.mts +8 -0
- package/dist/ui/primitives/collapsible.d.ts +8 -0
- package/dist/ui/primitives/collapsible.js +72 -0
- package/dist/ui/primitives/collapsible.js.map +1 -0
- package/dist/ui/primitives/collapsible.mjs +36 -0
- package/dist/ui/primitives/collapsible.mjs.map +1 -0
- package/dist/ui/primitives/context-menu.d.mts +29 -0
- package/dist/ui/primitives/context-menu.d.ts +29 -0
- package/dist/ui/primitives/context-menu.js +203 -0
- package/dist/ui/primitives/context-menu.js.map +1 -0
- package/dist/ui/primitives/context-menu.mjs +155 -0
- package/dist/ui/primitives/context-menu.mjs.map +1 -0
- package/dist/ui/primitives/dialog.d.mts +26 -0
- package/dist/ui/primitives/dialog.d.ts +26 -0
- package/dist/ui/primitives/dialog.js +192 -0
- package/dist/ui/primitives/dialog.js.map +1 -0
- package/dist/ui/primitives/dialog.mjs +148 -0
- package/dist/ui/primitives/dialog.mjs.map +1 -0
- package/dist/ui/primitives/drawer.d.mts +26 -0
- package/dist/ui/primitives/drawer.d.ts +26 -0
- package/dist/ui/primitives/drawer.js +146 -0
- package/dist/ui/primitives/drawer.js.map +1 -0
- package/dist/ui/primitives/drawer.mjs +103 -0
- package/dist/ui/primitives/drawer.mjs.map +1 -0
- package/dist/ui/primitives/input.d.mts +5 -0
- package/dist/ui/primitives/input.d.ts +5 -0
- package/dist/ui/primitives/input.js +46 -0
- package/dist/ui/primitives/input.js.map +1 -0
- package/dist/ui/primitives/input.mjs +22 -0
- package/dist/ui/primitives/input.mjs.map +1 -0
- package/dist/ui/primitives/item.d.mts +27 -0
- package/dist/ui/primitives/item.d.ts +27 -0
- package/dist/ui/primitives/item.js +221 -0
- package/dist/ui/primitives/item.js.map +1 -0
- package/dist/ui/primitives/item.mjs +188 -0
- package/dist/ui/primitives/item.mjs.map +1 -0
- package/dist/ui/primitives/kbd.d.mts +15 -0
- package/dist/ui/primitives/kbd.d.ts +15 -0
- package/dist/ui/primitives/kbd.js +45 -0
- package/dist/ui/primitives/kbd.js.map +1 -0
- package/dist/ui/primitives/kbd.mjs +21 -0
- package/dist/ui/primitives/kbd.mjs.map +1 -0
- package/dist/ui/primitives/label.d.mts +6 -0
- package/dist/ui/primitives/label.d.ts +6 -0
- package/dist/ui/primitives/label.js +55 -0
- package/dist/ui/primitives/label.js.map +1 -0
- package/dist/ui/primitives/label.mjs +21 -0
- package/dist/ui/primitives/label.mjs.map +1 -0
- package/dist/ui/primitives/resizable.d.mts +10 -0
- package/dist/ui/primitives/resizable.d.ts +10 -0
- package/dist/ui/primitives/resizable.js +68 -0
- package/dist/ui/primitives/resizable.js.map +1 -0
- package/dist/ui/primitives/resizable.mjs +42 -0
- package/dist/ui/primitives/resizable.mjs.map +1 -0
- package/dist/ui/primitives/save-status.d.mts +31 -0
- package/dist/ui/primitives/save-status.d.ts +31 -0
- package/dist/ui/primitives/save-status.js +150 -0
- package/dist/ui/primitives/save-status.js.map +1 -0
- package/dist/ui/primitives/save-status.mjs +126 -0
- package/dist/ui/primitives/save-status.mjs.map +1 -0
- package/dist/ui/primitives/separator.d.mts +6 -0
- package/dist/ui/primitives/separator.d.ts +6 -0
- package/dist/ui/primitives/separator.js +62 -0
- package/dist/ui/primitives/separator.js.map +1 -0
- package/dist/ui/primitives/separator.mjs +28 -0
- package/dist/ui/primitives/separator.mjs.map +1 -0
- package/dist/ui/primitives/sheet.d.mts +16 -0
- package/dist/ui/primitives/sheet.d.ts +16 -0
- package/dist/ui/primitives/sheet.js +170 -0
- package/dist/ui/primitives/sheet.js.map +1 -0
- package/dist/ui/primitives/sheet.mjs +129 -0
- package/dist/ui/primitives/sheet.mjs.map +1 -0
- package/dist/ui/primitives/sidebar.d.mts +74 -0
- package/dist/ui/primitives/sidebar.d.ts +74 -0
- package/dist/ui/primitives/sidebar.js +697 -0
- package/dist/ui/primitives/sidebar.js.map +1 -0
- package/dist/ui/primitives/sidebar.mjs +651 -0
- package/dist/ui/primitives/sidebar.mjs.map +1 -0
- package/dist/ui/primitives/skeleton.d.mts +5 -0
- package/dist/ui/primitives/skeleton.d.ts +5 -0
- package/dist/ui/primitives/skeleton.js +40 -0
- package/dist/ui/primitives/skeleton.js.map +1 -0
- package/dist/ui/primitives/skeleton.mjs +16 -0
- package/dist/ui/primitives/skeleton.mjs.map +1 -0
- package/dist/ui/primitives/slider.d.mts +20 -0
- package/dist/ui/primitives/slider.d.ts +20 -0
- package/dist/ui/primitives/slider.js +112 -0
- package/dist/ui/primitives/slider.js.map +1 -0
- package/dist/ui/primitives/slider.mjs +78 -0
- package/dist/ui/primitives/slider.mjs.map +1 -0
- package/dist/ui/primitives/sortable.d.mts +50 -0
- package/dist/ui/primitives/sortable.d.ts +50 -0
- package/dist/ui/primitives/sortable.js +456 -0
- package/dist/ui/primitives/sortable.js.map +1 -0
- package/dist/ui/primitives/sortable.mjs +435 -0
- package/dist/ui/primitives/sortable.mjs.map +1 -0
- package/dist/ui/primitives/switch.d.mts +6 -0
- package/dist/ui/primitives/switch.d.ts +6 -0
- package/dist/ui/primitives/switch.js +67 -0
- package/dist/ui/primitives/switch.js.map +1 -0
- package/dist/ui/primitives/switch.mjs +33 -0
- package/dist/ui/primitives/switch.mjs.map +1 -0
- package/dist/ui/primitives/table.d.mts +12 -0
- package/dist/ui/primitives/table.d.ts +12 -0
- package/dist/ui/primitives/table.js +130 -0
- package/dist/ui/primitives/table.js.map +1 -0
- package/dist/ui/primitives/table.mjs +89 -0
- package/dist/ui/primitives/table.mjs.map +1 -0
- package/dist/ui/primitives/tabs.d.mts +9 -0
- package/dist/ui/primitives/tabs.d.ts +9 -0
- package/dist/ui/primitives/tabs.js +86 -0
- package/dist/ui/primitives/tabs.js.map +1 -0
- package/dist/ui/primitives/tabs.mjs +49 -0
- package/dist/ui/primitives/tabs.mjs.map +1 -0
- package/dist/ui/primitives/textarea.d.mts +5 -0
- package/dist/ui/primitives/textarea.d.ts +5 -0
- package/dist/ui/primitives/textarea.js +43 -0
- package/dist/ui/primitives/textarea.js.map +1 -0
- package/dist/ui/primitives/textarea.mjs +19 -0
- package/dist/ui/primitives/textarea.mjs.map +1 -0
- package/dist/ui/primitives/tooltip.d.mts +9 -0
- package/dist/ui/primitives/tooltip.d.ts +9 -0
- package/dist/ui/primitives/tooltip.js +94 -0
- package/dist/ui/primitives/tooltip.js.map +1 -0
- package/dist/ui/primitives/tooltip.mjs +57 -0
- package/dist/ui/primitives/tooltip.mjs.map +1 -0
- package/docs/agents.md +72 -29
- package/docs/api/README.md +6 -3
- package/docs/api/appObjects.d.ts +1 -1
- package/docs/api/appObjects.md +1 -1
- package/docs/api/by-path.md +29 -0
- package/docs/api/objects.d.ts +72 -5
- package/docs/api/objects.md +84 -7
- package/docs/api/processLists.d.ts +247 -0
- package/docs/api/processLists.md +409 -0
- package/docs/api/reports.d.ts +476 -0
- package/docs/api/reports.md +539 -0
- package/docs/api/scheduleBuy.d.ts +12 -8
- package/docs/api/scheduleBuy.md +13 -10
- package/docs/api/seo.d.ts +406 -3
- package/docs/api/seo.md +505 -1
- package/docs/api/workspaceBackup.d.ts +324 -0
- package/docs/api/workspaceBackup.md +427 -0
- package/package.json +174 -5
- package/stacks/6sense.json +46 -0
- package/stacks/README.md +20 -0
- package/stacks/apollo.json +128 -0
- package/stacks/clay.json +59 -0
- package/stacks/gong.json +72 -0
- package/stacks/hubspot.json +162 -0
- package/stacks/intercom.json +53 -0
- package/stacks/mailchimp.json +71 -0
- package/stacks/outreach.json +86 -0
- package/stacks/salesforce.json +94 -0
- package/stacks/stack.schema.json +32 -0
- package/stacks/starter.json +22 -0
- package/styles/styles.css +2 -0
- package/styles/theme.css +181 -0
- package/styles/tokens.css +284 -0
- package/templates/next-app/README.md.tmpl +35 -0
- package/templates/next-app/_pages/redirect-page.tsx.tmpl +5 -0
- package/templates/next-app/_pages/route-page.tsx.tmpl +9 -0
- package/templates/next-app/app/globals.css.tmpl +8 -0
- package/templates/next-app/app/layout.tsx.tmpl +19 -0
- package/templates/next-app/components/app-shell.tsx.tmpl +34 -0
- package/templates/next-app/dot-env.local.example.tmpl +6 -0
- package/templates/next-app/dot-gitignore.tmpl +7 -0
- package/templates/next-app/lib/nav.ts.tmpl +2 -0
- package/templates/next-app/next.config.mjs.tmpl +4 -0
- package/templates/next-app/package.json.tmpl +25 -0
- package/templates/next-app/postcss.config.mjs.tmpl +4 -0
- package/templates/next-app/tsconfig.json.tmpl +21 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgentBuilder: list, create, edit and delete AI agents (voice and chat). Left column is the agent list;
|
|
5
|
+
* the editor has four sections: Persona (name, role, how it talks), Voice (voice, instructions per call
|
|
6
|
+
* type), Knowledge (AgentKnowledgePanel) and Skills (AgentSkillsPanel).
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. listVoiceAgents GET /voice/agents?search&page&page_size (read)
|
|
10
|
+
* 2. getVoiceAgent GET /voice/agents/{agent_id} (read)
|
|
11
|
+
* 3. listPresetVoices GET /voice/voice-library/presets (read)
|
|
12
|
+
* 4. createVoiceAgent POST /voice/agents { role, persona, identity } (write)
|
|
13
|
+
* 5. updateVoiceAgent PUT /voice/agents/{agent_id} (write)
|
|
14
|
+
* persona / identity / knowledge are replaced whole: always send the full sub-object.
|
|
15
|
+
* 6. deleteVoiceAgent DELETE /voice/agents/{agent_id} (destructive, ConfirmSpendDialog)
|
|
16
|
+
*
|
|
17
|
+
* Saving a draft never places a call or sends a message; activating (`agent_status: "active"`) is just a
|
|
18
|
+
* write, but the agent can then be picked by dialer sessions, so the toggle is explicit.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useEffect, useState } from "react";
|
|
22
|
+
import { Button, Input, Label, Skeleton, Switch, Textarea } from "@graph8/sdk/ui";
|
|
23
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
24
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
25
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
26
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
27
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
28
|
+
import { g8Delete, g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
|
|
29
|
+
import { DEFAULT_PERSONA, bodyOf, listOf, percent } from "../lib/agent-types";
|
|
30
|
+
import type { AgentDetail, AgentSummary } from "../lib/agent-types";
|
|
31
|
+
import { AgentKnowledgePanel } from "./agent-knowledge-panel";
|
|
32
|
+
import { AgentSkillsPanel } from "./agent-skills-panel";
|
|
33
|
+
|
|
34
|
+
export interface AgentBuilderProps {
|
|
35
|
+
/** Controlled selection. Without it the builder keeps its own. */
|
|
36
|
+
agentId?: string | null;
|
|
37
|
+
onSelect?: (agentId: string | null) => void;
|
|
38
|
+
/** Called after a create, update or delete so the host can refresh its own lists. */
|
|
39
|
+
onChanged?: () => void;
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type Section = "persona" | "voice" | "knowledge" | "skills";
|
|
44
|
+
const SECTIONS: Array<{ id: Section; label: string }> = [
|
|
45
|
+
{ id: "persona", label: "Persona" },
|
|
46
|
+
{ id: "voice", label: "Voice" },
|
|
47
|
+
{ id: "knowledge", label: "Knowledge" },
|
|
48
|
+
{ id: "skills", label: "Skills" },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const blankDraft = (): AgentDetail => ({ agent_id: "", agent_status: "inactive", role: "SDR", persona: { ...DEFAULT_PERSONA }, identity: {}, use_company_knowledge: true });
|
|
52
|
+
|
|
53
|
+
function Dial({ label, value, onChange, low, high }: { label: string; value: number; onChange: (v: number) => void; low: string; high: string }) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="flex flex-col gap-1">
|
|
56
|
+
<Label>{label}: {percent(value)}</Label>
|
|
57
|
+
<input type="range" aria-label={label} min={0} max={1} step={0.05} value={value} onChange={(e) => onChange(Number(e.target.value))} />
|
|
58
|
+
<div className="flex justify-between text-xs text-muted-foreground"><span>{low}</span><span>{high}</span></div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function AgentBuilder({ agentId: controlled, onSelect, onChanged, className }: AgentBuilderProps) {
|
|
64
|
+
const [inner, setInner] = useState<string | null>(null);
|
|
65
|
+
const selected = controlled !== undefined ? controlled : inner;
|
|
66
|
+
const [creating, setCreating] = useState(false);
|
|
67
|
+
const [search, setSearch] = useState("");
|
|
68
|
+
const [section, setSection] = useState<Section>("persona");
|
|
69
|
+
const [draft, setDraft] = useState<AgentDetail | null>(null);
|
|
70
|
+
const [deleting, setDeleting] = useState(false);
|
|
71
|
+
|
|
72
|
+
const choose = (id: string | null) => { setInner(id); setCreating(false); onSelect?.(id); };
|
|
73
|
+
const agents = useG8Query((signal) => g8Get("/voice/agents", { search: search.trim() || undefined, page_size: 50 }, { signal }), [search]);
|
|
74
|
+
const detail = useG8Query((signal) => g8Get(`/voice/agents/${selected}`, undefined, { signal }), [selected], { enabled: Boolean(selected) && !creating });
|
|
75
|
+
const voices = useG8Query((signal) => g8Get("/voice/voice-library/presets", { limit: 100 }, { signal }), []);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
const agent = bodyOf<{ agent?: AgentDetail | null }>(detail.data).agent;
|
|
79
|
+
if (agent && !creating) setDraft({ ...agent, persona: { ...DEFAULT_PERSONA, ...agent.persona }, identity: agent.identity ?? {} });
|
|
80
|
+
}, [detail.data, creating]);
|
|
81
|
+
|
|
82
|
+
const save = useG8Mutation(
|
|
83
|
+
(d: AgentDetail) => {
|
|
84
|
+
const body = { role: d.role, persona: d.persona, identity: d.identity, use_company_knowledge: d.use_company_knowledge, agent_status: d.agent_status };
|
|
85
|
+
return d.agent_id ? g8Put(`/voice/agents/${d.agent_id}`, body) : g8Post("/voice/agents", { ...body, entity_type: "agent" });
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
onSuccess: (res) => {
|
|
89
|
+
const id = bodyOf<{ agent?: { agent_id?: string } }>(res).agent?.agent_id;
|
|
90
|
+
void agents.refetch();
|
|
91
|
+
// Keep the new id on the draft at once: a second click (or a retry after a later step fails) must update, never create a twin.
|
|
92
|
+
if (!draft?.agent_id && id) { setDraft((d) => d && { ...d, agent_id: id }); choose(id); }
|
|
93
|
+
else void detail.refetch();
|
|
94
|
+
onChanged?.();
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
const remove = useG8Mutation((id: string) => g8Delete(`/voice/agents/${id}`), {
|
|
99
|
+
onSuccess: () => { setDeleting(false); setDraft(null); choose(null); void agents.refetch(); onChanged?.(); },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const rows = listOf<AgentSummary>(agents.data, "agents");
|
|
103
|
+
const presets = listOf<{ voice_id?: string; id?: string; name?: string; label?: string }>(voices.data, "items");
|
|
104
|
+
const setPersona = (patch: Partial<AgentDetail["persona"]>) => setDraft((d) => d && { ...d, persona: { ...d.persona, ...patch } });
|
|
105
|
+
const valid = Boolean(draft?.persona.agent_name.trim() && draft?.persona.persona.trim() && draft?.persona.description.trim() && draft?.role.trim());
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className={`grid gap-4 md:grid-cols-[16rem_1fr] ${className ?? ""}`}>
|
|
109
|
+
<aside className="flex flex-col gap-2">
|
|
110
|
+
<div className="flex gap-2">
|
|
111
|
+
<Input aria-label="Search agents" placeholder="Search agents" value={search} onChange={(e) => setSearch(e.target.value)} />
|
|
112
|
+
<Button size="sm" onClick={() => { setCreating(true); setDraft(blankDraft()); setSection("persona"); onSelect?.(null); }}>New agent</Button>
|
|
113
|
+
</div>
|
|
114
|
+
{agents.error ? <ApiError error={agents.error} title="Could not load agents" onRetry={() => void agents.refetch()} /> : null}
|
|
115
|
+
{agents.loading && rows.length === 0 ? Array.from({ length: 4 }, (_, i) => <Skeleton key={i} className="h-12 w-full" />) : null}
|
|
116
|
+
<ul className="flex flex-col gap-1">
|
|
117
|
+
{rows.map((a) => (
|
|
118
|
+
<li key={a.agent_id}>
|
|
119
|
+
<button type="button" data-testid={`agent-${a.agent_id}`} onClick={() => choose(a.agent_id)}
|
|
120
|
+
className={`w-full rounded-md border p-2 text-left text-sm ${selected === a.agent_id && !creating ? "border-primary bg-accent" : ""}`}>
|
|
121
|
+
<span className="font-medium">{a.agent_name}</span>
|
|
122
|
+
<span className="block text-xs text-muted-foreground">{a.role} - {a.agent_status}</span>
|
|
123
|
+
</button>
|
|
124
|
+
</li>
|
|
125
|
+
))}
|
|
126
|
+
</ul>
|
|
127
|
+
{!agents.loading && !agents.error && rows.length === 0 ? (
|
|
128
|
+
<EmptyState title="No agents yet" description="An agent has a persona, a voice, knowledge and skills." />
|
|
129
|
+
) : null}
|
|
130
|
+
</aside>
|
|
131
|
+
|
|
132
|
+
<section className="flex min-w-0 flex-col gap-3">
|
|
133
|
+
{!draft && !creating ? <EmptyState title="Pick an agent" description="Choose one on the left or create a new agent." /> : null}
|
|
134
|
+
{detail.error ? <ApiError error={detail.error} title="Could not load the agent" onRetry={() => void detail.refetch()} /> : null}
|
|
135
|
+
{draft ? (
|
|
136
|
+
<>
|
|
137
|
+
<div role="tablist" className="flex gap-1 border-b">
|
|
138
|
+
{SECTIONS.map((s) => (
|
|
139
|
+
<button key={s.id} role="tab" type="button" aria-selected={section === s.id} disabled={!draft.agent_id && (s.id === "knowledge" || s.id === "skills")}
|
|
140
|
+
className={`px-3 py-2 text-sm disabled:opacity-40 ${section === s.id ? "border-b-2 border-primary font-medium" : "text-muted-foreground"}`} onClick={() => setSection(s.id)}>{s.label}</button>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{section === "persona" ? (
|
|
145
|
+
<div className="flex flex-col gap-3">
|
|
146
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
147
|
+
<div className="flex flex-col gap-1"><Label htmlFor="agent-name">Name</Label><Input id="agent-name" value={draft.persona.agent_name} onChange={(e) => setPersona({ agent_name: e.target.value })} /></div>
|
|
148
|
+
<div className="flex flex-col gap-1"><Label htmlFor="agent-role">Role</Label><Input id="agent-role" value={draft.role} onChange={(e) => setDraft({ ...draft, role: e.target.value })} /></div>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="flex flex-col gap-1"><Label htmlFor="agent-desc">What it is for</Label><Input id="agent-desc" value={draft.persona.description} onChange={(e) => setPersona({ description: e.target.value })} /></div>
|
|
151
|
+
<div className="flex flex-col gap-1"><Label htmlFor="agent-persona">Personality and behaviour</Label><Textarea id="agent-persona" rows={5} value={draft.persona.persona} onChange={(e) => setPersona({ persona: e.target.value })} /></div>
|
|
152
|
+
<div className="grid gap-3 sm:grid-cols-3">
|
|
153
|
+
<Dial label="Formality" low="Casual" high="Formal" value={draft.persona.formality_level} onChange={(v) => setPersona({ formality_level: v })} />
|
|
154
|
+
<Dial label="Conciseness" low="Detailed" high="Brief" value={draft.persona.conciseness_level} onChange={(v) => setPersona({ conciseness_level: v })} />
|
|
155
|
+
<Dial label="Assertiveness" low="Gentle" high="Direct" value={draft.persona.assertiveness_level} onChange={(v) => setPersona({ assertiveness_level: v })} />
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
) : null}
|
|
159
|
+
|
|
160
|
+
{section === "voice" ? (
|
|
161
|
+
<div className="flex flex-col gap-3">
|
|
162
|
+
<div className="flex flex-col gap-1">
|
|
163
|
+
<Label htmlFor="agent-voice">Voice</Label>
|
|
164
|
+
<select id="agent-voice" className="h-9 rounded-md border bg-background px-2 text-sm" value={draft.identity.voice ?? ""} onChange={(e) => setDraft({ ...draft, identity: { ...draft.identity, voice: e.target.value || null } })}>
|
|
165
|
+
<option value="">Default voice</option>
|
|
166
|
+
{presets.map((v) => { const id = v.voice_id ?? v.id ?? ""; return <option key={id} value={id}>{v.name ?? v.label ?? id}</option>; })}
|
|
167
|
+
</select>
|
|
168
|
+
</div>
|
|
169
|
+
<div className="flex flex-col gap-1"><Label htmlFor="agent-inbound">Inbound call instructions</Label><Textarea id="agent-inbound" rows={3} value={draft.persona.inbound_instructions ?? ""} onChange={(e) => setPersona({ inbound_instructions: e.target.value || null })} /></div>
|
|
170
|
+
<div className="flex flex-col gap-1"><Label htmlFor="agent-outbound">Outbound call instructions</Label><Textarea id="agent-outbound" rows={3} value={draft.persona.outbound_instructions ?? ""} onChange={(e) => setPersona({ outbound_instructions: e.target.value || null })} /></div>
|
|
171
|
+
<div className="flex flex-col gap-1"><Label htmlFor="agent-voicemail">Voicemail message</Label><Textarea id="agent-voicemail" rows={2} value={draft.persona.outbound_voicemail_prompt ?? ""} onChange={(e) => setPersona({ outbound_voicemail_prompt: e.target.value || null })} /></div>
|
|
172
|
+
</div>
|
|
173
|
+
) : null}
|
|
174
|
+
|
|
175
|
+
{section === "knowledge" && draft.agent_id ? (
|
|
176
|
+
<AgentKnowledgePanel key={draft.agent_id} agentId={draft.agent_id} useCompanyKnowledge={draft.use_company_knowledge} onToggleCompanyKnowledge={(v) => setDraft({ ...draft, use_company_knowledge: v })} />
|
|
177
|
+
) : null}
|
|
178
|
+
{section === "skills" && draft.agent_id ? <AgentSkillsPanel key={draft.agent_id} agentId={draft.agent_id} /> : null}
|
|
179
|
+
|
|
180
|
+
{section === "persona" || section === "voice" ? (
|
|
181
|
+
<div className="flex flex-wrap items-center gap-3 border-t pt-3">
|
|
182
|
+
<label className="flex items-center gap-2 text-sm"><Switch checked={draft.agent_status === "active"} aria-label="Active" onCheckedChange={(on) => setDraft({ ...draft, agent_status: on ? "active" : "inactive" })} />Active (can be used for calls and chats)</label>
|
|
183
|
+
{save.error ? <ApiError error={save.error} title="Could not save the agent" /> : null}
|
|
184
|
+
<div className="ml-auto flex gap-2">
|
|
185
|
+
{draft.agent_id ? <Button variant="ghost" onClick={() => setDeleting(true)}>Delete</Button> : null}
|
|
186
|
+
<Button loading={save.loading} disabled={!valid} onClick={() => void save.mutate(draft)}>{draft.agent_id ? "Save changes" : "Create agent"}</Button>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
) : null}
|
|
190
|
+
</>
|
|
191
|
+
) : null}
|
|
192
|
+
</section>
|
|
193
|
+
|
|
194
|
+
<ConfirmSpendDialog open={deleting} tier="destructive" title={`Delete agent "${draft?.persona.agent_name ?? ""}"`} description="Its settings, attached skills and uploaded documents are removed. Past calls stay in the call log."
|
|
195
|
+
affected={1} confirming={remove.loading} onConfirm={() => draft?.agent_id && void remove.mutate(draft.agent_id)} onCancel={() => setDeleting(false)} />
|
|
196
|
+
{remove.error ? <ApiError error={remove.error} title="Could not delete the agent" /> : null}
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgentKnowledgePanel: what an agent knows. Two sources: the agent's own documents (PDF, DOCX or text,
|
|
5
|
+
* embedded into a private vector store) and the organisation's shared knowledge (read-only here), which
|
|
6
|
+
* the agent uses when "Use company knowledge" is on.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. updateVoiceAgent PUT /voice/agents/{agent_id} { use_company_knowledge } (write)
|
|
10
|
+
* 2. listAgentDocuments GET /voice/agents/{agent_id}/documents (read)
|
|
11
|
+
* 3. uploadAgentDocuments POST /voice/agents/{agent_id}/documents/upload (multipart) (billable, ConfirmSpendDialog)
|
|
12
|
+
* 4. deleteAgentDocument DELETE /voice/agent-documents/{document_id} (destructive, ConfirmSpendDialog)
|
|
13
|
+
* 5. searchKnowledge GET /knowledge/search?query&limit (read)
|
|
14
|
+
*
|
|
15
|
+
* Uploading extracts and embeds text with a paid model: the dialog states the file count and a rough
|
|
16
|
+
* credit estimate before anything is sent. 5 MiB maximum per upload through the proxy (the API itself allows 25 MB per file).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { useEffect, useRef, useState } from "react";
|
|
20
|
+
import { Button, Input, Skeleton, Switch } from "@graph8/sdk/ui";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
23
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
24
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
25
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
26
|
+
import { g8Delete, g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
|
|
27
|
+
import { listOf } from "../lib/agent-types";
|
|
28
|
+
|
|
29
|
+
export interface AgentKnowledgePanelProps {
|
|
30
|
+
agentId: string;
|
|
31
|
+
useCompanyKnowledge: boolean;
|
|
32
|
+
/** Called after the toggle was saved to the agent. */
|
|
33
|
+
onToggleCompanyKnowledge?: (value: boolean) => void;
|
|
34
|
+
/** Rough credits per file shown in the upload confirmation. Default 5. */
|
|
35
|
+
creditsPerFile?: number;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface DocRow { document_id?: string; id?: string; filename?: string; name?: string; status?: string; chunk_count?: number }
|
|
40
|
+
interface KnowledgeHit { id?: string; concept_id?: string; title?: string; summary?: string; snippet?: string; concept_type?: string }
|
|
41
|
+
|
|
42
|
+
/** The app's /api/g8 proxy caps a request body at 5 MiB, so that (not the 25 MB the API allows) is the real limit per upload. */
|
|
43
|
+
const MAX_BYTES = 5 * 1024 * 1024 - 64 * 1024; // headroom for multipart boundaries and headers
|
|
44
|
+
|
|
45
|
+
export function AgentKnowledgePanel({ agentId, useCompanyKnowledge, onToggleCompanyKnowledge, creditsPerFile = 5, className }: AgentKnowledgePanelProps) {
|
|
46
|
+
const fileInput = useRef<HTMLInputElement>(null);
|
|
47
|
+
const [pending, setPending] = useState<File[] | null>(null);
|
|
48
|
+
const [tooBig, setTooBig] = useState<string | null>(null);
|
|
49
|
+
const [removing, setRemoving] = useState<DocRow | null>(null);
|
|
50
|
+
const [query, setQuery] = useState("");
|
|
51
|
+
const [submitted, setSubmitted] = useState("");
|
|
52
|
+
|
|
53
|
+
// Everything below is tied to one agent: drop pending dialogs and search state when it changes.
|
|
54
|
+
useEffect(() => { setPending(null); setRemoving(null); setTooBig(null); setQuery(""); setSubmitted(""); }, [agentId]);
|
|
55
|
+
|
|
56
|
+
const docs = useG8Query((signal) => g8Get(`/voice/agents/${agentId}/documents`, undefined, { signal }), [agentId]);
|
|
57
|
+
const hits = useG8Query((signal) => g8Get("/knowledge/search", { query: submitted, limit: 8 }, { signal }), [submitted], { enabled: submitted.length > 0 });
|
|
58
|
+
|
|
59
|
+
const toggle = useG8Mutation((v: boolean) => g8Put(`/voice/agents/${agentId}`, { use_company_knowledge: v }), { onSuccess: (_r, v) => onToggleCompanyKnowledge?.(v) });
|
|
60
|
+
const upload = useG8Mutation(
|
|
61
|
+
(files: File[]) => {
|
|
62
|
+
const form = new FormData();
|
|
63
|
+
for (const f of files) form.append("files", f);
|
|
64
|
+
return g8Post(`/voice/agents/${agentId}/documents/upload`, form);
|
|
65
|
+
},
|
|
66
|
+
{ onSuccess: () => { setPending(null); void docs.refetch(); } },
|
|
67
|
+
);
|
|
68
|
+
const remove = useG8Mutation((id: string) => g8Delete(`/voice/agent-documents/${id}`), { onSuccess: () => { setRemoving(null); void docs.refetch(); } });
|
|
69
|
+
|
|
70
|
+
const rows = listOf<DocRow>(docs.data, "documents", "items", "files");
|
|
71
|
+
const hitRows = listOf<KnowledgeHit>(hits.data, "items");
|
|
72
|
+
|
|
73
|
+
const onPick = (list: FileList | null) => {
|
|
74
|
+
const files = Array.from(list ?? []);
|
|
75
|
+
const total = files.reduce((n, f) => n + f.size, 0);
|
|
76
|
+
const big = files.find((f) => f.size > MAX_BYTES);
|
|
77
|
+
setTooBig(big ? `${big.name} is over 5 MB.` : total > MAX_BYTES ? "Those files add up to more than 5 MB; upload them in smaller batches." : null);
|
|
78
|
+
if (files.length && !big && total <= MAX_BYTES) setPending(files);
|
|
79
|
+
if (fileInput.current) fileInput.current.value = "";
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className={`flex flex-col gap-5 ${className ?? ""}`}>
|
|
84
|
+
<label className="flex items-center gap-2 text-sm">
|
|
85
|
+
<Switch aria-label="Use company knowledge" checked={useCompanyKnowledge} disabled={toggle.loading} onCheckedChange={(v) => void toggle.mutate(v)} />
|
|
86
|
+
Use company knowledge (what your organisation has written down) in answers
|
|
87
|
+
</label>
|
|
88
|
+
{toggle.error ? <ApiError error={toggle.error} title="Could not change the setting" /> : null}
|
|
89
|
+
|
|
90
|
+
<div className="flex flex-col gap-2">
|
|
91
|
+
<div className="flex items-center justify-between">
|
|
92
|
+
<h3 className="text-sm font-semibold">This agent's documents</h3>
|
|
93
|
+
<Button size="sm" variant="outline" onClick={() => fileInput.current?.click()}>Upload documents</Button>
|
|
94
|
+
<input ref={fileInput} type="file" multiple hidden aria-label="Choose documents" accept=".pdf,.docx,.txt,.md" onChange={(e) => onPick(e.target.files)} />
|
|
95
|
+
</div>
|
|
96
|
+
{tooBig ? <p role="alert" className="text-sm text-destructive">{tooBig}</p> : null}
|
|
97
|
+
{docs.error ? <ApiError error={docs.error} title="Could not load documents" onRetry={() => void docs.refetch()} /> : null}
|
|
98
|
+
{docs.loading && rows.length === 0 ? <Skeleton className="h-10 w-full" /> : null}
|
|
99
|
+
<ul className="divide-y rounded-md border">
|
|
100
|
+
{rows.map((d) => {
|
|
101
|
+
const id = d.document_id ?? d.id ?? "";
|
|
102
|
+
return (
|
|
103
|
+
<li key={id} data-testid={`doc-${id}`} className="flex items-center justify-between gap-3 p-2 text-sm">
|
|
104
|
+
<span className="truncate">{d.filename ?? d.name ?? id}</span>
|
|
105
|
+
<span className="text-xs text-muted-foreground">{d.status ?? ""}{d.chunk_count != null ? ` - ${d.chunk_count} chunks` : ""}</span>
|
|
106
|
+
<Button size="sm" variant="ghost" onClick={() => setRemoving(d)}>Remove</Button>
|
|
107
|
+
</li>
|
|
108
|
+
);
|
|
109
|
+
})}
|
|
110
|
+
</ul>
|
|
111
|
+
{!docs.loading && !docs.error && rows.length === 0 ? <EmptyState title="No documents" description="Upload product sheets, scripts or FAQs the agent should answer from." /> : null}
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div className="flex flex-col gap-2">
|
|
115
|
+
<h3 className="text-sm font-semibold">Company knowledge</h3>
|
|
116
|
+
<form className="flex gap-2" onSubmit={(e) => { e.preventDefault(); setSubmitted(query.trim()); }}>
|
|
117
|
+
<Input aria-label="Search company knowledge" placeholder="Search what the agent can draw on" value={query} onChange={(e) => setQuery(e.target.value)} />
|
|
118
|
+
<Button type="submit" size="sm" variant="outline" disabled={!query.trim()}>Search</Button>
|
|
119
|
+
</form>
|
|
120
|
+
{hits.error ? <ApiError error={hits.error} title="Search failed" onRetry={() => void hits.refetch()} /> : null}
|
|
121
|
+
<ul className="flex flex-col gap-2">
|
|
122
|
+
{hitRows.map((h, i) => (
|
|
123
|
+
<li key={h.concept_id ?? h.id ?? i} className="rounded-md border p-2 text-sm">
|
|
124
|
+
<span className="font-medium">{h.title ?? h.concept_id ?? h.id}</span>
|
|
125
|
+
{h.concept_type ? <span className="ml-2 text-xs text-muted-foreground">{h.concept_type}</span> : null}
|
|
126
|
+
<p className="text-muted-foreground">{h.summary ?? h.snippet ?? ""}</p>
|
|
127
|
+
</li>
|
|
128
|
+
))}
|
|
129
|
+
</ul>
|
|
130
|
+
{submitted && !hits.loading && !hits.error && hitRows.length === 0 ? <p className="text-sm text-muted-foreground">Nothing found for "{submitted}".</p> : null}
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{upload.error ? <ApiError error={upload.error} title="Could not upload" /> : null}
|
|
134
|
+
{remove.error ? <ApiError error={remove.error} title="Could not remove the document" /> : null}
|
|
135
|
+
<ConfirmSpendDialog open={pending != null} tier="billable" title={`Index ${pending?.length ?? 0} document${pending?.length === 1 ? "" : "s"}`}
|
|
136
|
+
description="The text is extracted and embedded with a paid model so the agent can search it."
|
|
137
|
+
estimatedCredits={(pending?.length ?? 0) * creditsPerFile} confirmLabel="Upload" confirming={upload.loading}
|
|
138
|
+
onConfirm={() => pending && void upload.mutate(pending)} onCancel={() => setPending(null)} />
|
|
139
|
+
<ConfirmSpendDialog open={removing != null} tier="destructive" title={`Remove "${removing?.filename ?? removing?.name ?? "document"}"`}
|
|
140
|
+
description="The agent stops using it straight away." affected={1} confirming={remove.loading}
|
|
141
|
+
onConfirm={() => removing && void remove.mutate(removing.document_id ?? removing.id ?? "")} onCancel={() => setRemoving(null)} />
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgentRuns: durable agent runs, live. Start a run (pick a specialist, give it a goal), watch its status
|
|
5
|
+
* and credits as it works (polled, the API has no streams), cancel it, and decide the approvals a run
|
|
6
|
+
* stops on before it acts.
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. listAgentRuns GET /agent/runs?statuses&limit&offset (read)
|
|
10
|
+
* 2. listAgentSpecialists GET /agent/specialists (read) the agent_id values a run accepts
|
|
11
|
+
* 3. createAgentRun POST /agent/runs { agent_id, title, goal } (write; spends credits as it runs, ConfirmSpendDialog)
|
|
12
|
+
* 4. getAgentRun GET /agent/runs/{run_id} (read) polled until terminal
|
|
13
|
+
* 5. cancelAgentRun POST /agent/runs/{run_id}/cancel (destructive, ConfirmSpendDialog)
|
|
14
|
+
* 6. listAgentApprovals GET /agent/approvals (read)
|
|
15
|
+
* 7. decideAgentApproval POST /agent/approvals/{approval_id}/decide { decision } (write; approving lets the run act, ConfirmSpendDialog)
|
|
16
|
+
*
|
|
17
|
+
* `workflow_started: false` on a create answer is normal (the worker picks the row up later); the run is
|
|
18
|
+
* saved either way and shows up in the list.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useEffect, useState } from "react";
|
|
22
|
+
import { Button, Input, Label, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
23
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
24
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
25
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
26
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
27
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
28
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
29
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
30
|
+
import { bodyOf, listOf } from "../lib/agent-types";
|
|
31
|
+
import type { AgentRun } from "../lib/agent-types";
|
|
32
|
+
|
|
33
|
+
export interface AgentRunsProps {
|
|
34
|
+
/** Only runs of this agent. */
|
|
35
|
+
agentId?: string;
|
|
36
|
+
pageSize?: number;
|
|
37
|
+
/** Poll interval for the selected run, in ms. Default 3000. */
|
|
38
|
+
pollMs?: number;
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface Approval { id: string; summary: string; risk_level: string; work_item_id: string; decision?: string | null; is_expired?: boolean }
|
|
43
|
+
interface Specialist { agent_id?: string; id?: string; name?: string; title?: string; description?: string }
|
|
44
|
+
|
|
45
|
+
const TERMINAL = new Set(["completed", "succeeded", "failed", "cancelled", "canceled"]);
|
|
46
|
+
export const isTerminalRun = (r: Pick<AgentRun, "status"> | undefined): boolean => TERMINAL.has(String(r?.status ?? "").toLowerCase());
|
|
47
|
+
const STATUSES = ["running", "queued", "completed", "failed", "cancelled"];
|
|
48
|
+
|
|
49
|
+
export function AgentRuns({ agentId, pageSize = 20, pollMs = 3000, className }: AgentRunsProps) {
|
|
50
|
+
const [status, setStatus] = useState("");
|
|
51
|
+
const [limit, setLimit] = useState(pageSize);
|
|
52
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
53
|
+
const [form, setForm] = useState<{ agent_id: string; title: string; goal: string } | null>(null);
|
|
54
|
+
const [confirmStart, setConfirmStart] = useState(false);
|
|
55
|
+
const [cancelling, setCancelling] = useState<AgentRun | null>(null);
|
|
56
|
+
const [approving, setApproving] = useState<Approval | null>(null);
|
|
57
|
+
const [deciding, setDeciding] = useState<string | null>(null);
|
|
58
|
+
|
|
59
|
+
const runs = useG8Query((signal) => g8Get("/agent/runs", { statuses: status || undefined, agent_id: agentId, limit }, { signal }), [status, agentId, limit]);
|
|
60
|
+
const approvals = useG8Query((signal) => g8Get("/agent/approvals", undefined, { signal }), []);
|
|
61
|
+
const specialists = useG8Query((signal) => g8Get("/agent/specialists", undefined, { signal }), [], { enabled: form != null });
|
|
62
|
+
const detail = useJobPoll<AgentRun>((signal) => g8Get<AgentRun>(`/agent/runs/${selected}`, undefined, { signal }).then((r) => bodyOf<AgentRun>(r)), {
|
|
63
|
+
enabled: selected != null, isTerminal: isTerminalRun, intervalMs: pollMs, maxIntervalMs: pollMs * 4, onDone: () => void runs.refetch(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Switching runs keeps `enabled` true, so restart the poll explicitly.
|
|
67
|
+
useEffect(() => { if (selected) detail.start(); }, [selected]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
68
|
+
|
|
69
|
+
// A run that stops on an approval keeps polling: refresh the approvals on every poll that lands.
|
|
70
|
+
useEffect(() => { if (selected && detail.attempts > 0) void approvals.refetch(); }, [detail.attempts]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
71
|
+
|
|
72
|
+
const start = useG8Mutation(() => g8Post<AgentRun>("/agent/runs", { agent_id: form?.agent_id ?? agentId, title: form?.title.trim(), goal: form?.goal.trim() }), {
|
|
73
|
+
onSuccess: (res) => { setConfirmStart(false); setForm(null); setSelected(bodyOf<AgentRun>(res).work_item_id ?? null); void runs.refetch(); },
|
|
74
|
+
onError: () => setConfirmStart(false),
|
|
75
|
+
});
|
|
76
|
+
const cancel = useG8Mutation((id: string) => g8Post(`/agent/runs/${id}/cancel`, {}), { onSuccess: () => { setCancelling(null); void runs.refetch(); detail.start(); } });
|
|
77
|
+
const decide = useG8Mutation(async (v: { id: string; decision: "approve" | "reject" }) => {
|
|
78
|
+
setDeciding(v.id);
|
|
79
|
+
try { return await g8Post(`/agent/approvals/${v.id}/decide`, { decision: v.decision }); } finally { setDeciding(null); }
|
|
80
|
+
}, { onSuccess: () => { setApproving(null); void approvals.refetch(); void runs.refetch(); } });
|
|
81
|
+
|
|
82
|
+
const rows = listOf<AgentRun>(runs.data, "runs");
|
|
83
|
+
const total = bodyOf<{ total?: number }>(runs.data).total ?? rows.length;
|
|
84
|
+
// With an agentId the screen is about that agent's runs: only show approvals of runs it lists.
|
|
85
|
+
const shownRunIds = new Set(rows.map((r) => r.work_item_id));
|
|
86
|
+
const pending = listOf<Approval>(approvals.data).filter((a) => !a.decision && !a.is_expired && (!agentId || shownRunIds.has(a.work_item_id)));
|
|
87
|
+
const bench = listOf<Specialist>(specialists.data, "items");
|
|
88
|
+
const current = detail.status?.work_item_id === selected ? detail.status : undefined;
|
|
89
|
+
const formOk = Boolean(form?.title.trim() && form.goal.trim() && (form.agent_id || agentId));
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div className={`flex flex-col gap-4 ${className ?? ""}`}>
|
|
93
|
+
{pending.length > 0 ? (
|
|
94
|
+
<div className="rounded-md border border-amber-300 p-3" data-testid="approvals">
|
|
95
|
+
<h3 className="mb-2 text-sm font-semibold">Waiting for your decision ({pending.length})</h3>
|
|
96
|
+
<ul className="flex flex-col gap-2">
|
|
97
|
+
{pending.map((a) => (
|
|
98
|
+
<li key={a.id} className="flex flex-wrap items-center justify-between gap-2 text-sm">
|
|
99
|
+
<span>{a.summary}<span className="ml-2 text-xs text-muted-foreground">risk: {a.risk_level}</span></span>
|
|
100
|
+
<span className="flex gap-2">
|
|
101
|
+
<Button size="sm" variant="outline" loading={deciding === a.id} disabled={deciding != null && deciding !== a.id} onClick={() => void decide.mutate({ id: a.id, decision: "reject" })}>Reject</Button>
|
|
102
|
+
<Button size="sm" disabled={deciding != null} onClick={() => setApproving(a)}>Approve</Button>
|
|
103
|
+
</span>
|
|
104
|
+
</li>
|
|
105
|
+
))}
|
|
106
|
+
</ul>
|
|
107
|
+
</div>
|
|
108
|
+
) : null}
|
|
109
|
+
{decide.error ? <ApiError error={decide.error} title="Could not record the decision" /> : null}
|
|
110
|
+
|
|
111
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
112
|
+
<select aria-label="Status" className="h-9 rounded-md border bg-background px-2 text-sm" value={status} onChange={(e) => { setStatus(e.target.value); setLimit(pageSize); }}>
|
|
113
|
+
<option value="">All statuses</option>
|
|
114
|
+
{STATUSES.map((s) => <option key={s} value={s}>{s}</option>)}
|
|
115
|
+
</select>
|
|
116
|
+
<Button className="ml-auto" size="sm" onClick={() => setForm({ agent_id: agentId ?? "", title: "", goal: "" })}>Start a run</Button>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{form ? (
|
|
120
|
+
<div className="flex flex-col gap-3 rounded-md border p-3">
|
|
121
|
+
{!agentId ? (
|
|
122
|
+
<div className="flex flex-col gap-1"><Label htmlFor="run-agent">Specialist</Label>
|
|
123
|
+
<select id="run-agent" className="h-9 rounded-md border bg-background px-2 text-sm" value={form.agent_id} onChange={(e) => setForm({ ...form, agent_id: e.target.value })}>
|
|
124
|
+
<option value="">Choose a specialist</option>
|
|
125
|
+
{bench.map((s) => { const id = s.agent_id ?? s.id ?? ""; return <option key={id} value={id}>{s.name ?? s.title ?? id}</option>; })}
|
|
126
|
+
</select></div>
|
|
127
|
+
) : null}
|
|
128
|
+
<div className="flex flex-col gap-1"><Label htmlFor="run-title">Title</Label><Input id="run-title" value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} /></div>
|
|
129
|
+
<div className="flex flex-col gap-1"><Label htmlFor="run-goal">Goal</Label><Textarea id="run-goal" rows={3} value={form.goal} onChange={(e) => setForm({ ...form, goal: e.target.value })} /></div>
|
|
130
|
+
{start.error ? <ApiError error={start.error} title="Could not start the run" /> : null}
|
|
131
|
+
<div className="flex justify-end gap-2"><Button variant="ghost" onClick={() => setForm(null)}>Cancel</Button><Button disabled={!formOk} onClick={() => setConfirmStart(true)}>Start run</Button></div>
|
|
132
|
+
</div>
|
|
133
|
+
) : null}
|
|
134
|
+
|
|
135
|
+
{runs.error ? <ApiError error={runs.error} title="Could not load runs" onRetry={() => void runs.refetch()} /> : null}
|
|
136
|
+
{runs.loading && rows.length === 0 ? Array.from({ length: 3 }, (_, i) => <Skeleton key={i} className="h-12 w-full" />) : null}
|
|
137
|
+
<ul className="divide-y rounded-md border">
|
|
138
|
+
{rows.map((r) => (
|
|
139
|
+
<li key={r.work_item_id}>
|
|
140
|
+
<button type="button" data-testid={`run-${r.work_item_id}`} onClick={() => setSelected(r.work_item_id)} className={`flex w-full items-center justify-between gap-3 p-2 text-left text-sm ${selected === r.work_item_id ? "bg-accent" : ""}`}>
|
|
141
|
+
<span className="font-medium">{r.title}</span>
|
|
142
|
+
<span className="text-xs text-muted-foreground">{r.status}{r.credits_spent ? ` - ${r.credits_spent} credits` : ""}</span>
|
|
143
|
+
</button>
|
|
144
|
+
</li>
|
|
145
|
+
))}
|
|
146
|
+
</ul>
|
|
147
|
+
{!runs.loading && !runs.error && rows.length === 0 ? <EmptyState title="No runs yet" description="A run is one goal worked on by an agent, step by step, with approvals where it matters." /> : null}
|
|
148
|
+
{rows.length < total ? <Button variant="outline" size="sm" onClick={() => setLimit(limit + pageSize)}>Load more</Button> : null}
|
|
149
|
+
|
|
150
|
+
{selected ? (
|
|
151
|
+
<div className="rounded-md border p-3" data-testid="run-detail">
|
|
152
|
+
{current ? (
|
|
153
|
+
<>
|
|
154
|
+
<div className="flex items-center justify-between"><h3 className="font-semibold">{current.title}</h3><span className="text-sm">{current.status}{detail.polling ? " (updating)" : ""}</span></div>
|
|
155
|
+
<dl className="mt-2 grid grid-cols-2 gap-1 text-sm">
|
|
156
|
+
<dt className="text-muted-foreground">Current step</dt><dd>{current.current_step ?? "-"}</dd>
|
|
157
|
+
<dt className="text-muted-foreground">Next action</dt><dd>{current.next_action ?? "-"}</dd>
|
|
158
|
+
<dt className="text-muted-foreground">Credits</dt><dd>{current.credits_spent ?? 0}{current.credit_budget ? ` of ${current.credit_budget}` : ""}</dd>
|
|
159
|
+
</dl>
|
|
160
|
+
{!isTerminalRun(current) ? <Button className="mt-3" size="sm" variant="outline" onClick={() => setCancelling(current)}>Cancel run</Button> : null}
|
|
161
|
+
</>
|
|
162
|
+
) : detail.error ? <ApiError error={detail.error} title="Could not load the run" onRetry={() => detail.start()} /> : <Skeleton className="h-16 w-full" />}
|
|
163
|
+
{cancel.error ? <ApiError error={cancel.error} title="Could not cancel the run" /> : null}
|
|
164
|
+
</div>
|
|
165
|
+
) : null}
|
|
166
|
+
|
|
167
|
+
<ConfirmSpendDialog open={confirmStart} tier="billable" title={`Start "${form?.title ?? "run"}"`} description="The agent works toward the goal and spends credits as it goes. You can cancel at any time."
|
|
168
|
+
confirmLabel="Start run" confirming={start.loading} onConfirm={() => void start.mutate(undefined)} onCancel={() => setConfirmStart(false)} />
|
|
169
|
+
<ConfirmSpendDialog open={cancelling != null} tier="destructive" title={`Cancel "${cancelling?.title ?? "run"}"`} description="Work stops where it is. Steps already taken are not undone."
|
|
170
|
+
confirmLabel="Cancel run" confirming={cancel.loading} onConfirm={() => cancelling && void cancel.mutate(cancelling.work_item_id)} onCancel={() => setCancelling(null)} />
|
|
171
|
+
<ConfirmSpendDialog open={approving != null} tier="external" title="Approve this action" description={approving?.summary}
|
|
172
|
+
confirmLabel="Approve" confirming={decide.loading} onConfirm={() => approving && void decide.mutate({ id: approving.id, decision: "approve" })} onCancel={() => setApproving(null)} />
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|