@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,107 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* KnowledgeBrowser: the company knowledge agents draw on. A status strip (entries, how many are draft, stable or deprecated, sync state),
|
|
5
|
+
* search (meaning-based, with an excerpt) or browse (filter by type and lifecycle), the list, and the open entry in a side panel
|
|
6
|
+
* (KnowledgeDetail, deep link ?concept=). "New entry" writes one (KnowledgeForm). "Export" downloads everything as a single archive.
|
|
7
|
+
* The list loads 25 at a time and "Load more" asks the API for more, up to its maximum of 100; at the maximum it says so.
|
|
8
|
+
*
|
|
9
|
+
* SDK operations, in order:
|
|
10
|
+
* 1. getKnowledgeStatus GET /knowledge/status (read)
|
|
11
|
+
* 2. listKnowledge GET /knowledge?concept_type&lifecycle&limit (read)
|
|
12
|
+
* 3. searchKnowledge GET /knowledge/search?query&limit=50 (read) when the search box has text
|
|
13
|
+
* 4. exportKnowledge GET /knowledge/export (read) a link to the archive (opened in a new tab)
|
|
14
|
+
* 5. KnowledgeDetail / KnowledgeForm see their files (get, history, sources, related, author, verify, deprecate)
|
|
15
|
+
*/
|
|
16
|
+
import { useMemo, useState } from "react";
|
|
17
|
+
import { BookOpen, Plus } from "lucide-react";
|
|
18
|
+
import { Button, Input } from "@graph8/sdk/ui";
|
|
19
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
20
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
21
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
22
|
+
import { StatStrip } from "@/g8/components/screen/stat-strip";
|
|
23
|
+
import { StatusPill, toneForStatus } from "@/g8/components/screen/status-pill";
|
|
24
|
+
import { Toolbar } from "@/g8/components/screen/toolbar";
|
|
25
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
26
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
27
|
+
import { g8Get } from "@/g8/lib/g8-client";
|
|
28
|
+
import { bodyOf, listOf } from "../lib/agent-types";
|
|
29
|
+
import { LIFECYCLES } from "../lib/knowledge-utils";
|
|
30
|
+
import type { KnowledgeSummary } from "../lib/knowledge-utils";
|
|
31
|
+
import { LoadMore, useGrowingList } from "../lib/load-more";
|
|
32
|
+
import { KnowledgeDetail } from "./knowledge-detail";
|
|
33
|
+
import { KnowledgeForm } from "./knowledge-form";
|
|
34
|
+
|
|
35
|
+
export interface KnowledgeBrowserProps {
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface Hit { summary?: KnowledgeSummary; excerpt?: string }
|
|
40
|
+
interface Status { concept_count?: number; counts_by_lifecycle?: Record<string, number>; sync_state?: string; capabilities?: Record<string, boolean> }
|
|
41
|
+
|
|
42
|
+
// A constant: the form resets its draft when `mode` changes identity, so it must not be rebuilt on every render.
|
|
43
|
+
const NEW_MODE = { kind: "new" } as const;
|
|
44
|
+
|
|
45
|
+
export function KnowledgeBrowser({ className }: KnowledgeBrowserProps) {
|
|
46
|
+
const [open, setOpen] = useUrlState("concept", "");
|
|
47
|
+
const [view, setView] = useUrlState("view", "");
|
|
48
|
+
const [search, setSearch] = useState("");
|
|
49
|
+
const [type, setType] = useState("");
|
|
50
|
+
const [lifecycle, setLifecycle] = useState("");
|
|
51
|
+
const [creating, setCreating] = useState(false);
|
|
52
|
+
const q = search.trim();
|
|
53
|
+
const status = useG8Query((signal) => g8Get("/knowledge/status", undefined, { signal }), []);
|
|
54
|
+
const list = useGrowingList<KnowledgeSummary>(async (limit, signal) => listOf<KnowledgeSummary>(await g8Get("/knowledge", { concept_type: type.trim() || undefined, lifecycle: lifecycle || undefined, limit }, { signal }), "items", "concepts"), [type, lifecycle], { pageSize: 25, max: 100, enabled: !q });
|
|
55
|
+
const hits = useG8Query((signal) => g8Get("/knowledge/search", { query: q, limit: 50 }, { signal }), [q], { enabled: Boolean(q) });
|
|
56
|
+
const st = bodyOf<Status>(status.data);
|
|
57
|
+
const lc = st.counts_by_lifecycle ?? {};
|
|
58
|
+
const rows: Array<{ s: KnowledgeSummary; excerpt?: string }> = useMemo(() => q
|
|
59
|
+
? listOf<Hit>(hits.data).filter((h) => h.summary).map((h) => ({ s: h.summary as KnowledgeSummary, excerpt: h.excerpt }))
|
|
60
|
+
: list.items.map((s) => ({ s })), [q, hits.data, list.items]);
|
|
61
|
+
const types = useMemo(() => [...new Set(list.items.map((s) => s.concept_type).filter(Boolean))], [list.items]);
|
|
62
|
+
const loading = q ? hits.loading && !hits.data : list.loading;
|
|
63
|
+
const error = q ? hits.error : list.error;
|
|
64
|
+
const expanded = view === "full" && Boolean(open);
|
|
65
|
+
const refresh = () => { void status.refetch(); list.reload(); };
|
|
66
|
+
|
|
67
|
+
const listing = (
|
|
68
|
+
<div className="flex flex-col gap-3">
|
|
69
|
+
<Toolbar search={{ value: search, onChange: setSearch, placeholder: "Search company knowledge" }}
|
|
70
|
+
filters={q ? undefined : <><Input aria-label="Filter by type" placeholder="Any type" list="kn-filter-types" className="h-8 w-36" value={type} onChange={(e) => setType(e.target.value)} />
|
|
71
|
+
<datalist id="kn-filter-types">{types.map((t) => <option key={t} value={t} />)}</datalist>
|
|
72
|
+
<select aria-label="Lifecycle" className="h-8 rounded-md border bg-background px-2 text-sm" value={lifecycle} onChange={(e) => setLifecycle(e.target.value)}><option value="">Any state</option>{LIFECYCLES.map((l) => <option key={l} value={l}>{l}</option>)}</select></>}
|
|
73
|
+
actions={<><Button size="sm" variant="outline" asChild><a href="/api/g8/knowledge/export" target="_blank" rel="noreferrer">Export</a></Button><Button size="sm" onClick={() => setCreating(true)}><Plus className="mr-1 size-4" />New entry</Button></>} />
|
|
74
|
+
{error ? <ApiError error={error} title="Could not load company knowledge" onRetry={() => (q ? void hits.refetch() : list.reload())} /> : null}
|
|
75
|
+
{loading ? <p className="text-sm text-muted-foreground" aria-busy="true">Loading knowledge...</p> : null}
|
|
76
|
+
<ul className="divide-y rounded-md border">
|
|
77
|
+
{rows.map(({ s, excerpt }) => (
|
|
78
|
+
<li key={s.concept_id} className={open === s.concept_id ? "bg-accent" : ""}>
|
|
79
|
+
<button type="button" data-testid={`concept-${s.concept_id}`} className="flex w-full items-start justify-between gap-3 px-3 py-2 text-left text-sm" onClick={() => setOpen(s.concept_id)}>
|
|
80
|
+
<span className="min-w-0"><span className="block truncate font-medium">{s.title}</span><span className="block truncate text-xs text-muted-foreground">{excerpt || s.logical_path || s.concept_type}</span></span>
|
|
81
|
+
<span className="flex shrink-0 items-center gap-1.5"><span className="text-xs text-muted-foreground">{s.concept_type}</span><StatusPill tone={toneForStatus(s.knowledge_status)} dot>{s.knowledge_status ?? "unknown"}</StatusPill></span>
|
|
82
|
+
</button>
|
|
83
|
+
</li>
|
|
84
|
+
))}
|
|
85
|
+
</ul>
|
|
86
|
+
{!loading && !error && rows.length === 0 ? <EmptyState icon={<BookOpen strokeWidth={1.5} />} title={q ? "Nothing matches" : "No knowledge yet"} description={q ? "Try other words." : "Write down how your company sells, prices and supports, so agents can use it."} action={q ? undefined : <Button onClick={() => setCreating(true)}>New entry</Button>} /> : null}
|
|
87
|
+
{q && rows.length >= 50 ? <p className="text-center text-xs text-muted-foreground">Showing the 50 best matches. Narrow the search to see others.</p> : null}
|
|
88
|
+
{!q ? <LoadMore list={list} noun="entries" /> : null}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div className={`flex flex-col gap-4 ${className ?? ""}`}>
|
|
94
|
+
<StatStrip loading={status.loading && !status.data} items={status.error ? [] : [
|
|
95
|
+
{ label: "Entries", value: String(st.concept_count ?? 0) },
|
|
96
|
+
{ label: "Stable", value: String(lc.stable ?? 0), tone: "positive" },
|
|
97
|
+
{ label: "Draft", value: String(lc.draft ?? 0), tone: "warning" },
|
|
98
|
+
{ label: "Deprecated", value: String(lc.deprecated ?? 0) },
|
|
99
|
+
{ label: "Sync", value: st.sync_state ?? "unknown" },
|
|
100
|
+
]} />
|
|
101
|
+
{status.error ? <ApiError compact error={status.error} title="Could not load the knowledge status" onRetry={() => void status.refetch()} /> : null}
|
|
102
|
+
<SplitView list={listing} detailOpen={Boolean(open)} onCloseDetail={() => { setOpen(""); setView(""); }} expanded={expanded} onExpandedChange={(e) => setView(e ? "full" : "")} detailWidth="xl"
|
|
103
|
+
detail={open ? <KnowledgeDetail key={open} conceptId={open} onClose={() => { setOpen(""); setView(""); }} onExpand={expanded ? undefined : () => setView("full")} onCollapse={expanded ? () => setView("") : undefined} onOpenConcept={setOpen} onChanged={refresh} /> : null} />
|
|
104
|
+
<KnowledgeForm mode={creating ? NEW_MODE : null} typeSuggestions={types} onClose={() => setCreating(false)} onSaved={(id) => { refresh(); if (id) setOpen(id); }} />
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* KnowledgeDetail: one company knowledge entry in full. Tabs: Content (the Markdown, warnings), History (every version and amendment, newest
|
|
5
|
+
* first, "Load more" follows the cursor), Sources (what it was written from) and Related (linked entries, which open in place).
|
|
6
|
+
* Actions for authored entries (generated ones come from your CRM and campaigns and are read only):
|
|
7
|
+
* Edit draft / Propose new version (KnowledgeForm), Verify (approve or reject, with the authority that signs off), Deprecate (a reason, optionally
|
|
8
|
+
* the entry that replaces it; destructive, confirmed).
|
|
9
|
+
*
|
|
10
|
+
* SDK operations, in order:
|
|
11
|
+
* 1. getKnowledge GET /knowledge/{concept_id} (read)
|
|
12
|
+
* 2. getKnowledgeHistory GET /knowledge/{concept_id}/history?cursor&limit (read)
|
|
13
|
+
* 3. knowledgeSources GET /knowledge/{concept_id}/sources (read)
|
|
14
|
+
* 4. relatedKnowledge GET /knowledge/{concept_id}/related?depth=1&limit=20 (read)
|
|
15
|
+
* 5. verifyAuthoredKnowledge POST /knowledge/authored/{concept_id}/verify { approve, authority } (write)
|
|
16
|
+
* 6. deprecateAuthoredKnowledge POST /knowledge/authored/{concept_id}/deprecate { reason, superseded_by? } (destructive, ConfirmSpendDialog)
|
|
17
|
+
* 7. KnowledgeForm: create/update draft/amend (see knowledge-form.tsx)
|
|
18
|
+
*/
|
|
19
|
+
import { useEffect, useState } from "react";
|
|
20
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label, Skeleton } from "@graph8/sdk/ui";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
23
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
24
|
+
import { StatusPill, toneForStatus } from "@/g8/components/screen/status-pill";
|
|
25
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
26
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
27
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
28
|
+
import { bodyOf, listOf } from "../lib/agent-types";
|
|
29
|
+
import { bodyFromMarkdown, conceptOf, isAuthored } from "../lib/knowledge-utils";
|
|
30
|
+
import { KnowledgeForm } from "./knowledge-form";
|
|
31
|
+
import type { KnowledgeFormMode } from "./knowledge-form";
|
|
32
|
+
|
|
33
|
+
export interface KnowledgeDetailProps {
|
|
34
|
+
conceptId: string;
|
|
35
|
+
onClose?: () => void;
|
|
36
|
+
onExpand?: () => void;
|
|
37
|
+
onCollapse?: () => void;
|
|
38
|
+
/** Open another entry (from Related). */
|
|
39
|
+
onOpenConcept?: (conceptId: string) => void;
|
|
40
|
+
/** After any change, so the host can refresh its list. */
|
|
41
|
+
onChanged?: () => void;
|
|
42
|
+
className?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type Row = Record<string, unknown>;
|
|
46
|
+
const str = (v: unknown): string => (typeof v === "string" ? v : typeof v === "number" ? String(v) : "");
|
|
47
|
+
|
|
48
|
+
function History({ conceptId }: { conceptId: string }) {
|
|
49
|
+
const [rows, setRows] = useState<Row[]>([]);
|
|
50
|
+
const [cursor, setCursor] = useState<string | undefined>();
|
|
51
|
+
const [next, setNext] = useState<string | undefined>();
|
|
52
|
+
const q = useG8Query((signal) => g8Get(`/knowledge/${encodeURIComponent(conceptId)}/history`, { limit: 25, cursor }, { signal }), [conceptId, cursor]);
|
|
53
|
+
useEffect(() => { setRows([]); setCursor(undefined); setNext(undefined); }, [conceptId]);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!q.data) return;
|
|
56
|
+
const page = listOf<Row>(q.data, "items", "versions", "revisions", "history");
|
|
57
|
+
setRows((prev) => (cursor ? [...prev, ...page] : page));
|
|
58
|
+
const body = bodyOf<{ next_cursor?: string | null }>(q.data);
|
|
59
|
+
setNext(body.next_cursor ?? (q.data as { pagination?: { next_cursor?: string | null } }).pagination?.next_cursor ?? undefined);
|
|
60
|
+
}, [q.data]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
61
|
+
if (q.error) return <ApiError error={q.error} title="Could not load the history" onRetry={() => void q.refetch()} />;
|
|
62
|
+
if (q.loading && rows.length === 0) return <Skeleton className="h-16 w-full" />;
|
|
63
|
+
if (rows.length === 0) return <p className="text-sm text-muted-foreground">No revisions recorded.</p>;
|
|
64
|
+
return (
|
|
65
|
+
<div className="flex flex-col gap-2">
|
|
66
|
+
<ul className="divide-y rounded-md border" aria-label="Revisions">
|
|
67
|
+
{rows.map((r, i) => (
|
|
68
|
+
<li key={i} className="flex flex-wrap items-center justify-between gap-2 px-3 py-2 text-sm">
|
|
69
|
+
<span>{str(r.summary ?? r.title ?? r.change ?? r.action ?? r.version ?? r.revision) || `Revision ${rows.length - i}`}<span className="block text-xs text-muted-foreground">{str(r.actor ?? r.author ?? r.actor_id)}</span></span>
|
|
70
|
+
<span className="flex items-center gap-2">{str(r.knowledge_status ?? r.status) ? <StatusPill tone={toneForStatus(str(r.knowledge_status ?? r.status))}>{str(r.knowledge_status ?? r.status)}</StatusPill> : null}<span className="text-xs text-muted-foreground">{str(r.created_at ?? r.at ?? r.recorded_at) ? new Date(str(r.created_at ?? r.at ?? r.recorded_at)).toLocaleString() : ""}</span></span>
|
|
71
|
+
</li>
|
|
72
|
+
))}
|
|
73
|
+
</ul>
|
|
74
|
+
{next ? <Button size="sm" variant="outline" className="self-center" loading={q.loading} onClick={() => setCursor(next)}>Load more revisions</Button> : null}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function KnowledgeDetail({ conceptId, onClose, onExpand, onCollapse, onOpenConcept, onChanged, className }: KnowledgeDetailProps) {
|
|
80
|
+
const [form, setForm] = useState<KnowledgeFormMode | null>(null);
|
|
81
|
+
const [verifying, setVerifying] = useState<{ approve: boolean; authority: string } | null>(null);
|
|
82
|
+
const [deprecating, setDeprecating] = useState<{ reason: string; superseded_by: string } | null>(null);
|
|
83
|
+
const [confirmDeprecate, setConfirmDeprecate] = useState(false);
|
|
84
|
+
useEffect(() => { setForm(null); setVerifying(null); setDeprecating(null); setConfirmDeprecate(false); }, [conceptId]);
|
|
85
|
+
const enc = encodeURIComponent(conceptId);
|
|
86
|
+
const concept = useG8Query((signal) => g8Get(`/knowledge/${enc}`, undefined, { signal }), [conceptId]);
|
|
87
|
+
const sources = useG8Query((signal) => g8Get(`/knowledge/${enc}/sources`, undefined, { signal }), [conceptId]);
|
|
88
|
+
const related = useG8Query((signal) => g8Get(`/knowledge/${enc}/related`, { depth: 1, limit: 20 }, { signal }), [conceptId]);
|
|
89
|
+
const c = conceptOf(concept.data);
|
|
90
|
+
const done = () => { void concept.refetch(); onChanged?.(); };
|
|
91
|
+
const verify = useG8Mutation((v: { approve: boolean; authority: string }) => g8Post(`/knowledge/authored/${enc}/verify`, v), { onSuccess: () => { setVerifying(null); done(); } });
|
|
92
|
+
const deprecate = useG8Mutation((v: { reason: string; superseded_by: string }) => g8Post(`/knowledge/authored/${enc}/deprecate`, { reason: v.reason, ...(v.superseded_by ? { superseded_by: v.superseded_by } : {}) }), { onSuccess: () => { setConfirmDeprecate(false); setDeprecating(null); done(); }, onError: () => setConfirmDeprecate(false) });
|
|
93
|
+
|
|
94
|
+
if (concept.error) return <ApiError error={concept.error} title="Could not load the entry" onRetry={() => void concept.refetch()} />;
|
|
95
|
+
if (!c) return <Skeleton className="h-40 w-full" aria-label="Loading entry" />;
|
|
96
|
+
const s = c.summary;
|
|
97
|
+
const authored = isAuthored(s);
|
|
98
|
+
const status = s.knowledge_status ?? "";
|
|
99
|
+
const loaded = { title: s.title, concept_type: s.concept_type, body: bodyFromMarkdown(c.markdown) };
|
|
100
|
+
const srcRows = listOf<{ resource?: string; title?: string | null; locator?: string | null }>(sources.data);
|
|
101
|
+
const relRows = listOf<{ concept_id?: string; title?: string; concept_type?: string; summary?: { concept_id?: string; title?: string } }>(related.data);
|
|
102
|
+
|
|
103
|
+
const actions = authored ? (
|
|
104
|
+
<>
|
|
105
|
+
{status === "draft" ? <Button size="sm" variant="outline" onClick={() => setForm({ kind: "edit", conceptId, loaded })}>Edit draft</Button> : null}
|
|
106
|
+
{status === "stable" ? <Button size="sm" variant="outline" onClick={() => setForm({ kind: "amend", conceptId, loaded })}>Propose new version</Button> : null}
|
|
107
|
+
{status !== "deprecated" ? <Button size="sm" variant="outline" onClick={() => setVerifying({ approve: true, authority: "" })}>Verify</Button> : null}
|
|
108
|
+
{status !== "deprecated" ? <Button size="sm" variant="ghost" onClick={() => setDeprecating({ reason: "", superseded_by: "" })}>Deprecate</Button> : null}
|
|
109
|
+
</>
|
|
110
|
+
) : undefined;
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<>
|
|
114
|
+
<DetailPanel className={className} title={s.title} subtitle={s.logical_path ?? s.concept_type} onClose={onClose} onExpand={onExpand} onCollapse={onCollapse} actions={actions}
|
|
115
|
+
badges={<><StatusPill tone={toneForStatus(status)} dot>{status || "unknown"}</StatusPill>{s.trust_tier ? <StatusPill tone="neutral">{s.trust_tier.replace(/_/g, " ")}</StatusPill> : null}{s.stale ? <StatusPill tone="warning">stale</StatusPill> : null}{!authored ? <StatusPill tone="info" title="Built from your records; change the source, not the entry">generated</StatusPill> : null}</>}
|
|
116
|
+
tabs={[
|
|
117
|
+
{ id: "content", label: "Content", content: (
|
|
118
|
+
<div className="flex flex-col gap-3">
|
|
119
|
+
{(c.warnings ?? []).map((w) => <p key={w} role="status" className="rounded-md border border-warning-foreground/25 bg-warning p-2 text-sm text-warning-foreground">{w}</p>)}
|
|
120
|
+
<pre className="whitespace-pre-wrap text-sm">{bodyFromMarkdown(c.markdown) || "This entry has no content."}</pre>
|
|
121
|
+
</div>) },
|
|
122
|
+
{ id: "history", label: "History", content: <History conceptId={conceptId} /> },
|
|
123
|
+
{ id: "sources", label: "Sources", count: srcRows.length, content: sources.error ? <ApiError error={sources.error} title="Could not load the sources" onRetry={() => void sources.refetch()} /> : srcRows.length === 0 ? <p className="text-sm text-muted-foreground">No sources recorded.</p> : (
|
|
124
|
+
<ul className="divide-y rounded-md border">{srcRows.map((r, i) => <li key={i} className="px-3 py-2 text-sm">{r.title || r.resource}{r.locator ? <span className="block text-xs text-muted-foreground">{r.locator}</span> : null}</li>)}</ul>) },
|
|
125
|
+
{ id: "related", label: "Related", count: relRows.length, content: related.error ? <ApiError error={related.error} title="Could not load related entries" onRetry={() => void related.refetch()} /> : relRows.length === 0 ? <p className="text-sm text-muted-foreground">Nothing linked yet.</p> : (
|
|
126
|
+
<ul className="divide-y rounded-md border">{relRows.map((r, i) => { const id = r.concept_id ?? r.summary?.concept_id ?? ""; const title = r.title ?? r.summary?.title ?? id; return <li key={id || i}><button type="button" className="w-full px-3 py-2 text-left text-sm hover:bg-accent" disabled={!id || !onOpenConcept} onClick={() => onOpenConcept?.(id)}>{title}{r.concept_type ? <span className="ml-2 text-xs text-muted-foreground">{r.concept_type}</span> : null}</button></li>; })}</ul>) },
|
|
127
|
+
]}>
|
|
128
|
+
{null}
|
|
129
|
+
</DetailPanel>
|
|
130
|
+
<KnowledgeForm mode={form} onClose={() => setForm(null)} onSaved={() => done()} />
|
|
131
|
+
<Dialog open={verifying != null} onOpenChange={(o) => !o && setVerifying(null)}>
|
|
132
|
+
<DialogContent>
|
|
133
|
+
<DialogHeader><DialogTitle>Verify this entry</DialogTitle><DialogDescription>Say who signs it off. Approved entries are trusted more by agents.</DialogDescription></DialogHeader>
|
|
134
|
+
<div className="flex flex-col gap-1"><Label htmlFor="kn-authority">Authority</Label><Input id="kn-authority" placeholder="Head of Sales" value={verifying?.authority ?? ""} onChange={(e) => setVerifying((v) => v && { ...v, authority: e.target.value })} /></div>
|
|
135
|
+
{verify.error ? <ApiError error={verify.error} title="Could not record the decision" /> : null}
|
|
136
|
+
<DialogFooter>
|
|
137
|
+
<Button variant="ghost" onClick={() => setVerifying(null)}>Cancel</Button>
|
|
138
|
+
<Button variant="outline" loading={verify.loading} disabled={!verifying?.authority.trim()} onClick={() => verifying && void verify.mutate({ approve: false, authority: verifying.authority.trim() })}>Reject</Button>
|
|
139
|
+
<Button loading={verify.loading} disabled={!verifying?.authority.trim()} onClick={() => verifying && void verify.mutate({ approve: true, authority: verifying.authority.trim() })}>Approve</Button>
|
|
140
|
+
</DialogFooter>
|
|
141
|
+
</DialogContent>
|
|
142
|
+
</Dialog>
|
|
143
|
+
<Dialog open={deprecating != null && !confirmDeprecate} onOpenChange={(o) => !o && setDeprecating(null)}>
|
|
144
|
+
<DialogContent>
|
|
145
|
+
<DialogHeader><DialogTitle>Deprecate this entry</DialogTitle><DialogDescription>Agents stop relying on it. The history is kept.</DialogDescription></DialogHeader>
|
|
146
|
+
<div className="flex flex-col gap-1"><Label htmlFor="kn-reason">Reason</Label><Input id="kn-reason" value={deprecating?.reason ?? ""} onChange={(e) => setDeprecating((v) => v && { ...v, reason: e.target.value })} /></div>
|
|
147
|
+
<div className="flex flex-col gap-1"><Label htmlFor="kn-superseded">Replaced by (entry id, optional)</Label><Input id="kn-superseded" value={deprecating?.superseded_by ?? ""} onChange={(e) => setDeprecating((v) => v && { ...v, superseded_by: e.target.value })} /></div>
|
|
148
|
+
{deprecate.error ? <ApiError error={deprecate.error} title="Could not deprecate the entry" /> : null}
|
|
149
|
+
<DialogFooter><Button variant="ghost" onClick={() => setDeprecating(null)}>Cancel</Button><Button disabled={!deprecating?.reason.trim()} onClick={() => setConfirmDeprecate(true)}>Continue</Button></DialogFooter>
|
|
150
|
+
</DialogContent>
|
|
151
|
+
</Dialog>
|
|
152
|
+
<ConfirmSpendDialog open={confirmDeprecate} tier="destructive" title={`Deprecate "${s.title}"`} description="Agents stop relying on this entry. It stays in the history, but cannot be made current again." affected={1}
|
|
153
|
+
confirmLabel="Deprecate" confirming={deprecate.loading} onConfirm={() => deprecating && void deprecate.mutate(deprecating)} onCancel={() => setConfirmDeprecate(false)} />
|
|
154
|
+
</>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* KnowledgeForm: write a company knowledge entry in a side sheet.
|
|
5
|
+
* mode "new" a new draft entry (createAuthoredKnowledge).
|
|
6
|
+
* mode "edit" change a DRAFT entry in place (updateAuthoredKnowledgeDraft).
|
|
7
|
+
* mode "amend" propose a new version of a published entry (createAuthoredKnowledgeAmendment); it is reviewed before it replaces the old one.
|
|
8
|
+
* The API takes the whole entry, so edit and amend start from the LOADED entry and stay disabled until it has loaded; Save is only
|
|
9
|
+
* enabled when something changed. Nothing here spends credits or reaches outside the workspace.
|
|
10
|
+
*
|
|
11
|
+
* SDK operations:
|
|
12
|
+
* createAuthoredKnowledge POST /knowledge/authored { title, concept_type, body } (write)
|
|
13
|
+
* updateAuthoredKnowledgeDraft PATCH /knowledge/authored/{concept_id} { title, concept_type, body } (write)
|
|
14
|
+
* createAuthoredKnowledgeAmendment POST /knowledge/authored/{concept_id}/amendments { title, concept_type, body } (write)
|
|
15
|
+
*/
|
|
16
|
+
import { useEffect, useState } from "react";
|
|
17
|
+
import { Button, Input, Label, Textarea } from "@graph8/sdk/ui";
|
|
18
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
19
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
20
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
21
|
+
import { g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
22
|
+
import { bodyOf } from "../lib/agent-types";
|
|
23
|
+
import type { KnowledgeInput } from "../lib/knowledge-utils";
|
|
24
|
+
|
|
25
|
+
export type KnowledgeFormMode = { kind: "new" } | { kind: "edit"; conceptId: string; loaded: KnowledgeInput } | { kind: "amend"; conceptId: string; loaded: KnowledgeInput };
|
|
26
|
+
|
|
27
|
+
export interface KnowledgeFormProps {
|
|
28
|
+
mode: KnowledgeFormMode | null;
|
|
29
|
+
/** Types already in use, offered as suggestions. */
|
|
30
|
+
typeSuggestions?: string[];
|
|
31
|
+
onClose: () => void;
|
|
32
|
+
onSaved?: (conceptId: string | undefined) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const BLANK: KnowledgeInput = { title: "", concept_type: "", body: "" };
|
|
36
|
+
const COPY = { new: ["New knowledge entry", "Create entry"], edit: ["Edit draft", "Save draft"], amend: ["Propose a new version", "Propose version"] } as const;
|
|
37
|
+
|
|
38
|
+
export function KnowledgeForm({ mode, typeSuggestions = [], onClose, onSaved }: KnowledgeFormProps) {
|
|
39
|
+
const [d, setD] = useState<KnowledgeInput>(BLANK);
|
|
40
|
+
const seed = mode && mode.kind !== "new" ? mode.loaded : BLANK;
|
|
41
|
+
useEffect(() => { setD(mode && mode.kind !== "new" ? mode.loaded : BLANK); }, [mode]);
|
|
42
|
+
const save = useG8Mutation(async () => {
|
|
43
|
+
if (!mode) return undefined;
|
|
44
|
+
const res = mode.kind === "new" ? await g8Post("/knowledge/authored", d)
|
|
45
|
+
: mode.kind === "edit" ? await g8Patch(`/knowledge/authored/${encodeURIComponent(mode.conceptId)}`, d)
|
|
46
|
+
: await g8Post(`/knowledge/authored/${encodeURIComponent(mode.conceptId)}/amendments`, d);
|
|
47
|
+
return bodyOf<{ concept_id?: string }>(res).concept_id;
|
|
48
|
+
}, { onSuccess: (id) => { onSaved?.(id); onClose(); } });
|
|
49
|
+
const set = (patch: Partial<KnowledgeInput>) => setD((x) => ({ ...x, ...patch }));
|
|
50
|
+
const changed = d.title !== seed.title || d.concept_type !== seed.concept_type || d.body !== seed.body;
|
|
51
|
+
const valid = d.title.trim() && d.concept_type.trim() && d.body.trim();
|
|
52
|
+
const [title, cta] = COPY[mode?.kind ?? "new"];
|
|
53
|
+
return (
|
|
54
|
+
<FormSheet open={mode != null} onOpenChange={(o) => !o && onClose()} title={title} width="xl"
|
|
55
|
+
description={mode?.kind === "amend" ? "The new version is reviewed before it replaces the published one." : "Written in Markdown. Agents can draw on it once it is published."}
|
|
56
|
+
footer={<><Button variant="ghost" onClick={onClose}>Cancel</Button><Button loading={save.loading} disabled={!valid || !changed} onClick={() => void save.mutate(undefined)}>{cta}</Button></>}>
|
|
57
|
+
<div className="flex flex-col gap-3">
|
|
58
|
+
<div className="flex flex-col gap-1"><Label htmlFor="kn-title">Title</Label><Input id="kn-title" value={d.title} onChange={(e) => set({ title: e.target.value })} /></div>
|
|
59
|
+
<div className="flex flex-col gap-1">
|
|
60
|
+
<Label htmlFor="kn-type">Type</Label>
|
|
61
|
+
<Input id="kn-type" list="kn-types" placeholder="playbook, policy, faq" value={d.concept_type} onChange={(e) => set({ concept_type: e.target.value })} />
|
|
62
|
+
<datalist id="kn-types">{typeSuggestions.map((t) => <option key={t} value={t} />)}</datalist>
|
|
63
|
+
</div>
|
|
64
|
+
<div className="flex flex-col gap-1"><Label htmlFor="kn-body">Content (Markdown)</Label><Textarea id="kn-body" rows={14} className="font-mono text-xs" value={d.body} onChange={(e) => set({ body: e.target.value })} /></div>
|
|
65
|
+
{save.error ? <ApiError error={save.error} title="Could not save the entry" /> : null}
|
|
66
|
+
</div>
|
|
67
|
+
</FormSheet>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/** NameDialog: a small dialog that asks for one name (the copy of an agent, a template). Used by the duplicate and save-as-template buttons. SDK operations: none. */
|
|
4
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Input, Label } from "@graph8/sdk/ui";
|
|
5
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
6
|
+
|
|
7
|
+
export interface NameDialogProps {
|
|
8
|
+
open: boolean;
|
|
9
|
+
title: string;
|
|
10
|
+
description: string;
|
|
11
|
+
label: string;
|
|
12
|
+
value: string;
|
|
13
|
+
onChange: (value: string) => void;
|
|
14
|
+
confirm: string;
|
|
15
|
+
busy: boolean;
|
|
16
|
+
error: unknown;
|
|
17
|
+
onCancel: () => void;
|
|
18
|
+
onConfirm: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function NameDialog({ open, title, description, label, value, onChange, confirm, busy, error, onCancel, onConfirm }: NameDialogProps) {
|
|
22
|
+
return (
|
|
23
|
+
<Dialog open={open} onOpenChange={(o) => !o && !busy && onCancel()}>
|
|
24
|
+
<DialogContent>
|
|
25
|
+
<DialogHeader><DialogTitle>{title}</DialogTitle><DialogDescription>{description}</DialogDescription></DialogHeader>
|
|
26
|
+
<div className="flex flex-col gap-1"><Label htmlFor="agent-action-name">{label}</Label><Input id="agent-action-name" value={value} onChange={(e) => onChange(e.target.value)} /></div>
|
|
27
|
+
{error ? <ApiError error={error} title="That did not work" /> : null}
|
|
28
|
+
<DialogFooter><Button variant="ghost" disabled={busy} onClick={onCancel}>Cancel</Button><Button loading={busy} disabled={!value.trim()} onClick={onConfirm}>{confirm}</Button></DialogFooter>
|
|
29
|
+
</DialogContent>
|
|
30
|
+
</Dialog>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ReceptionistButton: set up the workspace receptionist (an agent that answers inbound calls) and switch it on or off. There is no call that
|
|
5
|
+
* reads whether it is on, so Turn on and Turn off are explicit and safe to repeat. Turning it on lets it answer real callers, so on and off
|
|
6
|
+
* go through ConfirmSpendDialog (external).
|
|
7
|
+
*
|
|
8
|
+
* SDK operations, in order:
|
|
9
|
+
* 1. ensureReceptionistAgent POST /voice/agents/receptionist (write) creates it when missing
|
|
10
|
+
* 2. toggleReceptionistAgent PATCH /voice/agents/receptionist?enabled (write, confirmed as external)
|
|
11
|
+
*/
|
|
12
|
+
import { useState } from "react";
|
|
13
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@graph8/sdk/ui";
|
|
14
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
15
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
16
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
17
|
+
import { g8Patch, g8Post } from "@/g8/lib/g8-client";
|
|
18
|
+
|
|
19
|
+
export function ReceptionistButton() {
|
|
20
|
+
const [open, setOpen] = useState(false);
|
|
21
|
+
const [pending, setPending] = useState<boolean | null>(null);
|
|
22
|
+
const [result, setResult] = useState<string | null>(null);
|
|
23
|
+
const ensure = useG8Mutation(() => g8Post("/voice/agents/receptionist", {}), { onSuccess: () => setResult("The receptionist is set up. Turn it on when it is ready to take calls.") });
|
|
24
|
+
const toggle = useG8Mutation((enabled: boolean) => g8Patch("/voice/agents/receptionist", {}, { query: { enabled } }), { onSuccess: (_r, on) => { setPending(null); setResult(on ? "The receptionist is on." : "The receptionist is off.") }, onError: () => setPending(null) });
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
<Button variant="outline" onClick={() => { setResult(null); ensure.reset(); toggle.reset(); setOpen(true); }}>Receptionist</Button>
|
|
28
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
29
|
+
<DialogContent>
|
|
30
|
+
<DialogHeader><DialogTitle>Receptionist</DialogTitle><DialogDescription>An agent that answers your inbound calls and routes them. Set it up once, then switch it on or off.</DialogDescription></DialogHeader>
|
|
31
|
+
{result ? <p role="status" className="text-sm">{result}</p> : null}
|
|
32
|
+
{ensure.error ? <ApiError error={ensure.error} title="Could not set up the receptionist" /> : null}
|
|
33
|
+
{toggle.error ? <ApiError error={toggle.error} title="Could not change the receptionist" /> : null}
|
|
34
|
+
<DialogFooter className="gap-2">
|
|
35
|
+
<Button variant="outline" loading={ensure.loading} onClick={() => void ensure.mutate(undefined)}>Set up</Button>
|
|
36
|
+
<Button variant="outline" onClick={() => setPending(false)}>Turn off</Button>
|
|
37
|
+
<Button onClick={() => setPending(true)}>Turn on</Button>
|
|
38
|
+
</DialogFooter>
|
|
39
|
+
</DialogContent>
|
|
40
|
+
</Dialog>
|
|
41
|
+
<ConfirmSpendDialog open={pending != null} tier="external" title={pending ? "Turn the receptionist on" : "Turn the receptionist off"}
|
|
42
|
+
description={pending ? "The receptionist starts answering real callers on your numbers." : "Inbound calls stop going to the receptionist."}
|
|
43
|
+
confirmLabel={pending ? "Turn on" : "Turn off"} confirming={toggle.loading} onConfirm={() => pending != null && void toggle.mutate(pending)} onCancel={() => setPending(null)} />
|
|
44
|
+
</>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SaveAsTemplateButton: keep an agent as a reusable template of this workspace; anyone can start a new agent from it (FromTemplateButton).
|
|
5
|
+
*
|
|
6
|
+
* SDK operations:
|
|
7
|
+
* saveAgentAsTemplate POST /voice/agents/{agent_id}/save-as-template?template_name (write)
|
|
8
|
+
*/
|
|
9
|
+
import { useState } from "react";
|
|
10
|
+
import { Button } from "@graph8/sdk/ui";
|
|
11
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
12
|
+
import { g8Post } from "@/g8/lib/g8-client";
|
|
13
|
+
import type { AgentSummary } from "../lib/agent-types";
|
|
14
|
+
import { NameDialog } from "./name-dialog";
|
|
15
|
+
|
|
16
|
+
export interface SaveAsTemplateButtonProps {
|
|
17
|
+
agent: Pick<AgentSummary, "agent_id" | "agent_name">;
|
|
18
|
+
variant?: "outline" | "ghost";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function SaveAsTemplateButton({ agent, variant = "outline" }: SaveAsTemplateButtonProps) {
|
|
22
|
+
const [open, setOpen] = useState(false);
|
|
23
|
+
const [name, setName] = useState("");
|
|
24
|
+
const save = useG8Mutation((n: string) => g8Post(`/voice/agents/${encodeURIComponent(agent.agent_id)}/save-as-template`, {}, { query: { template_name: n } }), { onSuccess: () => setOpen(false) });
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
<Button size="sm" variant={variant} onClick={() => { save.reset(); setName(agent.agent_name); setOpen(true); }}>Save as template</Button>
|
|
28
|
+
<NameDialog open={open} title={`Save ${agent.agent_name} as a template`} description="Anyone in the workspace can start a new agent from it." label="Template name"
|
|
29
|
+
value={name} onChange={setName} confirm="Save template" busy={save.loading} error={save.error} onCancel={() => setOpen(false)} onConfirm={() => void save.mutate(name.trim())} />
|
|
30
|
+
</>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SkillEditor: create or edit one skill (a single-purpose action an agent can run) in a side sheet.
|
|
5
|
+
* Edit: the skill is loaded first and the form stays disabled until it has loaded (a blank form never overwrites the real skill);
|
|
6
|
+
* Save sends only the fields you changed (a runtime config goes whole, because voice validates it as a unit).
|
|
7
|
+
* Also: enable or disable, see the {placeholders} the skill asks for, check a template, delete.
|
|
8
|
+
* Create: an LLM skill (model + prompt) or an API skill (endpoint, method, headers, body).
|
|
9
|
+
* Deleting a skill removes it from every agent that uses it, so it is destructive and confirmed.
|
|
10
|
+
*
|
|
11
|
+
* SDK operations, in order:
|
|
12
|
+
* 1. getSkill GET /skills/{action_id} (read)
|
|
13
|
+
* 2. extractVariables GET /skills/{action_id}/variables (read)
|
|
14
|
+
* 3. listModels GET /skills/models (read) LLM model choices
|
|
15
|
+
* 4. validateTemplate POST /skills/validate { template | runtime_type + config } (write, free: warns about double braces)
|
|
16
|
+
* 5. createSkill POST /skills { name, runtime_type, llm_config | api_config } (write)
|
|
17
|
+
* 6. updateSkill PUT /skills/{action_id} { changed fields } (write)
|
|
18
|
+
* 7. deleteSkill DELETE /skills/{action_id} (destructive, ConfirmSpendDialog)
|
|
19
|
+
*/
|
|
20
|
+
import { useEffect, useRef, useState } from "react";
|
|
21
|
+
import { Button, Input, Label, Skeleton, Switch, Textarea } from "@graph8/sdk/ui";
|
|
22
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
23
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
24
|
+
import { FormSheet } from "@/g8/components/screen/form-sheet";
|
|
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 { BLANK_DRAFT, HTTP_METHODS, draftOf, newSkillBody, parseHeaders, skillOf, skillPatch } from "../lib/skill-utils";
|
|
30
|
+
import type { SkillDraft } from "../lib/skill-utils";
|
|
31
|
+
|
|
32
|
+
export type SkillEditorTarget = { kind: "edit"; id: string } | { kind: "new"; runtime: "llm" | "api" };
|
|
33
|
+
|
|
34
|
+
export interface SkillEditorProps {
|
|
35
|
+
/** The skill to edit or the kind to create; null closes the sheet. */
|
|
36
|
+
target: SkillEditorTarget | null;
|
|
37
|
+
onClose: () => void;
|
|
38
|
+
/** After a create, save or delete. */
|
|
39
|
+
onChanged?: (event: "created" | "updated" | "deleted", id?: string) => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function SkillEditor({ target, onClose, onChanged }: SkillEditorProps) {
|
|
43
|
+
const editId = target?.kind === "edit" ? target.id : "";
|
|
44
|
+
const runtime = target?.kind === "new" ? target.runtime : undefined;
|
|
45
|
+
const [d, setD] = useState<SkillDraft>(BLANK_DRAFT);
|
|
46
|
+
const [deleting, setDeleting] = useState(false);
|
|
47
|
+
const seeded = useRef<string>("");
|
|
48
|
+
const loaded = useG8Query((signal) => g8Get(`/skills/${encodeURIComponent(editId)}`, undefined, { signal }), [editId], { enabled: Boolean(editId) });
|
|
49
|
+
// useG8Query keeps the previous skill's data while the next loads: only the skill fetched for THIS id counts as loaded.
|
|
50
|
+
const got = editId ? skillOf(loaded.data) : undefined;
|
|
51
|
+
const skill = got && got.action_id === editId ? got : undefined;
|
|
52
|
+
const vars = useG8Query((signal) => g8Get(`/skills/${encodeURIComponent(editId)}/variables`, undefined, { signal }), [editId], { enabled: Boolean(editId) && Boolean(skill) });
|
|
53
|
+
const kind = skill?.runtime_type ?? runtime;
|
|
54
|
+
const models = useG8Query((signal) => g8Get("/skills/models", undefined, { signal }), [], { enabled: kind === "llm" });
|
|
55
|
+
const set = (patch: Partial<SkillDraft>) => setD((x) => ({ ...x, ...patch }));
|
|
56
|
+
|
|
57
|
+
// Seed the form from the LOADED skill, once per skill (a refetch must not overwrite what the person is typing).
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (target?.kind === "new") { seeded.current = "new"; setD(BLANK_DRAFT); setDeleting(false); return; }
|
|
60
|
+
if (skill && seeded.current !== skill.action_id) { seeded.current = skill.action_id; setD(draftOf(skill)); }
|
|
61
|
+
}, [target, skill]);
|
|
62
|
+
useEffect(() => { if (!target) { seeded.current = ""; } }, [target]);
|
|
63
|
+
|
|
64
|
+
const patch = skill ? skillPatch(skill, d) : {};
|
|
65
|
+
const headersOk = d.headers.trim() === "" || parseHeaders(d.headers) != null;
|
|
66
|
+
const numbersOk = kind !== "llm" || (Number.isFinite(Number(d.temperature)) && Number(d.max_tokens) > 0);
|
|
67
|
+
const filled = d.name.trim() && (kind === "llm" ? d.model && d.prompt_template.trim() : d.endpoint.trim());
|
|
68
|
+
const valid = Boolean(filled) && headersOk && numbersOk;
|
|
69
|
+
|
|
70
|
+
const create = useG8Mutation(async () => {
|
|
71
|
+
const res = await g8Post("/skills", newSkillBody(kind as "llm" | "api", d));
|
|
72
|
+
return skillOf(res)?.action_id;
|
|
73
|
+
}, { onSuccess: (id) => { onChanged?.("created", id); onClose(); } });
|
|
74
|
+
const save = useG8Mutation(() => g8Put(`/skills/${encodeURIComponent(editId)}`, patch), { onSuccess: () => { seeded.current = ""; void loaded.refetch(); void vars.refetch(); onChanged?.("updated", editId); } });
|
|
75
|
+
const remove = useG8Mutation(() => g8Delete(`/skills/${encodeURIComponent(editId)}`), { onSuccess: () => { setDeleting(false); onChanged?.("deleted", editId); onClose(); }, onError: () => setDeleting(false) });
|
|
76
|
+
const check = useG8Mutation(() => g8Post("/skills/validate", kind === "llm" ? { template: d.prompt_template } : { runtime_type: "api", api_config: { endpoint: d.endpoint, body_template: d.body_template, headers: parseHeaders(d.headers) ?? {} } }));
|
|
77
|
+
|
|
78
|
+
const modelRows = listOf<{ id: string; label?: string }>(models.data, "models");
|
|
79
|
+
const checked = bodyOf<{ variables?: string[]; warnings?: string[] }>(check.data);
|
|
80
|
+
const names = (bodyOf<{ variables?: string[] }>(vars.data).variables ?? []) as string[];
|
|
81
|
+
const waiting = Boolean(editId) && !skill;
|
|
82
|
+
const title = target?.kind === "new" ? (runtime === "api" ? "New API skill" : "New LLM skill") : skill?.name ?? "Skill";
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<FormSheet open={target != null} onOpenChange={(o) => !o && onClose()} title={title} width="lg"
|
|
87
|
+
description={target?.kind === "new" ? "Use {variable} placeholders for values the agent fills in when it runs the skill." : "Changes apply to every agent that uses this skill."}
|
|
88
|
+
footer={<>
|
|
89
|
+
{editId ? <Button variant="ghost" className="mr-auto" disabled={!skill} onClick={() => setDeleting(true)}>Delete</Button> : null}
|
|
90
|
+
<Button variant="ghost" onClick={onClose}>Cancel</Button>
|
|
91
|
+
{target?.kind === "new" ? <Button loading={create.loading} disabled={!valid} onClick={() => void create.mutate(undefined)}>Create skill</Button>
|
|
92
|
+
: <Button loading={save.loading} disabled={!skill || Object.keys(patch).length === 0 || !valid} onClick={() => void save.mutate(undefined)}>Save changes</Button>}
|
|
93
|
+
</>}>
|
|
94
|
+
{loaded.error ? <ApiError error={loaded.error} title="Could not load the skill" onRetry={() => void loaded.refetch()} /> : null}
|
|
95
|
+
{waiting && !loaded.error ? <Skeleton className="h-40 w-full" aria-label="Loading skill" /> : null}
|
|
96
|
+
{!waiting ? (
|
|
97
|
+
<fieldset className="flex flex-col gap-3 border-0 p-0" disabled={save.loading || create.loading}>
|
|
98
|
+
<div className="flex flex-col gap-1"><Label htmlFor="sk-name">Name</Label><Input id="sk-name" value={d.name} onChange={(e) => set({ name: e.target.value })} /></div>
|
|
99
|
+
<div className="flex flex-col gap-1"><Label htmlFor="sk-desc">What it does</Label><Input id="sk-desc" value={d.description} onChange={(e) => set({ description: e.target.value })} /></div>
|
|
100
|
+
<div className="flex flex-col gap-1"><Label htmlFor="sk-cat">Category</Label><Input id="sk-cat" value={d.category} onChange={(e) => set({ category: e.target.value })} /></div>
|
|
101
|
+
{editId ? <label className="flex items-center gap-2 text-sm"><Switch checked={d.enabled} onCheckedChange={(v) => set({ enabled: v })} aria-label="Enabled" />Enabled</label> : null}
|
|
102
|
+
{kind === "llm" ? (
|
|
103
|
+
<>
|
|
104
|
+
<div className="flex flex-col gap-1">
|
|
105
|
+
<Label htmlFor="sk-model">Model</Label>
|
|
106
|
+
<select id="sk-model" className="h-9 rounded-md border bg-background px-2 text-sm" value={d.model} onChange={(e) => set({ model: e.target.value })}>
|
|
107
|
+
<option value="">Choose a model</option>
|
|
108
|
+
{d.model && !modelRows.some((m) => m.id === d.model) ? <option value={d.model}>{d.model}</option> : null}
|
|
109
|
+
{modelRows.map((m) => <option key={m.id} value={m.id}>{m.label ?? m.id}</option>)}
|
|
110
|
+
</select>
|
|
111
|
+
</div>
|
|
112
|
+
<div className="flex flex-col gap-1"><Label htmlFor="sk-prompt">Prompt</Label><Textarea id="sk-prompt" rows={8} value={d.prompt_template} onChange={(e) => set({ prompt_template: e.target.value })} /></div>
|
|
113
|
+
<div className="grid grid-cols-2 gap-3">
|
|
114
|
+
<div className="flex flex-col gap-1"><Label htmlFor="sk-temp">Temperature (0 to 2)</Label><Input id="sk-temp" inputMode="decimal" value={d.temperature} onChange={(e) => set({ temperature: e.target.value })} /></div>
|
|
115
|
+
<div className="flex flex-col gap-1"><Label htmlFor="sk-max">Max tokens</Label><Input id="sk-max" inputMode="numeric" value={d.max_tokens} onChange={(e) => set({ max_tokens: e.target.value })} /></div>
|
|
116
|
+
</div>
|
|
117
|
+
</>
|
|
118
|
+
) : (
|
|
119
|
+
<>
|
|
120
|
+
<div className="grid grid-cols-[7rem_1fr] gap-3">
|
|
121
|
+
<div className="flex flex-col gap-1"><Label htmlFor="sk-method">Method</Label>
|
|
122
|
+
<select id="sk-method" className="h-9 rounded-md border bg-background px-2 text-sm" value={d.method} onChange={(e) => set({ method: e.target.value })}>{HTTP_METHODS.map((m) => <option key={m} value={m}>{m}</option>)}</select></div>
|
|
123
|
+
<div className="flex flex-col gap-1"><Label htmlFor="sk-endpoint">Endpoint</Label><Input id="sk-endpoint" placeholder="https://api.example.com/items/{item_id}" value={d.endpoint} onChange={(e) => set({ endpoint: e.target.value })} /></div>
|
|
124
|
+
</div>
|
|
125
|
+
<div className="flex flex-col gap-1"><Label htmlFor="sk-headers">Headers (JSON)</Label><Textarea id="sk-headers" rows={3} className="font-mono text-xs" value={d.headers} onChange={(e) => set({ headers: e.target.value })} /></div>
|
|
126
|
+
{!headersOk ? <p role="alert" className="text-sm text-error-foreground">Headers must be a JSON object of text values.</p> : null}
|
|
127
|
+
<div className="flex flex-col gap-1"><Label htmlFor="sk-body">Body template</Label><Textarea id="sk-body" rows={4} className="font-mono text-xs" value={d.body_template} onChange={(e) => set({ body_template: e.target.value })} /></div>
|
|
128
|
+
<label className="flex items-center gap-2 text-sm"><Switch checked={d.requires_auth} onCheckedChange={(v) => set({ requires_auth: v })} aria-label="Requires auth" />Requires authentication</label>
|
|
129
|
+
</>
|
|
130
|
+
)}
|
|
131
|
+
{names.length ? <p className="text-xs text-muted-foreground">Asks for: {names.map((n) => `{${n}}`).join(", ")}</p> : null}
|
|
132
|
+
<div className="flex items-center gap-2">
|
|
133
|
+
<Button size="sm" variant="outline" loading={check.loading} disabled={kind === "llm" ? !d.prompt_template.trim() : !d.endpoint.trim()} onClick={() => void check.mutate(undefined)}>Check placeholders</Button>
|
|
134
|
+
{check.data !== undefined ? <span role="status" className="text-xs text-muted-foreground">{(checked.variables ?? []).length} placeholders{(checked.warnings ?? []).length ? `, ${(checked.warnings ?? []).join(" ")}` : ", no warnings"}</span> : null}
|
|
135
|
+
</div>
|
|
136
|
+
{check.error ? <ApiError error={check.error} title="Could not check the template" /> : null}
|
|
137
|
+
{create.error ? <ApiError error={create.error} title="Could not create the skill" /> : null}
|
|
138
|
+
{save.error ? <ApiError error={save.error} title="Could not save the skill" /> : null}
|
|
139
|
+
{remove.error ? <ApiError error={remove.error} title="Could not delete the skill" /> : null}
|
|
140
|
+
</fieldset>
|
|
141
|
+
) : null}
|
|
142
|
+
</FormSheet>
|
|
143
|
+
<ConfirmSpendDialog open={deleting} tier="destructive" title={`Delete "${skill?.name ?? "this skill"}"`} description="Every agent that uses it loses it, and workflows that call it stop working." affected={1}
|
|
144
|
+
confirmLabel="Delete" confirming={remove.loading} onConfirm={() => void remove.mutate(undefined)} onCancel={() => setDeleting(false)} />
|
|
145
|
+
</>
|
|
146
|
+
);
|
|
147
|
+
}
|