@graph8/sdk 0.362.0 → 0.373.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -10
- package/blocks/_foundation/README.md +78 -1
- package/blocks/_foundation/block.json +123 -6
- package/blocks/_foundation/components/api-error.tsx +9 -7
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +2 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +93 -2
- package/blocks/agents/block.json +343 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +29 -10
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +119 -0
- package/blocks/agents/components/agent-runs.tsx +19 -10
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +226 -0
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +222 -0
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +38 -11
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +33 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +4 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +38 -18
- package/blocks/appointments/components/meetings-screen.tsx +7 -4
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/appointments/components/scheduling-screen.tsx +2 -1
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +17 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +17 -6
- package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +163 -0
- package/blocks/campaigns/components/icp-editor.tsx +22 -17
- package/blocks/campaigns/components/persona-detail.tsx +181 -0
- package/blocks/campaigns/components/persona-editor.tsx +19 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +161 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
- package/blocks/crm/README.md +34 -0
- package/blocks/crm/block.json +107 -2
- package/blocks/crm/components/companies-screen.tsx +359 -0
- package/blocks/crm/components/company-detail.tsx +10 -8
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +10 -7
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +377 -0
- package/blocks/crm/components/dedupe-review.tsx +13 -6
- package/blocks/crm/components/import-wizard.tsx +7 -9
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +6 -4
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +399 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +9 -1
- package/blocks/crm/lib/crm-types.ts +26 -2
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +160 -5
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +25 -13
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +31 -10
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +66 -26
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +5 -5
- package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +45 -4
- package/blocks/dialer/block.json +84 -3
- package/blocks/dialer/components/call-contact-button.tsx +269 -0
- package/blocks/dialer/components/call-log-screen.tsx +208 -0
- package/blocks/dialer/components/call-log.tsx +23 -7
- package/blocks/dialer/components/call-queue.tsx +4 -4
- package/blocks/dialer/components/dialer-screen.tsx +361 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +61 -12
- package/blocks/dialer/components/softphone.tsx +4 -2
- package/blocks/enrichment/README.md +29 -7
- package/blocks/enrichment/block.json +49 -9
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
- package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
- package/blocks/enrichment/components/waterfall-config.tsx +66 -17
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +139 -18
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +4 -2
- package/blocks/forms/components/form-embed.tsx +4 -2
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +56 -19
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +17 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +32 -3
- package/blocks/inbox/block.json +29 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +63 -1
- package/blocks/integrations/block.json +234 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +119 -0
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +13 -8
- package/blocks/integrations/components/integrations-screen.tsx +152 -0
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +11 -9
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +179 -0
- package/blocks/integrations/components/webhook-manager.tsx +43 -25
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +11 -1
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +13 -10
- package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/block.json +13 -8
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +20 -1
- package/blocks/lists/block.json +69 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +20 -9
- package/blocks/lists/components/list-picker.tsx +17 -3
- package/blocks/lists/components/list-table.tsx +12 -6
- package/blocks/lists/components/lists-screen.tsx +346 -0
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +33 -8
- package/blocks/lists/components/suppressions-screen.tsx +264 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +69 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
- package/blocks/mailboxes/components/domain-detail.tsx +148 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
- package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
- package/blocks/mailboxes/components/warmup-health.tsx +24 -22
- package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +157 -24
- package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/block.json +28 -14
- package/blocks/nurture/components/nurture-builder.tsx +13 -3
- package/blocks/nurture/components/nurture-enroll.tsx +12 -10
- package/blocks/nurture/components/nurture-form.tsx +27 -10
- package/blocks/nurture/components/nurture-screen.tsx +26 -11
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +18 -1
- package/blocks/objects/block.json +42 -21
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +10 -2
- package/blocks/objects/components/object-schema-editor.tsx +4 -2
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +140 -23
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +9 -4
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-list.tsx +2 -1
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/quotes/README.md +13 -0
- package/blocks/quotes/block.json +65 -13
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +5 -3
- package/blocks/quotes/components/quote-related.tsx +8 -4
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +55 -9
- package/blocks/sequencer/block.json +126 -19
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +224 -60
- package/blocks/sequencer/components/sequence-detail.tsx +261 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
- package/blocks/sequencer/components/sequence-list.tsx +34 -9
- package/blocks/sequencer/components/sequence-stats.tsx +11 -10
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +162 -0
- package/blocks/sequencer/components/step-editor.tsx +14 -12
- package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +19 -0
- package/blocks/social-ads/block.json +233 -1
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
- package/blocks/social-ads/components/audience-sync.tsx +17 -9
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +138 -0
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +155 -0
- package/blocks/social-ads/components/creative-gallery.tsx +35 -7
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +7 -3
- package/blocks/social-ads/components/post-detail.tsx +163 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +187 -0
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +21 -10
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +26 -16
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +16 -7
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +8 -5
- package/blocks/webchat/components/conversation-list.tsx +8 -4
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +10 -4
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +202 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +4 -2
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +3109 -1977
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +122 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +27 -20
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +17 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +55 -17
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +147 -37
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +64 -0
- package/stacks/clay/pages/companies.tsx.tmpl +31 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +73 -27
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +98 -35
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +104 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +181 -64
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +52 -20
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +68 -34
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +106 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +117 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +85 -31
- package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +115 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +40 -9
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/app/layout.tsx.tmpl +7 -2
- package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgentRunDetail: one agent run in full. Title, status, agent, current step, next action, credits against the budget,
|
|
5
|
+
* created and completed times, the approvals this run is waiting on (approve or reject), and Cancel while it is live.
|
|
6
|
+
* The run is polled until it is terminal (the API has no streams).
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. getAgentRun GET /agent/runs/{run_id} (read) polled
|
|
10
|
+
* 2. listAgentApprovals GET /agent/approvals (read) this run's pending ones
|
|
11
|
+
* 3. decideAgentApproval POST /agent/approvals/{approval_id}/decide { decision } (write; approving lets the run act: ConfirmSpendDialog, external)
|
|
12
|
+
* 4. cancelAgentRun POST /agent/runs/{run_id}/cancel (destructive, ConfirmSpendDialog)
|
|
13
|
+
* 5. steerAgentWorkItem POST /agent/work-items/{work_item_id}/steer { client_turn_id, instruction } (write; while the run is live)
|
|
14
|
+
* The same client_turn_id is reused when a send is retried, so an instruction is never applied twice.
|
|
15
|
+
* Approvals that gate a Work coordination action cannot be decided with an API key: the error is shown as is.
|
|
16
|
+
*/
|
|
17
|
+
import { useEffect, useRef, useState } from "react";
|
|
18
|
+
import { Button, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
19
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
20
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
21
|
+
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
22
|
+
import { StatusPill, toneForStatus } from "@/g8/components/screen/status-pill";
|
|
23
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
24
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
25
|
+
import { useJobPoll } from "@/g8/hooks/use-job-poll";
|
|
26
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
27
|
+
import { bodyOf, listOf } from "../lib/agent-types";
|
|
28
|
+
import type { AgentRun } from "../lib/agent-types";
|
|
29
|
+
import { isTerminalRun } from "./agent-runs";
|
|
30
|
+
|
|
31
|
+
export interface AgentRunDetailProps {
|
|
32
|
+
runId: string;
|
|
33
|
+
/** Poll interval in ms. Default 3000. */
|
|
34
|
+
pollMs?: number;
|
|
35
|
+
/** Called after a cancel or a decision so the host can refresh its run list. */
|
|
36
|
+
onChanged?: () => void;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface Approval { id: string; summary: string; risk_level: string; work_item_id: string; decision?: string | null; is_expired?: boolean }
|
|
41
|
+
|
|
42
|
+
const when = (v?: string | null): string => (v ? new Date(v).toLocaleString() : "");
|
|
43
|
+
|
|
44
|
+
export function AgentRunDetail({ runId, pollMs = 3000, onChanged, className }: AgentRunDetailProps) {
|
|
45
|
+
const [cancelling, setCancelling] = useState(false);
|
|
46
|
+
const [approving, setApproving] = useState<Approval | null>(null);
|
|
47
|
+
const [deciding, setDeciding] = useState<string | null>(null);
|
|
48
|
+
const [steerText, setSteerText] = useState("");
|
|
49
|
+
const turnId = useRef<string | null>(null);
|
|
50
|
+
const poll = useJobPoll<AgentRun>((signal) => g8Get<AgentRun>(`/agent/runs/${encodeURIComponent(runId)}`, undefined, { signal }).then((r) => bodyOf<AgentRun>(r)), {
|
|
51
|
+
isTerminal: isTerminalRun, intervalMs: pollMs, maxIntervalMs: pollMs * 4, onDone: () => onChanged?.(),
|
|
52
|
+
});
|
|
53
|
+
const approvals = useG8Query((signal) => g8Get("/agent/approvals", undefined, { signal }), [runId]);
|
|
54
|
+
useEffect(() => { poll.start(); setCancelling(false); setApproving(null); }, [runId]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
55
|
+
useEffect(() => { if (poll.attempts > 0) void approvals.refetch(); }, [poll.attempts]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
56
|
+
|
|
57
|
+
const cancel = useG8Mutation(() => g8Post(`/agent/runs/${encodeURIComponent(runId)}/cancel`, {}), { onSuccess: () => { setCancelling(false); poll.start(); onChanged?.(); }, onError: () => setCancelling(false) });
|
|
58
|
+
const decide = useG8Mutation(async (v: { id: string; decision: "approve" | "reject" }) => {
|
|
59
|
+
setDeciding(v.id);
|
|
60
|
+
try { return await g8Post(`/agent/approvals/${encodeURIComponent(v.id)}/decide`, { decision: v.decision }); } finally { setDeciding(null); }
|
|
61
|
+
}, { onSuccess: () => { setApproving(null); void approvals.refetch(); onChanged?.(); }, onError: () => setApproving(null) });
|
|
62
|
+
|
|
63
|
+
const steer = useG8Mutation((instruction: string) => {
|
|
64
|
+
turnId.current ??= globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
65
|
+
return g8Post(`/agent/work-items/${encodeURIComponent(runId)}/steer`, { client_turn_id: turnId.current, instruction });
|
|
66
|
+
}, { onSuccess: () => { turnId.current = null; setSteerText(""); poll.start(); } });
|
|
67
|
+
useEffect(() => { setSteerText(""); turnId.current = null; steer.reset(); }, [runId]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
68
|
+
|
|
69
|
+
const run = poll.status?.work_item_id === runId ? poll.status : undefined;
|
|
70
|
+
const pending = listOf<Approval>(approvals.data).filter((a) => a.work_item_id === runId && !a.decision && !a.is_expired);
|
|
71
|
+
if (!run) return poll.error ? <ApiError error={poll.error} title="Could not load the run" onRetry={() => poll.start()} /> : <Skeleton className="h-40 w-full" aria-label="Loading run" />;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className={`space-y-4 ${className ?? ""}`} data-testid="run-detail">
|
|
75
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
76
|
+
<StatusPill tone={toneForStatus(run.status)} dot>{run.status}</StatusPill>
|
|
77
|
+
{poll.polling ? <span className="text-xs text-muted-foreground">updating</span> : null}
|
|
78
|
+
{!isTerminalRun(run) ? <Button className="ml-auto" size="sm" variant="outline" onClick={() => setCancelling(true)}>Cancel run</Button> : null}
|
|
79
|
+
</div>
|
|
80
|
+
<KeyValueList columns={2} items={[
|
|
81
|
+
{ label: "Title", value: run.title, wide: true },
|
|
82
|
+
{ label: "Agent", value: run.agent_id, mono: true },
|
|
83
|
+
{ label: "Run id", value: run.work_item_id, mono: true },
|
|
84
|
+
{ label: "Current step", value: run.current_step },
|
|
85
|
+
{ label: "Next action", value: run.next_action },
|
|
86
|
+
{ label: "Credits", value: `${run.credits_spent ?? 0}${run.credit_budget ? ` of ${run.credit_budget}` : ""}` },
|
|
87
|
+
{ label: "Started", value: when(run.created_at) },
|
|
88
|
+
{ label: "Finished", value: when(run.completed_at) },
|
|
89
|
+
]} />
|
|
90
|
+
{pending.length > 0 ? (
|
|
91
|
+
<section className="space-y-2 rounded-md border p-3" aria-label="Approvals" data-testid="run-approvals">
|
|
92
|
+
<h4 className="text-sm font-medium">Waiting for your decision ({pending.length})</h4>
|
|
93
|
+
{pending.map((a) => (
|
|
94
|
+
<div key={a.id} className="flex flex-wrap items-center justify-between gap-2 text-sm">
|
|
95
|
+
<span>{a.summary}<span className="ml-2 text-xs text-muted-foreground">risk: {a.risk_level}</span></span>
|
|
96
|
+
<span className="flex gap-2">
|
|
97
|
+
<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>
|
|
98
|
+
<Button size="sm" disabled={deciding != null} onClick={() => setApproving(a)}>Approve</Button>
|
|
99
|
+
</span>
|
|
100
|
+
</div>
|
|
101
|
+
))}
|
|
102
|
+
</section>
|
|
103
|
+
) : null}
|
|
104
|
+
{!isTerminalRun(run) ? (
|
|
105
|
+
<section className="space-y-2" aria-label="Steer the run">
|
|
106
|
+
<Textarea aria-label="Instruction for the agent" rows={2} placeholder="Tell the agent what to do differently" value={steerText} onChange={(e) => { if (e.target.value !== steerText) turnId.current = null; setSteerText(e.target.value); }} />
|
|
107
|
+
<div className="flex justify-end"><Button size="sm" variant="outline" loading={steer.loading} disabled={!steerText.trim()} onClick={() => void steer.mutate(steerText.trim())}>Send instruction</Button></div>
|
|
108
|
+
{steer.error ? <ApiError error={steer.error} title="Could not send the instruction" /> : null}
|
|
109
|
+
</section>
|
|
110
|
+
) : null}
|
|
111
|
+
{decide.error ? <ApiError error={decide.error} title="Could not record the decision" /> : null}
|
|
112
|
+
{cancel.error ? <ApiError error={cancel.error} title="Could not cancel the run" /> : null}
|
|
113
|
+
<ConfirmSpendDialog open={cancelling} tier="destructive" title={`Cancel "${run.title}"`} description="Work stops where it is. Steps already taken are not undone."
|
|
114
|
+
confirmLabel="Cancel run" confirming={cancel.loading} onConfirm={() => void cancel.mutate(undefined)} onCancel={() => setCancelling(false)} />
|
|
115
|
+
<ConfirmSpendDialog open={approving != null} tier="external" title="Approve this action" description={approving?.summary}
|
|
116
|
+
confirmLabel="Approve" confirming={decide.loading} onConfirm={() => approving && void decide.mutate({ id: approving.id, decision: "approve" })} onCancel={() => setApproving(null)} />
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -36,6 +36,13 @@ export interface AgentRunsProps {
|
|
|
36
36
|
pageSize?: number;
|
|
37
37
|
/** Poll interval for the selected run, in ms. Default 3000. */
|
|
38
38
|
pollMs?: number;
|
|
39
|
+
/** Controlled selection (a run id). With it, the host owns which run is open (for example from the URL). */
|
|
40
|
+
selectedId?: string | null;
|
|
41
|
+
onSelectRun?: (runId: string | null) => void;
|
|
42
|
+
/** Hide the built-in detail box when the host shows the run elsewhere (see AgentRunDetail). */
|
|
43
|
+
hideDetail?: boolean;
|
|
44
|
+
/** Change this number to reload the list (for example after AgentRunDetail cancelled a run). */
|
|
45
|
+
refreshKey?: number;
|
|
39
46
|
className?: string;
|
|
40
47
|
}
|
|
41
48
|
|
|
@@ -46,28 +53,30 @@ const TERMINAL = new Set(["completed", "succeeded", "failed", "cancelled", "canc
|
|
|
46
53
|
export const isTerminalRun = (r: Pick<AgentRun, "status"> | undefined): boolean => TERMINAL.has(String(r?.status ?? "").toLowerCase());
|
|
47
54
|
const STATUSES = ["running", "queued", "completed", "failed", "cancelled"];
|
|
48
55
|
|
|
49
|
-
export function AgentRuns({ agentId, pageSize = 20, pollMs = 3000, className }: AgentRunsProps) {
|
|
56
|
+
export function AgentRuns({ agentId, pageSize = 20, pollMs = 3000, selectedId, onSelectRun, hideDetail, refreshKey, className }: AgentRunsProps) {
|
|
50
57
|
const [status, setStatus] = useState("");
|
|
51
58
|
const [limit, setLimit] = useState(pageSize);
|
|
52
|
-
const [
|
|
59
|
+
const [innerSel, setInnerSel] = useState<string | null>(null);
|
|
60
|
+
const selected = selectedId !== undefined ? selectedId : innerSel;
|
|
61
|
+
const setSelected = (id: string | null) => { setInnerSel(id); onSelectRun?.(id); };
|
|
53
62
|
const [form, setForm] = useState<{ agent_id: string; title: string; goal: string } | null>(null);
|
|
54
63
|
const [confirmStart, setConfirmStart] = useState(false);
|
|
55
64
|
const [cancelling, setCancelling] = useState<AgentRun | null>(null);
|
|
56
65
|
const [approving, setApproving] = useState<Approval | null>(null);
|
|
57
66
|
const [deciding, setDeciding] = useState<string | null>(null);
|
|
58
67
|
|
|
59
|
-
const runs = useG8Query((signal) => g8Get("/agent/runs", { statuses: status || undefined, agent_id: agentId, limit }, { signal }), [status, agentId, limit]);
|
|
68
|
+
const runs = useG8Query((signal) => g8Get("/agent/runs", { statuses: status || undefined, agent_id: agentId, limit }, { signal }), [status, agentId, limit, refreshKey]);
|
|
60
69
|
const approvals = useG8Query((signal) => g8Get("/agent/approvals", undefined, { signal }), []);
|
|
61
70
|
const specialists = useG8Query((signal) => g8Get("/agent/specialists", undefined, { signal }), [], { enabled: form != null });
|
|
62
71
|
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(),
|
|
72
|
+
enabled: selected != null && !hideDetail, isTerminal: isTerminalRun, intervalMs: pollMs, maxIntervalMs: pollMs * 4, onDone: () => void runs.refetch(),
|
|
64
73
|
});
|
|
65
74
|
|
|
66
75
|
// Switching runs keeps `enabled` true, so restart the poll explicitly.
|
|
67
|
-
useEffect(() => { if (selected) detail.start(); }, [selected]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
76
|
+
useEffect(() => { if (selected && !hideDetail) detail.start(); }, [selected]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
68
77
|
|
|
69
78
|
// 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
|
|
79
|
+
useEffect(() => { if (selected && !hideDetail && detail.attempts > 0) void approvals.refetch(); }, [detail.attempts]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
71
80
|
|
|
72
81
|
const start = useG8Mutation(() => g8Post<AgentRun>("/agent/runs", { agent_id: form?.agent_id ?? agentId, title: form?.title.trim(), goal: form?.goal.trim() }), {
|
|
73
82
|
onSuccess: (res) => { setConfirmStart(false); setForm(null); setSelected(bodyOf<AgentRun>(res).work_item_id ?? null); void runs.refetch(); },
|
|
@@ -91,8 +100,8 @@ export function AgentRuns({ agentId, pageSize = 20, pollMs = 3000, className }:
|
|
|
91
100
|
return (
|
|
92
101
|
<div className={`flex flex-col gap-4 ${className ?? ""}`}>
|
|
93
102
|
{pending.length > 0 ? (
|
|
94
|
-
<div className="rounded-md border border-
|
|
95
|
-
<h3 className="mb-2 text-sm font-
|
|
103
|
+
<div className="rounded-md border border-warning-foreground/30 p-3" data-testid="approvals">
|
|
104
|
+
<h3 className="mb-2 text-sm font-medium">Waiting for your decision ({pending.length})</h3>
|
|
96
105
|
<ul className="flex flex-col gap-2">
|
|
97
106
|
{pending.map((a) => (
|
|
98
107
|
<li key={a.id} className="flex flex-wrap items-center justify-between gap-2 text-sm">
|
|
@@ -147,11 +156,11 @@ export function AgentRuns({ agentId, pageSize = 20, pollMs = 3000, className }:
|
|
|
147
156
|
{!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
157
|
{rows.length < total ? <Button variant="outline" size="sm" onClick={() => setLimit(limit + pageSize)}>Load more</Button> : null}
|
|
149
158
|
|
|
150
|
-
{selected ? (
|
|
159
|
+
{selected && !hideDetail ? (
|
|
151
160
|
<div className="rounded-md border p-3" data-testid="run-detail">
|
|
152
161
|
{current ? (
|
|
153
162
|
<>
|
|
154
|
-
<div className="flex items-center justify-between"><h3 className="font-
|
|
163
|
+
<div className="flex items-center justify-between"><h3 className="font-medium">{current.title}</h3><span className="text-sm">{current.status}{detail.polling ? " (updating)" : ""}</span></div>
|
|
155
164
|
<dl className="mt-2 grid grid-cols-2 gap-1 text-sm">
|
|
156
165
|
<dt className="text-muted-foreground">Current step</dt><dd>{current.current_step ?? "-"}</dd>
|
|
157
166
|
<dt className="text-muted-foreground">Next action</dt><dd>{current.next_action ?? "-"}</dd>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* 3. attachSkillsToAgent POST /voice/agents/{agent_id}/skills { action_ids } (write)
|
|
11
11
|
* 4. detachSkillFromAgent DELETE /voice/agents/{agent_id}/skills/{action_id} (destructive, ConfirmSpendDialog)
|
|
12
12
|
* 5. listModels GET /skills/models (read)
|
|
13
|
-
* 6. createSkill POST /skills { name, runtime_type: "llm", llm_config } (write)
|
|
13
|
+
* 6. createSkill POST /skills { name, runtime_type: "llm", llm_config: { model, prompt_template } } (write)
|
|
14
14
|
*
|
|
15
15
|
* Detaching never deletes the skill; it only stops this agent from using it.
|
|
16
16
|
*/
|
|
@@ -42,16 +42,16 @@ export function AgentSkillsPanel({ agentId, onChanged, className }: AgentSkillsP
|
|
|
42
42
|
const [attachingId, setAttachingId] = useState<string | null>(null);
|
|
43
43
|
useEffect(() => { setDetaching(null); setCreating(null); setAttachingId(null); }, [agentId]);
|
|
44
44
|
|
|
45
|
-
const attached = useG8Query((signal) => g8Get(`/voice/agents/${agentId}/skills`, undefined, { signal }), [agentId]);
|
|
45
|
+
const attached = useG8Query((signal) => g8Get(`/voice/agents/${encodeURIComponent(agentId)}/skills`, undefined, { signal }), [agentId]);
|
|
46
46
|
const all = useG8Query((signal) => g8Get("/skills", undefined, { signal }), []);
|
|
47
47
|
const models = useG8Query((signal) => g8Get("/skills/models", undefined, { signal }), [], { enabled: creating != null });
|
|
48
48
|
|
|
49
49
|
const changed = () => { void attached.refetch(); void all.refetch(); onChanged?.(); };
|
|
50
|
-
const attach = useG8Mutation((actionId: string) => g8Post(`/voice/agents/${agentId}/skills`, { action_ids: [actionId] }), { onSuccess: changed });
|
|
50
|
+
const attach = useG8Mutation((actionId: string) => g8Post(`/voice/agents/${encodeURIComponent(agentId)}/skills`, { action_ids: [actionId] }), { onSuccess: changed });
|
|
51
51
|
const attachOne = async (id: string) => { setAttachingId(id); await attach.mutate(id); setAttachingId(null); };
|
|
52
|
-
const detach = useG8Mutation((actionId: string) => g8Delete(`/voice/agents/${agentId}/skills/${actionId}`), { onSuccess: () => { setDetaching(null); changed(); } });
|
|
52
|
+
const detach = useG8Mutation((actionId: string) => g8Delete(`/voice/agents/${encodeURIComponent(agentId)}/skills/${encodeURIComponent(actionId)}`), { onSuccess: () => { setDetaching(null); changed(); } });
|
|
53
53
|
const create = useG8Mutation(
|
|
54
|
-
(v: NonNullable<typeof creating>) => g8Post("/skills", { name: v.name, description: v.description || undefined, runtime_type: "llm", llm_config: {
|
|
54
|
+
(v: NonNullable<typeof creating>) => g8Post("/skills", { name: v.name, description: v.description || undefined, runtime_type: "llm", llm_config: { model: v.model, prompt_template: v.prompt } }),
|
|
55
55
|
{ onSuccess: () => { setCreating(null); changed(); } },
|
|
56
56
|
);
|
|
57
57
|
|
|
@@ -63,7 +63,7 @@ export function AgentSkillsPanel({ agentId, onChanged, className }: AgentSkillsP
|
|
|
63
63
|
return (
|
|
64
64
|
<div className={`flex flex-col gap-5 ${className ?? ""}`}>
|
|
65
65
|
<div className="flex flex-col gap-2">
|
|
66
|
-
<h3 className="text-sm font-
|
|
66
|
+
<h3 className="text-sm font-medium">Attached skills</h3>
|
|
67
67
|
{attached.error ? <ApiError error={attached.error} title="Could not load the agent's skills" onRetry={() => void attached.refetch()} /> : null}
|
|
68
68
|
{attached.loading && mine.length === 0 ? <Skeleton className="h-10 w-full" /> : null}
|
|
69
69
|
<ul className="divide-y rounded-md border">
|
|
@@ -79,7 +79,7 @@ export function AgentSkillsPanel({ agentId, onChanged, className }: AgentSkillsP
|
|
|
79
79
|
|
|
80
80
|
<div className="flex flex-col gap-2">
|
|
81
81
|
<div className="flex items-center justify-between">
|
|
82
|
-
<h3 className="text-sm font-
|
|
82
|
+
<h3 className="text-sm font-medium">Available skills</h3>
|
|
83
83
|
<Button size="sm" variant="outline" onClick={() => setCreating({ name: "", description: "", prompt: "", model: "" })}>New skill</Button>
|
|
84
84
|
</div>
|
|
85
85
|
{all.error ? <ApiError error={all.error} title="Could not load skills" onRetry={() => void all.refetch()} /> : null}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgentTestHistory: the saved test cases of one agent and every run of them. Cases can be renamed or re-worded (only the fields you
|
|
5
|
+
* change are sent) and deleted (destructive, confirmed); the run history shows when each case ran and whether it passed.
|
|
6
|
+
* Running a test stays in AgentTestConsole; both are free (dry runs).
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. listAgentTestCases GET /voice/agents/{agent_id}/test-cases (read)
|
|
10
|
+
* 2. updateAgentTestCase PUT /voice/agents/{agent_id}/test-cases/{test_case_id} { changed fields } (write)
|
|
11
|
+
* 3. deleteAgentTestCase DELETE /voice/agents/{agent_id}/test-cases/{test_case_id} (destructive, ConfirmSpendDialog)
|
|
12
|
+
* 4. listAgentTestRuns GET /voice/agents/{agent_id}/test-runs?limit&test_case_id (read) "Load more" grows the limit
|
|
13
|
+
*/
|
|
14
|
+
import { useEffect, useState } from "react";
|
|
15
|
+
import { Button, Input, Label, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
16
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
17
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
18
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
19
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
20
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
21
|
+
import { StatusPill, 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 { g8Delete, g8Get, g8Put } from "@/g8/lib/g8-client";
|
|
25
|
+
import { listOf } from "../lib/agent-types";
|
|
26
|
+
import { LoadMore, useGrowingList } from "../lib/load-more";
|
|
27
|
+
|
|
28
|
+
export interface AgentTestHistoryProps {
|
|
29
|
+
agentId: string;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface Case { test_case_id?: string; id?: string; name: string; input_message?: string; channel?: string; assertion_type?: string; expected_output?: unknown }
|
|
34
|
+
interface TestRun { id?: string; run_id?: string; test_case_id?: string; assertion_result?: string; created_at?: string; mode?: string }
|
|
35
|
+
|
|
36
|
+
const caseId = (c: Case): string => c.test_case_id ?? c.id ?? "";
|
|
37
|
+
const CHANNELS = ["chat", "inbound", "outbound"] as const;
|
|
38
|
+
const ASSERTIONS = ["contains", "not_contains", "exact", "refuses", "expects_tool", "cites_collection", "llm_judge"] as const;
|
|
39
|
+
/** Only these compare against a single text; the others keep their expected_output / config untouched (edit them in graph8). */
|
|
40
|
+
const TEXT_TYPES: readonly string[] = ["contains", "not_contains", "exact"];
|
|
41
|
+
const expectedText = (v: unknown): string => (typeof v === "string" ? v : v == null ? "" : JSON.stringify(v));
|
|
42
|
+
|
|
43
|
+
/** Only the fields whose value differs from the loaded case. */
|
|
44
|
+
export function changedFields(before: Case, draft: { name: string; input_message: string; channel: string; assertion_type: string; expected_output: string }): Record<string, string> {
|
|
45
|
+
const base = { name: before.name, input_message: before.input_message ?? "", channel: before.channel ?? "chat", assertion_type: before.assertion_type ?? "contains", expected_output: expectedText(before.expected_output) };
|
|
46
|
+
return Object.fromEntries(Object.entries(draft).filter(([k, v]) => v !== (base as Record<string, string>)[k] && !(k === "expected_output" && !TEXT_TYPES.includes(draft.assertion_type))));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function EditCase({ agentId, row, onClose, onSaved }: { agentId: string; row: Case | null; onClose: () => void; onSaved: () => void }) {
|
|
50
|
+
const [d, setD] = useState({ name: "", input_message: "", channel: "chat", assertion_type: "contains", expected_output: "" });
|
|
51
|
+
useEffect(() => { if (row) setD({ name: row.name, input_message: row.input_message ?? "", channel: row.channel ?? "chat", assertion_type: row.assertion_type ?? "contains", expected_output: expectedText(row.expected_output) }); }, [row]);
|
|
52
|
+
const diff = row ? changedFields(row, d) : {};
|
|
53
|
+
const save = useG8Mutation(() => g8Put(`/voice/agents/${encodeURIComponent(agentId)}/test-cases/${encodeURIComponent(caseId(row as Case))}`, diff), { onSuccess: () => { onSaved(); onClose(); } });
|
|
54
|
+
const needsText = TEXT_TYPES.includes(d.assertion_type);
|
|
55
|
+
const set = (patch: Partial<typeof d>) => setD((x) => ({ ...x, ...patch }));
|
|
56
|
+
return (
|
|
57
|
+
<FormSheet open={row != null} onOpenChange={(o) => !o && onClose()} title="Edit test case" description="Only the fields you change are saved."
|
|
58
|
+
footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button loading={save.loading} disabled={Object.keys(diff).length === 0 || !d.name.trim() || !d.input_message.trim() || (needsText && !d.expected_output.trim())} onClick={() => void save.mutate(undefined)}>Save changes</Button></>}>
|
|
59
|
+
<div className="flex flex-col gap-3">
|
|
60
|
+
<div className="flex flex-col gap-1"><Label htmlFor="tc-name">Name</Label><Input id="tc-name" value={d.name} onChange={(e) => set({ name: e.target.value })} /></div>
|
|
61
|
+
<div className="flex flex-col gap-1"><Label htmlFor="tc-input">What the person says</Label><Textarea id="tc-input" rows={3} value={d.input_message} onChange={(e) => set({ input_message: e.target.value })} /></div>
|
|
62
|
+
<div className="grid grid-cols-2 gap-3">
|
|
63
|
+
<div className="flex flex-col gap-1"><Label htmlFor="tc-channel">Channel</Label>
|
|
64
|
+
<select id="tc-channel" className="h-9 rounded-md border bg-background px-2 text-sm" value={d.channel} onChange={(e) => set({ channel: e.target.value })}>{CHANNELS.map((c) => <option key={c} value={c}>{c}</option>)}</select></div>
|
|
65
|
+
<div className="flex flex-col gap-1"><Label htmlFor="tc-assertion">The agent</Label>
|
|
66
|
+
<select id="tc-assertion" className="h-9 rounded-md border bg-background px-2 text-sm" value={d.assertion_type} onChange={(e) => set({ assertion_type: e.target.value })}>{ASSERTIONS.map((c) => <option key={c} value={c}>{c.replace("_", " ")}</option>)}</select></div>
|
|
67
|
+
</div>
|
|
68
|
+
{needsText ? <div className="flex flex-col gap-1"><Label htmlFor="tc-expected">Text to check</Label><Input id="tc-expected" value={d.expected_output} onChange={(e) => set({ expected_output: e.target.value })} /></div> : null}
|
|
69
|
+
{save.error ? <ApiError error={save.error} title="Could not save the case" /> : null}
|
|
70
|
+
</div>
|
|
71
|
+
</FormSheet>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function AgentTestHistory({ agentId, className }: AgentTestHistoryProps) {
|
|
76
|
+
const [editing, setEditing] = useState<Case | null>(null);
|
|
77
|
+
const [deleting, setDeleting] = useState<Case | null>(null);
|
|
78
|
+
const [only, setOnly] = useState("");
|
|
79
|
+
useEffect(() => { setEditing(null); setDeleting(null); setOnly(""); }, [agentId]);
|
|
80
|
+
const cases = useG8Query((signal) => g8Get(`/voice/agents/${encodeURIComponent(agentId)}/test-cases`, undefined, { signal }), [agentId]);
|
|
81
|
+
const runs = useGrowingList<TestRun>(async (limit, signal) => listOf<TestRun>(await g8Get(`/voice/agents/${encodeURIComponent(agentId)}/test-runs`, { limit, test_case_id: only || undefined }, { signal }), "items", "runs"), [agentId, only], { pageSize: 25, max: 200 });
|
|
82
|
+
const remove = useG8Mutation((id: string) => g8Delete(`/voice/agents/${encodeURIComponent(agentId)}/test-cases/${encodeURIComponent(id)}`), { onSuccess: (_r, id) => { setDeleting(null); if (only === id) setOnly(""); void cases.refetch(); runs.reload(); } });
|
|
83
|
+
const rows = listOf<Case>(cases.data, "items", "test_cases", "cases");
|
|
84
|
+
const nameOf = (id?: string) => rows.find((c) => caseId(c) === id)?.name ?? "Ad-hoc message";
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className={`flex flex-col gap-4 ${className ?? ""}`}>
|
|
88
|
+
<SectionCard title="Saved test cases" description="Each case is a message plus what the agent must do." flush>
|
|
89
|
+
{cases.error ? <div className="p-3"><ApiError error={cases.error} title="Could not load the test cases" onRetry={() => void cases.refetch()} /></div> : null}
|
|
90
|
+
{cases.loading && !cases.data ? <Skeleton className="m-3 h-10" /> : null}
|
|
91
|
+
<ul className="divide-y">
|
|
92
|
+
{rows.map((c) => (
|
|
93
|
+
<li key={caseId(c)} data-testid={`case-${caseId(c)}`} className="flex flex-wrap items-center justify-between gap-2 px-4 py-2 text-sm">
|
|
94
|
+
<span><span className="font-medium">{c.name}</span><span className="block text-xs text-muted-foreground">{c.assertion_type ?? "contains"}: {expectedText(c.expected_output) || "no text"}</span></span>
|
|
95
|
+
<span className="flex gap-1"><Button size="sm" variant="ghost" onClick={() => setOnly(only === caseId(c) ? "" : caseId(c))}>{only === caseId(c) ? "All runs" : "Runs"}</Button><Button size="sm" variant="outline" onClick={() => setEditing(c)}>Edit</Button><Button size="sm" variant="ghost" onClick={() => setDeleting(c)}>Delete</Button></span>
|
|
96
|
+
</li>
|
|
97
|
+
))}
|
|
98
|
+
</ul>
|
|
99
|
+
{!cases.loading && !cases.error && rows.length === 0 ? <EmptyState compact title="No saved cases" description="Save a dry run from the Test tab to keep it here." /> : null}
|
|
100
|
+
</SectionCard>
|
|
101
|
+
<SectionCard title="Run history" description={only ? `Runs of ${nameOf(only)}` : "Every test run, newest first"} flush>
|
|
102
|
+
{runs.error ? <div className="p-3"><ApiError error={runs.error} title="Could not load the run history" onRetry={runs.reload} /></div> : null}
|
|
103
|
+
{runs.loading ? <Skeleton className="m-3 h-10" /> : null}
|
|
104
|
+
<ul className="divide-y">
|
|
105
|
+
{runs.items.map((r, i) => (
|
|
106
|
+
<li key={r.id ?? r.run_id ?? i} className="flex items-center justify-between gap-2 px-4 py-2 text-sm">
|
|
107
|
+
<span>{nameOf(r.test_case_id)}</span>
|
|
108
|
+
<span className="flex items-center gap-2"><StatusPill tone={toneForStatus(r.assertion_result === "pass" ? "completed" : r.assertion_result === "fail" || r.assertion_result === "error" ? "failed" : "pending")} dot>{r.assertion_result ?? "not judged"}</StatusPill><span className="text-xs text-muted-foreground">{r.created_at ? new Date(r.created_at).toLocaleString() : ""}</span></span>
|
|
109
|
+
</li>
|
|
110
|
+
))}
|
|
111
|
+
</ul>
|
|
112
|
+
{!runs.loading && !runs.error && runs.items.length === 0 ? <EmptyState compact title="No runs yet" description="Run a dry run or the whole sweep from the Test tab." /> : null}
|
|
113
|
+
<LoadMore list={runs} noun="runs" />
|
|
114
|
+
</SectionCard>
|
|
115
|
+
{remove.error ? <ApiError error={remove.error} title="Could not delete the case" /> : null}
|
|
116
|
+
<EditCase agentId={agentId} row={editing} onClose={() => setEditing(null)} onSaved={() => { void cases.refetch(); }} />
|
|
117
|
+
<ConfirmSpendDialog open={deleting != null} tier="destructive" title={`Delete "${deleting?.name ?? "case"}"`} description="The case and its place in the regression sweep are removed. Past runs stay in the history." affected={1}
|
|
118
|
+
confirmLabel="Delete" confirming={remove.loading} onConfirm={() => deleting && void remove.mutate(caseId(deleting))} onCancel={() => setDeleting(null)} />
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgentsScreen: the finished agents workspace. Two tabs.
|
|
5
|
+
* Agents: stat strip (agents, active, inactive), search and status filter, the agent list, and for the open agent
|
|
6
|
+
* (deep link ?id=) a detail panel with Configure (persona, voice, knowledge, skills, active switch, delete),
|
|
7
|
+
* Test (free dry runs, saved cases, the activation gate, run a skill live) and Runs.
|
|
8
|
+
* Runs: every agent run in the workspace as a list; opening one (deep link ?run=) shows the full run (AgentRunDetail:
|
|
9
|
+
* status, step, next action, credits, times, its approvals to approve or reject, cancel). Start a run from here.
|
|
10
|
+
* "New agent" opens a form sheet (name, role, what it is for, personality); the agent starts inactive and opens
|
|
11
|
+
* in the detail panel so voice, knowledge and skills can be added next.
|
|
12
|
+
*
|
|
13
|
+
* More tabs: Approvals (the inbox for everything agents ask permission for), Skill library (every skill, create/edit/delete),
|
|
14
|
+
* Company knowledge (the entries agents draw on: browse, search, author, verify, deprecate), Operators (autonomous
|
|
15
|
+
* monitors: on/off and the trust dial) and Assistant (sessions with the graph8 assistant). The open agent also has
|
|
16
|
+
* Conversations (its chat history and a message box), Activity (calls, skill runs, skill performance) and Test history
|
|
17
|
+
* (saved cases, edit/delete, every run). The header has From a template and Receptionist; an open agent can be
|
|
18
|
+
* duplicated or saved as a template.
|
|
19
|
+
*
|
|
20
|
+
* Composes: AgentBuilder (hideList), AgentTestConsole, AgentRuns, AgentApprovals, SkillLibrary, KnowledgeBrowser,
|
|
21
|
+
* AgentOperators, AssistantSessions, AgentConversations, AgentActivity, AgentTestHistory. Billable, external and
|
|
22
|
+
* destructive actions (document upload, run a skill, start/cancel a run, approve, send a chat message, turn an operator
|
|
23
|
+
* on, delete anything) stay inside those parts and are confirmed there with ConfirmSpendDialog.
|
|
24
|
+
*
|
|
25
|
+
* SDK operations of its own:
|
|
26
|
+
* 1. GET /voice/agents?search&page_size=100 (voice.listAgents) read
|
|
27
|
+
* 2. POST /voice/agents { role, persona, identity, entity_type, agent_status: "inactive" } (voice.createAgent) write
|
|
28
|
+
* 3. PUT /voice/agents/{agent_id} { agent_status } (voice.updateAgent) write activate / deactivate from the panel
|
|
29
|
+
* Slot props: renderRowActions, renderDetailActions, extraDetailTabs, toolbarActions.
|
|
30
|
+
*/
|
|
31
|
+
import { useRef, useState, type ReactNode } from "react";
|
|
32
|
+
import { Bot, Plus } from "lucide-react";
|
|
33
|
+
import { Button, Input, Label, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
34
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
35
|
+
import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
|
|
36
|
+
import { byPage, hasNextOf } from "@/g8/lib/paging";
|
|
37
|
+
import { LoadMore } from "@/g8/components/paging-notices";
|
|
38
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
39
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
40
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
41
|
+
import { g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
|
|
42
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
43
|
+
import { EntityAvatar } from "@/g8/components/screen/entity-avatar";
|
|
44
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
45
|
+
import { ScreenLayout } from "@/g8/components/screen/screen-layout";
|
|
46
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
47
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
48
|
+
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
49
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
50
|
+
import { DEFAULT_PERSONA, bodyOf, listOf } from "../lib/agent-types";
|
|
51
|
+
import type { AgentSummary } from "../lib/agent-types";
|
|
52
|
+
import { AgentActivity } from "./agent-activity";
|
|
53
|
+
import { AgentApprovals } from "./agent-approvals";
|
|
54
|
+
import { AgentBuilder } from "./agent-builder";
|
|
55
|
+
import { AgentConversations } from "./agent-conversations";
|
|
56
|
+
import { AgentOperators } from "./agent-operators";
|
|
57
|
+
import { AgentRunDetail } from "./agent-run-detail";
|
|
58
|
+
import { AgentRuns } from "./agent-runs";
|
|
59
|
+
import { AgentTestConsole } from "./agent-test-console";
|
|
60
|
+
import { AgentTestHistory } from "./agent-test-history";
|
|
61
|
+
import { AssistantSessions } from "./assistant-sessions";
|
|
62
|
+
import { DuplicateAgentButton } from "./duplicate-agent-button";
|
|
63
|
+
import { FromTemplateButton } from "./from-template-button";
|
|
64
|
+
import { KnowledgeBrowser } from "./knowledge-browser";
|
|
65
|
+
import { ReceptionistButton } from "./receptionist-button";
|
|
66
|
+
import { SaveAsTemplateButton } from "./save-as-template-button";
|
|
67
|
+
import { SkillLibrary } from "./skill-library";
|
|
68
|
+
|
|
69
|
+
export interface AgentsScreenProps {
|
|
70
|
+
/** Row-level extra actions (for example "Use in a campaign"). */
|
|
71
|
+
renderRowActions?: (agent: AgentSummary) => ReactNode;
|
|
72
|
+
renderDetailActions?: (agent: AgentSummary) => ReactNode;
|
|
73
|
+
extraDetailTabs?: Array<{ id: string; label: string; render: (agent: AgentSummary) => ReactNode }>;
|
|
74
|
+
toolbarActions?: ReactNode;
|
|
75
|
+
className?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const SCREEN_TABS = [
|
|
79
|
+
{ id: "agents", label: "Agents" }, { id: "runs", label: "Runs" }, { id: "approvals", label: "Approvals" }, { id: "skills", label: "Skill library" },
|
|
80
|
+
{ id: "knowledge", label: "Company knowledge" }, { id: "operators", label: "Operators" }, { id: "assistant", label: "Assistant" },
|
|
81
|
+
];
|
|
82
|
+
const TAB_IDS = SCREEN_TABS.map((t) => t.id);
|
|
83
|
+
const FILTERS = [{ id: "", label: "All" }, { id: "active", label: "Active" }, { id: "inactive", label: "Inactive" }];
|
|
84
|
+
|
|
85
|
+
interface NewAgent { name: string; role: string; description: string; persona: string }
|
|
86
|
+
const blank = (): NewAgent => ({ name: "", role: "SDR", description: "", persona: "" });
|
|
87
|
+
const tone = (status: string) => (status === "active" ? "positive" : status === "archived" ? "neutral" : "warning");
|
|
88
|
+
|
|
89
|
+
/** Create sheet. Keeps the id of an agent it already created so a retry after a later failure never makes a twin. */
|
|
90
|
+
function NewAgentSheet({ open, onOpenChange, onCreated }: { open: boolean; onOpenChange: (o: boolean) => void; onCreated: (id: string) => void }) {
|
|
91
|
+
const [form, setForm] = useState<NewAgent>(blank());
|
|
92
|
+
const created = useRef<string | null>(null);
|
|
93
|
+
const create = useG8Mutation(async () => {
|
|
94
|
+
if (created.current) return created.current;
|
|
95
|
+
const res = await g8Post("/voice/agents", {
|
|
96
|
+
entity_type: "agent", role: form.role.trim(), agent_status: "inactive", use_company_knowledge: true, identity: {},
|
|
97
|
+
persona: { ...DEFAULT_PERSONA, agent_name: form.name.trim(), description: form.description.trim(), persona: form.persona.trim() },
|
|
98
|
+
});
|
|
99
|
+
const id = bodyOf<{ agent?: { agent_id?: string } }>(res).agent?.agent_id ?? "";
|
|
100
|
+
created.current = id || null;
|
|
101
|
+
return id;
|
|
102
|
+
}, { onSuccess: (id) => { created.current = null; setForm(blank()); onOpenChange(false); if (id) onCreated(id); } });
|
|
103
|
+
const set = (patch: Partial<NewAgent>) => setForm((f) => ({ ...f, ...patch }));
|
|
104
|
+
const valid = Boolean(form.name.trim() && form.role.trim() && form.description.trim() && form.persona.trim());
|
|
105
|
+
return (
|
|
106
|
+
<FormSheet open={open} onOpenChange={onOpenChange} title="New agent" description="It starts inactive. Add a voice, knowledge and skills, test it, then turn it on."
|
|
107
|
+
footer={<><Button variant="ghost" onClick={() => onOpenChange(false)}>Cancel</Button><Button loading={create.loading} disabled={!valid} onClick={() => void create.mutate(undefined)}>Create agent</Button></>}>
|
|
108
|
+
<div className="flex flex-col gap-3">
|
|
109
|
+
<div className="flex flex-col gap-1"><Label htmlFor="na-name">Name</Label><Input id="na-name" value={form.name} onChange={(e) => set({ name: e.target.value })} /></div>
|
|
110
|
+
<div className="flex flex-col gap-1"><Label htmlFor="na-role">Role</Label><Input id="na-role" value={form.role} onChange={(e) => set({ role: e.target.value })} /></div>
|
|
111
|
+
<div className="flex flex-col gap-1"><Label htmlFor="na-desc">What it is for</Label><Input id="na-desc" value={form.description} onChange={(e) => set({ description: e.target.value })} /></div>
|
|
112
|
+
<div className="flex flex-col gap-1"><Label htmlFor="na-persona">Personality and behaviour</Label><Textarea id="na-persona" rows={5} value={form.persona} onChange={(e) => set({ persona: e.target.value })} /></div>
|
|
113
|
+
{create.error ? <ApiError error={create.error} title="Could not create the agent" /> : null}
|
|
114
|
+
</div>
|
|
115
|
+
</FormSheet>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function AgentRow({ agent, selected, onOpen, extra }: { agent: AgentSummary; selected: boolean; onOpen: () => void; extra?: ReactNode }) {
|
|
120
|
+
return (
|
|
121
|
+
<li className={`flex items-center gap-3 border-b px-3 py-2 ${selected ? "bg-accent" : ""}`}>
|
|
122
|
+
<button type="button" data-testid={`agent-${agent.agent_id}`} onClick={onOpen} className="flex min-w-0 flex-1 items-center gap-3 text-left">
|
|
123
|
+
<EntityAvatar name={agent.agent_name} kind="person" size="md" />
|
|
124
|
+
<span className="min-w-0 flex-1">
|
|
125
|
+
<span className="block truncate text-sm font-medium">{agent.agent_name}</span>
|
|
126
|
+
<span className="block truncate text-xs text-muted-foreground">{agent.role}{agent.description ? ` - ${agent.description}` : ""}</span>
|
|
127
|
+
</span>
|
|
128
|
+
<StatusPill tone={tone(agent.agent_status)} dot>{agent.agent_status}</StatusPill>
|
|
129
|
+
</button>
|
|
130
|
+
{extra}
|
|
131
|
+
</li>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Agents per read; "Load more" appends the next page (the list used to stop silently at 100). */
|
|
136
|
+
const AGENT_PAGE = 100;
|
|
137
|
+
|
|
138
|
+
export function AgentsScreen({ renderRowActions, renderDetailActions, extraDetailTabs, toolbarActions, className }: AgentsScreenProps) {
|
|
139
|
+
const [tab, setTab] = useUrlState("tab", "agents");
|
|
140
|
+
const [selected, setSelected] = useUrlState("id", "");
|
|
141
|
+
const [search, setSearch] = useState("");
|
|
142
|
+
const [filter, setFilter] = useState("");
|
|
143
|
+
const [creating, setCreating] = useState(false);
|
|
144
|
+
const [run, setRun] = useUrlState("run", "");
|
|
145
|
+
const [view, setView] = useUrlState("view", "");
|
|
146
|
+
const closeAgent = () => { setSelected(""); setView(""); };
|
|
147
|
+
const closeRun = () => { setRun(""); setView(""); };
|
|
148
|
+
const expand = { expanded: view === "full", onExpandedChange: (e: boolean) => setView(e ? "full" : "") };
|
|
149
|
+
const [runsKey, setRunsKey] = useState(0);
|
|
150
|
+
const q = search.trim();
|
|
151
|
+
|
|
152
|
+
const agents = useLoadMoreList(byPage<AgentSummary>(AGENT_PAGE, async (page, signal) => {
|
|
153
|
+
const env = await g8Get("/voice/agents", { search: q || undefined, page, page_size: AGENT_PAGE }, { signal });
|
|
154
|
+
const rows = listOf<AgentSummary>(env, "agents");
|
|
155
|
+
return { rows, hasNext: hasNextOf(env, AGENT_PAGE, rows.length) };
|
|
156
|
+
}), [q]);
|
|
157
|
+
const all = agents.data ?? [];
|
|
158
|
+
const rows = filter ? all.filter((a) => a.agent_status === filter) : all;
|
|
159
|
+
// Resolve the open agent from the list first, else by id, so a search, a filter or a cold deep link never hides it.
|
|
160
|
+
const inList = all.find((a) => a.agent_id === selected);
|
|
161
|
+
const byId = useG8Query((signal) => g8Get(`/voice/agents/${encodeURIComponent(selected)}`, undefined, { signal }), [selected], { enabled: Boolean(selected) && !inList && !agents.loading });
|
|
162
|
+
const fetched = bodyOf<{ agent?: { agent_id: string; agent_status: string; role: string; persona?: { agent_name?: string; description?: string } } }>(byId.data).agent;
|
|
163
|
+
const open: AgentSummary | undefined = inList ?? (fetched ? { agent_id: fetched.agent_id, agent_name: fetched.persona?.agent_name ?? "Agent", role: fetched.role, agent_status: fetched.agent_status, description: fetched.persona?.description } : undefined);
|
|
164
|
+
const active = all.filter((a) => a.agent_status === "active").length;
|
|
165
|
+
|
|
166
|
+
const toggle = useG8Mutation((a: AgentSummary) => g8Put(`/voice/agents/${encodeURIComponent(a.agent_id)}`, { agent_status: a.agent_status === "active" ? "inactive" : "active" }), { onSuccess: () => { void agents.refetch(); void byId.refetch(); } });
|
|
167
|
+
const current = TAB_IDS.includes(tab) ? tab : "agents";
|
|
168
|
+
const openRun = (id: string) => { setRun(id); setTab("runs"); };
|
|
169
|
+
|
|
170
|
+
const detail = open ? (
|
|
171
|
+
<DetailPanel title={open.agent_name} subtitle={open.role} avatar={<EntityAvatar name={open.agent_name} kind="person" size="lg" />}
|
|
172
|
+
badges={<StatusPill tone={tone(open.agent_status)} dot>{open.agent_status}</StatusPill>} onClose={closeAgent} onExpand={() => setView("full")}
|
|
173
|
+
actions={<>
|
|
174
|
+
<Button size="sm" variant="outline" loading={toggle.loading} onClick={() => void toggle.mutate(open)}>{open.agent_status === "active" ? "Deactivate" : "Activate"}</Button>
|
|
175
|
+
<DuplicateAgentButton agent={open} onDuplicated={(id) => { void agents.refetch(); setSelected(id); }} />
|
|
176
|
+
<SaveAsTemplateButton agent={open} />
|
|
177
|
+
{renderDetailActions?.(open)}
|
|
178
|
+
</>}
|
|
179
|
+
tabs={[
|
|
180
|
+
{ id: "configure", label: "Configure", content: <AgentBuilder key={`${open.agent_id}:${open.agent_status}`} hideList agentId={open.agent_id} onSelect={(id) => setSelected(id ?? "")} onChanged={() => void agents.refetch()} /> },
|
|
181
|
+
{ id: "test", label: "Test", content: <AgentTestConsole agentId={open.agent_id} /> },
|
|
182
|
+
{ id: "runs", label: "Runs", content: <AgentRuns agentId={open.agent_id} /> },
|
|
183
|
+
{ id: "conversations", label: "Conversations", content: <AgentConversations agentId={open.agent_id} /> },
|
|
184
|
+
{ id: "activity", label: "Activity", content: <AgentActivity agentId={open.agent_id} /> },
|
|
185
|
+
{ id: "test-history", label: "Test history", content: <AgentTestHistory agentId={open.agent_id} /> },
|
|
186
|
+
...(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(open) })),
|
|
187
|
+
]}>
|
|
188
|
+
{toggle.error ? <ApiError error={toggle.error} title="Could not change the status" /> : null}
|
|
189
|
+
</DetailPanel>
|
|
190
|
+
) : selected && !agents.loading && !byId.loading && (byId.error || byId.data) ? <EmptyState title="Agent not found" description="It may have been deleted." /> : selected ? <Skeleton className="m-4 h-40" aria-label="Loading agent" /> : null;
|
|
191
|
+
|
|
192
|
+
const list = (
|
|
193
|
+
<div className="flex flex-col gap-3">
|
|
194
|
+
<Toolbar search={{ value: search, onChange: setSearch, placeholder: "Search agents" }} actions={toolbarActions}
|
|
195
|
+
filters={<div role="group" aria-label="Status" className="flex gap-1">{FILTERS.map((f) => <Button key={f.id || "all"} size="sm" variant={filter === f.id ? "default" : "outline"} onClick={() => setFilter(f.id)}>{f.label}</Button>)}</div>} />
|
|
196
|
+
{agents.error ? <ApiError error={agents.error} title="Could not load agents" onRetry={() => void agents.refetch()} /> : null}
|
|
197
|
+
{agents.loading && all.length === 0 ? <div className="space-y-2">{[0, 1, 2, 3].map((i) => <Skeleton key={i} className="h-14 w-full" />)}</div> : null}
|
|
198
|
+
<ul className="rounded-md border">
|
|
199
|
+
{rows.map((a) => <AgentRow key={a.agent_id} agent={a} selected={a.agent_id === selected} onOpen={() => setSelected(a.agent_id)} extra={renderRowActions?.(a)} />)}
|
|
200
|
+
</ul>
|
|
201
|
+
<LoadMore shown={all.length} noun="agents" hasMore={agents.hasMore} loading={agents.loadingMore} onLoadMore={agents.loadMore} />
|
|
202
|
+
{!agents.loading && !agents.error && rows.length === 0 ? (
|
|
203
|
+
<EmptyState title={all.length ? "No agents match" : "No agents yet"} description="An agent has a persona, a voice, knowledge and skills."
|
|
204
|
+
action={all.length ? undefined : <Button onClick={() => setCreating(true)}>New agent</Button>} />
|
|
205
|
+
) : null}
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<ScreenLayout className={className} title="Agents" description="Build AI agents, prove they work before they talk to anyone, and follow the work they do."
|
|
211
|
+
icon={<Bot className="size-5" />} tabs={SCREEN_TABS} activeTab={current} onTabChange={setTab}
|
|
212
|
+
actions={<><FromTemplateButton onCreated={(id) => { void agents.refetch(); setSelected(id); setTab("agents"); }} /><ReceptionistButton /><Button onClick={() => setCreating(true)}><Plus className="mr-1 size-4" />New agent</Button></>}
|
|
213
|
+
stats={current === "agents" ? [
|
|
214
|
+
{ label: "Agents", value: agents.hasMore ? `${all.length}+` : String(all.length) },
|
|
215
|
+
{ label: "Active", value: String(active), tone: "positive" },
|
|
216
|
+
{ label: "Inactive", value: String(all.filter((a) => a.agent_status === "inactive").length), tone: "warning" },
|
|
217
|
+
] : undefined}>
|
|
218
|
+
{current === "approvals" ? <AgentApprovals onOpenRun={openRun} /> : current === "skills" ? <SkillLibrary /> : current === "knowledge" ? <KnowledgeBrowser />
|
|
219
|
+
: current === "operators" ? <AgentOperators /> : current === "assistant" ? <AssistantSessions onOpenRun={openRun} /> : current === "runs" ? (
|
|
220
|
+
<SplitView list={<AgentRuns hideDetail selectedId={run || null} onSelectRun={(id) => setRun(id ?? "")} refreshKey={runsKey} />} detailOpen={Boolean(run)} onCloseDetail={closeRun} {...expand} detailWidth="lg"
|
|
221
|
+
detail={run ? <DetailPanel title="Run" subtitle={run} onClose={closeRun} onExpand={() => setView("full")} tabs={[{ id: "run", label: "Run", content: <AgentRunDetail key={run} runId={run} onChanged={() => setRunsKey((k) => k + 1)} /> }]} /> : null} />
|
|
222
|
+
) : <SplitView list={list} detail={detail} detailOpen={Boolean(selected)} onCloseDetail={closeAgent} {...expand} detailWidth="xl" />}
|
|
223
|
+
<NewAgentSheet open={creating} onOpenChange={setCreating} onCreated={(id) => { setSelected(id); setTab("agents"); void agents.refetch(); }} />
|
|
224
|
+
</ScreenLayout>
|
|
225
|
+
);
|
|
226
|
+
}
|