@graph8/sdk 0.362.0 → 0.373.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -10
- package/blocks/_foundation/README.md +78 -1
- package/blocks/_foundation/block.json +123 -6
- package/blocks/_foundation/components/api-error.tsx +9 -7
- package/blocks/_foundation/components/brand-provider.tsx +23 -0
- package/blocks/_foundation/components/confirm-spend-dialog.tsx +15 -8
- package/blocks/_foundation/components/confirmed-action.tsx +90 -0
- package/blocks/_foundation/components/connect-graph8.tsx +280 -0
- package/blocks/_foundation/components/empty-state.tsx +5 -2
- package/blocks/_foundation/components/list-select.tsx +112 -0
- package/blocks/_foundation/components/paging-notices.tsx +55 -0
- package/blocks/_foundation/components/screen/detail-panel.tsx +2 -1
- package/blocks/_foundation/components/screen/filter-controls.tsx +92 -0
- package/blocks/_foundation/components/screen/index.ts +2 -0
- package/blocks/_foundation/components/screen/record-link.tsx +76 -0
- package/blocks/_foundation/components/screen/split-view.tsx +1 -1
- package/blocks/_foundation/components/screen/time-series-chart-recharts.tsx +65 -0
- package/blocks/_foundation/components/screen/time-series-chart.tsx +15 -97
- package/blocks/_foundation/components/screen/time-series-format.ts +57 -0
- package/blocks/_foundation/hooks/use-all-pages.ts +54 -0
- package/blocks/_foundation/hooks/use-brand.ts +52 -0
- package/blocks/_foundation/hooks/use-g8-mutation.ts +23 -0
- package/blocks/_foundation/hooks/use-g8-query.ts +127 -1
- package/blocks/_foundation/hooks/use-load-more-list.ts +61 -0
- package/blocks/_foundation/hooks/use-paginated.ts +120 -0
- package/blocks/_foundation/hooks/use-row-activation.ts +57 -0
- package/blocks/_foundation/hooks/use-url-filters.ts +93 -0
- package/blocks/_foundation/lib/g8-cache.ts +454 -0
- package/blocks/_foundation/lib/g8-client.ts +1 -1
- package/blocks/_foundation/lib/paging.ts +57 -0
- package/blocks/_foundation/lib/plural.ts +23 -0
- package/blocks/_foundation/lib/use-debounced-value.ts +13 -0
- package/blocks/_foundation/server/connect-route.ts +15 -0
- package/blocks/_foundation/server/connect.ts +127 -0
- package/blocks/_foundation/server/local-key.ts +70 -0
- package/blocks/_foundation/server/proxy.ts +103 -3
- package/blocks/_foundation/server/route.ts +16 -4
- package/blocks/agents/README.md +93 -2
- package/blocks/agents/block.json +343 -2
- package/blocks/agents/components/agent-activity.tsx +112 -0
- package/blocks/agents/components/agent-approvals.tsx +135 -0
- package/blocks/agents/components/agent-builder.tsx +29 -10
- package/blocks/agents/components/agent-conversations.tsx +143 -0
- package/blocks/agents/components/agent-knowledge-panel.tsx +4 -3
- package/blocks/agents/components/agent-operators.tsx +164 -0
- package/blocks/agents/components/agent-run-detail.tsx +119 -0
- package/blocks/agents/components/agent-runs.tsx +19 -10
- package/blocks/agents/components/agent-skills-panel.tsx +7 -7
- package/blocks/agents/components/agent-test-history.tsx +121 -0
- package/blocks/agents/components/agents-screen.tsx +226 -0
- package/blocks/agents/components/assistant-sessions.tsx +150 -0
- package/blocks/agents/components/duplicate-agent-button.tsx +42 -0
- package/blocks/agents/components/from-template-button.tsx +65 -0
- package/blocks/agents/components/knowledge-browser.tsx +107 -0
- package/blocks/agents/components/knowledge-detail.tsx +156 -0
- package/blocks/agents/components/knowledge-form.tsx +69 -0
- package/blocks/agents/components/name-dialog.tsx +32 -0
- package/blocks/agents/components/receptionist-button.tsx +46 -0
- package/blocks/agents/components/save-as-template-button.tsx +32 -0
- package/blocks/agents/components/skill-editor.tsx +147 -0
- package/blocks/agents/components/skill-library.tsx +111 -0
- package/blocks/agents/lib/agent-types.ts +7 -1
- package/blocks/agents/lib/facts.tsx +13 -0
- package/blocks/agents/lib/knowledge-utils.ts +49 -0
- package/blocks/agents/lib/load-more.tsx +141 -0
- package/blocks/agents/lib/skill-utils.ts +91 -0
- package/blocks/agents/recipes/create-agent-and-attach-skill.ts +1 -1
- package/blocks/analytics/README.md +13 -0
- package/blocks/analytics/block.json +45 -1
- package/blocks/analytics/components/analytics-screen.tsx +222 -0
- package/blocks/analytics/components/kpi-tiles.tsx +1 -1
- package/blocks/analytics/components/leaderboard.tsx +14 -7
- package/blocks/analytics/components/report-builder.tsx +38 -11
- package/blocks/analytics/components/report-detail.tsx +84 -0
- package/blocks/analytics/components/trend-chart.tsx +62 -0
- package/blocks/appointments/README.md +6 -0
- package/blocks/appointments/block.json +33 -2
- package/blocks/appointments/components/availability-editor.tsx +17 -5
- package/blocks/appointments/components/booking-page.tsx +2 -2
- package/blocks/appointments/components/event-type-editor.tsx +2 -1
- package/blocks/appointments/components/event-type-related.tsx +4 -2
- package/blocks/appointments/components/meeting-contact-links.tsx +49 -0
- package/blocks/appointments/components/meeting-detail.tsx +2 -2
- package/blocks/appointments/components/meeting-list.tsx +38 -18
- package/blocks/appointments/components/meetings-screen.tsx +7 -4
- package/blocks/appointments/components/record-meetings-tab.tsx +92 -0
- package/blocks/appointments/components/scheduling-screen.tsx +2 -1
- package/blocks/block.schema.json +18 -0
- package/blocks/block.types.ts +17 -0
- package/blocks/campaigns/README.md +41 -11
- package/blocks/campaigns/block.json +105 -42
- package/blocks/campaigns/components/campaign-assets.tsx +17 -6
- package/blocks/campaigns/components/campaign-launch-step.tsx +114 -26
- package/blocks/campaigns/components/campaign-list.tsx +7 -3
- package/blocks/campaigns/components/campaign-metrics.tsx +1 -1
- package/blocks/campaigns/components/campaign-wizard.tsx +52 -14
- package/blocks/campaigns/components/campaigns-screen.tsx +267 -0
- package/blocks/campaigns/components/icp-detail.tsx +163 -0
- package/blocks/campaigns/components/icp-editor.tsx +22 -17
- package/blocks/campaigns/components/persona-detail.tsx +181 -0
- package/blocks/campaigns/components/persona-editor.tsx +19 -11
- package/blocks/campaigns/lib/campaigns-utils.ts +138 -5
- package/blocks/campaigns/recipes/attach-audience-and-launch.ts +16 -3
- package/blocks/campaigns/recipes/generate-ideas-and-create-campaign.ts +8 -4
- package/blocks/coach/README.md +10 -0
- package/blocks/coach/block.json +16 -1
- package/blocks/coach/components/call-scorecard.tsx +7 -7
- package/blocks/coach/components/coach-screen.tsx +222 -0
- package/blocks/company-intelligence/README.md +132 -0
- package/blocks/company-intelligence/block.json +318 -0
- package/blocks/company-intelligence/components/company-intelligence-screen.tsx +114 -0
- package/blocks/company-intelligence/components/company-intelligence-tab.tsx +88 -0
- package/blocks/company-intelligence/components/company-profile.tsx +68 -0
- package/blocks/company-intelligence/components/intelligence-sources.tsx +63 -0
- package/blocks/company-intelligence/components/report-detail.tsx +189 -0
- package/blocks/company-intelligence/components/research-reports.tsx +136 -0
- package/blocks/company-intelligence/components/research-run.tsx +161 -0
- package/blocks/company-intelligence/components/source-detail.tsx +117 -0
- package/blocks/company-intelligence/components/source-view.tsx +274 -0
- package/blocks/company-intelligence/components/wiki-document-detail.tsx +116 -0
- package/blocks/company-intelligence/components/wiki-documents.tsx +66 -0
- package/blocks/company-intelligence/hooks/use-intelligence.ts +173 -0
- package/blocks/company-intelligence/lib/intelligence-types.ts +230 -0
- package/blocks/company-intelligence/lib/safe-markdown.tsx +150 -0
- package/blocks/company-intelligence/recipes/read-competitors-and-news.ts +33 -0
- package/blocks/company-intelligence/recipes/research-your-website.ts +43 -0
- package/blocks/company-intelligence/recipes/write-the-research-reports.ts +54 -0
- package/blocks/copilot/README.md +13 -0
- package/blocks/copilot/block.json +14 -1
- package/blocks/copilot/components/copilot-memory.tsx +6 -2
- package/blocks/copilot/components/copilot-panel.tsx +14 -5
- package/blocks/copilot/components/copilot-screen.tsx +148 -0
- package/blocks/copilot/components/copilot-team-chat.tsx +13 -6
- package/blocks/crm/README.md +34 -0
- package/blocks/crm/block.json +107 -2
- package/blocks/crm/components/companies-screen.tsx +359 -0
- package/blocks/crm/components/company-detail.tsx +10 -8
- package/blocks/crm/components/company-table.tsx +11 -4
- package/blocks/crm/components/contact-detail.tsx +10 -7
- package/blocks/crm/components/contact-lookup-link.tsx +44 -0
- package/blocks/crm/components/contact-table.tsx +10 -3
- package/blocks/crm/components/contacts-screen.tsx +377 -0
- package/blocks/crm/components/dedupe-review.tsx +13 -6
- package/blocks/crm/components/import-wizard.tsx +7 -9
- package/blocks/crm/components/list-select.tsx +20 -0
- package/blocks/crm/components/notes-panel.tsx +4 -4
- package/blocks/crm/components/record-engagement-tab.tsx +103 -0
- package/blocks/crm/components/record-form.tsx +6 -4
- package/blocks/crm/components/task-list.tsx +4 -4
- package/blocks/crm/components/tasks-screen.tsx +399 -0
- package/blocks/crm/hooks/use-all-lists.ts +18 -0
- package/blocks/crm/hooks/use-custom-fields.ts +6 -1
- package/blocks/crm/hooks/use-page-query.ts +9 -1
- package/blocks/crm/lib/crm-types.ts +26 -2
- package/blocks/crm/lib/read-lists.ts +16 -0
- package/blocks/crm/lib/screen-parts.tsx +105 -0
- package/blocks/deals/README.md +24 -6
- package/blocks/deals/block.json +160 -5
- package/blocks/deals/components/close-deal-dialog.tsx +169 -0
- package/blocks/deals/components/deal-contacts-editor.tsx +134 -0
- package/blocks/deals/components/deal-detail.tsx +25 -13
- package/blocks/deals/components/deal-filters.tsx +65 -0
- package/blocks/deals/components/deal-line-items.tsx +120 -0
- package/blocks/deals/components/deal-notes-tab.tsx +97 -0
- package/blocks/deals/components/deal-related.tsx +31 -10
- package/blocks/deals/components/deal-tasks-tab.tsx +88 -0
- package/blocks/deals/components/deals-screen.tsx +66 -26
- package/blocks/deals/components/forecast-view.tsx +4 -4
- package/blocks/deals/components/pipeline-board.tsx +8 -6
- package/blocks/deals/components/pipeline-editor.tsx +132 -0
- package/blocks/deals/components/record-deals-tab.tsx +62 -0
- package/blocks/deals/components/stage-editor.tsx +5 -5
- package/blocks/deals/hooks/use-pipeline-deals.ts +21 -4
- package/blocks/deals/hooks/use-pipelines.ts +1 -1
- package/blocks/deals/lib/deal-extras.ts +156 -0
- package/blocks/dialer/README.md +45 -4
- package/blocks/dialer/block.json +84 -3
- package/blocks/dialer/components/call-contact-button.tsx +269 -0
- package/blocks/dialer/components/call-log-screen.tsx +208 -0
- package/blocks/dialer/components/call-log.tsx +23 -7
- package/blocks/dialer/components/call-queue.tsx +4 -4
- package/blocks/dialer/components/dialer-screen.tsx +361 -0
- package/blocks/dialer/components/dialer-session-detail.tsx +153 -0
- package/blocks/dialer/components/disposition-panel.tsx +1 -1
- package/blocks/dialer/components/live-calls-table.tsx +7 -3
- package/blocks/dialer/components/record-calls-tab.tsx +101 -0
- package/blocks/dialer/components/session-start-form.tsx +61 -12
- package/blocks/dialer/components/softphone.tsx +4 -2
- package/blocks/enrichment/README.md +29 -7
- package/blocks/enrichment/block.json +49 -9
- package/blocks/enrichment/components/bulk-enrich-panel.tsx +83 -29
- package/blocks/enrichment/components/email-verify-badge.tsx +31 -11
- package/blocks/enrichment/components/enrich-button.tsx +32 -9
- package/blocks/enrichment/components/enrichment-job-detail.tsx +78 -0
- package/blocks/enrichment/components/enrichment-screen.tsx +341 -0
- package/blocks/enrichment/components/waterfall-config.tsx +66 -17
- package/blocks/enrichment/lib/enrichment-types.ts +39 -0
- package/blocks/enrichment/recipes/bulk-enrich-and-poll.ts +15 -4
- package/blocks/enrichment/recipes/waterfall-pipeline.ts +3 -2
- package/blocks/forms/README.md +8 -0
- package/blocks/forms/block.json +139 -18
- package/blocks/forms/components/form-activity.tsx +71 -0
- package/blocks/forms/components/form-backfill.tsx +108 -0
- package/blocks/forms/components/form-builder.tsx +4 -2
- package/blocks/forms/components/form-embed.tsx +4 -2
- package/blocks/forms/components/form-groups.tsx +151 -0
- package/blocks/forms/components/form-mapping-editor.tsx +6 -9
- package/blocks/forms/components/form-route-history.tsx +72 -0
- package/blocks/forms/components/form-saved-views.tsx +81 -0
- package/blocks/forms/components/forms-screen.tsx +56 -19
- package/blocks/forms/components/more-rows.tsx +30 -0
- package/blocks/forms/components/submissions-export.tsx +67 -0
- package/blocks/forms/components/submissions-table.tsx +17 -6
- package/blocks/forms/lib/forms-extra.ts +67 -0
- package/blocks/inbox/README.md +32 -3
- package/blocks/inbox/block.json +29 -2
- package/blocks/inbox/components/inbox-list.tsx +7 -3
- package/blocks/inbox/components/inbox-screen.tsx +294 -27
- package/blocks/inbox/components/record-conversations-tab.tsx +154 -0
- package/blocks/inbox/components/reply-composer.tsx +65 -9
- package/blocks/inbox/components/tag-and-assign.tsx +4 -2
- package/blocks/inbox/components/thread-badge.tsx +2 -2
- package/blocks/inbox/components/thread-view.tsx +1 -1
- package/blocks/inbox/hooks/use-inbox-thread.ts +1 -1
- package/blocks/inbox/hooks/use-inbox-threads.ts +1 -0
- package/blocks/inbox/lib/inbox-types.ts +47 -1
- package/blocks/inbox/recipes/01-triage-and-reply.ts +12 -2
- package/blocks/integrations/README.md +63 -1
- package/blocks/integrations/block.json +234 -15
- package/blocks/integrations/components/ad-accounts.tsx +82 -0
- package/blocks/integrations/components/connection-health.tsx +59 -0
- package/blocks/integrations/components/crm-detail.tsx +119 -0
- package/blocks/integrations/components/crm-records.tsx +103 -0
- package/blocks/integrations/components/field-mappings.tsx +61 -0
- package/blocks/integrations/components/integration-list.tsx +13 -8
- package/blocks/integrations/components/integrations-screen.tsx +152 -0
- package/blocks/integrations/components/migration-panel.tsx +133 -0
- package/blocks/integrations/components/sugarcrm-connect.tsx +95 -0
- package/blocks/integrations/components/sync-status.tsx +11 -9
- package/blocks/integrations/components/sync-triggers.tsx +134 -0
- package/blocks/integrations/components/webhook-detail.tsx +179 -0
- package/blocks/integrations/components/webhook-manager.tsx +43 -25
- package/blocks/integrations/components/workspace-tools.tsx +127 -0
- package/blocks/integrations/lib/crm-utils.ts +39 -0
- package/blocks/integrations/lib/facts.tsx +13 -0
- package/blocks/integrations/lib/integrations-utils.ts +11 -1
- package/blocks/integrations/lib/load-more.tsx +141 -0
- package/blocks/intent/README.md +14 -0
- package/blocks/intent/block.json +27 -1
- package/blocks/intent/components/account-intent-score.tsx +5 -5
- package/blocks/intent/components/intent-feed.tsx +13 -10
- package/blocks/intent/components/intent-keyword-manager.tsx +16 -12
- package/blocks/intent/components/intent-screen.tsx +265 -0
- package/blocks/intent/components/keyword-detail.tsx +124 -0
- package/blocks/intent/lib/intent-utils.ts +4 -4
- package/blocks/lifecycle/block.json +13 -8
- package/blocks/lifecycle/components/account-related.tsx +8 -1
- package/blocks/lifecycle/components/churn-list.tsx +5 -2
- package/blocks/lifecycle/components/customer-table.tsx +1 -1
- package/blocks/lifecycle/components/lifecycle-board.tsx +3 -1
- package/blocks/lifecycle/components/lifecycle-screen.tsx +3 -1
- package/blocks/lifecycle/lib/lifecycle.ts +1 -1
- package/blocks/lists/README.md +20 -1
- package/blocks/lists/block.json +69 -3
- package/blocks/lists/components/add-company-people-to-list-button.tsx +73 -0
- package/blocks/lists/components/add-to-list-button.tsx +20 -9
- package/blocks/lists/components/list-picker.tsx +17 -3
- package/blocks/lists/components/list-table.tsx +12 -6
- package/blocks/lists/components/lists-screen.tsx +346 -0
- package/blocks/lists/components/record-lists-tab.tsx +99 -0
- package/blocks/lists/components/suppression-manager.tsx +33 -8
- package/blocks/lists/components/suppressions-screen.tsx +264 -0
- package/blocks/mailboxes/README.md +17 -0
- package/blocks/mailboxes/block.json +69 -17
- package/blocks/mailboxes/components/connect-mailbox.tsx +32 -21
- package/blocks/mailboxes/components/domain-detail.tsx +148 -0
- package/blocks/mailboxes/components/domain-purchase.tsx +20 -14
- package/blocks/mailboxes/components/mailbox-detail.tsx +157 -0
- package/blocks/mailboxes/components/mailbox-list.tsx +8 -3
- package/blocks/mailboxes/components/mailboxes-screen.tsx +228 -0
- package/blocks/mailboxes/components/warmup-health.tsx +24 -22
- package/blocks/mailboxes/lib/mailboxes-utils.ts +26 -5
- package/blocks/mailboxes/lib/use-provider-domains.ts +23 -0
- package/blocks/newsletter/README.md +12 -0
- package/blocks/newsletter/block.json +157 -24
- package/blocks/newsletter/components/issue-scheduler.tsx +4 -3
- package/blocks/newsletter/components/issue-versions.tsx +93 -0
- package/blocks/newsletter/components/more-rows.tsx +30 -0
- package/blocks/newsletter/components/newsletter-editor.tsx +1 -1
- package/blocks/newsletter/components/newsletter-list-syncs.tsx +121 -0
- package/blocks/newsletter/components/newsletter-screen.tsx +39 -14
- package/blocks/newsletter/components/newsletter-settings.tsx +211 -0
- package/blocks/newsletter/components/newsletter-stats.tsx +2 -2
- package/blocks/newsletter/components/newsletter-templates.tsx +133 -0
- package/blocks/newsletter/components/subscriber-table.tsx +5 -7
- package/blocks/newsletter/components/subscriber-timeline.tsx +65 -0
- package/blocks/newsletter/lib/newsletter-extra.ts +60 -0
- package/blocks/nurture/block.json +28 -14
- package/blocks/nurture/components/nurture-builder.tsx +13 -3
- package/blocks/nurture/components/nurture-enroll.tsx +12 -10
- package/blocks/nurture/components/nurture-form.tsx +27 -10
- package/blocks/nurture/components/nurture-screen.tsx +26 -11
- package/blocks/nurture/components/nurture-stats.tsx +5 -4
- package/blocks/nurture/lib/nurture-types.ts +1 -1
- package/blocks/objects/README.md +18 -1
- package/blocks/objects/block.json +42 -21
- package/blocks/objects/components/attribute-field-input.tsx +14 -1
- package/blocks/objects/components/object-not-found.tsx +3 -1
- package/blocks/objects/components/object-record-detail.tsx +10 -2
- package/blocks/objects/components/object-schema-editor.tsx +4 -2
- package/blocks/objects/components/object-table.tsx +13 -3
- package/blocks/objects/components/objects-screen.tsx +147 -0
- package/blocks/objects/components/record-roles-panel.tsx +15 -5
- package/blocks/objects/lib/object-types.ts +1 -1
- package/blocks/pages/README.md +14 -0
- package/blocks/pages/block.json +140 -23
- package/blocks/pages/components/brand-kit-editor.tsx +5 -2
- package/blocks/pages/components/content-library.tsx +9 -4
- package/blocks/pages/components/more-rows.tsx +30 -0
- package/blocks/pages/components/page-ai-tools.tsx +196 -0
- package/blocks/pages/components/page-analytics.tsx +176 -0
- package/blocks/pages/components/page-clone.tsx +99 -0
- package/blocks/pages/components/page-domains.tsx +167 -0
- package/blocks/pages/components/page-editor.tsx +1 -1
- package/blocks/pages/components/page-generation-status.tsx +48 -0
- package/blocks/pages/components/page-list.tsx +2 -1
- package/blocks/pages/components/page-tracking.tsx +137 -0
- package/blocks/pages/components/pages-screen.tsx +57 -16
- package/blocks/pages/components/seo-report.tsx +6 -1
- package/blocks/pages/lib/studio.ts +7 -0
- package/blocks/phone-numbers/README.md +74 -0
- package/blocks/phone-numbers/block.json +262 -0
- package/blocks/phone-numbers/components/a2p-registration.tsx +230 -0
- package/blocks/phone-numbers/components/assign-number.tsx +104 -0
- package/blocks/phone-numbers/components/number-search.tsx +239 -0
- package/blocks/phone-numbers/components/number-settings.tsx +134 -0
- package/blocks/phone-numbers/components/number-usage.tsx +97 -0
- package/blocks/phone-numbers/components/phone-number-detail.tsx +84 -0
- package/blocks/phone-numbers/components/phone-number-list.tsx +110 -0
- package/blocks/phone-numbers/components/phone-numbers-screen.tsx +201 -0
- package/blocks/phone-numbers/components/register-owned-number.tsx +98 -0
- package/blocks/phone-numbers/components/release-number.tsx +126 -0
- package/blocks/phone-numbers/hooks/use-phone-numbers.ts +58 -0
- package/blocks/phone-numbers/lib/phone-numbers-utils.ts +202 -0
- package/blocks/phone-numbers/recipes/01-search-and-buy-number.ts +49 -0
- package/blocks/phone-numbers/recipes/02-assign-number-to-rep.ts +25 -0
- package/blocks/phone-numbers/recipes/03-audit-numbers.ts +36 -0
- package/blocks/phone-numbers/recipes/04-register-text-messaging.ts +39 -0
- package/blocks/pickers/README.md +23 -0
- package/blocks/pickers/block.json +103 -0
- package/blocks/pickers/components/company-picker.tsx +90 -0
- package/blocks/pickers/components/contact-picker.tsx +131 -0
- package/blocks/pickers/components/member-picker.tsx +99 -0
- package/blocks/pickers/components/search-picker.tsx +98 -0
- package/blocks/pickers/hooks/use-current-member.ts +104 -0
- package/blocks/pickers/hooks/use-picker-pages.ts +73 -0
- package/blocks/pickers/lib/use-debounced.ts +13 -0
- package/blocks/quotes/README.md +13 -0
- package/blocks/quotes/block.json +65 -13
- package/blocks/quotes/components/create-quote-from-deal.tsx +48 -0
- package/blocks/quotes/components/product-picker.tsx +3 -1
- package/blocks/quotes/components/quote-builder.tsx +1 -1
- package/blocks/quotes/components/quote-lifecycle-actions.tsx +146 -0
- package/blocks/quotes/components/quote-list.tsx +5 -3
- package/blocks/quotes/components/quote-related.tsx +8 -4
- package/blocks/quotes/components/quote-viewer.tsx +7 -2
- package/blocks/quotes/components/record-quotes-tab.tsx +90 -0
- package/blocks/radar/README.md +10 -0
- package/blocks/radar/block.json +41 -1
- package/blocks/radar/components/competitor-battle-card.tsx +1 -1
- package/blocks/radar/components/competitor-detail.tsx +117 -0
- package/blocks/radar/components/competitor-tracker.tsx +3 -1
- package/blocks/radar/components/radar-overview.tsx +4 -4
- package/blocks/radar/components/radar-report.tsx +4 -2
- package/blocks/radar/components/radar-screen.tsx +214 -0
- package/blocks/radar/components/report-detail.tsx +104 -0
- package/blocks/search/README.md +10 -0
- package/blocks/search/block.json +16 -1
- package/blocks/search/components/filter-builder.tsx +3 -3
- package/blocks/search/components/prospect-results.tsx +5 -3
- package/blocks/search/components/prospect-search-screen.tsx +235 -0
- package/blocks/search/components/saved-searches.tsx +4 -1
- package/blocks/search/hooks/use-filter-options.ts +20 -6
- package/blocks/search/lib/search-types.ts +2 -0
- package/blocks/sequencer/README.md +55 -9
- package/blocks/sequencer/block.json +126 -19
- package/blocks/sequencer/components/add-to-sequence-button.tsx +82 -0
- package/blocks/sequencer/components/enroll-dialog.tsx +11 -5
- package/blocks/sequencer/components/record-sequences-tab.tsx +126 -0
- package/blocks/sequencer/components/sequence-builder.tsx +224 -60
- package/blocks/sequencer/components/sequence-detail.tsx +261 -0
- package/blocks/sequencer/components/sequence-enrollments.tsx +116 -0
- package/blocks/sequencer/components/sequence-list.tsx +34 -9
- package/blocks/sequencer/components/sequence-stats.tsx +11 -10
- package/blocks/sequencer/components/sequence-steps-panel.tsx +103 -0
- package/blocks/sequencer/components/sequences-screen.tsx +162 -0
- package/blocks/sequencer/components/step-editor.tsx +14 -12
- package/blocks/sequencer/lib/sequencer-utils.ts +133 -10
- package/blocks/sequencer/recipes/create-multichannel-sequence.ts +33 -5
- package/blocks/sequencer/recipes/launch-and-enroll.ts +19 -2
- package/blocks/social-ads/README.md +19 -0
- package/blocks/social-ads/block.json +233 -1
- package/blocks/social-ads/components/ad-campaign-list.tsx +9 -5
- package/blocks/social-ads/components/ads-screen.tsx +152 -0
- package/blocks/social-ads/components/audience-sync-detail.tsx +194 -0
- package/blocks/social-ads/components/audience-sync.tsx +17 -9
- package/blocks/social-ads/components/campaign-ads.tsx +166 -0
- package/blocks/social-ads/components/campaign-detail.tsx +138 -0
- package/blocks/social-ads/components/campaign-editor.tsx +134 -0
- package/blocks/social-ads/components/creative-concepts.tsx +74 -0
- package/blocks/social-ads/components/creative-detail.tsx +155 -0
- package/blocks/social-ads/components/creative-gallery.tsx +35 -7
- package/blocks/social-ads/components/creative-revisions.tsx +57 -0
- package/blocks/social-ads/components/creative-tools.tsx +176 -0
- package/blocks/social-ads/components/post-composer.tsx +7 -3
- package/blocks/social-ads/components/post-detail.tsx +163 -0
- package/blocks/social-ads/components/social-ideas.tsx +64 -0
- package/blocks/social-ads/components/social-performance.tsx +80 -0
- package/blocks/social-ads/components/social-post-list.tsx +11 -3
- package/blocks/social-ads/components/social-screen.tsx +187 -0
- package/blocks/social-ads/lib/ads-extra.ts +67 -0
- package/blocks/surveys/README.md +91 -0
- package/blocks/surveys/block.json +299 -0
- package/blocks/surveys/components/account-nps.tsx +69 -0
- package/blocks/surveys/components/contact-survey-history.tsx +52 -0
- package/blocks/surveys/components/org-summary.tsx +82 -0
- package/blocks/surveys/components/sending-domains.tsx +113 -0
- package/blocks/surveys/components/survey-form.tsx +126 -0
- package/blocks/surveys/components/survey-responses.tsx +145 -0
- package/blocks/surveys/components/survey-results.tsx +72 -0
- package/blocks/surveys/components/survey-send-history.tsx +58 -0
- package/blocks/surveys/components/survey-send.tsx +162 -0
- package/blocks/surveys/components/survey-triggers.tsx +166 -0
- package/blocks/surveys/components/surveys-screen.tsx +234 -0
- package/blocks/surveys/lib/surveys-types.ts +228 -0
- package/blocks/surveys/recipes/create-an-nps-survey.ts +22 -0
- package/blocks/surveys/recipes/send-a-survey-and-follow-it.ts +27 -0
- package/blocks/surveys/recipes/survey-score-report.ts +27 -0
- package/blocks/visitors/README.md +10 -0
- package/blocks/visitors/block.json +25 -1
- package/blocks/visitors/components/snippet-install.tsx +21 -10
- package/blocks/visitors/components/traffic-chart.tsx +4 -4
- package/blocks/visitors/components/visitor-company-card.tsx +26 -16
- package/blocks/visitors/components/visitor-detail.tsx +105 -0
- package/blocks/visitors/components/visitor-feed.tsx +16 -7
- package/blocks/visitors/components/visitors-screen.tsx +166 -0
- package/blocks/webchat/README.md +10 -0
- package/blocks/webchat/block.json +21 -7
- package/blocks/webchat/components/chat-widget-config.tsx +8 -5
- package/blocks/webchat/components/conversation-list.tsx +8 -4
- package/blocks/webchat/components/webchat-screen.tsx +245 -0
- package/blocks/workbench/README.md +11 -0
- package/blocks/workbench/block.json +48 -1
- package/blocks/workbench/components/column-recipe-editor.tsx +5 -3
- package/blocks/workbench/components/run-progress.tsx +1 -1
- package/blocks/workbench/components/workbench-grid.tsx +10 -4
- package/blocks/workbench/components/workbench-row-detail.tsx +104 -0
- package/blocks/workbench/components/workbench-screen.tsx +202 -0
- package/blocks/workbench/components/workbench-sources.tsx +194 -0
- package/blocks/workbench/components/workbench-table-list.tsx +14 -2
- package/blocks/workflows/README.md +13 -0
- package/blocks/workflows/block.json +28 -9
- package/blocks/workflows/components/run-history.tsx +31 -9
- package/blocks/workflows/components/trigger-config.tsx +3 -1
- package/blocks/workflows/components/workflow-editor.tsx +4 -2
- package/blocks/workflows/components/workflow-list.tsx +13 -6
- package/blocks/workflows/components/workflows-screen.tsx +193 -0
- package/dist/cli.js +3109 -1977
- package/dist/errors.d.mts +34 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +46 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.mjs +21 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.browser.js +11 -5
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.mjs +11 -5
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.d.mts +164 -25
- package/dist/index.d.ts +164 -25
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react.browser.js +10 -4
- package/dist/react.browser.js.map +1 -1
- package/dist/react.browser.mjs +10 -4
- package/dist/react.browser.mjs.map +1 -1
- package/dist/react.d.mts +142 -5
- package/dist/react.d.ts +142 -5
- package/dist/react.js +10 -4
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +10 -4
- package/dist/react.mjs.map +1 -1
- package/dist/types/blocks/add.d.ts +11 -4
- package/dist/types/blocks/brand.d.ts +122 -0
- package/dist/types/blocks/compose.d.ts +53 -0
- package/dist/types/blocks/create.d.ts +27 -20
- package/dist/types/blocks/doctor.d.ts +6 -1
- package/dist/types/blocks/flows.d.ts +7 -1
- package/dist/types/blocks/navfile.d.ts +35 -0
- package/dist/types/blocks/orgBrand.d.ts +34 -0
- package/dist/types/blocks/paths.d.ts +4 -0
- package/dist/types/blocks/prefixes.d.ts +37 -0
- package/dist/types/blocks/registry.d.ts +17 -0
- package/dist/types/blocks/screens.d.ts +53 -0
- package/dist/types/cli.d.ts +5 -0
- package/dist/types/errors.d.ts +33 -0
- package/dist/types/http.d.ts +2 -18
- package/dist/types/version.d.ts +2 -2
- package/docs/agents.md +15 -4
- package/docs/api/README.md +2 -2
- package/docs/api/by-path.md +1 -0
- package/docs/api/desk.d.ts +98 -2
- package/docs/api/desk.md +123 -1
- package/docs/api/inbox.d.ts +10 -2
- package/docs/api/inbox.md +10 -2
- package/docs/api/seo.d.ts +19 -1
- package/docs/api/seo.md +24 -1
- package/package.json +16 -2
- package/stacks/6sense/pages/companies.tsx.tmpl +21 -0
- package/stacks/6sense/pages/contacts.tsx.tmpl +19 -0
- package/stacks/6sense/pages/tasks.tsx.tmpl +19 -0
- package/stacks/6sense.json +55 -17
- package/stacks/apollo/pages/calls.tsx.tmpl +23 -0
- package/stacks/apollo/pages/companies.tsx.tmpl +33 -0
- package/stacks/apollo/pages/contacts.tsx.tmpl +51 -0
- package/stacks/apollo/pages/deals.tsx.tmpl +17 -0
- package/stacks/apollo/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/apollo/pages/lists.tsx.tmpl +12 -0
- package/stacks/apollo/pages/meetings.tsx.tmpl +13 -0
- package/stacks/apollo/pages/sequences.tsx.tmpl +12 -0
- package/stacks/apollo/pages/tasks.tsx.tmpl +20 -0
- package/stacks/apollo.json +147 -37
- package/stacks/calendly/pages/companies.tsx.tmpl +19 -0
- package/stacks/calendly/pages/contacts.tsx.tmpl +21 -0
- package/stacks/calendly/pages/meetings.tsx.tmpl +13 -0
- package/stacks/calendly/pages/tasks.tsx.tmpl +19 -0
- package/stacks/calendly.json +64 -0
- package/stacks/clay/pages/companies.tsx.tmpl +31 -0
- package/stacks/clay/pages/contacts.tsx.tmpl +31 -0
- package/stacks/clay/pages/lists.tsx.tmpl +12 -0
- package/stacks/clay/pages/tasks.tsx.tmpl +19 -0
- package/stacks/clay.json +73 -27
- package/stacks/gong/pages/calls.tsx.tmpl +23 -0
- package/stacks/gong/pages/companies.tsx.tmpl +22 -0
- package/stacks/gong/pages/contacts.tsx.tmpl +32 -0
- package/stacks/gong/pages/lists.tsx.tmpl +12 -0
- package/stacks/gong/pages/meetings.tsx.tmpl +13 -0
- package/stacks/gong/pages/tasks.tsx.tmpl +19 -0
- package/stacks/gong.json +98 -35
- package/stacks/hootsuite/pages/companies.tsx.tmpl +22 -0
- package/stacks/hootsuite/pages/contacts.tsx.tmpl +28 -0
- package/stacks/hootsuite/pages/lists.tsx.tmpl +12 -0
- package/stacks/hootsuite/pages/tasks.tsx.tmpl +19 -0
- package/stacks/hootsuite.json +104 -0
- package/stacks/hubspot/pages/companies.tsx.tmpl +26 -0
- package/stacks/hubspot/pages/contacts.tsx.tmpl +45 -0
- package/stacks/hubspot/pages/deals.tsx.tmpl +20 -0
- package/stacks/hubspot/pages/inbox.tsx.tmpl +103 -0
- package/stacks/hubspot/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/hubspot/pages/lists.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/meetings.tsx.tmpl +13 -0
- package/stacks/hubspot/pages/quotes.tsx.tmpl +11 -0
- package/stacks/hubspot/pages/sequences.tsx.tmpl +12 -0
- package/stacks/hubspot/pages/tasks.tsx.tmpl +20 -0
- package/stacks/hubspot.json +181 -64
- package/stacks/intercom/pages/companies.tsx.tmpl +19 -0
- package/stacks/intercom/pages/contacts.tsx.tmpl +21 -0
- package/stacks/intercom/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/intercom/pages/tasks.tsx.tmpl +19 -0
- package/stacks/intercom.json +52 -20
- package/stacks/mailchimp/pages/companies.tsx.tmpl +22 -0
- package/stacks/mailchimp/pages/contacts.tsx.tmpl +28 -0
- package/stacks/mailchimp/pages/lists.tsx.tmpl +12 -0
- package/stacks/mailchimp/pages/tasks.tsx.tmpl +19 -0
- package/stacks/mailchimp.json +68 -34
- package/stacks/outreach/pages/calls.tsx.tmpl +23 -0
- package/stacks/outreach/pages/companies.tsx.tmpl +22 -0
- package/stacks/outreach/pages/contacts.tsx.tmpl +46 -0
- package/stacks/outreach/pages/inbox-contact.tsx.tmpl +13 -0
- package/stacks/outreach/pages/lists.tsx.tmpl +12 -0
- package/stacks/outreach/pages/meetings.tsx.tmpl +13 -0
- package/stacks/outreach/pages/sequences.tsx.tmpl +12 -0
- package/stacks/outreach/pages/tasks.tsx.tmpl +19 -0
- package/stacks/outreach.json +106 -25
- package/stacks/pipedrive/pages/companies.tsx.tmpl +23 -0
- package/stacks/pipedrive/pages/contacts.tsx.tmpl +27 -0
- package/stacks/pipedrive/pages/deals.tsx.tmpl +20 -0
- package/stacks/pipedrive/pages/inbox.tsx.tmpl +103 -0
- package/stacks/pipedrive/pages/meetings.tsx.tmpl +13 -0
- package/stacks/pipedrive/pages/quotes.tsx.tmpl +11 -0
- package/stacks/pipedrive/pages/tasks.tsx.tmpl +20 -0
- package/stacks/pipedrive.json +117 -0
- package/stacks/salesforce/pages/companies.tsx.tmpl +26 -0
- package/stacks/salesforce/pages/contacts.tsx.tmpl +32 -0
- package/stacks/salesforce/pages/deals.tsx.tmpl +20 -0
- package/stacks/salesforce/pages/lifecycle.tsx.tmpl +23 -0
- package/stacks/salesforce/pages/lists.tsx.tmpl +12 -0
- package/stacks/salesforce/pages/quotes.tsx.tmpl +11 -0
- package/stacks/salesforce/pages/tasks.tsx.tmpl +20 -0
- package/stacks/salesforce.json +85 -31
- package/stacks/zoominfo/pages/companies.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/contacts.tsx.tmpl +31 -0
- package/stacks/zoominfo/pages/lists.tsx.tmpl +12 -0
- package/stacks/zoominfo/pages/tasks.tsx.tmpl +19 -0
- package/stacks/zoominfo.json +115 -0
- package/styles/styles.css +1 -1
- package/templates/next-app/README.md.tmpl +40 -9
- package/templates/next-app/app/globals.css.tmpl +349 -64
- package/templates/next-app/app/layout.tsx.tmpl +7 -2
- package/templates/next-app/components/app-shell.tsx.tmpl +104 -88
- package/templates/next-app/components/shell/quick-jump.tsx.tmpl +4 -0
- package/templates/next-app/dot-gitignore.tmpl +2 -0
- package/templates/next-app/package.json.tmpl +1 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgentApprovals: the approvals inbox for everything agents ask permission for. Three parts:
|
|
5
|
+
* Oversight what needs you now (approvals, blockers) and what agents fixed on their own.
|
|
6
|
+
* Waiting each pending approval with Approve (lets the agent act: external, ConfirmSpendDialog), Reject, or Request changes
|
|
7
|
+
* (sends your feedback back instead of deciding).
|
|
8
|
+
* History decided approvals, filterable by decision, each with its signed receipts (who contributed what).
|
|
9
|
+
* Approvals that gate a Work coordination action cannot be decided with an API key; the error is shown as is.
|
|
10
|
+
*
|
|
11
|
+
* SDK operations, in order:
|
|
12
|
+
* 1. getAgentOversight GET /agent/oversight?limit=20 (read)
|
|
13
|
+
* 2. listAgentApprovals GET /agent/approvals (read) pending
|
|
14
|
+
* 3. decideAgentApproval POST /agent/approvals/{approval_id}/decide { decision } (write; approving is confirmed as external)
|
|
15
|
+
* 4. requestAgentApprovalChanges POST /agent/approvals/{approval_id}/request-changes { feedback } (write)
|
|
16
|
+
* 5. listAgentApprovalHistory GET /agent/approvals/history?decision (read)
|
|
17
|
+
* 6. getAgentApprovalReceipts GET /agent/approvals/{approval_id}/receipts (read)
|
|
18
|
+
*/
|
|
19
|
+
import { useState } from "react";
|
|
20
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
21
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
22
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
23
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
24
|
+
import { SectionCard } from "@/g8/components/screen/section-card";
|
|
25
|
+
import { StatusPill, toneForStatus } from "@/g8/components/screen/status-pill";
|
|
26
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
27
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
28
|
+
import { g8Get, g8Post } from "@/g8/lib/g8-client";
|
|
29
|
+
import { bodyOf, listOf } from "../lib/agent-types";
|
|
30
|
+
|
|
31
|
+
export interface AgentApprovalsProps {
|
|
32
|
+
/** Open the run an approval belongs to. */
|
|
33
|
+
onOpenRun?: (runId: string) => void;
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface Approval { id: string; summary: string; risk_level: string; work_item_id: string; decision?: string | null; is_expired?: boolean; decided_at?: string | null; decided_by?: string | null; requested_at?: string | null }
|
|
38
|
+
interface Receipt { id: string; action: string; entity_type: string; entity_id: string; contributed_by_user_id?: string | null; contributed_by_role_at_time?: string | null }
|
|
39
|
+
// The only decision words the history filter accepts (agent_router.py).
|
|
40
|
+
const DECISIONS: Array<[string, string]> = [["", "Any decision"], ["approve", "Approved"], ["reject", "Rejected"], ["edit_and_approve", "Edited and approved"], ["dismiss", "Dismissed"]];
|
|
41
|
+
/** Rows per kind the oversight feed returns; a full kind means there may be more. */
|
|
42
|
+
const OVERSIGHT_LIMIT = 20;
|
|
43
|
+
const when = (v?: string | null): string => (v ? new Date(v).toLocaleString() : "");
|
|
44
|
+
const enc = encodeURIComponent;
|
|
45
|
+
|
|
46
|
+
function Receipts({ approvalId }: { approvalId: string }) {
|
|
47
|
+
const q = useG8Query((signal) => g8Get(`/agent/approvals/${enc(approvalId)}/receipts`, undefined, { signal }), [approvalId]);
|
|
48
|
+
const rows = bodyOf<{ receipts?: Receipt[] }>(q.data).receipts ?? [];
|
|
49
|
+
if (q.error) return <ApiError error={q.error} compact title="Could not load the receipts" onRetry={() => void q.refetch()} />;
|
|
50
|
+
if (q.loading && !q.data) return <Skeleton className="h-8 w-full" />;
|
|
51
|
+
if (rows.length === 0) return <p className="text-xs text-muted-foreground">No receipts were signed for this approval.</p>;
|
|
52
|
+
return <ul className="divide-y rounded-md border text-xs" aria-label="Receipts">{rows.map((r) => <li key={r.id} className="px-3 py-1.5">{r.action} on {r.entity_type} {r.entity_id}<span className="ml-2 text-muted-foreground">{r.contributed_by_user_id ?? "system"}{r.contributed_by_role_at_time ? `, ${r.contributed_by_role_at_time}` : ""}</span></li>)}</ul>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function AgentApprovals({ onOpenRun, className }: AgentApprovalsProps) {
|
|
56
|
+
const [decision, setDecision] = useState("");
|
|
57
|
+
const [approving, setApproving] = useState<Approval | null>(null);
|
|
58
|
+
const [changes, setChanges] = useState<{ approval: Approval; feedback: string } | null>(null);
|
|
59
|
+
const [showReceipts, setShowReceipts] = useState<string | null>(null);
|
|
60
|
+
const oversight = useG8Query((signal) => g8Get("/agent/oversight", { limit: OVERSIGHT_LIMIT }, { signal }), []);
|
|
61
|
+
const pending = useG8Query((signal) => g8Get("/agent/approvals", undefined, { signal }), []);
|
|
62
|
+
const history = useG8Query((signal) => g8Get("/agent/approvals/history", { decision: decision || undefined }, { signal }), [decision]);
|
|
63
|
+
const waiting = listOf<Approval>(pending.data).filter((a) => !a.decision && !a.is_expired);
|
|
64
|
+
const decided = listOf<Approval>(history.data);
|
|
65
|
+
const ov = bodyOf<{ approvals?: unknown[]; blockers?: Array<{ title?: string; summary?: string }>; autonomous_fixes?: Array<{ title?: string; summary?: string }> }>(oversight.data);
|
|
66
|
+
const refresh = () => { void pending.refetch(); void history.refetch(); void oversight.refetch(); };
|
|
67
|
+
|
|
68
|
+
const decide = useG8Mutation((v: { id: string; decision: "approve" | "reject" }) => g8Post(`/agent/approvals/${enc(v.id)}/decide`, { decision: v.decision }), { onSuccess: () => { setApproving(null); refresh(); }, onError: () => setApproving(null) });
|
|
69
|
+
const ask = useG8Mutation((v: { id: string; feedback: string }) => g8Post(`/agent/approvals/${enc(v.id)}/request-changes`, { feedback: v.feedback }), { onSuccess: () => { setChanges(null); refresh(); } });
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className={`flex flex-col gap-4 ${className ?? ""}`}>
|
|
73
|
+
<SectionCard title="Needs you" description="Approvals and blockers across all agents, and what agents fixed themselves">
|
|
74
|
+
{oversight.error ? <ApiError error={oversight.error} title="Could not load the oversight feed" onRetry={() => void oversight.refetch()} /> : null}
|
|
75
|
+
{oversight.loading && !oversight.data ? <Skeleton className="h-10 w-full" /> : null}
|
|
76
|
+
{oversight.data ? (
|
|
77
|
+
<dl className="grid grid-cols-3 gap-3 text-sm" data-testid="oversight">
|
|
78
|
+
<div><dt className="text-xs text-muted-foreground">Approvals</dt><dd>{(ov.approvals ?? []).length}</dd></div>
|
|
79
|
+
<div><dt className="text-xs text-muted-foreground">Blockers</dt><dd>{(ov.blockers ?? []).length}</dd></div>
|
|
80
|
+
<div><dt className="text-xs text-muted-foreground">Fixed on their own</dt><dd>{(ov.autonomous_fixes ?? []).length}</dd></div>
|
|
81
|
+
</dl>
|
|
82
|
+
) : null}
|
|
83
|
+
{[ov.approvals, ov.blockers, ov.autonomous_fixes].some((l) => (l ?? []).length >= OVERSIGHT_LIMIT) ? <p className="mt-2 text-xs text-muted-foreground">Showing the first {OVERSIGHT_LIMIT} of each kind. Older items are in the history below.</p> : null}
|
|
84
|
+
{(ov.blockers ?? []).length ? <ul className="mt-3 list-disc pl-5 text-sm">{(ov.blockers ?? []).map((b, i) => <li key={i}>{b.title ?? b.summary}</li>)}</ul> : null}
|
|
85
|
+
</SectionCard>
|
|
86
|
+
|
|
87
|
+
<SectionCard title={`Waiting for a decision (${waiting.length})`} flush>
|
|
88
|
+
{pending.error ? <div className="p-3"><ApiError error={pending.error} title="Could not load the approvals" onRetry={() => void pending.refetch()} /></div> : null}
|
|
89
|
+
{pending.loading && !pending.data ? <Skeleton className="m-3 h-10" /> : null}
|
|
90
|
+
<ul className="divide-y">
|
|
91
|
+
{waiting.map((a) => (
|
|
92
|
+
<li key={a.id} data-testid={`approval-${a.id}`} className="flex flex-wrap items-center justify-between gap-2 px-4 py-2 text-sm">
|
|
93
|
+
<span>{a.summary}<span className="ml-2 text-xs text-muted-foreground">risk: {a.risk_level}</span>{onOpenRun ? <button type="button" className="ml-2 text-xs underline" onClick={() => onOpenRun(a.work_item_id)}>Open run</button> : null}</span>
|
|
94
|
+
<span className="flex gap-2">
|
|
95
|
+
<Button size="sm" variant="ghost" onClick={() => setChanges({ approval: a, feedback: "" })}>Request changes</Button>
|
|
96
|
+
<Button size="sm" variant="outline" disabled={decide.loading} onClick={() => void decide.mutate({ id: a.id, decision: "reject" })}>Reject</Button>
|
|
97
|
+
<Button size="sm" disabled={decide.loading} onClick={() => setApproving(a)}>Approve</Button>
|
|
98
|
+
</span>
|
|
99
|
+
</li>
|
|
100
|
+
))}
|
|
101
|
+
</ul>
|
|
102
|
+
{!pending.loading && !pending.error && waiting.length === 0 ? <EmptyState compact title="Nothing is waiting" description="When an agent needs permission to act, it shows up here." /> : null}
|
|
103
|
+
{decide.error ? <div className="p-3"><ApiError error={decide.error} title="Could not record the decision" /></div> : null}
|
|
104
|
+
</SectionCard>
|
|
105
|
+
|
|
106
|
+
<SectionCard title="History" flush actions={<select aria-label="Decision" className="h-8 rounded-md border bg-background px-2 text-sm" value={decision} onChange={(e) => setDecision(e.target.value)}>{DECISIONS.map(([v, l]) => <option key={v} value={v}>{l}</option>)}</select>}>
|
|
107
|
+
{history.error ? <div className="p-3"><ApiError error={history.error} title="Could not load the history" onRetry={() => void history.refetch()} /></div> : null}
|
|
108
|
+
{history.loading && !history.data ? <Skeleton className="m-3 h-10" /> : null}
|
|
109
|
+
<ul className="divide-y">
|
|
110
|
+
{decided.map((a) => (
|
|
111
|
+
<li key={a.id} className="px-4 py-2 text-sm">
|
|
112
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
113
|
+
<span>{a.summary}</span>
|
|
114
|
+
<span className="flex items-center gap-2"><StatusPill tone={toneForStatus(a.decision ?? "")} dot>{a.decision ?? "undecided"}</StatusPill><span className="text-xs text-muted-foreground">{when(a.decided_at ?? a.requested_at)}</span><Button size="sm" variant="ghost" aria-expanded={showReceipts === a.id} onClick={() => setShowReceipts(showReceipts === a.id ? null : a.id)}>Receipts</Button></span>
|
|
115
|
+
</div>
|
|
116
|
+
{showReceipts === a.id ? <div className="mt-2"><Receipts approvalId={a.id} /></div> : null}
|
|
117
|
+
</li>
|
|
118
|
+
))}
|
|
119
|
+
</ul>
|
|
120
|
+
{!history.loading && !history.error && decided.length === 0 ? <EmptyState compact title="No decisions yet" /> : null}
|
|
121
|
+
</SectionCard>
|
|
122
|
+
|
|
123
|
+
<Dialog open={changes != null} onOpenChange={(o) => !o && setChanges(null)}>
|
|
124
|
+
<DialogContent>
|
|
125
|
+
<DialogHeader><DialogTitle>Request changes</DialogTitle><DialogDescription>{changes?.approval.summary}</DialogDescription></DialogHeader>
|
|
126
|
+
<Textarea aria-label="Feedback" rows={4} placeholder="What should the agent change?" value={changes?.feedback ?? ""} onChange={(e) => setChanges((c) => c && { ...c, feedback: e.target.value })} />
|
|
127
|
+
{ask.error ? <ApiError error={ask.error} title="Could not send the feedback" /> : null}
|
|
128
|
+
<DialogFooter><Button variant="ghost" onClick={() => setChanges(null)}>Cancel</Button><Button loading={ask.loading} disabled={!changes?.feedback.trim()} onClick={() => changes && void ask.mutate({ id: changes.approval.id, feedback: changes.feedback.trim() })}>Send feedback</Button></DialogFooter>
|
|
129
|
+
</DialogContent>
|
|
130
|
+
</Dialog>
|
|
131
|
+
<ConfirmSpendDialog open={approving != null} tier="external" title="Approve this action" description={approving?.summary}
|
|
132
|
+
confirmLabel="Approve" confirming={decide.loading} onConfirm={() => approving && void decide.mutate({ id: approving.id, decision: "approve" })} onCancel={() => setApproving(null)} />
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
import { useEffect, useState } from "react";
|
|
22
22
|
import { Button, Input, Label, Skeleton, Switch, Textarea } from "@graph8/sdk/ui";
|
|
23
23
|
import { ApiError } from "@/g8/components/api-error";
|
|
24
|
+
import { useLoadMoreList } from "@/g8/hooks/use-load-more-list";
|
|
25
|
+
import { byPage, hasNextOf } from "@/g8/lib/paging";
|
|
26
|
+
import { FirstNotice, LoadMore } from "@/g8/components/paging-notices";
|
|
24
27
|
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
25
28
|
import { EmptyState } from "@/g8/components/empty-state";
|
|
26
29
|
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
@@ -37,6 +40,8 @@ export interface AgentBuilderProps {
|
|
|
37
40
|
onSelect?: (agentId: string | null) => void;
|
|
38
41
|
/** Called after a create, update or delete so the host can refresh its own lists. */
|
|
39
42
|
onChanged?: () => void;
|
|
43
|
+
/** Hide the built-in agent list and "New agent" button when the host (for example AgentsScreen) provides its own. */
|
|
44
|
+
hideList?: boolean;
|
|
40
45
|
className?: string;
|
|
41
46
|
}
|
|
42
47
|
|
|
@@ -60,7 +65,12 @@ function Dial({ label, value, onChange, low, high }: { label: string; value: num
|
|
|
60
65
|
);
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
/** Agents per read; "Load more" appends the next page (the list used to stop silently at 50). */
|
|
69
|
+
const AGENT_PAGE = 50;
|
|
70
|
+
/** The voice presets API has no paging: a full read is announced under the select. */
|
|
71
|
+
const VOICE_LIMIT = 100;
|
|
72
|
+
|
|
73
|
+
export function AgentBuilder({ agentId: controlled, onSelect, onChanged, hideList, className }: AgentBuilderProps) {
|
|
64
74
|
const [inner, setInner] = useState<string | null>(null);
|
|
65
75
|
const selected = controlled !== undefined ? controlled : inner;
|
|
66
76
|
const [creating, setCreating] = useState(false);
|
|
@@ -70,9 +80,13 @@ export function AgentBuilder({ agentId: controlled, onSelect, onChanged, classNa
|
|
|
70
80
|
const [deleting, setDeleting] = useState(false);
|
|
71
81
|
|
|
72
82
|
const choose = (id: string | null) => { setInner(id); setCreating(false); onSelect?.(id); };
|
|
73
|
-
const agents =
|
|
83
|
+
const agents = useLoadMoreList(byPage<AgentSummary>(AGENT_PAGE, async (page, signal) => {
|
|
84
|
+
const env = await g8Get("/voice/agents", { search: search.trim() || undefined, page, page_size: AGENT_PAGE }, { signal });
|
|
85
|
+
const rows = listOf<AgentSummary>(env, "agents");
|
|
86
|
+
return { rows, hasNext: hasNextOf(env, AGENT_PAGE, rows.length) };
|
|
87
|
+
}), [search], { enabled: !hideList });
|
|
74
88
|
const detail = useG8Query((signal) => g8Get(`/voice/agents/${selected}`, undefined, { signal }), [selected], { enabled: Boolean(selected) && !creating });
|
|
75
|
-
const voices = useG8Query((signal) => g8Get("/voice/voice-library/presets", { limit:
|
|
89
|
+
const voices = useG8Query((signal) => g8Get("/voice/voice-library/presets", { limit: VOICE_LIMIT }, { signal }), []);
|
|
76
90
|
|
|
77
91
|
useEffect(() => {
|
|
78
92
|
const agent = bodyOf<{ agent?: AgentDetail | null }>(detail.data).agent;
|
|
@@ -87,7 +101,7 @@ export function AgentBuilder({ agentId: controlled, onSelect, onChanged, classNa
|
|
|
87
101
|
{
|
|
88
102
|
onSuccess: (res) => {
|
|
89
103
|
const id = bodyOf<{ agent?: { agent_id?: string } }>(res).agent?.agent_id;
|
|
90
|
-
void agents.refetch();
|
|
104
|
+
if (!hideList) void agents.refetch();
|
|
91
105
|
// Keep the new id on the draft at once: a second click (or a retry after a later step fails) must update, never create a twin.
|
|
92
106
|
if (!draft?.agent_id && id) { setDraft((d) => d && { ...d, agent_id: id }); choose(id); }
|
|
93
107
|
else void detail.refetch();
|
|
@@ -96,17 +110,19 @@ export function AgentBuilder({ agentId: controlled, onSelect, onChanged, classNa
|
|
|
96
110
|
},
|
|
97
111
|
);
|
|
98
112
|
const remove = useG8Mutation((id: string) => g8Delete(`/voice/agents/${id}`), {
|
|
99
|
-
onSuccess: () => { setDeleting(false); setDraft(null); choose(null); void agents.refetch(); onChanged?.(); },
|
|
113
|
+
onSuccess: () => { setDeleting(false); setDraft(null); choose(null); if (!hideList) void agents.refetch(); onChanged?.(); },
|
|
100
114
|
});
|
|
101
115
|
|
|
102
|
-
const rows =
|
|
116
|
+
const rows = agents.data ?? [];
|
|
103
117
|
const presets = listOf<{ voice_id?: string; id?: string; name?: string; label?: string }>(voices.data, "items");
|
|
104
118
|
const setPersona = (patch: Partial<AgentDetail["persona"]>) => setDraft((d) => d && { ...d, persona: { ...d.persona, ...patch } });
|
|
119
|
+
// The hook keeps the previous agent's data while the next one loads: until it arrives the old form must not be shown (or saved) as the new agent.
|
|
120
|
+
const stale = Boolean(draft?.agent_id && selected && draft.agent_id !== selected && !creating);
|
|
105
121
|
const valid = Boolean(draft?.persona.agent_name.trim() && draft?.persona.persona.trim() && draft?.persona.description.trim() && draft?.role.trim());
|
|
106
122
|
|
|
107
123
|
return (
|
|
108
|
-
<div className={`grid gap-4 md:grid-cols-[16rem_1fr] ${className ?? ""}`}>
|
|
109
|
-
<aside className="flex flex-col gap-2">
|
|
124
|
+
<div className={`grid gap-4 ${hideList ? "" : "md:grid-cols-[16rem_1fr]"} ${className ?? ""}`}>
|
|
125
|
+
{hideList ? null : <aside className="flex flex-col gap-2">
|
|
110
126
|
<div className="flex gap-2">
|
|
111
127
|
<Input aria-label="Search agents" placeholder="Search agents" value={search} onChange={(e) => setSearch(e.target.value)} />
|
|
112
128
|
<Button size="sm" onClick={() => { setCreating(true); setDraft(blankDraft()); setSection("persona"); onSelect?.(null); }}>New agent</Button>
|
|
@@ -124,15 +140,17 @@ export function AgentBuilder({ agentId: controlled, onSelect, onChanged, classNa
|
|
|
124
140
|
</li>
|
|
125
141
|
))}
|
|
126
142
|
</ul>
|
|
143
|
+
<LoadMore shown={rows.length} noun="agents" hasMore={agents.hasMore} loading={agents.loadingMore} onLoadMore={agents.loadMore} />
|
|
127
144
|
{!agents.loading && !agents.error && rows.length === 0 ? (
|
|
128
145
|
<EmptyState title="No agents yet" description="An agent has a persona, a voice, knowledge and skills." />
|
|
129
146
|
) : null}
|
|
130
|
-
</aside>
|
|
147
|
+
</aside>}
|
|
131
148
|
|
|
132
149
|
<section className="flex min-w-0 flex-col gap-3">
|
|
133
150
|
{!draft && !creating ? <EmptyState title="Pick an agent" description="Choose one on the left or create a new agent." /> : null}
|
|
134
151
|
{detail.error ? <ApiError error={detail.error} title="Could not load the agent" onRetry={() => void detail.refetch()} /> : null}
|
|
135
|
-
{
|
|
152
|
+
{stale && !detail.error ? <Skeleton className="h-64 w-full" aria-label="Loading agent" /> : null}
|
|
153
|
+
{draft && !stale ? (
|
|
136
154
|
<>
|
|
137
155
|
<div role="tablist" className="flex gap-1 border-b">
|
|
138
156
|
{SECTIONS.map((s) => (
|
|
@@ -165,6 +183,7 @@ export function AgentBuilder({ agentId: controlled, onSelect, onChanged, classNa
|
|
|
165
183
|
<option value="">Default voice</option>
|
|
166
184
|
{presets.map((v) => { const id = v.voice_id ?? v.id ?? ""; return <option key={id} value={id}>{v.name ?? v.label ?? id}</option>; })}
|
|
167
185
|
</select>
|
|
186
|
+
<FirstNotice shown={presets.length} limit={VOICE_LIMIT} noun="voices" />
|
|
168
187
|
</div>
|
|
169
188
|
<div className="flex flex-col gap-1"><Label htmlFor="agent-inbound">Inbound call instructions</Label><Textarea id="agent-inbound" rows={3} value={draft.persona.inbound_instructions ?? ""} onChange={(e) => setPersona({ inbound_instructions: e.target.value || null })} /></div>
|
|
170
189
|
<div className="flex flex-col gap-1"><Label htmlFor="agent-outbound">Outbound call instructions</Label><Textarea id="agent-outbound" rows={3} value={draft.persona.outbound_instructions ?? ""} onChange={(e) => setPersona({ outbound_instructions: e.target.value || null })} /></div>
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgentConversations: the chat history of one agent. The list of its conversations (newest first, "Load more" up to the API's 100),
|
|
5
|
+
* a conversation's messages, rename, hand-over to a human and delete, plus a message box to talk to the agent yourself.
|
|
6
|
+
* Sending a message is billable and the agent may run its attached skills while answering, so it goes through ConfirmSpendDialog.
|
|
7
|
+
* Deleting a conversation is destructive and confirmed.
|
|
8
|
+
*
|
|
9
|
+
* SDK operations, in order:
|
|
10
|
+
* 1. listChatAgentConversations POST /voice/chat-agent/conversations { agent_id, limit } (read, a POST with a body)
|
|
11
|
+
* 2. getChatAgentHistory POST /voice/chat-agent/history { agent_id, contact_id, session_id } (read)
|
|
12
|
+
* 3. renameChatAgentConversation PUT /voice/chat-agent/conversations/{chat_id}/name?agent_id { name } (write)
|
|
13
|
+
* 4. transferChatAgentConversation POST /voice/chat-agent/conversations/{chat_id}/transfer-to-human (write)
|
|
14
|
+
* 5. deleteChatAgentConversation DELETE /voice/chat-agent/conversations/{chat_id}?agent_id (destructive, ConfirmSpendDialog)
|
|
15
|
+
* 6. sendChatAgentMessage POST /voice/chat-agent/messages { agent_id, contact_id, message, session_id? } (billable, ConfirmSpendDialog)
|
|
16
|
+
*
|
|
17
|
+
* The history route does not match on contact_id, so any stable string works; `contactId` lets a host pass the person's own id.
|
|
18
|
+
*/
|
|
19
|
+
import { useEffect, useState } from "react";
|
|
20
|
+
import { MessageSquare } from "lucide-react";
|
|
21
|
+
import { Button, Input, Skeleton, Textarea } from "@graph8/sdk/ui";
|
|
22
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
23
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
24
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
25
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
26
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
27
|
+
import { g8Delete, g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
|
|
28
|
+
import { bodyOf, listOf } from "../lib/agent-types";
|
|
29
|
+
import { LoadMore, useGrowingList } from "../lib/load-more";
|
|
30
|
+
import { useBrand } from "@/g8/hooks/use-brand";
|
|
31
|
+
|
|
32
|
+
export interface AgentConversationsProps {
|
|
33
|
+
agentId: string;
|
|
34
|
+
/** Sent as contact_id on messages and history reads. Default "sdk". */
|
|
35
|
+
contactId?: string;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface Chat { id: string; name?: string; message_count?: number; updated_at?: string | null; created_at?: string | null }
|
|
40
|
+
interface Msg { role?: string; content?: unknown; created_at?: string }
|
|
41
|
+
|
|
42
|
+
const text = (v: unknown): string => (typeof v === "string" ? v : v == null ? "" : JSON.stringify(v));
|
|
43
|
+
const when = (v?: string | null): string => (v ? new Date(v).toLocaleString() : "");
|
|
44
|
+
|
|
45
|
+
function Transcript({ agentId, chat, contactId, refreshKey }: { agentId: string; chat: Chat; contactId: string; refreshKey: number }) {
|
|
46
|
+
const history = useG8Query((signal) => g8Post(`/voice/chat-agent/history`, { agent_id: agentId, contact_id: contactId, session_id: chat.id }, { signal }), [agentId, chat.id, contactId, refreshKey]);
|
|
47
|
+
const body = bodyOf<{ messages?: Msg[]; success?: boolean; error?: string }>(history.data);
|
|
48
|
+
const messages = listOf<Msg>(history.data, "messages", "items");
|
|
49
|
+
if (history.error) return <ApiError error={history.error} title="Could not load the conversation" onRetry={() => void history.refetch()} />;
|
|
50
|
+
if (history.loading && !history.data) return <Skeleton className="h-24 w-full" aria-label="Loading conversation" />;
|
|
51
|
+
if (body.success === false && body.error) return <p className="text-sm text-muted-foreground">{body.error}</p>;
|
|
52
|
+
if (!messages.length) return <p className="text-sm text-muted-foreground">No messages in this conversation yet.</p>;
|
|
53
|
+
return (
|
|
54
|
+
<ol className="flex flex-col gap-2" aria-label="Messages">
|
|
55
|
+
{messages.map((m, i) => (
|
|
56
|
+
<li key={i} className={`max-w-[85%] rounded-md border px-3 py-2 text-sm ${m.role === "user" ? "self-end bg-accent" : "self-start"}`}>
|
|
57
|
+
<span className="block text-xs text-muted-foreground">{m.role === "user" ? "Contact" : m.role === "assistant" ? "Agent" : (m.role ?? "")}</span>
|
|
58
|
+
<span className="whitespace-pre-wrap">{text(m.content)}</span>
|
|
59
|
+
</li>
|
|
60
|
+
))}
|
|
61
|
+
</ol>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function AgentConversations({ agentId, contactId = "sdk", className }: AgentConversationsProps) {
|
|
66
|
+
const brand = useBrand();
|
|
67
|
+
const [openId, setOpenId] = useState<string | null>(null);
|
|
68
|
+
const [renaming, setRenaming] = useState<{ id: string; name: string } | null>(null);
|
|
69
|
+
const [deleting, setDeleting] = useState<Chat | null>(null);
|
|
70
|
+
const [draft, setDraft] = useState("");
|
|
71
|
+
const [sending, setSending] = useState<string | null>(null);
|
|
72
|
+
const [turn, setTurn] = useState(0);
|
|
73
|
+
const list = useGrowingList<Chat>(async (limit, signal) => listOf<Chat>(await g8Post("/voice/chat-agent/conversations", { agent_id: agentId, limit }, { signal }), "chats", "items", "conversations"), [agentId], { pageSize: 25, max: 100 });
|
|
74
|
+
useEffect(() => { setOpenId(null); setRenaming(null); setDeleting(null); setDraft(""); setSending(null); }, [agentId]);
|
|
75
|
+
const open = list.items.find((c) => c.id === openId) ?? (openId ? { id: openId } : null);
|
|
76
|
+
const enc = encodeURIComponent;
|
|
77
|
+
|
|
78
|
+
const rename = useG8Mutation((v: { id: string; name: string }) => g8Put(`/voice/chat-agent/conversations/${enc(v.id)}/name`, { name: v.name }, { query: { agent_id: agentId } }), { onSuccess: () => { setRenaming(null); list.reload(); } });
|
|
79
|
+
const handover = useG8Mutation((id: string) => g8Post(`/voice/chat-agent/conversations/${enc(id)}/transfer-to-human`, {}), { onSuccess: () => list.reload() });
|
|
80
|
+
const remove = useG8Mutation((id: string) => g8Delete(`/voice/chat-agent/conversations/${enc(id)}`, { query: { agent_id: agentId } }), { onSuccess: (_r, id) => { setDeleting(null); if (openId === id) setOpenId(null); list.reload(); } });
|
|
81
|
+
const send = useG8Mutation(async (message: string) => {
|
|
82
|
+
const res = await g8Post("/voice/chat-agent/messages", { agent_id: agentId, contact_id: contactId, message, ...(openId ? { session_id: openId } : {}) });
|
|
83
|
+
return bodyOf<{ session_id?: string }>(res).session_id ?? null;
|
|
84
|
+
}, { onSuccess: (sid) => { setSending(null); setDraft(""); if (sid) setOpenId(sid); setTurn((t) => t + 1); list.reload(); }, onError: () => setSending(null) });
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className={`grid gap-4 lg:grid-cols-[18rem_1fr] ${className ?? ""}`}>
|
|
88
|
+
<div className="flex flex-col gap-2">
|
|
89
|
+
<div className="flex items-center justify-between"><h3 className="text-sm font-medium">Conversations</h3><Button size="sm" variant="outline" onClick={() => setOpenId(null)}>New chat</Button></div>
|
|
90
|
+
{list.error ? <ApiError error={list.error} title="Could not load conversations" onRetry={list.reload} /> : null}
|
|
91
|
+
{list.loading ? <Skeleton className="h-16 w-full" aria-label="Loading conversations" /> : null}
|
|
92
|
+
<ul className="divide-y rounded-md border">
|
|
93
|
+
{list.items.map((c) => (
|
|
94
|
+
<li key={c.id} className={openId === c.id ? "bg-accent" : ""}>
|
|
95
|
+
<button type="button" data-testid={`chat-${c.id}`} className="block w-full px-3 py-2 text-left text-sm" onClick={() => setOpenId(c.id)}>
|
|
96
|
+
<span className="block truncate font-medium">{c.name || "Chat"}</span>
|
|
97
|
+
<span className="block text-xs text-muted-foreground">{c.message_count ?? 0} messages, {when(c.updated_at ?? c.created_at)}</span>
|
|
98
|
+
</button>
|
|
99
|
+
</li>
|
|
100
|
+
))}
|
|
101
|
+
</ul>
|
|
102
|
+
{!list.loading && !list.error && list.items.length === 0 ? <EmptyState compact icon={<MessageSquare strokeWidth={1.5} />} title="No conversations yet" description="Send a message to start one." /> : null}
|
|
103
|
+
<LoadMore list={list} noun="conversations" />
|
|
104
|
+
</div>
|
|
105
|
+
<div className="flex min-w-0 flex-col gap-3">
|
|
106
|
+
{open ? (
|
|
107
|
+
<>
|
|
108
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
109
|
+
{renaming?.id === open.id ? (
|
|
110
|
+
<>
|
|
111
|
+
<Input aria-label="Conversation name" className="max-w-xs" value={renaming.name} onChange={(e) => setRenaming({ id: open.id, name: e.target.value })} />
|
|
112
|
+
<Button size="sm" loading={rename.loading} disabled={!renaming.name.trim()} onClick={() => void rename.mutate(renaming)}>Save name</Button>
|
|
113
|
+
<Button size="sm" variant="ghost" onClick={() => setRenaming(null)}>Cancel</Button>
|
|
114
|
+
</>
|
|
115
|
+
) : (
|
|
116
|
+
<>
|
|
117
|
+
<h3 className="text-sm font-medium">{(open as Chat).name || "Conversation"}</h3>
|
|
118
|
+
<Button size="sm" variant="outline" onClick={() => setRenaming({ id: open.id, name: (open as Chat).name ?? "" })}>Rename</Button>
|
|
119
|
+
<Button size="sm" variant="outline" loading={handover.loading} onClick={() => void handover.mutate(open.id)}>Hand over to a human</Button>
|
|
120
|
+
<Button size="sm" variant="ghost" onClick={() => setDeleting(open as Chat)}>Delete</Button>
|
|
121
|
+
</>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
{rename.error ? <ApiError error={rename.error} title="Could not rename the conversation" /> : null}
|
|
125
|
+
{handover.error ? <ApiError error={handover.error} title="Could not hand the conversation over" /> : null}
|
|
126
|
+
{handover.data ? <p role="status" className="text-sm text-muted-foreground">A person will pick this conversation up.</p> : null}
|
|
127
|
+
<Transcript agentId={agentId} chat={open as Chat} contactId={contactId} refreshKey={turn} />
|
|
128
|
+
</>
|
|
129
|
+
) : <p className="text-sm text-muted-foreground">Pick a conversation, or type below to start a new one.</p>}
|
|
130
|
+
<div className="flex flex-col gap-2 border-t pt-3">
|
|
131
|
+
<Textarea aria-label="Message to the agent" rows={3} value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="Say something to this agent" />
|
|
132
|
+
{send.error ? <ApiError error={send.error} title="Could not send the message" /> : null}
|
|
133
|
+
<Button className="self-end" disabled={!draft.trim()} onClick={() => setSending(draft.trim())}>Send message</Button>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
{remove.error ? <ApiError error={remove.error} title="Could not delete the conversation" /> : null}
|
|
137
|
+
<ConfirmSpendDialog open={sending != null} tier="billable" title="Send a message to this agent" description={`The agent answers with the model and may run the skills attached to it, which can reach outside ${brand.name}. Each turn spends credits.`}
|
|
138
|
+
confirmLabel="Send" confirming={send.loading} onConfirm={() => sending && void send.mutate(sending)} onCancel={() => setSending(null)} />
|
|
139
|
+
<ConfirmSpendDialog open={deleting != null} tier="destructive" title={`Delete "${deleting?.name || "this conversation"}"`} description="The messages are removed for everyone." affected={1}
|
|
140
|
+
confirmLabel="Delete" confirming={remove.loading} onConfirm={() => deleting && void remove.mutate(deleting.id)} onCancel={() => setDeleting(null)} />
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
@@ -25,6 +25,7 @@ import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
|
25
25
|
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
26
26
|
import { g8Delete, g8Get, g8Post, g8Put } from "@/g8/lib/g8-client";
|
|
27
27
|
import { listOf } from "../lib/agent-types";
|
|
28
|
+
import { plural } from "@/g8/lib/plural";
|
|
28
29
|
|
|
29
30
|
export interface AgentKnowledgePanelProps {
|
|
30
31
|
agentId: string;
|
|
@@ -89,7 +90,7 @@ export function AgentKnowledgePanel({ agentId, useCompanyKnowledge, onToggleComp
|
|
|
89
90
|
|
|
90
91
|
<div className="flex flex-col gap-2">
|
|
91
92
|
<div className="flex items-center justify-between">
|
|
92
|
-
<h3 className="text-sm font-
|
|
93
|
+
<h3 className="text-sm font-medium">This agent's documents</h3>
|
|
93
94
|
<Button size="sm" variant="outline" onClick={() => fileInput.current?.click()}>Upload documents</Button>
|
|
94
95
|
<input ref={fileInput} type="file" multiple hidden aria-label="Choose documents" accept=".pdf,.docx,.txt,.md" onChange={(e) => onPick(e.target.files)} />
|
|
95
96
|
</div>
|
|
@@ -112,7 +113,7 @@ export function AgentKnowledgePanel({ agentId, useCompanyKnowledge, onToggleComp
|
|
|
112
113
|
</div>
|
|
113
114
|
|
|
114
115
|
<div className="flex flex-col gap-2">
|
|
115
|
-
<h3 className="text-sm font-
|
|
116
|
+
<h3 className="text-sm font-medium">Company knowledge</h3>
|
|
116
117
|
<form className="flex gap-2" onSubmit={(e) => { e.preventDefault(); setSubmitted(query.trim()); }}>
|
|
117
118
|
<Input aria-label="Search company knowledge" placeholder="Search what the agent can draw on" value={query} onChange={(e) => setQuery(e.target.value)} />
|
|
118
119
|
<Button type="submit" size="sm" variant="outline" disabled={!query.trim()}>Search</Button>
|
|
@@ -132,7 +133,7 @@ export function AgentKnowledgePanel({ agentId, useCompanyKnowledge, onToggleComp
|
|
|
132
133
|
|
|
133
134
|
{upload.error ? <ApiError error={upload.error} title="Could not upload" /> : null}
|
|
134
135
|
{remove.error ? <ApiError error={remove.error} title="Could not remove the document" /> : null}
|
|
135
|
-
<ConfirmSpendDialog open={pending != null} tier="billable" title={`Index ${pending?.length ?? 0
|
|
136
|
+
<ConfirmSpendDialog open={pending != null} tier="billable" title={`Index ${plural(pending?.length ?? 0, "document")}`}
|
|
136
137
|
description="The text is extracted and embedded with a paid model so the agent can search it."
|
|
137
138
|
estimatedCredits={(pending?.length ?? 0) * creditsPerFile} confirmLabel="Upload" confirming={upload.loading}
|
|
138
139
|
onConfirm={() => pending && void upload.mutate(pending)} onCancel={() => setPending(null)} />
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AgentOperators: the autonomous operators that work in your workspace without being asked (each monitors something on a schedule,
|
|
5
|
+
* records findings, and acts only as far as its trust dial allows). A summary, the bench (turn each on or off), and for the open
|
|
6
|
+
* operator: Skills (the trust dial per skill: off, detect, propose or auto), Findings, Activity, Health and Scorecard.
|
|
7
|
+
* Turning an operator on, and raising a trust dial, start work that proceeds on its own and spends credits, so both go through
|
|
8
|
+
* ConfirmSpendDialog (billable). Deciding an operator's proposed fix (which changes your data) is not offered here; it stays in graph8.
|
|
9
|
+
*
|
|
10
|
+
* SDK operations, in order:
|
|
11
|
+
* 1. getAgentOperatorsSummary GET /agent/operators/summary (read)
|
|
12
|
+
* 2. listAgentOperators GET /agent/operators (read)
|
|
13
|
+
* 3. getAgentOperatorStatus GET /agent/operators/{operator}/status (read)
|
|
14
|
+
* 4. setAgentOperatorEnabled PUT /agent/operators/{operator}/enabled { enabled } (billable, ConfirmSpendDialog)
|
|
15
|
+
* 5. listAgentOperatorSkills GET /agent/operators/{operator}/skills (read)
|
|
16
|
+
* 6. setAgentOperatorTrustDial PUT /agent/operators/{operator}/skills/{skill_key} { autonomy } (billable, ConfirmSpendDialog)
|
|
17
|
+
* 7. listAgentOperatorFindings GET /agent/operators/{operator}/findings (read)
|
|
18
|
+
* 8. listAgentOperatorActivity GET /agent/operators/{operator}/activity?limit (read) "Load more" grows the limit
|
|
19
|
+
* 9. getAgentOperatorHealth GET /agent/operators/{operator}/health (read)
|
|
20
|
+
* 10. getAgentOperatorScorecard GET /agent/operators/{operator}/scorecard (read)
|
|
21
|
+
*/
|
|
22
|
+
import { useState } from "react";
|
|
23
|
+
import { Cpu } from "lucide-react";
|
|
24
|
+
import { Button, Skeleton } from "@graph8/sdk/ui";
|
|
25
|
+
import { ApiError } from "@/g8/components/api-error";
|
|
26
|
+
import { ConfirmSpendDialog } from "@/g8/components/confirm-spend-dialog";
|
|
27
|
+
import { EmptyState } from "@/g8/components/empty-state";
|
|
28
|
+
import { DetailPanel } from "@/g8/components/screen/detail-panel";
|
|
29
|
+
import { SplitView } from "@/g8/components/screen/split-view";
|
|
30
|
+
import { StatStrip } from "@/g8/components/screen/stat-strip";
|
|
31
|
+
import { StatusPill } from "@/g8/components/screen/status-pill";
|
|
32
|
+
import { useG8Mutation } from "@/g8/hooks/use-g8-mutation";
|
|
33
|
+
import { useG8Query } from "@/g8/hooks/use-g8-query";
|
|
34
|
+
import { useUrlState } from "@/g8/hooks/use-url-state";
|
|
35
|
+
import { g8Get, g8Put } from "@/g8/lib/g8-client";
|
|
36
|
+
import { FactList } from "../lib/facts";
|
|
37
|
+
import { bodyOf, listOf } from "../lib/agent-types";
|
|
38
|
+
import { LoadMore, useGrowingList } from "../lib/load-more";
|
|
39
|
+
|
|
40
|
+
export interface AgentOperatorsProps {
|
|
41
|
+
className?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface Operator { operator: string; title?: string; persona?: string; enabled?: boolean; has_run?: boolean; findings_total?: number; findings_critical?: number; detection?: boolean }
|
|
45
|
+
interface OperatorSkill { key: string; name?: string; title?: string; description?: string; autonomy?: string; default_autonomy?: string }
|
|
46
|
+
const LEVELS = ["OFF", "DETECT", "PROPOSE", "AUTO"] as const;
|
|
47
|
+
type Level = (typeof LEVELS)[number];
|
|
48
|
+
const enc = encodeURIComponent;
|
|
49
|
+
|
|
50
|
+
function Skills({ operator, onAsk }: { operator: string; onAsk: (v: { key: string; label: string; autonomy: Level }) => void }) {
|
|
51
|
+
const skills = useG8Query((signal) => g8Get(`/agent/operators/${enc(operator)}/skills`, undefined, { signal }), [operator]);
|
|
52
|
+
const rows = listOf<OperatorSkill>(skills.data, "items", "skills");
|
|
53
|
+
if (skills.error) return <ApiError error={skills.error} title="Could not load the skills" onRetry={() => void skills.refetch()} />;
|
|
54
|
+
if (skills.loading && !skills.data) return <Skeleton className="h-16 w-full" />;
|
|
55
|
+
if (rows.length === 0) return <p className="text-sm text-muted-foreground">This operator has no skills to tune.</p>;
|
|
56
|
+
return (
|
|
57
|
+
<ul className="divide-y rounded-md border" aria-label="Operator skills">
|
|
58
|
+
{rows.map((s) => (
|
|
59
|
+
<li key={s.key} className="flex flex-wrap items-center justify-between gap-2 px-3 py-2 text-sm">
|
|
60
|
+
<span><span className="font-medium">{s.name ?? s.title ?? s.key}</span>{s.description ? <span className="block text-xs text-muted-foreground">{s.description}</span> : null}</span>
|
|
61
|
+
<select aria-label={`Autonomy for ${s.name ?? s.title ?? s.key}`} className="h-8 rounded-md border bg-background px-2 text-sm" value={(s.autonomy ?? s.default_autonomy ?? "OFF").toUpperCase()}
|
|
62
|
+
onChange={(e) => onAsk({ key: s.key, label: s.name ?? s.title ?? s.key, autonomy: e.target.value as Level })}>
|
|
63
|
+
{LEVELS.map((l) => <option key={l} value={l}>{l.toLowerCase()}</option>)}
|
|
64
|
+
</select>
|
|
65
|
+
</li>
|
|
66
|
+
))}
|
|
67
|
+
</ul>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function Activity({ operator }: { operator: string }) {
|
|
72
|
+
const list = useGrowingList<Record<string, unknown>>(async (limit, signal) => listOf<Record<string, unknown>>(await g8Get(`/agent/operators/${enc(operator)}/activity`, { limit }, { signal }), "items"), [operator], { pageSize: 25, max: 200 });
|
|
73
|
+
if (list.error) return <ApiError error={list.error} title="Could not load the activity" onRetry={list.reload} />;
|
|
74
|
+
if (list.loading) return <Skeleton className="h-16 w-full" />;
|
|
75
|
+
if (list.items.length === 0) return <p className="text-sm text-muted-foreground">Nothing has been decided or fixed yet.</p>;
|
|
76
|
+
return (
|
|
77
|
+
<div className="flex flex-col gap-2">
|
|
78
|
+
<ul className="divide-y rounded-md border" aria-label="Operator activity">
|
|
79
|
+
{list.items.map((r, i) => <li key={String(r.task_id ?? r.id ?? i)} className="px-3 py-2 text-sm">{String(r.title ?? r.summary ?? r.check_id ?? "Activity")}<span className="block text-xs text-muted-foreground">{String(r.resolution ?? r.status ?? "")} {r.at || r.created_at ? new Date(String(r.at ?? r.created_at)).toLocaleString() : ""}</span></li>)}
|
|
80
|
+
</ul>
|
|
81
|
+
<LoadMore list={list} noun="events" />
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function Findings({ operator }: { operator: string }) {
|
|
87
|
+
const q = useG8Query((signal) => g8Get(`/agent/operators/${enc(operator)}/findings`, undefined, { signal }), [operator]);
|
|
88
|
+
const rows = listOf<Record<string, unknown>>(q.data, "items", "findings");
|
|
89
|
+
if (q.error) return <ApiError error={q.error} title="Could not load the findings" onRetry={() => void q.refetch()} />;
|
|
90
|
+
if (q.loading && !q.data) return <Skeleton className="h-16 w-full" />;
|
|
91
|
+
if (rows.length === 0) return <p className="text-sm text-muted-foreground">No open findings.</p>;
|
|
92
|
+
return <ul className="divide-y rounded-md border" aria-label="Findings">{rows.map((r, i) => <li key={String(r.id ?? r.check_id ?? i)} className="flex items-center justify-between gap-2 px-3 py-2 text-sm"><span>{String(r.title ?? r.summary ?? r.check_id ?? "Finding")}</span>{r.severity ? <StatusPill tone={String(r.severity) === "critical" ? "negative" : "warning"}>{String(r.severity)}</StatusPill> : null}</li>)}</ul>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function Facts({ path }: { path: string }) {
|
|
96
|
+
const q = useG8Query((signal) => g8Get(path, undefined, { signal }), [path]);
|
|
97
|
+
if (q.error) return <ApiError error={q.error} title="Could not load this" onRetry={() => void q.refetch()} />;
|
|
98
|
+
if (q.loading && !q.data) return <Skeleton className="h-16 w-full" />;
|
|
99
|
+
return <FactList data={bodyOf(q.data)} />;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function AgentOperators({ className }: AgentOperatorsProps) {
|
|
103
|
+
const [open, setOpen] = useUrlState("operator", "");
|
|
104
|
+
const [enabling, setEnabling] = useState<{ operator: Operator; enabled: boolean } | null>(null);
|
|
105
|
+
const [dial, setDial] = useState<{ key: string; label: string; autonomy: Level } | null>(null);
|
|
106
|
+
const [dialKey, setDialKey] = useState(0);
|
|
107
|
+
const summary = useG8Query((signal) => g8Get("/agent/operators/summary", undefined, { signal }), []);
|
|
108
|
+
const bench = useG8Query((signal) => g8Get("/agent/operators", undefined, { signal }), []);
|
|
109
|
+
const rows = listOf<Operator>(bench.data, "items", "operators");
|
|
110
|
+
const current = rows.find((o) => o.operator === open);
|
|
111
|
+
const sum = bodyOf<Record<string, number>>(summary.data);
|
|
112
|
+
|
|
113
|
+
const toggle = useG8Mutation((v: { operator: string; enabled: boolean }) => g8Put(`/agent/operators/${enc(v.operator)}/enabled`, { enabled: v.enabled }), { onSuccess: () => { setEnabling(null); void bench.refetch(); void summary.refetch(); }, onError: () => setEnabling(null) });
|
|
114
|
+
const setLevel = useG8Mutation((v: { operator: string; key: string; autonomy: Level }) => g8Put(`/agent/operators/${enc(v.operator)}/skills/${enc(v.key)}`, { autonomy: v.autonomy }), { onSuccess: () => { setDial(null); setDialKey((k) => k + 1); }, onError: () => { setDial(null); setDialKey((k) => k + 1); } });
|
|
115
|
+
|
|
116
|
+
const list = (
|
|
117
|
+
<div className="flex flex-col gap-3">
|
|
118
|
+
{bench.error ? <ApiError error={bench.error} title="Could not load the operators" onRetry={() => void bench.refetch()} /> : null}
|
|
119
|
+
{bench.loading && !bench.data ? <Skeleton className="h-20 w-full" aria-label="Loading operators" /> : null}
|
|
120
|
+
<ul className="divide-y rounded-md border">
|
|
121
|
+
{rows.map((o) => (
|
|
122
|
+
<li key={o.operator} className={`flex items-center gap-2 px-3 py-2 ${open === o.operator ? "bg-accent" : ""}`}>
|
|
123
|
+
<button type="button" data-testid={`operator-${o.operator}`} className="min-w-0 flex-1 text-left text-sm" onClick={() => setOpen(o.operator)}>
|
|
124
|
+
<span className="block truncate font-medium">{o.title ?? o.operator}</span>
|
|
125
|
+
<span className="block truncate text-xs text-muted-foreground">{o.detection === false ? "Always on" : `${o.findings_total ?? 0} findings, ${o.findings_critical ?? 0} critical`}</span>
|
|
126
|
+
</button>
|
|
127
|
+
{o.detection === false ? <StatusPill tone="positive" dot>on</StatusPill> : (
|
|
128
|
+
<Button size="sm" variant={o.enabled ? "outline" : "default"} onClick={() => setEnabling({ operator: o, enabled: !o.enabled })}>{o.enabled ? "Turn off" : "Turn on"}</Button>
|
|
129
|
+
)}
|
|
130
|
+
</li>
|
|
131
|
+
))}
|
|
132
|
+
</ul>
|
|
133
|
+
{!bench.loading && !bench.error && rows.length === 0 ? <EmptyState icon={<Cpu strokeWidth={1.5} />} title="No operators" description="Operators appear here once the workspace has them." /> : null}
|
|
134
|
+
{toggle.error ? <ApiError error={toggle.error} title="Could not change the operator" /> : null}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const detail = open ? (
|
|
139
|
+
<DetailPanel title={current?.title ?? open} subtitle={current?.persona} onClose={() => setOpen("")}
|
|
140
|
+
badges={current ? <StatusPill tone={current.enabled ? "positive" : "neutral"} dot>{current.enabled ? "on" : "off"}</StatusPill> : undefined}
|
|
141
|
+
tabs={current?.detection === false ? [{ id: "status", label: "Status", content: <Facts path={`/agent/operators/${enc(open)}/status`} /> }] : [
|
|
142
|
+
{ id: "skills", label: "Skills", content: <><Skills key={`${open}:${dialKey}`} operator={open} onAsk={setDial} />{setLevel.error ? <ApiError error={setLevel.error} title="Could not change the trust dial" /> : null}</> },
|
|
143
|
+
{ id: "findings", label: "Findings", content: <Findings operator={open} /> },
|
|
144
|
+
{ id: "activity", label: "Activity", content: <Activity operator={open} /> },
|
|
145
|
+
{ id: "health", label: "Health", content: <Facts path={`/agent/operators/${enc(open)}/health`} /> },
|
|
146
|
+
{ id: "scorecard", label: "Scorecard", content: <Facts path={`/agent/operators/${enc(open)}/scorecard`} /> },
|
|
147
|
+
{ id: "status", label: "Status", content: <Facts path={`/agent/operators/${enc(open)}/status`} /> },
|
|
148
|
+
]} />
|
|
149
|
+
) : null;
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div className={`flex flex-col gap-4 ${className ?? ""}`}>
|
|
153
|
+
<StatStrip loading={summary.loading && !summary.data} items={summary.error ? [] : Object.entries(sum).filter(([, v]) => typeof v === "number").slice(0, 5).map(([k, v]) => ({ label: k.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase()), value: String(v) }))} />
|
|
154
|
+
{summary.error ? <ApiError compact error={summary.error} title="Could not load the summary" onRetry={() => void summary.refetch()} /> : null}
|
|
155
|
+
<SplitView list={list} detail={detail} detailOpen={Boolean(open)} onCloseDetail={() => setOpen("")} detailWidth="lg" />
|
|
156
|
+
<ConfirmSpendDialog open={enabling != null} tier="billable" title={enabling?.enabled ? `Turn on ${enabling.operator.title ?? enabling.operator.operator}` : `Turn off ${enabling?.operator.title ?? enabling?.operator.operator ?? "operator"}`}
|
|
157
|
+
description={enabling?.enabled ? "Its monitors start running on a schedule and spend credits as they work, without being asked." : "Its monitors stop. Findings already recorded stay."}
|
|
158
|
+
confirmLabel={enabling?.enabled ? "Turn on" : "Turn off"} confirming={toggle.loading} onConfirm={() => enabling && void toggle.mutate({ operator: enabling.operator.operator, enabled: enabling.enabled })} onCancel={() => setEnabling(null)} />
|
|
159
|
+
<ConfirmSpendDialog open={dial != null} tier="billable" title={`Set ${dial?.label ?? "skill"} to ${dial?.autonomy.toLowerCase() ?? ""}`}
|
|
160
|
+
description="The trust dial decides how far this skill may go on its own when it finds something. Higher settings act without asking and spend credits."
|
|
161
|
+
confirmLabel="Set dial" confirming={setLevel.loading} onConfirm={() => dial && open && void setLevel.mutate({ operator: open, key: dial.key, autonomy: dial.autonomy })} onCancel={() => { setDial(null); setDialKey((k) => k + 1); }} />
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|