@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
|
@@ -100,8 +100,8 @@ export function AgentRuns({ agentId, pageSize = 20, pollMs = 3000, selectedId, o
|
|
|
100
100
|
return (
|
|
101
101
|
<div className={`flex flex-col gap-4 ${className ?? ""}`}>
|
|
102
102
|
{pending.length > 0 ? (
|
|
103
|
-
<div className="rounded-md border border-
|
|
104
|
-
<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>
|
|
105
105
|
<ul className="flex flex-col gap-2">
|
|
106
106
|
{pending.map((a) => (
|
|
107
107
|
<li key={a.id} className="flex flex-wrap items-center justify-between gap-2 text-sm">
|
|
@@ -160,7 +160,7 @@ export function AgentRuns({ agentId, pageSize = 20, pollMs = 3000, selectedId, o
|
|
|
160
160
|
<div className="rounded-md border p-3" data-testid="run-detail">
|
|
161
161
|
{current ? (
|
|
162
162
|
<>
|
|
163
|
-
<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>
|
|
164
164
|
<dl className="mt-2 grid grid-cols-2 gap-1 text-sm">
|
|
165
165
|
<dt className="text-muted-foreground">Current step</dt><dd>{current.current_step ?? "-"}</dd>
|
|
166
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
|
+
}
|
|
@@ -10,9 +10,17 @@
|
|
|
10
10
|
* "New agent" opens a form sheet (name, role, what it is for, personality); the agent starts inactive and opens
|
|
11
11
|
* in the detail panel so voice, knowledge and skills can be added next.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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.
|
|
16
24
|
*
|
|
17
25
|
* SDK operations of its own:
|
|
18
26
|
* 1. GET /voice/agents?search&page_size=100 (voice.listAgents) read
|
|
@@ -24,6 +32,9 @@ import { useRef, useState, type ReactNode } from "react";
|
|
|
24
32
|
import { Bot, Plus } from "lucide-react";
|
|
25
33
|
import { Button, Input, Label, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
26
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";
|
|
27
38
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
28
39
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
29
40
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
@@ -38,10 +49,22 @@ import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
|
38
49
|
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
39
50
|
import { DEFAULT_PERSONA, bodyOf, listOf } from "../lib/agent-types";
|
|
40
51
|
import type { AgentSummary } from "../lib/agent-types";
|
|
52
|
+
import { AgentActivity } from "./agent-activity";
|
|
53
|
+
import { AgentApprovals } from "./agent-approvals";
|
|
41
54
|
import { AgentBuilder } from "./agent-builder";
|
|
55
|
+
import { AgentConversations } from "./agent-conversations";
|
|
56
|
+
import { AgentOperators } from "./agent-operators";
|
|
42
57
|
import { AgentRunDetail } from "./agent-run-detail";
|
|
43
58
|
import { AgentRuns } from "./agent-runs";
|
|
44
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";
|
|
45
68
|
|
|
46
69
|
export interface AgentsScreenProps {
|
|
47
70
|
/** Row-level extra actions (for example "Use in a campaign"). */
|
|
@@ -52,7 +75,11 @@ export interface AgentsScreenProps {
|
|
|
52
75
|
className?: string;
|
|
53
76
|
}
|
|
54
77
|
|
|
55
|
-
const SCREEN_TABS = [
|
|
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);
|
|
56
83
|
const FILTERS = [{ id: "", label: "All" }, { id: "active", label: "Active" }, { id: "inactive", label: "Inactive" }];
|
|
57
84
|
|
|
58
85
|
interface NewAgent { name: string; role: string; description: string; persona: string }
|
|
@@ -105,6 +132,9 @@ function AgentRow({ agent, selected, onOpen, extra }: { agent: AgentSummary; sel
|
|
|
105
132
|
);
|
|
106
133
|
}
|
|
107
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
|
+
|
|
108
138
|
export function AgentsScreen({ renderRowActions, renderDetailActions, extraDetailTabs, toolbarActions, className }: AgentsScreenProps) {
|
|
109
139
|
const [tab, setTab] = useUrlState("tab", "agents");
|
|
110
140
|
const [selected, setSelected] = useUrlState("id", "");
|
|
@@ -119,8 +149,12 @@ export function AgentsScreen({ renderRowActions, renderDetailActions, extraDetai
|
|
|
119
149
|
const [runsKey, setRunsKey] = useState(0);
|
|
120
150
|
const q = search.trim();
|
|
121
151
|
|
|
122
|
-
const agents =
|
|
123
|
-
|
|
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 ?? [];
|
|
124
158
|
const rows = filter ? all.filter((a) => a.agent_status === filter) : all;
|
|
125
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.
|
|
126
160
|
const inList = all.find((a) => a.agent_id === selected);
|
|
@@ -129,20 +163,26 @@ export function AgentsScreen({ renderRowActions, renderDetailActions, extraDetai
|
|
|
129
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);
|
|
130
164
|
const active = all.filter((a) => a.agent_status === "active").length;
|
|
131
165
|
|
|
132
|
-
const toggle = useG8Mutation((a: AgentSummary) => g8Put(`/voice/agents/${a.agent_id}`, { agent_status: a.agent_status === "active" ? "inactive" : "active" }), { onSuccess: () => { void agents.refetch(); void byId.refetch(); } });
|
|
133
|
-
const current = tab
|
|
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"); };
|
|
134
169
|
|
|
135
170
|
const detail = open ? (
|
|
136
171
|
<DetailPanel title={open.agent_name} subtitle={open.role} avatar={<EntityAvatar name={open.agent_name} kind="person" size="lg" />}
|
|
137
172
|
badges={<StatusPill tone={tone(open.agent_status)} dot>{open.agent_status}</StatusPill>} onClose={closeAgent} onExpand={() => setView("full")}
|
|
138
173
|
actions={<>
|
|
139
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} />
|
|
140
177
|
{renderDetailActions?.(open)}
|
|
141
178
|
</>}
|
|
142
179
|
tabs={[
|
|
143
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()} /> },
|
|
144
181
|
{ id: "test", label: "Test", content: <AgentTestConsole agentId={open.agent_id} /> },
|
|
145
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} /> },
|
|
146
186
|
...(extraDetailTabs ?? []).map((t) => ({ id: t.id, label: t.label, content: t.render(open) })),
|
|
147
187
|
]}>
|
|
148
188
|
{toggle.error ? <ApiError error={toggle.error} title="Could not change the status" /> : null}
|
|
@@ -158,6 +198,7 @@ export function AgentsScreen({ renderRowActions, renderDetailActions, extraDetai
|
|
|
158
198
|
<ul className="rounded-md border">
|
|
159
199
|
{rows.map((a) => <AgentRow key={a.agent_id} agent={a} selected={a.agent_id === selected} onOpen={() => setSelected(a.agent_id)} extra={renderRowActions?.(a)} />)}
|
|
160
200
|
</ul>
|
|
201
|
+
<LoadMore shown={all.length} noun="agents" hasMore={agents.hasMore} loading={agents.loadingMore} onLoadMore={agents.loadMore} />
|
|
161
202
|
{!agents.loading && !agents.error && rows.length === 0 ? (
|
|
162
203
|
<EmptyState title={all.length ? "No agents match" : "No agents yet"} description="An agent has a persona, a voice, knowledge and skills."
|
|
163
204
|
action={all.length ? undefined : <Button onClick={() => setCreating(true)}>New agent</Button>} />
|
|
@@ -168,13 +209,14 @@ export function AgentsScreen({ renderRowActions, renderDetailActions, extraDetai
|
|
|
168
209
|
return (
|
|
169
210
|
<ScreenLayout className={className} title="Agents" description="Build AI agents, prove they work before they talk to anyone, and follow the work they do."
|
|
170
211
|
icon={<Bot className="size-5" />} tabs={SCREEN_TABS} activeTab={current} onTabChange={setTab}
|
|
171
|
-
actions={
|
|
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></>}
|
|
172
213
|
stats={current === "agents" ? [
|
|
173
|
-
{ label: "Agents", value: String(all.length) },
|
|
214
|
+
{ label: "Agents", value: agents.hasMore ? `${all.length}+` : String(all.length) },
|
|
174
215
|
{ label: "Active", value: String(active), tone: "positive" },
|
|
175
216
|
{ label: "Inactive", value: String(all.filter((a) => a.agent_status === "inactive").length), tone: "warning" },
|
|
176
217
|
] : undefined}>
|
|
177
|
-
{current === "
|
|
218
|
+
{current === "approvals" ? <AgentApprovals onOpenRun={openRun} /> : current === "skills" ? <SkillLibrary /> : current === "knowledge" ? <KnowledgeBrowser />
|
|
219
|
+
: current === "operators" ? <AgentOperators /> : current === "assistant" ? <AssistantSessions onOpenRun={openRun} /> : current === "runs" ? (
|
|
178
220
|
<SplitView list={<AgentRuns hideDetail selectedId={run || null} onSelectRun={(id) => setRun(id ?? "")} refreshKey={runsKey} />} detailOpen={Boolean(run)} onCloseDetail={closeRun} {...expand} detailWidth="lg"
|
|
179
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} />
|
|
180
222
|
) : <SplitView list={list} detail={detail} detailOpen={Boolean(selected)} onCloseDetail={closeAgent} {...expand} detailWidth="xl" />}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AssistantSessions: conversations with the graph8 assistant (the workspace agent that answers questions and starts jobs). The list of
|
|
5
|
+
* your sessions (pinned first, "Load more" pages through all of them), a session's messages and the steps the agent took, and the
|
|
6
|
+
* actions: new session, rename, pin, delete (destructive, confirmed), add a note (no model call; the agent reads it on its next turn)
|
|
7
|
+
* and "Ask" (one live turn: the agent answers, or reads the message as a job and starts a run).
|
|
8
|
+
* A live turn runs a model and may start autonomous work that spends credits, so Ask goes through ConfirmSpendDialog (billable).
|
|
9
|
+
* Live streaming is not available through the API; an answer arrives whole.
|
|
10
|
+
*
|
|
11
|
+
* SDK operations, in order:
|
|
12
|
+
* 1. listAgentSessions GET /agent/sessions?limit&offset (read)
|
|
13
|
+
* 2. createAgentSession POST /agent/sessions { title? } (write)
|
|
14
|
+
* 3. getAgentSessionState GET /agent/sessions/{session_id}/state (read) messages + steps
|
|
15
|
+
* 4. markAgentSessionRead PUT /agent/sessions/{session_id}/read { message_count } (write)
|
|
16
|
+
* 5. renameAgentSession POST /agent/sessions/{session_id}/rename { title } (write)
|
|
17
|
+
* 6. pinAgentSession POST /agent/sessions/{session_id}/pin { is_pinned } (write)
|
|
18
|
+
* 7. deleteAgentSession DELETE /agent/sessions/{session_id} (destructive, ConfirmSpendDialog)
|
|
19
|
+
* 8. postAgentSessionMessage POST /agent/sessions/{session_id}/messages { content } (write, no model call)
|
|
20
|
+
* 9. postAgentChat POST /agent/chat { message, session_id? } (billable, ConfirmSpendDialog)
|
|
21
|
+
*/
|
|
22
|
+
import { useEffect, useRef, useState } from "react";
|
|
23
|
+
import { MessagesSquare, Pin } from "lucide-react";
|
|
24
|
+
import { Button, Input, Skeleton, Textarea } 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 { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
29
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
30
|
+
import { g8Delete, g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
|
|
31
|
+
import { bodyOf, listOf } from "../lib/agent-types";
|
|
32
|
+
import { LoadMore, useLoadMoreList } from "../lib/load-more";
|
|
33
|
+
|
|
34
|
+
export interface AssistantSessionsProps {
|
|
35
|
+
/** Called when a live turn started a run, so the host can open it. */
|
|
36
|
+
onOpenRun?: (runId: string) => void;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface Session { id?: string | null; title?: string | null; is_pinned?: boolean | null; updated_at?: string | null; created_at?: string | null }
|
|
41
|
+
interface Message { role?: string; content?: unknown }
|
|
42
|
+
interface Step { title?: string; summary?: string; tool?: string; status?: string }
|
|
43
|
+
const enc = encodeURIComponent;
|
|
44
|
+
const text = (v: unknown): string => (typeof v === "string" ? v : v == null ? "" : JSON.stringify(v));
|
|
45
|
+
|
|
46
|
+
function Transcript({ sessionId, refreshKey }: { sessionId: string; refreshKey: number }) {
|
|
47
|
+
const state = useG8Query((signal) => g8Get(`/agent/sessions/${enc(sessionId)}/state`, undefined, { signal }), [sessionId, refreshKey]);
|
|
48
|
+
const body = bodyOf<{ messages?: Message[]; steps?: Step[] }>(state.data);
|
|
49
|
+
const messages = body.messages ?? [];
|
|
50
|
+
const lastRead = useRef<string>("");
|
|
51
|
+
const markRead = useG8Mutation((n: number) => g8Put(`/agent/sessions/${enc(sessionId)}/read`, { message_count: n }));
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const key = `${sessionId}:${messages.length}`;
|
|
54
|
+
if (state.data && messages.length > 0 && lastRead.current !== key) { lastRead.current = key; void markRead.mutate(messages.length); }
|
|
55
|
+
}, [state.data, messages.length, sessionId]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
56
|
+
if (state.error) return <ApiError error={state.error} title="Could not load the session" onRetry={() => void state.refetch()} />;
|
|
57
|
+
if (state.loading && !state.data) return <Skeleton className="h-24 w-full" aria-label="Loading session" />;
|
|
58
|
+
return (
|
|
59
|
+
<div className="flex flex-col gap-3">
|
|
60
|
+
{messages.length === 0 ? <p className="text-sm text-muted-foreground">No messages in this session yet.</p> : null}
|
|
61
|
+
<ol className="flex flex-col gap-2" aria-label="Messages">
|
|
62
|
+
{messages.map((m, i) => <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"}`}><span className="block text-xs text-muted-foreground">{m.role === "user" ? "You" : "Assistant"}</span><span className="whitespace-pre-wrap">{text(m.content)}</span></li>)}
|
|
63
|
+
</ol>
|
|
64
|
+
{(body.steps ?? []).length ? <details className="text-sm"><summary className="cursor-pointer text-muted-foreground">What the agent did ({body.steps?.length})</summary><ul className="mt-2 list-disc pl-5">{(body.steps ?? []).map((s, i) => <li key={i}>{s.title ?? s.summary ?? s.tool}{s.status ? ` (${s.status})` : ""}</li>)}</ul></details> : null}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function AssistantSessions({ onOpenRun, className }: AssistantSessionsProps) {
|
|
70
|
+
const [openId, setOpenId] = useState<string | null>(null);
|
|
71
|
+
const [draft, setDraft] = useState("");
|
|
72
|
+
const [asking, setAsking] = useState<string | null>(null);
|
|
73
|
+
const [renaming, setRenaming] = useState<string | null>(null);
|
|
74
|
+
const [title, setTitle] = useState("");
|
|
75
|
+
const [deleting, setDeleting] = useState<Session | null>(null);
|
|
76
|
+
const [reply, setReply] = useState<{ message?: string; kind?: string; work_item_id?: string } | null>(null);
|
|
77
|
+
const [turnKey, setTurn] = useState(0);
|
|
78
|
+
const list = useLoadMoreList<Session>(async (offset, limit, signal) => listOf<Session>(await g8Get("/agent/sessions", { limit, offset }, { signal }), "sessions", "items"), [], { pageSize: 50 });
|
|
79
|
+
const rows = [...list.items].sort((a, b) => Number(Boolean(b.is_pinned)) - Number(Boolean(a.is_pinned)));
|
|
80
|
+
const open = rows.find((s) => s.id === openId) ?? null;
|
|
81
|
+
|
|
82
|
+
const create = useG8Mutation(async () => bodyOf<Session>(await g8Post("/agent/sessions", {})).id ?? null, { onSuccess: (id) => { list.reload(); if (id) { setOpenId(id); setReply(null); } } });
|
|
83
|
+
const rename = useG8Mutation((v: { id: string; title: string }) => g8Post(`/agent/sessions/${enc(v.id)}/rename`, { title: v.title }), { onSuccess: () => { setRenaming(null); list.reload(); } });
|
|
84
|
+
const pin = useG8Mutation((v: { id: string; pinned: boolean }) => g8Post(`/agent/sessions/${enc(v.id)}/pin`, { is_pinned: v.pinned }), { onSuccess: () => list.reload() });
|
|
85
|
+
const remove = useG8Mutation((id: string) => g8Delete(`/agent/sessions/${enc(id)}`), { onSuccess: (_r, id) => { setDeleting(null); if (openId === id) setOpenId(null); list.reload(); }, onError: () => setDeleting(null) });
|
|
86
|
+
const note = useG8Mutation((v: { id: string; content: string }) => g8Post(`/agent/sessions/${enc(v.id)}/messages`, { content: v.content }), { onSuccess: () => { setDraft(""); setTurn((t) => t + 1); } });
|
|
87
|
+
// One id per turn, reused when the same turn is retried, so a retry after a timeout cannot be charged twice (the API dedupes on it within a session).
|
|
88
|
+
const turn = useRef<{ id: string; text: string } | null>(null);
|
|
89
|
+
const ask = useG8Mutation(async (message: string) => {
|
|
90
|
+
if (turn.current?.text !== message) turn.current = { id: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`, text: message };
|
|
91
|
+
return bodyOf<{ message?: string; kind?: string; work_item_id?: string; session_id?: string }>(await g8Post("/agent/chat", { message, ...(openId ? { session_id: openId, client_turn_id: turn.current.id } : {}) }));
|
|
92
|
+
},
|
|
93
|
+
{ onSuccess: (r) => { turn.current = null; setAsking(null); setDraft(""); setReply(r); setTurn((t) => t + 1); list.reload(); if (!openId && (r as { session_id?: string }).session_id) setOpenId((r as { session_id?: string }).session_id ?? null); }, onError: () => setAsking(null) });
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className={`grid gap-4 lg:grid-cols-[18rem_1fr] ${className ?? ""}`}>
|
|
97
|
+
<div className="flex flex-col gap-2">
|
|
98
|
+
<div className="flex items-center justify-between"><h3 className="text-sm font-medium">Sessions</h3><Button size="sm" variant="outline" loading={create.loading} onClick={() => void create.mutate(undefined)}>New session</Button></div>
|
|
99
|
+
{list.error ? <ApiError error={list.error} title="Could not load sessions" onRetry={list.reload} /> : null}
|
|
100
|
+
{list.loading ? <Skeleton className="h-16 w-full" aria-label="Loading sessions" /> : null}
|
|
101
|
+
<ul className="divide-y rounded-md border">
|
|
102
|
+
{rows.map((s) => (
|
|
103
|
+
<li key={s.id} className={s.id === openId ? "bg-accent" : ""}>
|
|
104
|
+
<button type="button" data-testid={`session-${s.id}`} className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm" onClick={() => { setOpenId(s.id ?? null); setReply(null); setRenaming(null); }}>
|
|
105
|
+
{s.is_pinned ? <Pin className="size-3.5 shrink-0" aria-label="Pinned" /> : null}
|
|
106
|
+
<span className="min-w-0"><span className="block truncate font-medium">{s.title || "Untitled session"}</span><span className="block text-xs text-muted-foreground">{s.updated_at ? new Date(s.updated_at).toLocaleString() : ""}</span></span>
|
|
107
|
+
</button>
|
|
108
|
+
</li>
|
|
109
|
+
))}
|
|
110
|
+
</ul>
|
|
111
|
+
{!list.loading && !list.error && rows.length === 0 ? <EmptyState compact icon={<MessagesSquare strokeWidth={1.5} />} title="No sessions yet" description="Ask the assistant something to start one." /> : null}
|
|
112
|
+
<LoadMore list={list} noun="sessions" />
|
|
113
|
+
{create.error ? <ApiError error={create.error} title="Could not start a session" /> : null}
|
|
114
|
+
</div>
|
|
115
|
+
<div className="flex min-w-0 flex-col gap-3">
|
|
116
|
+
{open?.id ? (
|
|
117
|
+
<>
|
|
118
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
119
|
+
{renaming === open.id ? (
|
|
120
|
+
<><Input aria-label="Session title" className="max-w-xs" value={title} onChange={(e) => setTitle(e.target.value)} /><Button size="sm" loading={rename.loading} disabled={!title.trim()} onClick={() => void rename.mutate({ id: open.id as string, title: title.trim() })}>Save title</Button><Button size="sm" variant="ghost" onClick={() => setRenaming(null)}>Cancel</Button></>
|
|
121
|
+
) : (
|
|
122
|
+
<><h3 className="text-sm font-medium">{open.title || "Untitled session"}</h3><Button size="sm" variant="outline" onClick={() => { setTitle(open.title ?? ""); setRenaming(open.id ?? null); }}>Rename</Button>
|
|
123
|
+
<Button size="sm" variant="outline" loading={pin.loading} onClick={() => void pin.mutate({ id: open.id as string, pinned: !open.is_pinned })}>{open.is_pinned ? "Unpin" : "Pin"}</Button><Button size="sm" variant="ghost" onClick={() => setDeleting(open)}>Delete</Button></>
|
|
124
|
+
)}
|
|
125
|
+
</div>
|
|
126
|
+
{rename.error ? <ApiError error={rename.error} title="Could not rename the session" /> : null}
|
|
127
|
+
{pin.error ? <ApiError error={pin.error} title="Could not pin the session" /> : null}
|
|
128
|
+
<Transcript sessionId={open.id} refreshKey={turnKey} />
|
|
129
|
+
</>
|
|
130
|
+
) : <p className="text-sm text-muted-foreground">Pick a session, or ask below to start a new one.</p>}
|
|
131
|
+
{reply?.message ? <p role="status" className="rounded-md border bg-accent px-3 py-2 text-sm" data-testid="assistant-reply">{reply.message}</p> : null}
|
|
132
|
+
{reply?.kind === "work_item" && reply.work_item_id ? <p role="status" className="text-sm">The assistant started a run. {onOpenRun ? <button type="button" className="underline" onClick={() => onOpenRun(reply.work_item_id as string)}>Open the run</button> : <span className="text-muted-foreground">Follow it on the Runs tab ({reply.work_item_id}).</span>}</p> : null}
|
|
133
|
+
<div className="flex flex-col gap-2 border-t pt-3">
|
|
134
|
+
<Textarea aria-label="Message to the assistant" rows={3} value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="Ask the assistant, or leave it a note" />
|
|
135
|
+
{ask.error ? <ApiError error={ask.error} title="The assistant could not answer" /> : null}
|
|
136
|
+
{note.error ? <ApiError error={note.error} title="Could not add the note" /> : null}
|
|
137
|
+
<div className="flex justify-end gap-2">
|
|
138
|
+
<Button variant="outline" loading={note.loading} disabled={!draft.trim() || !open?.id} onClick={() => open?.id && void note.mutate({ id: open.id, content: draft.trim() })}>Add a note</Button>
|
|
139
|
+
<Button disabled={!draft.trim()} onClick={() => setAsking(draft.trim())}>Ask</Button>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
{remove.error ? <ApiError error={remove.error} title="Could not delete the session" /> : null}
|
|
144
|
+
<ConfirmSpendDialog open={asking != null} tier="billable" title="Ask the assistant" description="The assistant runs a model for this turn and charges for it. If it reads your message as a job, it starts a run that works on its own and spends credits as it goes."
|
|
145
|
+
confirmLabel="Ask" confirming={ask.loading} onConfirm={() => asking && void ask.mutate(asking)} onCancel={() => setAsking(null)} />
|
|
146
|
+
<ConfirmSpendDialog open={deleting != null} tier="destructive" title={`Delete "${deleting?.title || "this session"}"`} description="The conversation and its history are removed." affected={1}
|
|
147
|
+
confirmLabel="Delete" confirming={remove.loading} onConfirm={() => deleting?.id && void remove.mutate(deleting.id)} onCancel={() => setDeleting(null)} />
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DuplicateAgentButton: copy an agent under a new name. The copy keeps the persona, voice, knowledge and skills and is a separate agent
|
|
5
|
+
* from then on. The copy is made once: if the dialog is retried after a failure, the id of the copy already made is reused.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations:
|
|
8
|
+
* cloneAgent POST /voice/agents/clone { source_agent_id, new_agent_name } (write)
|
|
9
|
+
*/
|
|
10
|
+
import { useRef, useState } from "react";
|
|
11
|
+
import { Button } from "@graph8/sdk/ui";
|
|
12
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
13
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
14
|
+
import { agentIdOf } from "../lib/agent-types";
|
|
15
|
+
import type { AgentSummary } from "../lib/agent-types";
|
|
16
|
+
import { NameDialog } from "./name-dialog";
|
|
17
|
+
|
|
18
|
+
export interface DuplicateAgentButtonProps {
|
|
19
|
+
agent: Pick<AgentSummary, "agent_id" | "agent_name">;
|
|
20
|
+
/** Called with the id of the new agent. */
|
|
21
|
+
onDuplicated?: (agentId: string) => void;
|
|
22
|
+
variant?: "outline" | "ghost";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function DuplicateAgentButton({ agent, onDuplicated, variant = "outline" }: DuplicateAgentButtonProps) {
|
|
26
|
+
const [open, setOpen] = useState(false);
|
|
27
|
+
const [name, setName] = useState("");
|
|
28
|
+
const made = useRef<string | null>(null);
|
|
29
|
+
const clone = useG8Mutation(async (n: string) => {
|
|
30
|
+
if (made.current) return made.current;
|
|
31
|
+
const id = agentIdOf(await g8Post("/voice/agents/clone", { source_agent_id: agent.agent_id, new_agent_name: n }));
|
|
32
|
+
made.current = id || null;
|
|
33
|
+
return id;
|
|
34
|
+
}, { onSuccess: (id) => { made.current = null; setOpen(false); if (id) onDuplicated?.(id); } });
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
<Button size="sm" variant={variant} onClick={() => { made.current = null; clone.reset(); setName(`Copy of ${agent.agent_name}`); setOpen(true); }}>Duplicate</Button>
|
|
38
|
+
<NameDialog open={open} title={`Duplicate ${agent.agent_name}`} description="The copy keeps the persona, voice, knowledge and skills. It is a separate agent from now on." label="Name of the copy"
|
|
39
|
+
value={name} onChange={setName} confirm="Duplicate" busy={clone.loading} error={clone.error} onCancel={() => setOpen(false)} onConfirm={() => void clone.mutate(name.trim())} />
|
|
40
|
+
</>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FromTemplateButton: start a new agent from a graph8 or workspace template. Pick a template, name the agent, and it is created as a copy
|
|
5
|
+
* (adjust it afterwards). The copy is made once: a retry after a failure reuses the id of the copy already made.
|
|
6
|
+
*
|
|
7
|
+
* SDK operations, in order:
|
|
8
|
+
* 1. listAgentTemplates GET /voice/agent-templates?entity_type=agent&include_system_templates=true (read)
|
|
9
|
+
* 2. cloneAgent POST /voice/agents/clone { source_agent_id, new_agent_name } (write)
|
|
10
|
+
*/
|
|
11
|
+
import { useRef, useState } from "react";
|
|
12
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
13
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
14
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
15
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
16
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
17
|
+
import { agentIdOf, listOf } from "../lib/agent-types";
|
|
18
|
+
|
|
19
|
+
export interface FromTemplateButtonProps {
|
|
20
|
+
/** Called with the id of the new agent. */
|
|
21
|
+
onCreated?: (agentId: string) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface Template { agent_id: string; agent_name: string; role?: string; description?: string | null }
|
|
25
|
+
|
|
26
|
+
export function FromTemplateButton({ onCreated }: FromTemplateButtonProps) {
|
|
27
|
+
const [open, setOpen] = useState(false);
|
|
28
|
+
const [picked, setPicked] = useState<Template | null>(null);
|
|
29
|
+
const [name, setName] = useState("");
|
|
30
|
+
const made = useRef<string | null>(null);
|
|
31
|
+
const templates = useG8Query((signal) => g8Get("/voice/agent-templates", { entity_type: "agent", include_system_templates: true }, { signal }), [], { enabled: open });
|
|
32
|
+
const create = useG8Mutation(async (v: { source: string; name: string }) => {
|
|
33
|
+
if (made.current) return made.current;
|
|
34
|
+
const id = agentIdOf(await g8Post("/voice/agents/clone", { source_agent_id: v.source, new_agent_name: v.name }));
|
|
35
|
+
made.current = id || null;
|
|
36
|
+
return id;
|
|
37
|
+
}, { onSuccess: (id) => { made.current = null; setOpen(false); setPicked(null); if (id) onCreated?.(id); } });
|
|
38
|
+
const rows = listOf<Template>(templates.data, "templates", "items");
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
<Button variant="outline" onClick={() => { made.current = null; create.reset(); setOpen(true); }}>From a template</Button>
|
|
42
|
+
<Dialog open={open} onOpenChange={(o) => !o && !create.loading && setOpen(false)}>
|
|
43
|
+
<DialogContent>
|
|
44
|
+
<DialogHeader><DialogTitle>Start from a template</DialogTitle><DialogDescription>Pick a template, name your agent, and adjust it afterwards.</DialogDescription></DialogHeader>
|
|
45
|
+
{templates.error ? <ApiError error={templates.error} title="Could not load templates" onRetry={() => void templates.refetch()} /> : null}
|
|
46
|
+
{templates.loading && !templates.data ? <Skeleton className="h-16 w-full" /> : null}
|
|
47
|
+
<ul className="max-h-64 divide-y overflow-y-auto rounded-md border">
|
|
48
|
+
{rows.map((t) => (
|
|
49
|
+
<li key={t.agent_id}>
|
|
50
|
+
<button type="button" aria-pressed={picked?.agent_id === t.agent_id} className={`block w-full px-3 py-2 text-left text-sm ${picked?.agent_id === t.agent_id ? "bg-accent" : ""}`} onClick={() => { setPicked(t); setName(t.agent_name); }}>
|
|
51
|
+
<span className="font-medium">{t.agent_name}</span>{t.role ? <span className="ml-2 text-xs text-muted-foreground">{t.role}</span> : null}
|
|
52
|
+
{t.description ? <span className="block text-xs text-muted-foreground">{t.description}</span> : null}
|
|
53
|
+
</button>
|
|
54
|
+
</li>
|
|
55
|
+
))}
|
|
56
|
+
</ul>
|
|
57
|
+
{!templates.loading && !templates.error && rows.length === 0 ? <p className="text-sm text-muted-foreground">No templates yet. Save an agent as a template first.</p> : null}
|
|
58
|
+
{picked ? <div className="flex flex-col gap-1"><Label htmlFor="template-agent-name">Name of the new agent</Label><Input id="template-agent-name" value={name} onChange={(e) => setName(e.target.value)} /></div> : null}
|
|
59
|
+
{create.error ? <ApiError error={create.error} title="Could not create the agent" /> : null}
|
|
60
|
+
<DialogFooter><Button variant="ghost" disabled={create.loading} onClick={() => setOpen(false)}>Cancel</Button><Button loading={create.loading} disabled={!picked || !name.trim()} onClick={() => picked && void create.mutate({ source: picked.agent_id, name: name.trim() })}>Create agent</Button></DialogFooter>
|
|
61
|
+
</DialogContent>
|
|
62
|
+
</Dialog>
|
|
63
|
+
</>
|
|
64
|
+
);
|
|
65
|
+
}
|