@open-mercato/core 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3045.b4b3320cc2
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/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +13 -1
- package/dist/helpers/integration/api.js +29 -16
- package/dist/helpers/integration/api.js.map +2 -2
- package/dist/helpers/integration/auth.js +11 -6
- package/dist/helpers/integration/auth.js.map +3 -3
- package/dist/modules/auth/commands/roles.js +9 -12
- package/dist/modules/auth/commands/roles.js.map +2 -2
- package/dist/modules/catalog/ai-agents-context.js +147 -0
- package/dist/modules/catalog/ai-agents-context.js.map +7 -0
- package/dist/modules/catalog/ai-agents.js +383 -0
- package/dist/modules/catalog/ai-agents.js.map +7 -0
- package/dist/modules/catalog/ai-tools/_shared.js +318 -0
- package/dist/modules/catalog/ai-tools/_shared.js.map +7 -0
- package/dist/modules/catalog/ai-tools/authoring-pack.js +391 -0
- package/dist/modules/catalog/ai-tools/authoring-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/categories-pack.js +167 -0
- package/dist/modules/catalog/ai-tools/categories-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/configuration-pack.js +120 -0
- package/dist/modules/catalog/ai-tools/configuration-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/media-tags-pack.js +107 -0
- package/dist/modules/catalog/ai-tools/media-tags-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/merchandising-pack.js +429 -0
- package/dist/modules/catalog/ai-tools/merchandising-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/mutation-pack.js +576 -0
- package/dist/modules/catalog/ai-tools/mutation-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/prices-offers-pack.js +208 -0
- package/dist/modules/catalog/ai-tools/prices-offers-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/products-pack.js +298 -0
- package/dist/modules/catalog/ai-tools/products-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/stats-pack.js +57 -0
- package/dist/modules/catalog/ai-tools/stats-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools/types.js +10 -0
- package/dist/modules/catalog/ai-tools/types.js.map +7 -0
- package/dist/modules/catalog/ai-tools/variants-pack.js +75 -0
- package/dist/modules/catalog/ai-tools/variants-pack.js.map +7 -0
- package/dist/modules/catalog/ai-tools.js +28 -0
- package/dist/modules/catalog/ai-tools.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +466 -0
- package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/products/page.js +7 -1
- package/dist/modules/catalog/backend/catalog/products/page.js.map +2 -2
- package/dist/modules/catalog/components/CatalogStatsCard.js +91 -0
- package/dist/modules/catalog/components/CatalogStatsCard.js.map +7 -0
- package/dist/modules/catalog/components/products/ProductsDataTable.js +23 -3
- package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
- package/dist/modules/catalog/events.js +7 -4
- package/dist/modules/catalog/events.js.map +2 -2
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js +59 -0
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js.map +7 -0
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js +17 -0
- package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js.map +7 -0
- package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js +1 -1
- package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js.map +2 -2
- package/dist/modules/catalog/widgets/injection-table.js +13 -1
- package/dist/modules/catalog/widgets/injection-table.js.map +2 -2
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js +94 -0
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js.map +7 -0
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js +17 -0
- package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js.map +7 -0
- package/dist/modules/customer_accounts/widgets/injection-table.js +9 -0
- package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
- package/dist/modules/customers/ai-agents-context.js +96 -0
- package/dist/modules/customers/ai-agents-context.js.map +7 -0
- package/dist/modules/customers/ai-agents.js +244 -0
- package/dist/modules/customers/ai-agents.js.map +7 -0
- package/dist/modules/customers/ai-tools/activities-tasks-pack.js +1015 -0
- package/dist/modules/customers/ai-tools/activities-tasks-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/addresses-tags-pack.js +134 -0
- package/dist/modules/customers/ai-tools/addresses-tags-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/companies-pack.js +249 -0
- package/dist/modules/customers/ai-tools/companies-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/deals-pack.js +348 -0
- package/dist/modules/customers/ai-tools/deals-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/people-pack.js +261 -0
- package/dist/modules/customers/ai-tools/people-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/settings-pack.js +102 -0
- package/dist/modules/customers/ai-tools/settings-pack.js.map +7 -0
- package/dist/modules/customers/ai-tools/types.js +10 -0
- package/dist/modules/customers/ai-tools/types.js.map +7 -0
- package/dist/modules/customers/ai-tools.js +20 -0
- package/dist/modules/customers/ai-tools.js.map +7 -0
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +469 -0
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +7 -0
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +17 -0
- package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +7 -0
- package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js +117 -0
- package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js.map +7 -0
- package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js +17 -0
- package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js.map +7 -0
- package/dist/modules/customers/widgets/injection-table.js +26 -0
- package/dist/modules/customers/widgets/injection-table.js.map +7 -0
- package/dist/modules/inbox_ops/ai-tools.js +4 -0
- package/dist/modules/inbox_ops/ai-tools.js.map +2 -2
- package/dist/modules/inbox_ops/lib/llmProvider.js +52 -7
- package/dist/modules/inbox_ops/lib/llmProvider.js.map +2 -2
- package/dist/modules/notifications/setup.js +13 -0
- package/dist/modules/notifications/setup.js.map +7 -0
- package/jest.config.cjs +1 -0
- package/jest.setup.ts +18 -0
- package/package.json +5 -3
- package/src/helpers/integration/api.ts +38 -16
- package/src/helpers/integration/auth.ts +13 -6
- package/src/modules/auth/commands/roles.ts +10 -12
- package/src/modules/catalog/AGENTS.md +11 -0
- package/src/modules/catalog/ai-agents-context.ts +239 -0
- package/src/modules/catalog/ai-agents.ts +525 -0
- package/src/modules/catalog/ai-tools/_shared.ts +487 -0
- package/src/modules/catalog/ai-tools/authoring-pack.ts +600 -0
- package/src/modules/catalog/ai-tools/categories-pack.ts +192 -0
- package/src/modules/catalog/ai-tools/configuration-pack.ts +218 -0
- package/src/modules/catalog/ai-tools/media-tags-pack.ts +127 -0
- package/src/modules/catalog/ai-tools/merchandising-pack.ts +608 -0
- package/src/modules/catalog/ai-tools/mutation-pack.ts +761 -0
- package/src/modules/catalog/ai-tools/prices-offers-pack.ts +376 -0
- package/src/modules/catalog/ai-tools/products-pack.ts +387 -0
- package/src/modules/catalog/ai-tools/stats-pack.ts +84 -0
- package/src/modules/catalog/ai-tools/types.ts +81 -0
- package/src/modules/catalog/ai-tools/variants-pack.ts +147 -0
- package/src/modules/catalog/ai-tools.ts +78 -0
- package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +597 -0
- package/src/modules/catalog/backend/catalog/products/page.tsx +23 -2
- package/src/modules/catalog/components/CatalogStatsCard.tsx +118 -0
- package/src/modules/catalog/components/products/ProductsDataTable.tsx +54 -6
- package/src/modules/catalog/events.ts +7 -4
- package/src/modules/catalog/i18n/de.json +17 -0
- package/src/modules/catalog/i18n/en.json +17 -0
- package/src/modules/catalog/i18n/es.json +17 -0
- package/src/modules/catalog/i18n/pl.json +17 -0
- package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.tsx +109 -0
- package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.ts +29 -0
- package/src/modules/catalog/widgets/injection/product-seo/widget.client.tsx +1 -1
- package/src/modules/catalog/widgets/injection-table.ts +12 -0
- package/src/modules/customer_accounts/i18n/de.json +5 -0
- package/src/modules/customer_accounts/i18n/en.json +5 -0
- package/src/modules/customer_accounts/i18n/es.json +5 -0
- package/src/modules/customer_accounts/i18n/pl.json +5 -0
- package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.tsx +136 -0
- package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.ts +43 -0
- package/src/modules/customer_accounts/widgets/injection-table.ts +9 -0
- package/src/modules/customers/AGENTS.md +13 -0
- package/src/modules/customers/ai-agents-context.ts +150 -0
- package/src/modules/customers/ai-agents.ts +355 -0
- package/src/modules/customers/ai-tools/activities-tasks-pack.ts +1248 -0
- package/src/modules/customers/ai-tools/addresses-tags-pack.ts +145 -0
- package/src/modules/customers/ai-tools/companies-pack.ts +362 -0
- package/src/modules/customers/ai-tools/deals-pack.ts +505 -0
- package/src/modules/customers/ai-tools/people-pack.ts +369 -0
- package/src/modules/customers/ai-tools/settings-pack.ts +121 -0
- package/src/modules/customers/ai-tools/types.ts +76 -0
- package/src/modules/customers/ai-tools.ts +34 -0
- package/src/modules/customers/i18n/de.json +25 -0
- package/src/modules/customers/i18n/en.json +25 -0
- package/src/modules/customers/i18n/es.json +25 -0
- package/src/modules/customers/i18n/pl.json +25 -0
- package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +580 -0
- package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +36 -0
- package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.tsx +191 -0
- package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.ts +37 -0
- package/src/modules/customers/widgets/injection-table.ts +41 -0
- package/src/modules/inbox_ops/ai-tools.ts +4 -0
- package/src/modules/inbox_ops/lib/llmProvider.ts +83 -7
- package/src/modules/notifications/setup.ts +11 -0
|
@@ -80,6 +80,31 @@
|
|
|
80
80
|
"customers.ai.actions.translate": "Translate",
|
|
81
81
|
"customers.ai.comingSoon": "Coming soon",
|
|
82
82
|
"customers.ai.prefix": "IA:",
|
|
83
|
+
"customers.ai_assistant.agents.account.label": "CRM Assistant",
|
|
84
|
+
"customers.ai_assistant.context.matchingPeople": "{count} contacts in view",
|
|
85
|
+
"customers.ai_assistant.context.selectedPeople": "{count} contacts selected",
|
|
86
|
+
"customers.ai_assistant.dealDetail.sheet.composerPlaceholder": "Pregunta sobre esta oportunidad, la etapa, el pipeline...",
|
|
87
|
+
"customers.ai_assistant.dealDetail.sheet.description": "Pregunta sobre esta oportunidad. Con el override de política de mutación habilitado, el asistente también puede proponer un cambio de etapa que debes confirmar antes de guardar.",
|
|
88
|
+
"customers.ai_assistant.dealDetail.sheet.title": "Asistente AI de clientes — oportunidad",
|
|
89
|
+
"customers.ai_assistant.dealDetail.trigger.ariaLabel": "Abrir asistente AI para esta oportunidad",
|
|
90
|
+
"customers.ai_assistant.dealDetail.trigger.label": "Preguntar a la IA",
|
|
91
|
+
"customers.ai_assistant.dock.subtitle": "Customers",
|
|
92
|
+
"customers.ai_assistant.popover.heading": "AI assistants",
|
|
93
|
+
"customers.ai_assistant.sheet.composerPlaceholder": "Pregunta sobre personas, empresas, oportunidades...",
|
|
94
|
+
"customers.ai_assistant.sheet.description": "Asistente de solo lectura. Pregunta sobre personas, empresas, oportunidades y actividades del alcance de esta lista.",
|
|
95
|
+
"customers.ai_assistant.sheet.dock": "Dock to side",
|
|
96
|
+
"customers.ai_assistant.sheet.selectionPill": "Actuando sobre {count} seleccionados",
|
|
97
|
+
"customers.ai_assistant.sheet.title": "Asistente AI de clientes",
|
|
98
|
+
"customers.ai_assistant.sheet.welcomeTitle": "CRM Assistant",
|
|
99
|
+
"customers.ai_assistant.suggestions.activityOverview": "Activity overview",
|
|
100
|
+
"customers.ai_assistant.suggestions.findCompanies": "Find related companies",
|
|
101
|
+
"customers.ai_assistant.suggestions.findDeals": "Show deals for selected people",
|
|
102
|
+
"customers.ai_assistant.suggestions.recentDeals": "Show recent deals",
|
|
103
|
+
"customers.ai_assistant.suggestions.searchPeople": "Search for a contact",
|
|
104
|
+
"customers.ai_assistant.suggestions.summarizeSelected": "Summarize selected contacts",
|
|
105
|
+
"customers.ai_assistant.suggestions.topCompanies": "List top companies",
|
|
106
|
+
"customers.ai_assistant.trigger.ariaLabel": "Abrir asistente AI para personas",
|
|
107
|
+
"customers.ai_assistant.trigger.label": "AI",
|
|
83
108
|
"customers.assignableStaff.loadError": "No se pudieron cargar los miembros del equipo. Verifica tus permisos y vuelve a intentarlo.",
|
|
84
109
|
"customers.audit.activities.create": "Crear actividad",
|
|
85
110
|
"customers.audit.activities.delete": "Eliminar actividad",
|
|
@@ -80,6 +80,31 @@
|
|
|
80
80
|
"customers.ai.actions.translate": "Przetłumacz",
|
|
81
81
|
"customers.ai.comingSoon": "Wkrótce",
|
|
82
82
|
"customers.ai.prefix": "AI:",
|
|
83
|
+
"customers.ai_assistant.agents.account.label": "CRM Assistant",
|
|
84
|
+
"customers.ai_assistant.context.matchingPeople": "{count} contacts in view",
|
|
85
|
+
"customers.ai_assistant.context.selectedPeople": "{count} contacts selected",
|
|
86
|
+
"customers.ai_assistant.dealDetail.sheet.composerPlaceholder": "Zapytaj o tę transakcję, etap, pipeline...",
|
|
87
|
+
"customers.ai_assistant.dealDetail.sheet.description": "Zapytaj o tę transakcję. Gdy włączony jest per-tenant override polityki mutacji, asystent może także zaproponować zmianę etapu, którą potwierdzasz przed zapisem.",
|
|
88
|
+
"customers.ai_assistant.dealDetail.sheet.title": "Asystent AI dla klientów — transakcja",
|
|
89
|
+
"customers.ai_assistant.dealDetail.trigger.ariaLabel": "Otwórz asystenta AI dla tej transakcji",
|
|
90
|
+
"customers.ai_assistant.dealDetail.trigger.label": "Zapytaj AI",
|
|
91
|
+
"customers.ai_assistant.dock.subtitle": "Customers",
|
|
92
|
+
"customers.ai_assistant.popover.heading": "AI assistants",
|
|
93
|
+
"customers.ai_assistant.sheet.composerPlaceholder": "Zapytaj o osoby, firmy, transakcje...",
|
|
94
|
+
"customers.ai_assistant.sheet.description": "Asystent tylko do odczytu. Zadawaj pytania o osoby, firmy, transakcje i aktywności w zasięgu tej listy.",
|
|
95
|
+
"customers.ai_assistant.sheet.dock": "Dock to side",
|
|
96
|
+
"customers.ai_assistant.sheet.selectionPill": "Działam na {count} zaznaczonych",
|
|
97
|
+
"customers.ai_assistant.sheet.title": "Asystent AI dla klientów",
|
|
98
|
+
"customers.ai_assistant.sheet.welcomeTitle": "CRM Assistant",
|
|
99
|
+
"customers.ai_assistant.suggestions.activityOverview": "Activity overview",
|
|
100
|
+
"customers.ai_assistant.suggestions.findCompanies": "Find related companies",
|
|
101
|
+
"customers.ai_assistant.suggestions.findDeals": "Show deals for selected people",
|
|
102
|
+
"customers.ai_assistant.suggestions.recentDeals": "Show recent deals",
|
|
103
|
+
"customers.ai_assistant.suggestions.searchPeople": "Search for a contact",
|
|
104
|
+
"customers.ai_assistant.suggestions.summarizeSelected": "Summarize selected contacts",
|
|
105
|
+
"customers.ai_assistant.suggestions.topCompanies": "List top companies",
|
|
106
|
+
"customers.ai_assistant.trigger.ariaLabel": "Otwórz asystenta AI dla osób",
|
|
107
|
+
"customers.ai_assistant.trigger.label": "AI",
|
|
83
108
|
"customers.assignableStaff.loadError": "Nie udało się załadować pracowników. Sprawdź uprawnienia i spróbuj ponownie.",
|
|
84
109
|
"customers.audit.activities.create": "Utwórz aktywność",
|
|
85
110
|
"customers.audit.activities.delete": "Usuń aktywność",
|
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Step 4.10 — Backend AiChat injection widget (client).
|
|
5
|
+
*
|
|
6
|
+
* Renders a compact, round, icon-only trigger in the DataTable
|
|
7
|
+
* `:search-trailing` injection slot (right next to the list search input).
|
|
8
|
+
* Clicking the trigger opens a popover listing the AI agents this widget
|
|
9
|
+
* exposes — currently `customers.account_assistant`, but the popover is
|
|
10
|
+
* the agreed extension point for additional customers-domain agents
|
|
11
|
+
* (selection digesters, deal-shapers, etc.). Picking an agent opens a
|
|
12
|
+
* right-side sheet embedding `<AiChat>` for that agent.
|
|
13
|
+
*
|
|
14
|
+
* `pageContext` shape matches spec §10.1 (view / recordType / recordId
|
|
15
|
+
* / extra). The host DataTable provides selection + total information
|
|
16
|
+
* through the `context` prop injected by `<InjectionSpot>`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import * as React from 'react'
|
|
20
|
+
import { Building2, ChevronDown, Handshake, PanelRightOpen, Search, Sparkles, Users } from 'lucide-react'
|
|
21
|
+
import { AiChat, type AiChatSuggestion, type AiChatContextItem } from '@open-mercato/ui/ai/AiChat'
|
|
22
|
+
import { useAiDock } from '@open-mercato/ui/ai/AiDock'
|
|
23
|
+
import { useAiChatSessions } from '@open-mercato/ui/ai/AiChatSessions'
|
|
24
|
+
import { ChatPaneTabs } from '@open-mercato/ui/ai/ChatPaneTabs'
|
|
25
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
26
|
+
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
27
|
+
import {
|
|
28
|
+
Dialog,
|
|
29
|
+
DialogContent,
|
|
30
|
+
DialogDescription,
|
|
31
|
+
DialogHeader,
|
|
32
|
+
DialogTitle,
|
|
33
|
+
} from '@open-mercato/ui/primitives/dialog'
|
|
34
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'
|
|
35
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
36
|
+
import { cn } from '@open-mercato/shared/lib/utils'
|
|
37
|
+
|
|
38
|
+
export const CUSTOMERS_AI_INJECT_AGENT_ID = 'customers.account_assistant'
|
|
39
|
+
|
|
40
|
+
export type CustomersAiInjectView = 'customers.people.list' | 'customers.companies.list'
|
|
41
|
+
|
|
42
|
+
export interface CustomersAiInjectPageContext {
|
|
43
|
+
view: CustomersAiInjectView
|
|
44
|
+
recordType: null
|
|
45
|
+
recordId: string | null
|
|
46
|
+
extra: {
|
|
47
|
+
selectedCount: number
|
|
48
|
+
totalMatching: number
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function computeCustomersAiInjectPageContext(
|
|
53
|
+
context: HostInjectionContext | undefined,
|
|
54
|
+
): CustomersAiInjectPageContext {
|
|
55
|
+
return buildPageContext(context)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface HostInjectionContext {
|
|
59
|
+
tableId?: string | null
|
|
60
|
+
title?: string
|
|
61
|
+
selectedRowIds?: string[]
|
|
62
|
+
selectedCount?: number
|
|
63
|
+
total?: number
|
|
64
|
+
totalMatching?: number
|
|
65
|
+
rowCount?: number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface AiAssistantTriggerProps {
|
|
69
|
+
context?: HostInjectionContext
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readString(value: unknown): string {
|
|
73
|
+
return typeof value === 'string' ? value : ''
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readNumber(value: unknown): number {
|
|
77
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
78
|
+
if (typeof value === 'string') {
|
|
79
|
+
const parsed = Number.parseInt(value, 10)
|
|
80
|
+
if (Number.isFinite(parsed)) return parsed
|
|
81
|
+
}
|
|
82
|
+
return 0
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveView(tableId: string | null | undefined): CustomersAiInjectView {
|
|
86
|
+
if (typeof tableId === 'string' && tableId.includes('companies')) {
|
|
87
|
+
return 'customers.companies.list'
|
|
88
|
+
}
|
|
89
|
+
return 'customers.people.list'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildPageContext(context: HostInjectionContext | undefined): CustomersAiInjectPageContext {
|
|
93
|
+
const selectedIdsRaw = Array.isArray(context?.selectedRowIds) ? context?.selectedRowIds ?? [] : []
|
|
94
|
+
const selectedIds = selectedIdsRaw.map(readString).filter((id) => id.length > 0)
|
|
95
|
+
const selectedCount = selectedIds.length > 0
|
|
96
|
+
? selectedIds.length
|
|
97
|
+
: readNumber(context?.selectedCount)
|
|
98
|
+
const totalMatching = readNumber(context?.totalMatching ?? context?.total ?? context?.rowCount)
|
|
99
|
+
const recordId = selectedIds.length > 0 ? selectedIds.join(',') : null
|
|
100
|
+
return {
|
|
101
|
+
view: resolveView(context?.tableId),
|
|
102
|
+
recordType: null,
|
|
103
|
+
recordId,
|
|
104
|
+
extra: {
|
|
105
|
+
selectedCount,
|
|
106
|
+
totalMatching,
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function useCustomerSuggestions(
|
|
112
|
+
view: CustomersAiInjectView,
|
|
113
|
+
hasSelection: boolean,
|
|
114
|
+
selectedCount: number,
|
|
115
|
+
): AiChatSuggestion[] {
|
|
116
|
+
const t = useT()
|
|
117
|
+
return React.useMemo(() => {
|
|
118
|
+
if (view === 'customers.companies.list') {
|
|
119
|
+
if (hasSelection) {
|
|
120
|
+
return [
|
|
121
|
+
{
|
|
122
|
+
label: t(
|
|
123
|
+
'customers.ai_assistant.suggestions.summarizeSelectedCompanies',
|
|
124
|
+
'Summarize selected companies',
|
|
125
|
+
),
|
|
126
|
+
prompt: `Give me a summary of my ${selectedCount} selected companies — size, industry, and recent activity`,
|
|
127
|
+
icon: <Building2 className="size-4" />,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
label: t(
|
|
131
|
+
'customers.ai_assistant.suggestions.dealsForSelectedCompanies',
|
|
132
|
+
'Show deals for selected companies',
|
|
133
|
+
),
|
|
134
|
+
prompt: `Show me all deals associated with my ${selectedCount} selected companies`,
|
|
135
|
+
icon: <Handshake className="size-4" />,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
label: t(
|
|
139
|
+
'customers.ai_assistant.suggestions.peopleAtSelectedCompanies',
|
|
140
|
+
'List people at selected companies',
|
|
141
|
+
),
|
|
142
|
+
prompt: `List the contacts (people) associated with my ${selectedCount} selected companies`,
|
|
143
|
+
icon: <Users className="size-4" />,
|
|
144
|
+
},
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
return [
|
|
148
|
+
{
|
|
149
|
+
label: t(
|
|
150
|
+
'customers.ai_assistant.suggestions.searchCompanies',
|
|
151
|
+
'Search for a company',
|
|
152
|
+
),
|
|
153
|
+
prompt: 'Search for companies by name, industry, or tax ID',
|
|
154
|
+
icon: <Search className="size-4" />,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
label: t(
|
|
158
|
+
'customers.ai_assistant.suggestions.topCompaniesByDeals',
|
|
159
|
+
'Top companies by deal value',
|
|
160
|
+
),
|
|
161
|
+
prompt: 'Show me the companies with the highest open deal value',
|
|
162
|
+
icon: <Handshake className="size-4" />,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
label: t(
|
|
166
|
+
'customers.ai_assistant.suggestions.companiesWithoutContacts',
|
|
167
|
+
'Companies missing contacts',
|
|
168
|
+
),
|
|
169
|
+
prompt: 'Find companies that have no associated people yet',
|
|
170
|
+
icon: <Users className="size-4" />,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
label: t(
|
|
174
|
+
'customers.ai_assistant.suggestions.companiesActivityOverview',
|
|
175
|
+
'Activity overview',
|
|
176
|
+
),
|
|
177
|
+
prompt: 'Give me an overview of recent company-level activities and interactions',
|
|
178
|
+
icon: <Building2 className="size-4" />,
|
|
179
|
+
},
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
if (hasSelection) {
|
|
183
|
+
return [
|
|
184
|
+
{
|
|
185
|
+
label: t('customers.ai_assistant.suggestions.summarizeSelected', 'Summarize selected contacts'),
|
|
186
|
+
prompt: `Give me a summary of my ${selectedCount} selected contacts — key details and recent activity`,
|
|
187
|
+
icon: <Users className="size-4" />,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
label: t('customers.ai_assistant.suggestions.findDeals', 'Show deals for selected people'),
|
|
191
|
+
prompt: `Show me all deals associated with my ${selectedCount} selected contacts`,
|
|
192
|
+
icon: <Handshake className="size-4" />,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
label: t('customers.ai_assistant.suggestions.findCompanies', 'Find related companies'),
|
|
196
|
+
prompt: `Find companies related to my ${selectedCount} selected contacts`,
|
|
197
|
+
icon: <Building2 className="size-4" />,
|
|
198
|
+
},
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
return [
|
|
202
|
+
{
|
|
203
|
+
label: t('customers.ai_assistant.suggestions.searchPeople', 'Search for a contact'),
|
|
204
|
+
prompt: 'Search for contacts by name, email, or company',
|
|
205
|
+
icon: <Search className="size-4" />,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
label: t('customers.ai_assistant.suggestions.recentDeals', 'Show recent deals'),
|
|
209
|
+
prompt: 'Show me the most recent deals and their current stages',
|
|
210
|
+
icon: <Handshake className="size-4" />,
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
label: t('customers.ai_assistant.suggestions.topCompanies', 'List top companies'),
|
|
214
|
+
prompt: 'List companies with the most associated contacts and deals',
|
|
215
|
+
icon: <Building2 className="size-4" />,
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
label: t('customers.ai_assistant.suggestions.activityOverview', 'Activity overview'),
|
|
219
|
+
prompt: 'Give me an overview of recent customer activities and interactions',
|
|
220
|
+
icon: <Users className="size-4" />,
|
|
221
|
+
},
|
|
222
|
+
]
|
|
223
|
+
}, [view, hasSelection, selectedCount, t])
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function useCustomerContextItems(pageContext: CustomersAiInjectPageContext): AiChatContextItem[] {
|
|
227
|
+
const t = useT()
|
|
228
|
+
return React.useMemo(() => {
|
|
229
|
+
const items: AiChatContextItem[] = []
|
|
230
|
+
const { selectedCount, totalMatching } = pageContext.extra
|
|
231
|
+
const isCompanies = pageContext.view === 'customers.companies.list'
|
|
232
|
+
if (selectedCount > 0) {
|
|
233
|
+
const key = isCompanies
|
|
234
|
+
? 'customers.ai_assistant.context.selectedCompanies'
|
|
235
|
+
: 'customers.ai_assistant.context.selectedPeople'
|
|
236
|
+
const fallback = isCompanies ? '{count} companies selected' : '{count} contacts selected'
|
|
237
|
+
items.push({ label: t(key, fallback).replace('{count}', String(selectedCount)) })
|
|
238
|
+
} else if (totalMatching > 0) {
|
|
239
|
+
const key = isCompanies
|
|
240
|
+
? 'customers.ai_assistant.context.matchingCompanies'
|
|
241
|
+
: 'customers.ai_assistant.context.matchingPeople'
|
|
242
|
+
const fallback = isCompanies ? '{count} companies in view' : '{count} contacts in view'
|
|
243
|
+
items.push({ label: t(key, fallback).replace('{count}', String(totalMatching)) })
|
|
244
|
+
}
|
|
245
|
+
return items
|
|
246
|
+
}, [pageContext, t])
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
interface CustomerAgentDescriptor {
|
|
250
|
+
id: string
|
|
251
|
+
label: string
|
|
252
|
+
description: string
|
|
253
|
+
icon: React.ReactNode
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function useCustomerAgents(): CustomerAgentDescriptor[] {
|
|
257
|
+
const t = useT()
|
|
258
|
+
return React.useMemo(
|
|
259
|
+
() => [
|
|
260
|
+
{
|
|
261
|
+
id: CUSTOMERS_AI_INJECT_AGENT_ID,
|
|
262
|
+
label: t('customers.ai_assistant.agents.account.label', 'CRM Assistant'),
|
|
263
|
+
description: t(
|
|
264
|
+
'customers.ai_assistant.agents.account.description',
|
|
265
|
+
'Explore people, companies, deals, and activities.',
|
|
266
|
+
),
|
|
267
|
+
icon: <Sparkles className="size-4" />,
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
[t],
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export default function AiAssistantTriggerWidget({ context }: AiAssistantTriggerProps) {
|
|
275
|
+
const t = useT()
|
|
276
|
+
const dock = useAiDock()
|
|
277
|
+
const [open, setOpen] = React.useState(false)
|
|
278
|
+
const [popoverOpen, setPopoverOpen] = React.useState(false)
|
|
279
|
+
const [activeAgent, setActiveAgent] = React.useState<string>(CUSTOMERS_AI_INJECT_AGENT_ID)
|
|
280
|
+
const [lastAgent, setLastAgent] = React.useState<string | null>(null)
|
|
281
|
+
const pageContext = React.useMemo(() => buildPageContext(context), [context])
|
|
282
|
+
const agents = useCustomerAgents()
|
|
283
|
+
|
|
284
|
+
const selectedCount = pageContext.extra.selectedCount
|
|
285
|
+
const hasSelection = selectedCount > 0
|
|
286
|
+
const suggestions = useCustomerSuggestions(pageContext.view, hasSelection, selectedCount)
|
|
287
|
+
const contextItems = useCustomerContextItems(pageContext)
|
|
288
|
+
|
|
289
|
+
const openAgent = React.useCallback((agentId: string) => {
|
|
290
|
+
setActiveAgent(agentId)
|
|
291
|
+
setLastAgent(agentId)
|
|
292
|
+
setPopoverOpen(false)
|
|
293
|
+
if (dock.state.assistant?.agent === agentId) {
|
|
294
|
+
dock.dock(dock.state.assistant)
|
|
295
|
+
setOpen(false)
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
setOpen(true)
|
|
299
|
+
}, [dock])
|
|
300
|
+
|
|
301
|
+
const handleSelectAgent = React.useCallback((agentId: string) => {
|
|
302
|
+
openAgent(agentId)
|
|
303
|
+
}, [openAgent])
|
|
304
|
+
|
|
305
|
+
const handleMainTriggerClick = React.useCallback(() => {
|
|
306
|
+
if (agents.length === 1) {
|
|
307
|
+
openAgent(agents[0].id)
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
if (lastAgent && agents.some((a) => a.id === lastAgent)) {
|
|
311
|
+
openAgent(lastAgent)
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
setPopoverOpen(true)
|
|
315
|
+
}, [agents, lastAgent, openAgent])
|
|
316
|
+
|
|
317
|
+
const handleDock = React.useCallback(() => {
|
|
318
|
+
const agent = agents.find((a) => a.id === activeAgent) ?? agents[0]
|
|
319
|
+
if (!agent) return
|
|
320
|
+
dock.dock({
|
|
321
|
+
agent: agent.id,
|
|
322
|
+
label: agent.label,
|
|
323
|
+
description: t('customers.ai_assistant.dock.subtitle', 'Customers'),
|
|
324
|
+
pageContext: pageContext as unknown as Record<string, unknown>,
|
|
325
|
+
placeholder: t(
|
|
326
|
+
'customers.ai_assistant.sheet.composerPlaceholder',
|
|
327
|
+
'Ask about people, companies, deals...',
|
|
328
|
+
),
|
|
329
|
+
suggestions,
|
|
330
|
+
contextItems,
|
|
331
|
+
welcomeTitle: t('customers.ai_assistant.sheet.welcomeTitle', 'CRM Assistant'),
|
|
332
|
+
welcomeDescription: hasSelection
|
|
333
|
+
? t(
|
|
334
|
+
'customers.ai_assistant.sheet.welcomeDescriptionSelection',
|
|
335
|
+
'Ready to explore your {count} selected contacts:',
|
|
336
|
+
).replace('{count}', String(selectedCount))
|
|
337
|
+
: t(
|
|
338
|
+
'customers.ai_assistant.sheet.welcomeDescriptionAll',
|
|
339
|
+
'Ask me anything about your customers, companies, and deals:',
|
|
340
|
+
),
|
|
341
|
+
})
|
|
342
|
+
setOpen(false)
|
|
343
|
+
}, [
|
|
344
|
+
activeAgent,
|
|
345
|
+
agents,
|
|
346
|
+
contextItems,
|
|
347
|
+
dock,
|
|
348
|
+
hasSelection,
|
|
349
|
+
pageContext,
|
|
350
|
+
selectedCount,
|
|
351
|
+
suggestions,
|
|
352
|
+
t,
|
|
353
|
+
])
|
|
354
|
+
|
|
355
|
+
const triggerLabel = t(
|
|
356
|
+
'customers.ai_assistant.trigger.ariaLabel',
|
|
357
|
+
'Open AI assistant for people',
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
const labelText = t('customers.ai_assistant.trigger.label', 'AI')
|
|
361
|
+
const moreAgentsLabel = t(
|
|
362
|
+
'customers.ai_assistant.trigger.moreAgentsAriaLabel',
|
|
363
|
+
'Choose an AI assistant',
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
return (
|
|
367
|
+
<>
|
|
368
|
+
<div className="inline-flex items-center">
|
|
369
|
+
<Button
|
|
370
|
+
type="button"
|
|
371
|
+
variant="outline"
|
|
372
|
+
onClick={handleMainTriggerClick}
|
|
373
|
+
data-ai-customers-inject-trigger=""
|
|
374
|
+
aria-label={triggerLabel}
|
|
375
|
+
title={triggerLabel}
|
|
376
|
+
className={cn(
|
|
377
|
+
'relative',
|
|
378
|
+
agents.length > 1 && 'rounded-r-none border-r-0',
|
|
379
|
+
)}
|
|
380
|
+
>
|
|
381
|
+
<Sparkles className="size-4" aria-hidden />
|
|
382
|
+
<span>{labelText}</span>
|
|
383
|
+
{hasSelection ? (
|
|
384
|
+
<span
|
|
385
|
+
className="absolute -top-1 -right-1 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-medium leading-none text-primary-foreground"
|
|
386
|
+
data-ai-customers-inject-selected-count={selectedCount}
|
|
387
|
+
>
|
|
388
|
+
{selectedCount}
|
|
389
|
+
</span>
|
|
390
|
+
) : null}
|
|
391
|
+
</Button>
|
|
392
|
+
{agents.length > 1 ? (
|
|
393
|
+
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
|
|
394
|
+
<PopoverTrigger asChild>
|
|
395
|
+
<IconButton
|
|
396
|
+
type="button"
|
|
397
|
+
variant="outline"
|
|
398
|
+
size="lg"
|
|
399
|
+
aria-label={moreAgentsLabel}
|
|
400
|
+
title={moreAgentsLabel}
|
|
401
|
+
className="rounded-l-none"
|
|
402
|
+
data-ai-customers-inject-picker=""
|
|
403
|
+
>
|
|
404
|
+
<ChevronDown className="size-4" aria-hidden />
|
|
405
|
+
</IconButton>
|
|
406
|
+
</PopoverTrigger>
|
|
407
|
+
<PopoverContent align="end" className="w-72 p-1">
|
|
408
|
+
<div className="px-3 pt-2 pb-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
409
|
+
{t('customers.ai_assistant.popover.heading', 'AI assistants')}
|
|
410
|
+
</div>
|
|
411
|
+
<div className="flex flex-col gap-0.5">
|
|
412
|
+
{agents.map((agent) => (
|
|
413
|
+
<button
|
|
414
|
+
key={agent.id}
|
|
415
|
+
type="button"
|
|
416
|
+
onClick={() => handleSelectAgent(agent.id)}
|
|
417
|
+
data-ai-customers-inject-agent-option={agent.id}
|
|
418
|
+
className="flex items-start gap-2 rounded-sm px-2 py-2 text-left text-sm hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground focus-visible:outline-none"
|
|
419
|
+
>
|
|
420
|
+
<span className="mt-0.5 inline-flex size-6 items-center justify-center rounded-full bg-secondary text-secondary-foreground">
|
|
421
|
+
{agent.icon}
|
|
422
|
+
</span>
|
|
423
|
+
<span className="flex-1 min-w-0">
|
|
424
|
+
<span className="block font-medium leading-tight">{agent.label}</span>
|
|
425
|
+
<span className="block text-xs text-muted-foreground leading-snug">
|
|
426
|
+
{agent.description}
|
|
427
|
+
</span>
|
|
428
|
+
</span>
|
|
429
|
+
</button>
|
|
430
|
+
))}
|
|
431
|
+
</div>
|
|
432
|
+
</PopoverContent>
|
|
433
|
+
</Popover>
|
|
434
|
+
) : null}
|
|
435
|
+
</div>
|
|
436
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
437
|
+
<DialogContent
|
|
438
|
+
className={cn(
|
|
439
|
+
// Mobile: full-screen sheet (no rounded corners, fills the
|
|
440
|
+
// viewport). Desktop (≥sm): right-anchored side sheet.
|
|
441
|
+
// The Dialog primitive ships a centering transform at the sm
|
|
442
|
+
// breakpoint (`sm:top-1/2 sm:left-1/2 sm:-translate-x-1/2
|
|
443
|
+
// sm:-translate-y-1/2 sm:inset-auto`); each must be overridden
|
|
444
|
+
// at the same `sm:` breakpoint or the panel renders half off
|
|
445
|
+
// the viewport.
|
|
446
|
+
'top-0 left-0 right-0 bottom-0 translate-x-0 translate-y-0 max-w-none w-screen h-svh max-h-svh rounded-none',
|
|
447
|
+
'sm:top-0 sm:bottom-0 sm:right-0 sm:left-auto sm:translate-x-0 sm:translate-y-0',
|
|
448
|
+
'sm:max-w-xl sm:w-[36rem] sm:rounded-l-2xl sm:h-screen sm:max-h-screen',
|
|
449
|
+
'flex flex-col gap-3 p-4 z-[70]',
|
|
450
|
+
)}
|
|
451
|
+
data-ai-customers-inject-sheet=""
|
|
452
|
+
>
|
|
453
|
+
<DialogHeader>
|
|
454
|
+
<div className="flex items-center gap-3 pr-8">
|
|
455
|
+
{/* Dock button lives on the LEFT — the Dialog primitive
|
|
456
|
+
auto-renders an X close button absolutely positioned in
|
|
457
|
+
the top-right corner, so anything we drop in the header's
|
|
458
|
+
right side visually collides with it. Mobile hides the
|
|
459
|
+
dock entirely (the side panel is desktop-only). */}
|
|
460
|
+
<IconButton
|
|
461
|
+
type="button"
|
|
462
|
+
variant="ghost"
|
|
463
|
+
size="sm"
|
|
464
|
+
aria-label={t('customers.ai_assistant.sheet.dock', 'Dock to side')}
|
|
465
|
+
title={t('customers.ai_assistant.sheet.dock', 'Dock to side')}
|
|
466
|
+
onClick={handleDock}
|
|
467
|
+
data-ai-customers-inject-dock=""
|
|
468
|
+
className="hidden lg:inline-flex shrink-0"
|
|
469
|
+
>
|
|
470
|
+
<PanelRightOpen className="size-4" aria-hidden />
|
|
471
|
+
</IconButton>
|
|
472
|
+
<DialogTitle className="flex-1 min-w-0 truncate">
|
|
473
|
+
{t('customers.ai_assistant.sheet.title', 'Customers AI assistant')}
|
|
474
|
+
</DialogTitle>
|
|
475
|
+
{hasSelection ? (
|
|
476
|
+
<span
|
|
477
|
+
className="shrink-0 inline-flex items-center rounded-full border border-border bg-secondary px-2 py-0.5 text-xs text-secondary-foreground"
|
|
478
|
+
data-ai-customers-inject-selection-pill=""
|
|
479
|
+
data-ai-customers-inject-selected-count={selectedCount}
|
|
480
|
+
>
|
|
481
|
+
{t(
|
|
482
|
+
'customers.ai_assistant.sheet.selectionPill',
|
|
483
|
+
'Acting on {count} selected',
|
|
484
|
+
).replace('{count}', String(selectedCount))}
|
|
485
|
+
</span>
|
|
486
|
+
) : null}
|
|
487
|
+
</div>
|
|
488
|
+
<DialogDescription>
|
|
489
|
+
{hasSelection
|
|
490
|
+
? t(
|
|
491
|
+
'customers.ai_assistant.sheet.descriptionWithSelection',
|
|
492
|
+
'Working with {count} selected contacts. Ask about their details, deals, companies, and activities.',
|
|
493
|
+
).replace('{count}', String(selectedCount))
|
|
494
|
+
: t(
|
|
495
|
+
'customers.ai_assistant.sheet.description',
|
|
496
|
+
'Your CRM assistant. Ask about people, companies, deals, and activities.',
|
|
497
|
+
)}
|
|
498
|
+
</DialogDescription>
|
|
499
|
+
</DialogHeader>
|
|
500
|
+
<CustomersChatBody
|
|
501
|
+
activeAgent={activeAgent}
|
|
502
|
+
pageContext={pageContext}
|
|
503
|
+
suggestions={suggestions}
|
|
504
|
+
contextItems={contextItems}
|
|
505
|
+
hasSelection={hasSelection}
|
|
506
|
+
selectedCount={selectedCount}
|
|
507
|
+
/>
|
|
508
|
+
</DialogContent>
|
|
509
|
+
</Dialog>
|
|
510
|
+
</>
|
|
511
|
+
)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
interface CustomersChatBodyProps {
|
|
515
|
+
activeAgent: string
|
|
516
|
+
pageContext: CustomersAiInjectPageContext
|
|
517
|
+
suggestions: AiChatSuggestion[]
|
|
518
|
+
contextItems: AiChatContextItem[]
|
|
519
|
+
hasSelection: boolean
|
|
520
|
+
selectedCount: number
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function CustomersChatBody({
|
|
524
|
+
activeAgent,
|
|
525
|
+
pageContext,
|
|
526
|
+
suggestions,
|
|
527
|
+
contextItems,
|
|
528
|
+
hasSelection,
|
|
529
|
+
selectedCount,
|
|
530
|
+
}: CustomersChatBodyProps) {
|
|
531
|
+
const t = useT()
|
|
532
|
+
const sessions = useAiChatSessions()
|
|
533
|
+
const session = sessions.getActiveSession(activeAgent)
|
|
534
|
+
|
|
535
|
+
// Lazily ensure an open session exists. Running `ensureSession` inside an
|
|
536
|
+
// effect (not inline during render) keeps the provider's setState calls
|
|
537
|
+
// outside of the render phase. The first frame may render without a
|
|
538
|
+
// session — that's fine, we render the tab strip alone until the next
|
|
539
|
+
// tick when the new session is committed and `getActiveSession` returns it.
|
|
540
|
+
React.useEffect(() => {
|
|
541
|
+
if (!session) sessions.ensureSession(activeAgent)
|
|
542
|
+
}, [activeAgent, session, sessions])
|
|
543
|
+
|
|
544
|
+
return (
|
|
545
|
+
<>
|
|
546
|
+
<ChatPaneTabs agentId={activeAgent} className="border-b" />
|
|
547
|
+
<div className="min-h-0 flex-1" data-ai-customers-inject-chat-container="">
|
|
548
|
+
{session ? (
|
|
549
|
+
<AiChat
|
|
550
|
+
// `key` forces a fresh mount when the active tab changes so the
|
|
551
|
+
// AI SDK's status doesn't leak across sessions.
|
|
552
|
+
key={session.id}
|
|
553
|
+
agent={activeAgent}
|
|
554
|
+
conversationId={session.conversationId}
|
|
555
|
+
pageContext={pageContext as unknown as Record<string, unknown>}
|
|
556
|
+
className="h-full"
|
|
557
|
+
placeholder={t(
|
|
558
|
+
'customers.ai_assistant.sheet.composerPlaceholder',
|
|
559
|
+
'Ask about people, companies, deals...',
|
|
560
|
+
)}
|
|
561
|
+
suggestions={suggestions}
|
|
562
|
+
contextItems={contextItems}
|
|
563
|
+
welcomeTitle={t('customers.ai_assistant.sheet.welcomeTitle', 'CRM Assistant')}
|
|
564
|
+
welcomeDescription={
|
|
565
|
+
hasSelection
|
|
566
|
+
? t(
|
|
567
|
+
'customers.ai_assistant.sheet.welcomeDescriptionSelection',
|
|
568
|
+
'Ready to explore your {count} selected contacts:',
|
|
569
|
+
).replace('{count}', String(selectedCount))
|
|
570
|
+
: t(
|
|
571
|
+
'customers.ai_assistant.sheet.welcomeDescriptionAll',
|
|
572
|
+
'Ask me anything about your customers, companies, and deals:',
|
|
573
|
+
)
|
|
574
|
+
}
|
|
575
|
+
/>
|
|
576
|
+
) : null}
|
|
577
|
+
</div>
|
|
578
|
+
</>
|
|
579
|
+
)
|
|
580
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'
|
|
2
|
+
import AiAssistantTriggerWidget from './widget.client'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Step 4.10 — Backend AiChat injection example.
|
|
6
|
+
*
|
|
7
|
+
* Demonstrates how a third-party module can drop `<AiChat>` onto a page
|
|
8
|
+
* it does NOT own (the customers People list) via the existing widget
|
|
9
|
+
* injection system. Targets spot `data-table:customers.people.list:header`,
|
|
10
|
+
* which is owned by the `DataTable` primitive in `packages/ui`.
|
|
11
|
+
*
|
|
12
|
+
* The trigger button opens a right-side sheet embedding
|
|
13
|
+
* `<AiChat agent="customers.account_assistant" pageContext={...} />`.
|
|
14
|
+
* `pageContext` follows the spec §10.1 shape:
|
|
15
|
+
*
|
|
16
|
+
* { view: 'customers.people.list',
|
|
17
|
+
* recordType: null,
|
|
18
|
+
* recordId: string, // "" or comma-separated UUIDs
|
|
19
|
+
* extra: { selectedCount, totalMatching } }
|
|
20
|
+
*
|
|
21
|
+
* Feature-gated behind `customers.people.view` + `ai_assistant.view`.
|
|
22
|
+
*/
|
|
23
|
+
const widget: InjectionWidgetModule<Record<string, unknown>, Record<string, unknown>> = {
|
|
24
|
+
metadata: {
|
|
25
|
+
id: 'customers.injection.ai-assistant-trigger',
|
|
26
|
+
title: 'Customers AI Assistant Trigger',
|
|
27
|
+
description:
|
|
28
|
+
'Renders an "Ask AI" button in the people list header that opens a sheet embedding the customers account assistant.',
|
|
29
|
+
features: ['customers.people.view', 'ai_assistant.view'],
|
|
30
|
+
priority: 100,
|
|
31
|
+
enabled: true,
|
|
32
|
+
},
|
|
33
|
+
Widget: AiAssistantTriggerWidget,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default widget
|