@graph8/sdk 0.370.0 → 0.374.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 +22 -3
- package/blocks/_foundation/README.md +61 -0
- package/blocks/_foundation/block.json +57 -0
- package/blocks/_foundation/components/auth-gate.tsx +363 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- 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 +1 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/auth.ts +815 -0
- package/blocks/agents/README.md +81 -4
- package/blocks/agents/block.json +311 -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 +17 -3
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +2 -2
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +20 -3
- package/blocks/agents/components/agent-runs.tsx +3 -3
- 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 +53 -11
- 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/components/analytics-screen.tsx +16 -3
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/report-builder.tsx +15 -2
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +27 -0
- package/blocks/appointments/components/booking-page.tsx +2 -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 +29 -13
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/campaigns/README.md +1 -1
- package/blocks/campaigns/block.json +34 -1
- package/blocks/campaigns/components/campaign-assets.tsx +1 -1
- package/blocks/campaigns/components/campaign-launch-step.tsx +16 -11
- package/blocks/campaigns/components/campaign-links.tsx +180 -0
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +10 -5
- package/blocks/campaigns/components/icp-detail.tsx +11 -2
- package/blocks/campaigns/components/icp-editor.tsx +8 -3
- package/blocks/campaigns/components/persona-detail.tsx +11 -2
- package/blocks/campaigns/components/persona-editor.tsx +6 -1
- package/blocks/campaigns/lib/campaigns-utils.ts +4 -4
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- 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 +165 -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/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +1 -1
- package/blocks/copilot/components/copilot-team-chat.tsx +6 -1
- package/blocks/crm/README.md +25 -0
- package/blocks/crm/block.json +257 -9
- package/blocks/crm/components/bulk-update.tsx +119 -0
- package/blocks/crm/components/companies-screen.tsx +23 -9
- package/blocks/crm/components/company-detail.tsx +2 -2
- package/blocks/crm/components/contact-detail.tsx +4 -4
- package/blocks/crm/components/contact-documents-tab.tsx +144 -0
- package/blocks/crm/components/contact-insights-tab.tsx +118 -0
- package/blocks/crm/components/contact-lookup-link.tsx +85 -0
- package/blocks/crm/components/contact-row-actions.tsx +43 -0
- package/blocks/crm/components/contact-tag-chips.tsx +113 -0
- package/blocks/crm/components/contacts-screen.tsx +77 -18
- package/blocks/crm/components/dedupe-review.tsx +6 -1
- package/blocks/crm/components/import-wizard.tsx +3 -7
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/owner-assign.tsx +132 -0
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +1 -1
- package/blocks/crm/components/record-open-links.tsx +79 -0
- package/blocks/crm/components/tag-assign.tsx +112 -0
- package/blocks/crm/components/tag-manager.tsx +178 -0
- package/blocks/crm/components/tasks-screen.tsx +35 -15
- package/blocks/crm/components/unlock-info-button.tsx +82 -0
- package/blocks/crm/components/verify-email-button.tsx +93 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-contact-tags.ts +29 -0
- package/blocks/crm/hooks/use-page-query.ts +3 -1
- package/blocks/crm/lib/crm-types.ts +14 -0
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/record-actions.ts +102 -0
- package/blocks/deals/README.md +25 -6
- package/blocks/deals/block.json +168 -2
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/create-deal-button.tsx +109 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +7 -5
- 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 +30 -9
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +57 -22
- 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 +1 -1
- package/blocks/deals/hooks/use-pipeline-deals.ts +17 -4
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +14 -5
- package/blocks/dialer/block.json +17 -2
- package/blocks/dialer/components/call-contact-button.tsx +26 -9
- package/blocks/dialer/components/call-log-screen.tsx +10 -2
- package/blocks/dialer/components/call-log.tsx +16 -4
- package/blocks/dialer/components/call-queue.tsx +3 -3
- package/blocks/dialer/components/dialer-screen.tsx +69 -27
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +58 -11
- package/blocks/dialer/components/softphone.tsx +1 -1
- package/blocks/documents/README.md +86 -0
- package/blocks/documents/block.json +378 -0
- package/blocks/documents/components/account-bar.tsx +48 -0
- package/blocks/documents/components/correspondence-form.tsx +95 -0
- package/blocks/documents/components/correspondence-import.tsx +106 -0
- package/blocks/documents/components/correspondence-panel.tsx +191 -0
- package/blocks/documents/components/documents-screen.tsx +76 -0
- package/blocks/documents/components/library-document-detail.tsx +175 -0
- package/blocks/documents/components/library-document-form.tsx +105 -0
- package/blocks/documents/components/library-folder-form.tsx +74 -0
- package/blocks/documents/components/library-panel.tsx +235 -0
- package/blocks/documents/components/library-upload.tsx +160 -0
- package/blocks/documents/components/question-row.tsx +130 -0
- package/blocks/documents/components/questionnaire-detail.tsx +197 -0
- package/blocks/documents/components/questionnaires-panel.tsx +173 -0
- package/blocks/documents/components/record-documents-tab.tsx +109 -0
- package/blocks/documents/lib/documents-types.ts +252 -0
- package/blocks/documents/lib/download.ts +64 -0
- package/blocks/documents/recipes/follow-a-questionnaire.ts +26 -0
- package/blocks/documents/recipes/log-account-correspondence.ts +27 -0
- package/blocks/documents/recipes/organize-the-library.ts +25 -0
- package/blocks/email-signatures/README.md +123 -0
- package/blocks/email-signatures/block.json +341 -0
- package/blocks/email-signatures/components/email-signatures-screen.tsx +166 -0
- package/blocks/email-signatures/components/signature-assign-template.tsx +119 -0
- package/blocks/email-signatures/components/signature-block-editor.tsx +87 -0
- package/blocks/email-signatures/components/signature-campaign-form.tsx +165 -0
- package/blocks/email-signatures/components/signature-campaigns.tsx +75 -0
- package/blocks/email-signatures/components/signature-identity-form.tsx +131 -0
- package/blocks/email-signatures/components/signature-mailboxes.tsx +195 -0
- package/blocks/email-signatures/components/signature-policy.tsx +130 -0
- package/blocks/email-signatures/components/signature-preview.tsx +119 -0
- package/blocks/email-signatures/components/signature-report.tsx +77 -0
- package/blocks/email-signatures/components/signature-template-details.tsx +112 -0
- package/blocks/email-signatures/components/signature-template-form.tsx +109 -0
- package/blocks/email-signatures/hooks/use-signatures-data.ts +93 -0
- package/blocks/email-signatures/lib/signatures-types.ts +392 -0
- package/blocks/email-signatures/recipes/assign-a-signature-to-a-new-hire.ts +32 -0
- package/blocks/email-signatures/recipes/create-a-template-and-make-it-default.ts +35 -0
- package/blocks/email-signatures/recipes/signature-engagement-report.ts +16 -0
- package/blocks/enrichment/README.md +38 -10
- package/blocks/enrichment/block.json +164 -12
- package/blocks/enrichment/components/auto-enrichment-configs.tsx +134 -0
- package/blocks/enrichment/components/auto-enrichment-settings.tsx +82 -0
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +79 -30
- package/blocks/enrichment/components/email-verify-badge.tsx +3 -3
- package/blocks/enrichment/components/enrich-button.tsx +1 -1
- package/blocks/enrichment/components/enrichment-activity.tsx +93 -0
- package/blocks/enrichment/components/enrichment-job-detail.tsx +18 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +103 -19
- package/blocks/enrichment/components/list-pipelines.tsx +97 -0
- package/blocks/enrichment/components/pipeline-detail.tsx +214 -0
- package/blocks/enrichment/components/pipeline-editor.tsx +144 -0
- package/blocks/enrichment/components/pipeline-templates.tsx +90 -0
- package/blocks/enrichment/components/waterfall-config.tsx +27 -7
- package/blocks/enrichment/lib/enrichment-types.ts +51 -0
- package/blocks/enrichment/lib/pipeline-types.ts +234 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/forms/README.md +10 -0
- package/blocks/forms/block.json +107 -0
- 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 +1 -1
- package/blocks/forms/components/form-embed.tsx +1 -1
- 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 +57 -20
- 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 +15 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +45 -4
- package/blocks/inbox/block.json +145 -8
- package/blocks/inbox/components/blocked-senders.tsx +103 -0
- package/blocks/inbox/components/compose-message.tsx +193 -0
- package/blocks/inbox/components/inbox-screen.tsx +54 -6
- package/blocks/inbox/components/inbox-search.tsx +106 -0
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/tag-manager.tsx +136 -0
- package/blocks/inbox/components/thread-actions.tsx +133 -0
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +3 -1
- package/blocks/inbox/hooks/use-inbox-mailboxes.ts +31 -0
- package/blocks/inbox/lib/inbox-manage.ts +227 -0
- package/blocks/integrations/README.md +54 -1
- package/blocks/integrations/block.json +189 -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 +20 -1
- 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 +4 -4
- package/blocks/integrations/components/integrations-screen.tsx +20 -3
- 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 +6 -6
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +26 -7
- package/blocks/integrations/components/webhook-manager.tsx +30 -17
- 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 +5 -0
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +8 -8
- package/blocks/intent/components/intent-keyword-manager.tsx +8 -8
- package/blocks/intent/lib/intent-utils.ts +4 -4
- 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/lists/README.md +7 -1
- package/blocks/lists/block.json +31 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +17 -7
- package/blocks/lists/components/list-picker.tsx +13 -3
- package/blocks/lists/components/list-table.tsx +1 -1
- package/blocks/lists/components/lists-screen.tsx +4 -2
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +27 -6
- package/blocks/lists/components/suppressions-screen.tsx +21 -12
- package/blocks/mailboxes/block.json +4 -0
- package/blocks/mailboxes/components/connect-mailbox.tsx +9 -8
- package/blocks/mailboxes/components/domain-detail.tsx +10 -4
- package/blocks/mailboxes/components/domain-purchase.tsx +14 -11
- package/blocks/mailboxes/components/mailbox-detail.tsx +3 -2
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +8 -3
- package/blocks/mailboxes/components/warmup-health.tsx +20 -20
- package/blocks/mailboxes/lib/mailboxes-utils.ts +4 -4
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +117 -0
- package/blocks/newsletter/components/issue-scheduler.tsx +2 -2
- 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 +38 -13
- 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/components/nurture-builder.tsx +10 -2
- package/blocks/nurture/components/nurture-enroll.tsx +6 -7
- package/blocks/nurture/components/nurture-form.tsx +5 -8
- package/blocks/nurture/components/nurture-screen.tsx +16 -5
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/objects/README.md +1 -1
- package/blocks/objects/block.json +13 -5
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-record-detail.tsx +8 -1
- package/blocks/objects/components/object-schema-editor.tsx +1 -1
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +123 -2
- 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-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/hooks/use-landing-page-paths.ts +60 -0
- 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/programmatic-pages/README.md +82 -0
- package/blocks/programmatic-pages/block.json +292 -0
- package/blocks/programmatic-pages/components/programmatic-campaign-form.tsx +136 -0
- package/blocks/programmatic-pages/components/programmatic-generate.tsx +112 -0
- package/blocks/programmatic-pages/components/programmatic-generated-pages.tsx +210 -0
- package/blocks/programmatic-pages/components/programmatic-overview.tsx +84 -0
- package/blocks/programmatic-pages/components/programmatic-pages-screen.tsx +221 -0
- package/blocks/programmatic-pages/components/programmatic-preview.tsx +66 -0
- package/blocks/programmatic-pages/components/programmatic-publish.tsx +102 -0
- package/blocks/programmatic-pages/components/programmatic-settings.tsx +96 -0
- package/blocks/programmatic-pages/components/programmatic-source-fields.tsx +75 -0
- package/blocks/programmatic-pages/components/programmatic-source.tsx +106 -0
- package/blocks/programmatic-pages/components/programmatic-variables.tsx +84 -0
- package/blocks/programmatic-pages/hooks/use-programmatic-workspace.ts +19 -0
- package/blocks/programmatic-pages/lib/programmatic-types.ts +330 -0
- package/blocks/programmatic-pages/recipes/create-a-programmatic-campaign.ts +23 -0
- package/blocks/programmatic-pages/recipes/render-and-follow.ts +21 -0
- package/blocks/programmatic-pages/recipes/review-then-publish.ts +17 -0
- package/blocks/quotes/README.md +38 -1
- package/blocks/quotes/block.json +170 -6
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/manual-products.tsx +144 -0
- package/blocks/quotes/components/quote-batch-actions.tsx +86 -0
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-documents.tsx +94 -0
- package/blocks/quotes/components/quote-evidence.tsx +95 -0
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +28 -8
- package/blocks/quotes/components/quote-related.tsx +3 -2
- package/blocks/quotes/components/quote-settings.tsx +184 -0
- package/blocks/quotes/components/quote-templates.tsx +189 -0
- package/blocks/quotes/components/quote-viewer.tsx +23 -5
- package/blocks/quotes/components/quotes-screen.tsx +15 -5
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/quotes/lib/quote-admin.ts +264 -0
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-screen.tsx +2 -2
- package/blocks/sequencer/README.md +61 -0
- package/blocks/sequencer/block.json +200 -6
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +2 -2
- package/blocks/sequencer/components/enrollment-withdraw.tsx +71 -0
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +32 -23
- package/blocks/sequencer/components/sequence-campaign-link.tsx +78 -0
- package/blocks/sequencer/components/sequence-detail.tsx +10 -2
- package/blocks/sequencer/components/sequence-edit-form.tsx +111 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +5 -2
- package/blocks/sequencer/components/sequence-list.tsx +1 -1
- package/blocks/sequencer/components/sequence-manage-actions.tsx +119 -0
- package/blocks/sequencer/components/sequence-senders-panel.tsx +84 -0
- package/blocks/sequencer/components/sequence-stats.tsx +8 -8
- package/blocks/sequencer/components/sequence-templates.tsx +160 -0
- package/blocks/sequencer/components/sequences-screen.tsx +39 -6
- package/blocks/sequencer/components/step-ai-assist.tsx +196 -0
- package/blocks/sequencer/components/step-editor.tsx +21 -12
- package/blocks/sequencer/components/step-templates.tsx +224 -0
- package/blocks/sequencer/lib/sequencer-manage.ts +235 -0
- package/blocks/sequencer/lib/sequencer-utils.ts +9 -5
- package/blocks/signals/README.md +39 -0
- package/blocks/signals/block.json +287 -0
- package/blocks/signals/components/auto-seed-panel.tsx +111 -0
- package/blocks/signals/components/listener-form.tsx +184 -0
- package/blocks/signals/components/matched-posts.tsx +195 -0
- package/blocks/signals/components/post-engagements.tsx +114 -0
- package/blocks/signals/components/record-social-tab.tsx +49 -0
- package/blocks/signals/components/social-listeners-screen.tsx +250 -0
- package/blocks/signals/components/social-settings.tsx +128 -0
- package/blocks/signals/components/source-lists.tsx +83 -0
- package/blocks/signals/components/top-engagers.tsx +68 -0
- package/blocks/signals/components/x-integration.tsx +129 -0
- package/blocks/signals/lib/signals-types.ts +228 -0
- package/blocks/signals/recipes/preview-and-create-a-listener.ts +16 -0
- package/blocks/signals/recipes/spend-against-caps.ts +14 -0
- package/blocks/signals/recipes/triage-matched-posts.ts +18 -0
- package/blocks/social-ads/README.md +8 -0
- package/blocks/social-ads/block.json +137 -4
- package/blocks/social-ads/components/audience-sync-detail.tsx +9 -3
- package/blocks/social-ads/components/audience-sync.tsx +6 -2
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +6 -1
- 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 +6 -1
- package/blocks/social-ads/components/creative-gallery.tsx +20 -2
- 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 +4 -2
- package/blocks/social-ads/components/post-detail.tsx +3 -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-screen.tsx +33 -6
- 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/components/snippet-install.tsx +18 -9
- package/blocks/visitors/components/traffic-chart.tsx +7 -6
- package/blocks/visitors/components/visitor-company-card.tsx +24 -14
- package/blocks/visitors/components/visitor-feed.tsx +5 -5
- package/blocks/web-analytics/README.md +102 -0
- package/blocks/web-analytics/block.json +292 -0
- package/blocks/web-analytics/components/acquisition-tab.tsx +154 -0
- package/blocks/web-analytics/components/analytics-filters.tsx +161 -0
- package/blocks/web-analytics/components/audience-tab.tsx +135 -0
- package/blocks/web-analytics/components/conversions-tab.tsx +128 -0
- package/blocks/web-analytics/components/engagement-tab.tsx +123 -0
- package/blocks/web-analytics/components/overview-tab.tsx +102 -0
- package/blocks/web-analytics/components/pages-tab.tsx +148 -0
- package/blocks/web-analytics/components/ranked-table.tsx +103 -0
- package/blocks/web-analytics/components/realtime-tab.tsx +103 -0
- package/blocks/web-analytics/components/report-card.tsx +71 -0
- package/blocks/web-analytics/components/web-analytics-screen.tsx +90 -0
- package/blocks/web-analytics/hooks/use-report.ts +48 -0
- package/blocks/web-analytics/lib/analytics-scope.ts +155 -0
- package/blocks/web-analytics/lib/page-path.ts +14 -0
- package/blocks/web-analytics/lib/web-analytics-types.ts +187 -0
- package/blocks/web-analytics/recipes/best-landing-pages.ts +26 -0
- package/blocks/web-analytics/recipes/form-conversion-check.ts +19 -0
- package/blocks/web-analytics/recipes/weekly-traffic-report.ts +37 -0
- package/blocks/webchat/components/chat-widget-config.tsx +1 -1
- package/blocks/webchat/components/conversation-list.tsx +1 -1
- package/blocks/workbench/README.md +54 -2
- package/blocks/workbench/block.json +234 -5
- package/blocks/workbench/components/column-recipe-editor.tsx +1 -1
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-auto-enrich.tsx +88 -0
- package/blocks/workbench/components/workbench-automation.tsx +36 -0
- package/blocks/workbench/components/workbench-connections.tsx +136 -0
- package/blocks/workbench/components/workbench-crm-import.tsx +91 -0
- package/blocks/workbench/components/workbench-datafile-import.tsx +154 -0
- package/blocks/workbench/components/workbench-grid.tsx +71 -8
- package/blocks/workbench/components/workbench-pipelines.tsx +173 -0
- package/blocks/workbench/components/workbench-row-actions.tsx +8 -1
- package/blocks/workbench/components/workbench-schedule.tsx +191 -0
- package/blocks/workbench/components/workbench-screen.tsx +65 -22
- package/blocks/workbench/components/workbench-send-to-sequence.tsx +116 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workbench/components/workbench-table-settings.tsx +53 -0
- package/blocks/workbench/components/workbench-triggers.tsx +178 -0
- package/blocks/workbench/hooks/use-connected-crms.ts +29 -0
- package/blocks/workbench/lib/api-status.ts +8 -0
- package/blocks/workbench/lib/automation-types.ts +257 -0
- package/blocks/workflows/components/workflow-editor.tsx +1 -1
- package/dist/cli.js +744 -394
- package/dist/index.browser.js +1 -1
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +1 -1
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +1 -1
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +1 -1
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/auth.d.ts +45 -0
- package/dist/types/blocks/brand.d.ts +11 -2
- package/dist/types/blocks/create.d.ts +11 -4
- package/dist/types/blocks/doctor.d.ts +1 -0
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/version.d.ts +2 -2
- package/docs/api/desk.d.ts +2 -2
- package/docs/api/desk.md +3 -3
- package/docs/api/forms.md +1 -1
- package/package.json +1 -1
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +21 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +37 -4
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +19 -36
- package/stacks/apollo/pages/contacts.tsx.tmpl +35 -95
- 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 +24 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +18 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +68 -9
- 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 +8 -4
- package/stacks/clay/pages/companies.tsx.tmpl +17 -1
- package/stacks/clay/pages/contacts.tsx.tmpl +14 -0
- package/stacks/clay/pages/lists.tsx.tmpl +24 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +16 -3
- 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 +34 -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 +23 -6
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +30 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +19 -4
- package/stacks/hubspot/pages/campaigns.tsx.tmpl +30 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +16 -35
- package/stacks/hubspot/pages/contacts.tsx.tmpl +35 -95
- package/stacks/hubspot/pages/deals.tsx.tmpl +14 -1
- package/stacks/hubspot/pages/forms.tsx.tmpl +36 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +12 -1
- 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/sequences.tsx.tmpl +18 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/web-analytics.tsx.tmpl +14 -0
- package/stacks/hubspot.json +66 -7
- 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 +18 -4
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +30 -0
- package/stacks/mailchimp/pages/forms.tsx.tmpl +36 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +22 -6
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/campaigns.tsx.tmpl +27 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +29 -66
- 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 +18 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +38 -9
- package/stacks/pipedrive/pages/companies.tsx.tmpl +11 -35
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +17 -35
- package/stacks/pipedrive/pages/deals.tsx.tmpl +10 -1
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +12 -1
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +14 -2
- package/stacks/salesforce/pages/companies.tsx.tmpl +16 -35
- package/stacks/salesforce/pages/contacts.tsx.tmpl +26 -35
- package/stacks/salesforce/pages/deals.tsx.tmpl +14 -1
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +17 -3
- package/stacks/zoominfo/pages/companies.tsx.tmpl +17 -1
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +16 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +24 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +36 -3
- package/styles/styles.css +1 -1
- package/templates/auth/auth-route.ts.tmpl +16 -0
- package/templates/auth/g8.auth.anonymous.ts.tmpl +15 -0
- package/templates/auth/g8.auth.basic.ts.tmpl +17 -0
- package/templates/auth/g8.auth.jwt.ts.tmpl +23 -0
- package/templates/auth/g8.auth.password.ts.tmpl +18 -0
- package/templates/auth/g8.auth.session.ts.tmpl +23 -0
- package/templates/auth/middleware.ts.tmpl +14 -0
- package/templates/next-app/README.md.tmpl +5 -0
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/components/app-shell.tsx.tmpl +6 -0
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgentApprovals: the approvals inbox for everything agents ask permission for. Three parts:
|
|
5
|
+
* Oversight what needs you now (approvals, blockers) and what agents fixed on their own.
|
|
6
|
+
* Waiting each pending approval with Approve (lets the agent act: external, ConfirmSpendDialog), Reject, or Request changes
|
|
7
|
+
* (sends your feedback back instead of deciding).
|
|
8
|
+
* History decided approvals, filterable by decision, each with its signed receipts (who contributed what).
|
|
9
|
+
* Approvals that gate a Work coordination action cannot be decided with an API key; the error is shown as is.
|
|
10
|
+
*
|
|
11
|
+
* SDK operations, in order:
|
|
12
|
+
* 1. getAgentOversight GET /agent/oversight?limit=20 (read)
|
|
13
|
+
* 2. listAgentApprovals GET /agent/approvals (read) pending
|
|
14
|
+
* 3. decideAgentApproval POST /agent/approvals/{approval_id}/decide { decision } (write; approving is confirmed as external)
|
|
15
|
+
* 4. requestAgentApprovalChanges POST /agent/approvals/{approval_id}/request-changes { feedback } (write)
|
|
16
|
+
* 5. listAgentApprovalHistory GET /agent/approvals/history?decision (read)
|
|
17
|
+
* 6. getAgentApprovalReceipts GET /agent/approvals/{approval_id}/receipts (read)
|
|
18
|
+
*/
|
|
19
|
+
import { useState } from "react";
|
|
20
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Skeleton, Textarea } 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 { SectionCard } from "@/g8/components/screen/section-card";
|
|
25
|
+
import { StatusPill, toneForStatus } from "@/g8/components/screen/status-pill";
|
|
26
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
27
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
28
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
29
|
+
import { bodyOf, listOf } from "../lib/agent-types";
|
|
30
|
+
|
|
31
|
+
export interface AgentApprovalsProps {
|
|
32
|
+
/** Open the run an approval belongs to. */
|
|
33
|
+
onOpenRun?: (runId: string) => void;
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface Approval { id: string; summary: string; risk_level: string; work_item_id: string; decision?: string | null; is_expired?: boolean; decided_at?: string | null; decided_by?: string | null; requested_at?: string | null }
|
|
38
|
+
interface Receipt { id: string; action: string; entity_type: string; entity_id: string; contributed_by_user_id?: string | null; contributed_by_role_at_time?: string | null }
|
|
39
|
+
// The only decision words the history filter accepts (agent_router.py).
|
|
40
|
+
const DECISIONS: Array<[string, string]> = [["", "Any decision"], ["approve", "Approved"], ["reject", "Rejected"], ["edit_and_approve", "Edited and approved"], ["dismiss", "Dismissed"]];
|
|
41
|
+
/** Rows per kind the oversight feed returns; a full kind means there may be more. */
|
|
42
|
+
const OVERSIGHT_LIMIT = 20;
|
|
43
|
+
const when = (v?: string | null): string => (v ? new Date(v).toLocaleString() : "");
|
|
44
|
+
const enc = encodeURIComponent;
|
|
45
|
+
|
|
46
|
+
function Receipts({ approvalId }: { approvalId: string }) {
|
|
47
|
+
const q = useG8Query((signal) => g8Get(`/agent/approvals/${enc(approvalId)}/receipts`, undefined, { signal }), [approvalId]);
|
|
48
|
+
const rows = bodyOf<{ receipts?: Receipt[] }>(q.data).receipts ?? [];
|
|
49
|
+
if (q.error) return <ApiError error={q.error} compact title="Could not load the receipts" onRetry={() => void q.refetch()} />;
|
|
50
|
+
if (q.loading && !q.data) return <Skeleton className="h-8 w-full" />;
|
|
51
|
+
if (rows.length === 0) return <p className="text-xs text-muted-foreground">No receipts were signed for this approval.</p>;
|
|
52
|
+
return <ul className="divide-y rounded-md border text-xs" aria-label="Receipts">{rows.map((r) => <li key={r.id} className="px-3 py-1.5">{r.action} on {r.entity_type} {r.entity_id}<span className="ml-2 text-muted-foreground">{r.contributed_by_user_id ?? "system"}{r.contributed_by_role_at_time ? `, ${r.contributed_by_role_at_time}` : ""}</span></li>)}</ul>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function AgentApprovals({ onOpenRun, className }: AgentApprovalsProps) {
|
|
56
|
+
const [decision, setDecision] = useState("");
|
|
57
|
+
const [approving, setApproving] = useState<Approval | null>(null);
|
|
58
|
+
const [changes, setChanges] = useState<{ approval: Approval; feedback: string } | null>(null);
|
|
59
|
+
const [showReceipts, setShowReceipts] = useState<string | null>(null);
|
|
60
|
+
const oversight = useG8Query((signal) => g8Get("/agent/oversight", { limit: OVERSIGHT_LIMIT }, { signal }), []);
|
|
61
|
+
const pending = useG8Query((signal) => g8Get("/agent/approvals", undefined, { signal }), []);
|
|
62
|
+
const history = useG8Query((signal) => g8Get("/agent/approvals/history", { decision: decision || undefined }, { signal }), [decision]);
|
|
63
|
+
const waiting = listOf<Approval>(pending.data).filter((a) => !a.decision && !a.is_expired);
|
|
64
|
+
const decided = listOf<Approval>(history.data);
|
|
65
|
+
const ov = bodyOf<{ approvals?: unknown[]; blockers?: Array<{ title?: string; summary?: string }>; autonomous_fixes?: Array<{ title?: string; summary?: string }> }>(oversight.data);
|
|
66
|
+
const refresh = () => { void pending.refetch(); void history.refetch(); void oversight.refetch(); };
|
|
67
|
+
|
|
68
|
+
const decide = useG8Mutation((v: { id: string; decision: "approve" | "reject" }) => g8Post(`/agent/approvals/${enc(v.id)}/decide`, { decision: v.decision }), { onSuccess: () => { setApproving(null); refresh(); }, onError: () => setApproving(null) });
|
|
69
|
+
const ask = useG8Mutation((v: { id: string; feedback: string }) => g8Post(`/agent/approvals/${enc(v.id)}/request-changes`, { feedback: v.feedback }), { onSuccess: () => { setChanges(null); refresh(); } });
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className={`flex flex-col gap-4 ${className ?? ""}`}>
|
|
73
|
+
<SectionCard title="Needs you" description="Approvals and blockers across all agents, and what agents fixed themselves">
|
|
74
|
+
{oversight.error ? <ApiError error={oversight.error} title="Could not load the oversight feed" onRetry={() => void oversight.refetch()} /> : null}
|
|
75
|
+
{oversight.loading && !oversight.data ? <Skeleton className="h-10 w-full" /> : null}
|
|
76
|
+
{oversight.data ? (
|
|
77
|
+
<dl className="grid grid-cols-3 gap-3 text-sm" data-testid="oversight">
|
|
78
|
+
<div><dt className="text-xs text-muted-foreground">Approvals</dt><dd>{(ov.approvals ?? []).length}</dd></div>
|
|
79
|
+
<div><dt className="text-xs text-muted-foreground">Blockers</dt><dd>{(ov.blockers ?? []).length}</dd></div>
|
|
80
|
+
<div><dt className="text-xs text-muted-foreground">Fixed on their own</dt><dd>{(ov.autonomous_fixes ?? []).length}</dd></div>
|
|
81
|
+
</dl>
|
|
82
|
+
) : null}
|
|
83
|
+
{[ov.approvals, ov.blockers, ov.autonomous_fixes].some((l) => (l ?? []).length >= OVERSIGHT_LIMIT) ? <p className="mt-2 text-xs text-muted-foreground">Showing the first {OVERSIGHT_LIMIT} of each kind. Older items are in the history below.</p> : null}
|
|
84
|
+
{(ov.blockers ?? []).length ? <ul className="mt-3 list-disc pl-5 text-sm">{(ov.blockers ?? []).map((b, i) => <li key={i}>{b.title ?? b.summary}</li>)}</ul> : null}
|
|
85
|
+
</SectionCard>
|
|
86
|
+
|
|
87
|
+
<SectionCard title={`Waiting for a decision (${waiting.length})`} flush>
|
|
88
|
+
{pending.error ? <div className="p-3"><ApiError error={pending.error} title="Could not load the approvals" onRetry={() => void pending.refetch()} /></div> : null}
|
|
89
|
+
{pending.loading && !pending.data ? <Skeleton className="m-3 h-10" /> : null}
|
|
90
|
+
<ul className="divide-y">
|
|
91
|
+
{waiting.map((a) => (
|
|
92
|
+
<li key={a.id} data-testid={`approval-${a.id}`} className="flex flex-wrap items-center justify-between gap-2 px-4 py-2 text-sm">
|
|
93
|
+
<span>{a.summary}<span className="ml-2 text-xs text-muted-foreground">risk: {a.risk_level}</span>{onOpenRun ? <button type="button" className="ml-2 text-xs underline" onClick={() => onOpenRun(a.work_item_id)}>Open run</button> : null}</span>
|
|
94
|
+
<span className="flex gap-2">
|
|
95
|
+
<Button size="sm" variant="ghost" onClick={() => setChanges({ approval: a, feedback: "" })}>Request changes</Button>
|
|
96
|
+
<Button size="sm" variant="outline" disabled={decide.loading} onClick={() => void decide.mutate({ id: a.id, decision: "reject" })}>Reject</Button>
|
|
97
|
+
<Button size="sm" disabled={decide.loading} onClick={() => setApproving(a)}>Approve</Button>
|
|
98
|
+
</span>
|
|
99
|
+
</li>
|
|
100
|
+
))}
|
|
101
|
+
</ul>
|
|
102
|
+
{!pending.loading && !pending.error && waiting.length === 0 ? <EmptyState compact title="Nothing is waiting" description="When an agent needs permission to act, it shows up here." /> : null}
|
|
103
|
+
{decide.error ? <div className="p-3"><ApiError error={decide.error} title="Could not record the decision" /></div> : null}
|
|
104
|
+
</SectionCard>
|
|
105
|
+
|
|
106
|
+
<SectionCard title="History" flush actions={<select aria-label="Decision" className="h-8 rounded-md border bg-background px-2 text-sm" value={decision} onChange={(e) => setDecision(e.target.value)}>{DECISIONS.map(([v, l]) => <option key={v} value={v}>{l}</option>)}</select>}>
|
|
107
|
+
{history.error ? <div className="p-3"><ApiError error={history.error} title="Could not load the history" onRetry={() => void history.refetch()} /></div> : null}
|
|
108
|
+
{history.loading && !history.data ? <Skeleton className="m-3 h-10" /> : null}
|
|
109
|
+
<ul className="divide-y">
|
|
110
|
+
{decided.map((a) => (
|
|
111
|
+
<li key={a.id} className="px-4 py-2 text-sm">
|
|
112
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
113
|
+
<span>{a.summary}</span>
|
|
114
|
+
<span className="flex items-center gap-2"><StatusPill tone={toneForStatus(a.decision ?? "")} dot>{a.decision ?? "undecided"}</StatusPill><span className="text-xs text-muted-foreground">{when(a.decided_at ?? a.requested_at)}</span><Button size="sm" variant="ghost" aria-expanded={showReceipts === a.id} onClick={() => setShowReceipts(showReceipts === a.id ? null : a.id)}>Receipts</Button></span>
|
|
115
|
+
</div>
|
|
116
|
+
{showReceipts === a.id ? <div className="mt-2"><Receipts approvalId={a.id} /></div> : null}
|
|
117
|
+
</li>
|
|
118
|
+
))}
|
|
119
|
+
</ul>
|
|
120
|
+
{!history.loading && !history.error && decided.length === 0 ? <EmptyState compact title="No decisions yet" /> : null}
|
|
121
|
+
</SectionCard>
|
|
122
|
+
|
|
123
|
+
<Dialog open={changes != null} onOpenChange={(o) => !o && setChanges(null)}>
|
|
124
|
+
<DialogContent>
|
|
125
|
+
<DialogHeader><DialogTitle>Request changes</DialogTitle><DialogDescription>{changes?.approval.summary}</DialogDescription></DialogHeader>
|
|
126
|
+
<Textarea aria-label="Feedback" rows={4} placeholder="What should the agent change?" value={changes?.feedback ?? ""} onChange={(e) => setChanges((c) => c && { ...c, feedback: e.target.value })} />
|
|
127
|
+
{ask.error ? <ApiError error={ask.error} title="Could not send the feedback" /> : null}
|
|
128
|
+
<DialogFooter><Button variant="ghost" onClick={() => setChanges(null)}>Cancel</Button><Button loading={ask.loading} disabled={!changes?.feedback.trim()} onClick={() => changes && void ask.mutate({ id: changes.approval.id, feedback: changes.feedback.trim() })}>Send feedback</Button></DialogFooter>
|
|
129
|
+
</DialogContent>
|
|
130
|
+
</Dialog>
|
|
131
|
+
<ConfirmSpendDialog open={approving != null} tier="external" title="Approve this action" description={approving?.summary}
|
|
132
|
+
confirmLabel="Approve" confirming={decide.loading} onConfirm={() => approving && void decide.mutate({ id: approving.id, decision: "approve" })} onCancel={() => setApproving(null)} />
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
import { useEffect, useState } from "react";
|
|
22
22
|
import { Button, Input, Label, Skeleton, Switch, Textarea } from "@graph8/sdk/ui";
|
|
23
23
|
import { ApiError } from "@/g8/components/api-error";
|
|
24
|
+
import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
|
|
25
|
+
import { byPage, hasNextOf } from "@/g8/lib/paging";
|
|
26
|
+
import { FirstNotice, LoadMore } from "@/g8/components/paging-notices";
|
|
24
27
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
25
28
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
26
29
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
@@ -62,6 +65,11 @@ function Dial({ label, value, onChange, low, high }: { label: string; value: num
|
|
|
62
65
|
);
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
/** Agents per read; "Load more" appends the next page (the list used to stop silently at 50). */
|
|
69
|
+
const AGENT_PAGE = 50;
|
|
70
|
+
/** The voice presets API has no paging: a full read is announced under the select. */
|
|
71
|
+
const VOICE_LIMIT = 100;
|
|
72
|
+
|
|
65
73
|
export function AgentBuilder({ agentId: controlled, onSelect, onChanged, hideList, className }: AgentBuilderProps) {
|
|
66
74
|
const [inner, setInner] = useState<string | null>(null);
|
|
67
75
|
const selected = controlled !== undefined ? controlled : inner;
|
|
@@ -72,9 +80,13 @@ export function AgentBuilder({ agentId: controlled, onSelect, onChanged, hideLis
|
|
|
72
80
|
const [deleting, setDeleting] = useState(false);
|
|
73
81
|
|
|
74
82
|
const choose = (id: string | null) => { setInner(id); setCreating(false); onSelect?.(id); };
|
|
75
|
-
const agents =
|
|
83
|
+
const agents = useLoadMoreList(byPage<AgentSummary>(AGENT_PAGE, async (page, signal) => {
|
|
84
|
+
const env = await g8Get("/voice/agents", { search: search.trim() || undefined, page, page_size: AGENT_PAGE }, { signal });
|
|
85
|
+
const rows = listOf<AgentSummary>(env, "agents");
|
|
86
|
+
return { rows, hasNext: hasNextOf(env, AGENT_PAGE, rows.length) };
|
|
87
|
+
}), [search], { enabled: !hideList });
|
|
76
88
|
const detail = useG8Query((signal) => g8Get(`/voice/agents/${selected}`, undefined, { signal }), [selected], { enabled: Boolean(selected) && !creating });
|
|
77
|
-
const voices = useG8Query((signal) => g8Get("/voice/voice-library/presets", { limit:
|
|
89
|
+
const voices = useG8Query((signal) => g8Get("/voice/voice-library/presets", { limit: VOICE_LIMIT }, { signal }), []);
|
|
78
90
|
|
|
79
91
|
useEffect(() => {
|
|
80
92
|
const agent = bodyOf<{ agent?: AgentDetail | null }>(detail.data).agent;
|
|
@@ -101,7 +113,7 @@ export function AgentBuilder({ agentId: controlled, onSelect, onChanged, hideLis
|
|
|
101
113
|
onSuccess: () => { setDeleting(false); setDraft(null); choose(null); if (!hideList) void agents.refetch(); onChanged?.(); },
|
|
102
114
|
});
|
|
103
115
|
|
|
104
|
-
const rows =
|
|
116
|
+
const rows = agents.data ?? [];
|
|
105
117
|
const presets = listOf<{ voice_id?: string; id?: string; name?: string; label?: string }>(voices.data, "items");
|
|
106
118
|
const setPersona = (patch: Partial<AgentDetail["persona"]>) => setDraft((d) => d && { ...d, persona: { ...d.persona, ...patch } });
|
|
107
119
|
// The hook keeps the previous agent's data while the next one loads: until it arrives the old form must not be shown (or saved) as the new agent.
|
|
@@ -128,6 +140,7 @@ export function AgentBuilder({ agentId: controlled, onSelect, onChanged, hideLis
|
|
|
128
140
|
</li>
|
|
129
141
|
))}
|
|
130
142
|
</ul>
|
|
143
|
+
<LoadMore shown={rows.length} noun="agents" hasMore={agents.hasMore} loading={agents.loadingMore} onLoadMore={agents.loadMore} />
|
|
131
144
|
{!agents.loading && !agents.error && rows.length === 0 ? (
|
|
132
145
|
<EmptyState title="No agents yet" description="An agent has a persona, a voice, knowledge and skills." />
|
|
133
146
|
) : null}
|
|
@@ -170,6 +183,7 @@ export function AgentBuilder({ agentId: controlled, onSelect, onChanged, hideLis
|
|
|
170
183
|
<option value="">Default voice</option>
|
|
171
184
|
{presets.map((v) => { const id = v.voice_id ?? v.id ?? ""; return <option key={id} value={id}>{v.name ?? v.label ?? id}</option>; })}
|
|
172
185
|
</select>
|
|
186
|
+
<FirstNotice shown={presets.length} limit={VOICE_LIMIT} noun="voices" />
|
|
173
187
|
</div>
|
|
174
188
|
<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>
|
|
175
189
|
<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>
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgentConversations: the chat history of one agent. The list of its conversations (newest first, "Load more" up to the API's 100),
|
|
5
|
+
* a conversation's messages, rename, hand-over to a human and delete, plus a message box to talk to the agent yourself.
|
|
6
|
+
* Sending a message is billable and the agent may run its attached skills while answering, so it goes through ConfirmSpendDialog.
|
|
7
|
+
* Deleting a conversation is destructive and confirmed.
|
|
8
|
+
*
|
|
9
|
+
* SDK operations, in order:
|
|
10
|
+
* 1. listChatAgentConversations POST /voice/chat-agent/conversations { agent_id, limit } (read, a POST with a body)
|
|
11
|
+
* 2. getChatAgentHistory POST /voice/chat-agent/history { agent_id, contact_id, session_id } (read)
|
|
12
|
+
* 3. renameChatAgentConversation PUT /voice/chat-agent/conversations/{chat_id}/name?agent_id { name } (write)
|
|
13
|
+
* 4. transferChatAgentConversation POST /voice/chat-agent/conversations/{chat_id}/transfer-to-human (write)
|
|
14
|
+
* 5. deleteChatAgentConversation DELETE /voice/chat-agent/conversations/{chat_id}?agent_id (destructive, ConfirmSpendDialog)
|
|
15
|
+
* 6. sendChatAgentMessage POST /voice/chat-agent/messages { agent_id, contact_id, message, session_id? } (billable, ConfirmSpendDialog)
|
|
16
|
+
*
|
|
17
|
+
* The history route does not match on contact_id, so any stable string works; `contactId` lets a host pass the person's own id.
|
|
18
|
+
*/
|
|
19
|
+
import { useEffect, useState } from "react";
|
|
20
|
+
import { MessageSquare } from "lucide-react";
|
|
21
|
+
import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
22
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
23
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
24
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
25
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
26
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
27
|
+
import { g8Delete, g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
|
|
28
|
+
import { bodyOf, listOf } from "../lib/agent-types";
|
|
29
|
+
import { LoadMore, useGrowingList } from "../lib/load-more";
|
|
30
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
31
|
+
|
|
32
|
+
export interface AgentConversationsProps {
|
|
33
|
+
agentId: string;
|
|
34
|
+
/** Sent as contact_id on messages and history reads. Default "sdk". */
|
|
35
|
+
contactId?: string;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface Chat { id: string; name?: string; message_count?: number; updated_at?: string | null; created_at?: string | null }
|
|
40
|
+
interface Msg { role?: string; content?: unknown; created_at?: string }
|
|
41
|
+
|
|
42
|
+
const text = (v: unknown): string => (typeof v === "string" ? v : v == null ? "" : JSON.stringify(v));
|
|
43
|
+
const when = (v?: string | null): string => (v ? new Date(v).toLocaleString() : "");
|
|
44
|
+
|
|
45
|
+
function Transcript({ agentId, chat, contactId, refreshKey }: { agentId: string; chat: Chat; contactId: string; refreshKey: number }) {
|
|
46
|
+
const history = useG8Query((signal) => g8Post(`/voice/chat-agent/history`, { agent_id: agentId, contact_id: contactId, session_id: chat.id }, { signal }), [agentId, chat.id, contactId, refreshKey]);
|
|
47
|
+
const body = bodyOf<{ messages?: Msg[]; success?: boolean; error?: string }>(history.data);
|
|
48
|
+
const messages = listOf<Msg>(history.data, "messages", "items");
|
|
49
|
+
if (history.error) return <ApiError error={history.error} title="Could not load the conversation" onRetry={() => void history.refetch()} />;
|
|
50
|
+
if (history.loading && !history.data) return <Skeleton className="h-24 w-full" aria-label="Loading conversation" />;
|
|
51
|
+
if (body.success === false && body.error) return <p className="text-sm text-muted-foreground">{body.error}</p>;
|
|
52
|
+
if (!messages.length) return <p className="text-sm text-muted-foreground">No messages in this conversation yet.</p>;
|
|
53
|
+
return (
|
|
54
|
+
<ol className="flex flex-col gap-2" aria-label="Messages">
|
|
55
|
+
{messages.map((m, i) => (
|
|
56
|
+
<li key={i} className={`max-w-[85%] rounded-md border px-3 py-2 text-sm ${m.role === "user" ? "self-end bg-accent" : "self-start"}`}>
|
|
57
|
+
<span className="block text-xs text-muted-foreground">{m.role === "user" ? "Contact" : m.role === "assistant" ? "Agent" : (m.role ?? "")}</span>
|
|
58
|
+
<span className="whitespace-pre-wrap">{text(m.content)}</span>
|
|
59
|
+
</li>
|
|
60
|
+
))}
|
|
61
|
+
</ol>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function AgentConversations({ agentId, contactId = "sdk", className }: AgentConversationsProps) {
|
|
66
|
+
const brand = useBrand();
|
|
67
|
+
const [openId, setOpenId] = useState<string | null>(null);
|
|
68
|
+
const [renaming, setRenaming] = useState<{ id: string; name: string } | null>(null);
|
|
69
|
+
const [deleting, setDeleting] = useState<Chat | null>(null);
|
|
70
|
+
const [draft, setDraft] = useState("");
|
|
71
|
+
const [sending, setSending] = useState<string | null>(null);
|
|
72
|
+
const [turn, setTurn] = useState(0);
|
|
73
|
+
const list = useGrowingList<Chat>(async (limit, signal) => listOf<Chat>(await g8Post("/voice/chat-agent/conversations", { agent_id: agentId, limit }, { signal }), "chats", "items", "conversations"), [agentId], { pageSize: 25, max: 100 });
|
|
74
|
+
useEffect(() => { setOpenId(null); setRenaming(null); setDeleting(null); setDraft(""); setSending(null); }, [agentId]);
|
|
75
|
+
const open = list.items.find((c) => c.id === openId) ?? (openId ? { id: openId } : null);
|
|
76
|
+
const enc = encodeURIComponent;
|
|
77
|
+
|
|
78
|
+
const rename = useG8Mutation((v: { id: string; name: string }) => g8Put(`/voice/chat-agent/conversations/${enc(v.id)}/name`, { name: v.name }, { query: { agent_id: agentId } }), { onSuccess: () => { setRenaming(null); list.reload(); } });
|
|
79
|
+
const handover = useG8Mutation((id: string) => g8Post(`/voice/chat-agent/conversations/${enc(id)}/transfer-to-human`, {}), { onSuccess: () => list.reload() });
|
|
80
|
+
const remove = useG8Mutation((id: string) => g8Delete(`/voice/chat-agent/conversations/${enc(id)}`, { query: { agent_id: agentId } }), { onSuccess: (_r, id) => { setDeleting(null); if (openId === id) setOpenId(null); list.reload(); } });
|
|
81
|
+
const send = useG8Mutation(async (message: string) => {
|
|
82
|
+
const res = await g8Post("/voice/chat-agent/messages", { agent_id: agentId, contact_id: contactId, message, ...(openId ? { session_id: openId } : {}) });
|
|
83
|
+
return bodyOf<{ session_id?: string }>(res).session_id ?? null;
|
|
84
|
+
}, { onSuccess: (sid) => { setSending(null); setDraft(""); if (sid) setOpenId(sid); setTurn((t) => t + 1); list.reload(); }, onError: () => setSending(null) });
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className={`grid gap-4 lg:grid-cols-[18rem_1fr] ${className ?? ""}`}>
|
|
88
|
+
<div className="flex flex-col gap-2">
|
|
89
|
+
<div className="flex items-center justify-between"><h3 className="text-sm font-medium">Conversations</h3><Button size="sm" variant="outline" onClick={() => setOpenId(null)}>New chat</Button></div>
|
|
90
|
+
{list.error ? <ApiError error={list.error} title="Could not load conversations" onRetry={list.reload} /> : null}
|
|
91
|
+
{list.loading ? <Skeleton className="h-16 w-full" aria-label="Loading conversations" /> : null}
|
|
92
|
+
<ul className="divide-y rounded-md border">
|
|
93
|
+
{list.items.map((c) => (
|
|
94
|
+
<li key={c.id} className={openId === c.id ? "bg-accent" : ""}>
|
|
95
|
+
<button type="button" data-testid={`chat-${c.id}`} className="block w-full px-3 py-2 text-left text-sm" onClick={() => setOpenId(c.id)}>
|
|
96
|
+
<span className="block truncate font-medium">{c.name || "Chat"}</span>
|
|
97
|
+
<span className="block text-xs text-muted-foreground">{c.message_count ?? 0} messages, {when(c.updated_at ?? c.created_at)}</span>
|
|
98
|
+
</button>
|
|
99
|
+
</li>
|
|
100
|
+
))}
|
|
101
|
+
</ul>
|
|
102
|
+
{!list.loading && !list.error && list.items.length === 0 ? <EmptyState compact icon={<MessageSquare strokeWidth={1.5} />} title="No conversations yet" description="Send a message to start one." /> : null}
|
|
103
|
+
<LoadMore list={list} noun="conversations" />
|
|
104
|
+
</div>
|
|
105
|
+
<div className="flex min-w-0 flex-col gap-3">
|
|
106
|
+
{open ? (
|
|
107
|
+
<>
|
|
108
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
109
|
+
{renaming?.id === open.id ? (
|
|
110
|
+
<>
|
|
111
|
+
<Input aria-label="Conversation name" className="max-w-xs" value={renaming.name} onChange={(e) => setRenaming({ id: open.id, name: e.target.value })} />
|
|
112
|
+
<Button size="sm" loading={rename.loading} disabled={!renaming.name.trim()} onClick={() => void rename.mutate(renaming)}>Save name</Button>
|
|
113
|
+
<Button size="sm" variant="ghost" onClick={() => setRenaming(null)}>Cancel</Button>
|
|
114
|
+
</>
|
|
115
|
+
) : (
|
|
116
|
+
<>
|
|
117
|
+
<h3 className="text-sm font-medium">{(open as Chat).name || "Conversation"}</h3>
|
|
118
|
+
<Button size="sm" variant="outline" onClick={() => setRenaming({ id: open.id, name: (open as Chat).name ?? "" })}>Rename</Button>
|
|
119
|
+
<Button size="sm" variant="outline" loading={handover.loading} onClick={() => void handover.mutate(open.id)}>Hand over to a human</Button>
|
|
120
|
+
<Button size="sm" variant="ghost" onClick={() => setDeleting(open as Chat)}>Delete</Button>
|
|
121
|
+
</>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
{rename.error ? <ApiError error={rename.error} title="Could not rename the conversation" /> : null}
|
|
125
|
+
{handover.error ? <ApiError error={handover.error} title="Could not hand the conversation over" /> : null}
|
|
126
|
+
{handover.data ? <p role="status" className="text-sm text-muted-foreground">A person will pick this conversation up.</p> : null}
|
|
127
|
+
<Transcript agentId={agentId} chat={open as Chat} contactId={contactId} refreshKey={turn} />
|
|
128
|
+
</>
|
|
129
|
+
) : <p className="text-sm text-muted-foreground">Pick a conversation, or type below to start a new one.</p>}
|
|
130
|
+
<div className="flex flex-col gap-2 border-t pt-3">
|
|
131
|
+
<Textarea aria-label="Message to the agent" rows={3} value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="Say something to this agent" />
|
|
132
|
+
{send.error ? <ApiError error={send.error} title="Could not send the message" /> : null}
|
|
133
|
+
<Button className="self-end" disabled={!draft.trim()} onClick={() => setSending(draft.trim())}>Send message</Button>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
{remove.error ? <ApiError error={remove.error} title="Could not delete the conversation" /> : null}
|
|
137
|
+
<ConfirmSpendDialog open={sending != null} tier="billable" title="Send a message to this agent" description={`The agent answers with the model and may run the skills attached to it, which can reach outside ${brand.name}. Each turn spends credits.`}
|
|
138
|
+
confirmLabel="Send" confirming={send.loading} onConfirm={() => sending && void send.mutate(sending)} onCancel={() => setSending(null)} />
|
|
139
|
+
<ConfirmSpendDialog open={deleting != null} tier="destructive" title={`Delete "${deleting?.name || "this conversation"}"`} description="The messages are removed for everyone." affected={1}
|
|
140
|
+
confirmLabel="Delete" confirming={remove.loading} onConfirm={() => deleting && void remove.mutate(deleting.id)} onCancel={() => setDeleting(null)} />
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
@@ -90,7 +90,7 @@ export function AgentKnowledgePanel({ agentId, useCompanyKnowledge, onToggleComp
|
|
|
90
90
|
|
|
91
91
|
<div className="flex flex-col gap-2">
|
|
92
92
|
<div className="flex items-center justify-between">
|
|
93
|
-
<h3 className="text-sm font-
|
|
93
|
+
<h3 className="text-sm font-medium">This agent's documents</h3>
|
|
94
94
|
<Button size="sm" variant="outline" onClick={() => fileInput.current?.click()}>Upload documents</Button>
|
|
95
95
|
<input ref={fileInput} type="file" multiple hidden aria-label="Choose documents" accept=".pdf,.docx,.txt,.md" onChange={(e) => onPick(e.target.files)} />
|
|
96
96
|
</div>
|
|
@@ -113,7 +113,7 @@ export function AgentKnowledgePanel({ agentId, useCompanyKnowledge, onToggleComp
|
|
|
113
113
|
</div>
|
|
114
114
|
|
|
115
115
|
<div className="flex flex-col gap-2">
|
|
116
|
-
<h3 className="text-sm font-
|
|
116
|
+
<h3 className="text-sm font-medium">Company knowledge</h3>
|
|
117
117
|
<form className="flex gap-2" onSubmit={(e) => { e.preventDefault(); setSubmitted(query.trim()); }}>
|
|
118
118
|
<Input aria-label="Search company knowledge" placeholder="Search what the agent can draw on" value={query} onChange={(e) => setQuery(e.target.value)} />
|
|
119
119
|
<Button type="submit" size="sm" variant="outline" disabled={!query.trim()}>Search</Button>
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgentOperators: the autonomous operators that work in your workspace without being asked (each monitors something on a schedule,
|
|
5
|
+
* records findings, and acts only as far as its trust dial allows). A summary, the bench (turn each on or off), and for the open
|
|
6
|
+
* operator: Skills (the trust dial per skill: off, detect, propose or auto), Findings, Activity, Health and Scorecard.
|
|
7
|
+
* Turning an operator on, and raising a trust dial, start work that proceeds on its own and spends credits, so both go through
|
|
8
|
+
* ConfirmSpendDialog (billable). Deciding an operator's proposed fix (which changes your data) is not offered here; it stays in graph8.
|
|
9
|
+
*
|
|
10
|
+
* SDK operations, in order:
|
|
11
|
+
* 1. getAgentOperatorsSummary GET /agent/operators/summary (read)
|
|
12
|
+
* 2. listAgentOperators GET /agent/operators (read)
|
|
13
|
+
* 3. getAgentOperatorStatus GET /agent/operators/{operator}/status (read)
|
|
14
|
+
* 4. setAgentOperatorEnabled PUT /agent/operators/{operator}/enabled { enabled } (billable, ConfirmSpendDialog)
|
|
15
|
+
* 5. listAgentOperatorSkills GET /agent/operators/{operator}/skills (read)
|
|
16
|
+
* 6. setAgentOperatorTrustDial PUT /agent/operators/{operator}/skills/{skill_key} { autonomy } (billable, ConfirmSpendDialog)
|
|
17
|
+
* 7. listAgentOperatorFindings GET /agent/operators/{operator}/findings (read)
|
|
18
|
+
* 8. listAgentOperatorActivity GET /agent/operators/{operator}/activity?limit (read) "Load more" grows the limit
|
|
19
|
+
* 9. getAgentOperatorHealth GET /agent/operators/{operator}/health (read)
|
|
20
|
+
* 10. getAgentOperatorScorecard GET /agent/operators/{operator}/scorecard (read)
|
|
21
|
+
*/
|
|
22
|
+
import { useState } from "react";
|
|
23
|
+
import { Cpu } from "lucide-react";
|
|
24
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
25
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
26
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
27
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
28
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
29
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
30
|
+
import { StatStrip } from "@/g8/components/screen/stat-strip";
|
|
31
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
32
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
33
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
34
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
35
|
+
import { g8Get, g8Put } from "@/g8/lib/g8-client";
|
|
36
|
+
import { FactList } from "../lib/facts";
|
|
37
|
+
import { bodyOf, listOf } from "../lib/agent-types";
|
|
38
|
+
import { LoadMore, useGrowingList } from "../lib/load-more";
|
|
39
|
+
|
|
40
|
+
export interface AgentOperatorsProps {
|
|
41
|
+
className?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface Operator { operator: string; title?: string; persona?: string; enabled?: boolean; has_run?: boolean; findings_total?: number; findings_critical?: number; detection?: boolean }
|
|
45
|
+
interface OperatorSkill { key: string; name?: string; title?: string; description?: string; autonomy?: string; default_autonomy?: string }
|
|
46
|
+
const LEVELS = ["OFF", "DETECT", "PROPOSE", "AUTO"] as const;
|
|
47
|
+
type Level = (typeof LEVELS)[number];
|
|
48
|
+
const enc = encodeURIComponent;
|
|
49
|
+
|
|
50
|
+
function Skills({ operator, onAsk }: { operator: string; onAsk: (v: { key: string; label: string; autonomy: Level }) => void }) {
|
|
51
|
+
const skills = useG8Query((signal) => g8Get(`/agent/operators/${enc(operator)}/skills`, undefined, { signal }), [operator]);
|
|
52
|
+
const rows = listOf<OperatorSkill>(skills.data, "items", "skills");
|
|
53
|
+
if (skills.error) return <ApiError error={skills.error} title="Could not load the skills" onRetry={() => void skills.refetch()} />;
|
|
54
|
+
if (skills.loading && !skills.data) return <Skeleton className="h-16 w-full" />;
|
|
55
|
+
if (rows.length === 0) return <p className="text-sm text-muted-foreground">This operator has no skills to tune.</p>;
|
|
56
|
+
return (
|
|
57
|
+
<ul className="divide-y rounded-md border" aria-label="Operator skills">
|
|
58
|
+
{rows.map((s) => (
|
|
59
|
+
<li key={s.key} className="flex flex-wrap items-center justify-between gap-2 px-3 py-2 text-sm">
|
|
60
|
+
<span><span className="font-medium">{s.name ?? s.title ?? s.key}</span>{s.description ? <span className="block text-xs text-muted-foreground">{s.description}</span> : null}</span>
|
|
61
|
+
<select aria-label={`Autonomy for ${s.name ?? s.title ?? s.key}`} className="h-8 rounded-md border bg-background px-2 text-sm" value={(s.autonomy ?? s.default_autonomy ?? "OFF").toUpperCase()}
|
|
62
|
+
onChange={(e) => onAsk({ key: s.key, label: s.name ?? s.title ?? s.key, autonomy: e.target.value as Level })}>
|
|
63
|
+
{LEVELS.map((l) => <option key={l} value={l}>{l.toLowerCase()}</option>)}
|
|
64
|
+
</select>
|
|
65
|
+
</li>
|
|
66
|
+
))}
|
|
67
|
+
</ul>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function Activity({ operator }: { operator: string }) {
|
|
72
|
+
const list = useGrowingList<Record<string, unknown>>(async (limit, signal) => listOf<Record<string, unknown>>(await g8Get(`/agent/operators/${enc(operator)}/activity`, { limit }, { signal }), "items"), [operator], { pageSize: 25, max: 200 });
|
|
73
|
+
if (list.error) return <ApiError error={list.error} title="Could not load the activity" onRetry={list.reload} />;
|
|
74
|
+
if (list.loading) return <Skeleton className="h-16 w-full" />;
|
|
75
|
+
if (list.items.length === 0) return <p className="text-sm text-muted-foreground">Nothing has been decided or fixed yet.</p>;
|
|
76
|
+
return (
|
|
77
|
+
<div className="flex flex-col gap-2">
|
|
78
|
+
<ul className="divide-y rounded-md border" aria-label="Operator activity">
|
|
79
|
+
{list.items.map((r, i) => <li key={String(r.task_id ?? r.id ?? i)} className="px-3 py-2 text-sm">{String(r.title ?? r.summary ?? r.check_id ?? "Activity")}<span className="block text-xs text-muted-foreground">{String(r.resolution ?? r.status ?? "")} {r.at || r.created_at ? new Date(String(r.at ?? r.created_at)).toLocaleString() : ""}</span></li>)}
|
|
80
|
+
</ul>
|
|
81
|
+
<LoadMore list={list} noun="events" />
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function Findings({ operator }: { operator: string }) {
|
|
87
|
+
const q = useG8Query((signal) => g8Get(`/agent/operators/${enc(operator)}/findings`, undefined, { signal }), [operator]);
|
|
88
|
+
const rows = listOf<Record<string, unknown>>(q.data, "items", "findings");
|
|
89
|
+
if (q.error) return <ApiError error={q.error} title="Could not load the findings" onRetry={() => void q.refetch()} />;
|
|
90
|
+
if (q.loading && !q.data) return <Skeleton className="h-16 w-full" />;
|
|
91
|
+
if (rows.length === 0) return <p className="text-sm text-muted-foreground">No open findings.</p>;
|
|
92
|
+
return <ul className="divide-y rounded-md border" aria-label="Findings">{rows.map((r, i) => <li key={String(r.id ?? r.check_id ?? i)} className="flex items-center justify-between gap-2 px-3 py-2 text-sm"><span>{String(r.title ?? r.summary ?? r.check_id ?? "Finding")}</span>{r.severity ? <StatusPill tone={String(r.severity) === "critical" ? "negative" : "warning"}>{String(r.severity)}</StatusPill> : null}</li>)}</ul>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function Facts({ path }: { path: string }) {
|
|
96
|
+
const q = useG8Query((signal) => g8Get(path, undefined, { signal }), [path]);
|
|
97
|
+
if (q.error) return <ApiError error={q.error} title="Could not load this" onRetry={() => void q.refetch()} />;
|
|
98
|
+
if (q.loading && !q.data) return <Skeleton className="h-16 w-full" />;
|
|
99
|
+
return <FactList data={bodyOf(q.data)} />;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function AgentOperators({ className }: AgentOperatorsProps) {
|
|
103
|
+
const [open, setOpen] = useUrlState("operator", "");
|
|
104
|
+
const [enabling, setEnabling] = useState<{ operator: Operator; enabled: boolean } | null>(null);
|
|
105
|
+
const [dial, setDial] = useState<{ key: string; label: string; autonomy: Level } | null>(null);
|
|
106
|
+
const [dialKey, setDialKey] = useState(0);
|
|
107
|
+
const summary = useG8Query((signal) => g8Get("/agent/operators/summary", undefined, { signal }), []);
|
|
108
|
+
const bench = useG8Query((signal) => g8Get("/agent/operators", undefined, { signal }), []);
|
|
109
|
+
const rows = listOf<Operator>(bench.data, "items", "operators");
|
|
110
|
+
const current = rows.find((o) => o.operator === open);
|
|
111
|
+
const sum = bodyOf<Record<string, number>>(summary.data);
|
|
112
|
+
|
|
113
|
+
const toggle = useG8Mutation((v: { operator: string; enabled: boolean }) => g8Put(`/agent/operators/${enc(v.operator)}/enabled`, { enabled: v.enabled }), { onSuccess: () => { setEnabling(null); void bench.refetch(); void summary.refetch(); }, onError: () => setEnabling(null) });
|
|
114
|
+
const setLevel = useG8Mutation((v: { operator: string; key: string; autonomy: Level }) => g8Put(`/agent/operators/${enc(v.operator)}/skills/${enc(v.key)}`, { autonomy: v.autonomy }), { onSuccess: () => { setDial(null); setDialKey((k) => k + 1); }, onError: () => { setDial(null); setDialKey((k) => k + 1); } });
|
|
115
|
+
|
|
116
|
+
const list = (
|
|
117
|
+
<div className="flex flex-col gap-3">
|
|
118
|
+
{bench.error ? <ApiError error={bench.error} title="Could not load the operators" onRetry={() => void bench.refetch()} /> : null}
|
|
119
|
+
{bench.loading && !bench.data ? <Skeleton className="h-20 w-full" aria-label="Loading operators" /> : null}
|
|
120
|
+
<ul className="divide-y rounded-md border">
|
|
121
|
+
{rows.map((o) => (
|
|
122
|
+
<li key={o.operator} className={`flex items-center gap-2 px-3 py-2 ${open === o.operator ? "bg-accent" : ""}`}>
|
|
123
|
+
<button type="button" data-testid={`operator-${o.operator}`} className="min-w-0 flex-1 text-left text-sm" onClick={() => setOpen(o.operator)}>
|
|
124
|
+
<span className="block truncate font-medium">{o.title ?? o.operator}</span>
|
|
125
|
+
<span className="block truncate text-xs text-muted-foreground">{o.detection === false ? "Always on" : `${o.findings_total ?? 0} findings, ${o.findings_critical ?? 0} critical`}</span>
|
|
126
|
+
</button>
|
|
127
|
+
{o.detection === false ? <StatusPill tone="positive" dot>on</StatusPill> : (
|
|
128
|
+
<Button size="sm" variant={o.enabled ? "outline" : "default"} onClick={() => setEnabling({ operator: o, enabled: !o.enabled })}>{o.enabled ? "Turn off" : "Turn on"}</Button>
|
|
129
|
+
)}
|
|
130
|
+
</li>
|
|
131
|
+
))}
|
|
132
|
+
</ul>
|
|
133
|
+
{!bench.loading && !bench.error && rows.length === 0 ? <EmptyState icon={<Cpu strokeWidth={1.5} />} title="No operators" description="Operators appear here once the workspace has them." /> : null}
|
|
134
|
+
{toggle.error ? <ApiError error={toggle.error} title="Could not change the operator" /> : null}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const detail = open ? (
|
|
139
|
+
<DetailPanel title={current?.title ?? open} subtitle={current?.persona} onClose={() => setOpen("")}
|
|
140
|
+
badges={current ? <StatusPill tone={current.enabled ? "positive" : "neutral"} dot>{current.enabled ? "on" : "off"}</StatusPill> : undefined}
|
|
141
|
+
tabs={current?.detection === false ? [{ id: "status", label: "Status", content: <Facts path={`/agent/operators/${enc(open)}/status`} /> }] : [
|
|
142
|
+
{ id: "skills", label: "Skills", content: <><Skills key={`${open}:${dialKey}`} operator={open} onAsk={setDial} />{setLevel.error ? <ApiError error={setLevel.error} title="Could not change the trust dial" /> : null}</> },
|
|
143
|
+
{ id: "findings", label: "Findings", content: <Findings operator={open} /> },
|
|
144
|
+
{ id: "activity", label: "Activity", content: <Activity operator={open} /> },
|
|
145
|
+
{ id: "health", label: "Health", content: <Facts path={`/agent/operators/${enc(open)}/health`} /> },
|
|
146
|
+
{ id: "scorecard", label: "Scorecard", content: <Facts path={`/agent/operators/${enc(open)}/scorecard`} /> },
|
|
147
|
+
{ id: "status", label: "Status", content: <Facts path={`/agent/operators/${enc(open)}/status`} /> },
|
|
148
|
+
]} />
|
|
149
|
+
) : null;
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div className={`flex flex-col gap-4 ${className ?? ""}`}>
|
|
153
|
+
<StatStrip loading={summary.loading && !summary.data} items={summary.error ? [] : Object.entries(sum).filter(([, v]) => typeof v === "number").slice(0, 5).map(([k, v]) => ({ label: k.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase()), value: String(v) }))} />
|
|
154
|
+
{summary.error ? <ApiError compact error={summary.error} title="Could not load the summary" onRetry={() => void summary.refetch()} /> : null}
|
|
155
|
+
<SplitView list={list} detail={detail} detailOpen={Boolean(open)} onCloseDetail={() => setOpen("")} detailWidth="lg" />
|
|
156
|
+
<ConfirmSpendDialog open={enabling != null} tier="billable" title={enabling?.enabled ? `Turn on ${enabling.operator.title ?? enabling.operator.operator}` : `Turn off ${enabling?.operator.title ?? enabling?.operator.operator ?? "operator"}`}
|
|
157
|
+
description={enabling?.enabled ? "Its monitors start running on a schedule and spend credits as they work, without being asked." : "Its monitors stop. Findings already recorded stay."}
|
|
158
|
+
confirmLabel={enabling?.enabled ? "Turn on" : "Turn off"} confirming={toggle.loading} onConfirm={() => enabling && void toggle.mutate({ operator: enabling.operator.operator, enabled: enabling.enabled })} onCancel={() => setEnabling(null)} />
|
|
159
|
+
<ConfirmSpendDialog open={dial != null} tier="billable" title={`Set ${dial?.label ?? "skill"} to ${dial?.autonomy.toLowerCase() ?? ""}`}
|
|
160
|
+
description="The trust dial decides how far this skill may go on its own when it finds something. Higher settings act without asking and spend credits."
|
|
161
|
+
confirmLabel="Set dial" confirming={setLevel.loading} onConfirm={() => dial && open && void setLevel.mutate({ operator: open, key: dial.key, autonomy: dial.autonomy })} onCancel={() => { setDial(null); setDialKey((k) => k + 1); }} />
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
* 2. listAgentApprovals GET /agent/approvals (read) this run's pending ones
|
|
11
11
|
* 3. decideAgentApproval POST /agent/approvals/{approval_id}/decide { decision } (write; approving lets the run act: ConfirmSpendDialog, external)
|
|
12
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.
|
|
13
15
|
* Approvals that gate a Work coordination action cannot be decided with an API key: the error is shown as is.
|
|
14
16
|
*/
|
|
15
|
-
import { useEffect, useState } from "react";
|
|
16
|
-
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
17
|
+
import { useEffect, useRef, useState } from "react";
|
|
18
|
+
import { Button, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
17
19
|
import { ApiError } from "@/g8/components/api-error";
|
|
18
20
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
19
21
|
import { KeyValueList } from "@/g8/components/screen/key-value-list";
|
|
@@ -43,6 +45,8 @@ export function AgentRunDetail({ runId, pollMs = 3000, onChanged, className }: A
|
|
|
43
45
|
const [cancelling, setCancelling] = useState(false);
|
|
44
46
|
const [approving, setApproving] = useState<Approval | null>(null);
|
|
45
47
|
const [deciding, setDeciding] = useState<string | null>(null);
|
|
48
|
+
const [steerText, setSteerText] = useState("");
|
|
49
|
+
const turnId = useRef<string | null>(null);
|
|
46
50
|
const poll = useJobPoll<AgentRun>((signal) => g8Get<AgentRun>(`/agent/runs/${encodeURIComponent(runId)}`, undefined, { signal }).then((r) => bodyOf<AgentRun>(r)), {
|
|
47
51
|
isTerminal: isTerminalRun, intervalMs: pollMs, maxIntervalMs: pollMs * 4, onDone: () => onChanged?.(),
|
|
48
52
|
});
|
|
@@ -56,6 +60,12 @@ export function AgentRunDetail({ runId, pollMs = 3000, onChanged, className }: A
|
|
|
56
60
|
try { return await g8Post(`/agent/approvals/${encodeURIComponent(v.id)}/decide`, { decision: v.decision }); } finally { setDeciding(null); }
|
|
57
61
|
}, { onSuccess: () => { setApproving(null); void approvals.refetch(); onChanged?.(); }, onError: () => setApproving(null) });
|
|
58
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
|
+
|
|
59
69
|
const run = poll.status?.work_item_id === runId ? poll.status : undefined;
|
|
60
70
|
const pending = listOf<Approval>(approvals.data).filter((a) => a.work_item_id === runId && !a.decision && !a.is_expired);
|
|
61
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" />;
|
|
@@ -79,7 +89,7 @@ export function AgentRunDetail({ runId, pollMs = 3000, onChanged, className }: A
|
|
|
79
89
|
]} />
|
|
80
90
|
{pending.length > 0 ? (
|
|
81
91
|
<section className="space-y-2 rounded-md border p-3" aria-label="Approvals" data-testid="run-approvals">
|
|
82
|
-
<h4 className="text-sm font-
|
|
92
|
+
<h4 className="text-sm font-medium">Waiting for your decision ({pending.length})</h4>
|
|
83
93
|
{pending.map((a) => (
|
|
84
94
|
<div key={a.id} className="flex flex-wrap items-center justify-between gap-2 text-sm">
|
|
85
95
|
<span>{a.summary}<span className="ml-2 text-xs text-muted-foreground">risk: {a.risk_level}</span></span>
|
|
@@ -91,6 +101,13 @@ export function AgentRunDetail({ runId, pollMs = 3000, onChanged, className }: A
|
|
|
91
101
|
))}
|
|
92
102
|
</section>
|
|
93
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}
|
|
94
111
|
{decide.error ? <ApiError error={decide.error} title="Could not record the decision" /> : null}
|
|
95
112
|
{cancel.error ? <ApiError error={cancel.error} title="Could not cancel the run" /> : null}
|
|
96
113
|
<ConfirmSpendDialog open={cancelling} tier="destructive" title={`Cancel "${run.title}"`} description="Work stops where it is. Steps already taken are not undone."
|