@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.
Files changed (163) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +13 -1
  3. package/dist/helpers/integration/api.js +29 -16
  4. package/dist/helpers/integration/api.js.map +2 -2
  5. package/dist/helpers/integration/auth.js +11 -6
  6. package/dist/helpers/integration/auth.js.map +3 -3
  7. package/dist/modules/auth/commands/roles.js +9 -12
  8. package/dist/modules/auth/commands/roles.js.map +2 -2
  9. package/dist/modules/catalog/ai-agents-context.js +147 -0
  10. package/dist/modules/catalog/ai-agents-context.js.map +7 -0
  11. package/dist/modules/catalog/ai-agents.js +383 -0
  12. package/dist/modules/catalog/ai-agents.js.map +7 -0
  13. package/dist/modules/catalog/ai-tools/_shared.js +318 -0
  14. package/dist/modules/catalog/ai-tools/_shared.js.map +7 -0
  15. package/dist/modules/catalog/ai-tools/authoring-pack.js +391 -0
  16. package/dist/modules/catalog/ai-tools/authoring-pack.js.map +7 -0
  17. package/dist/modules/catalog/ai-tools/categories-pack.js +167 -0
  18. package/dist/modules/catalog/ai-tools/categories-pack.js.map +7 -0
  19. package/dist/modules/catalog/ai-tools/configuration-pack.js +120 -0
  20. package/dist/modules/catalog/ai-tools/configuration-pack.js.map +7 -0
  21. package/dist/modules/catalog/ai-tools/media-tags-pack.js +107 -0
  22. package/dist/modules/catalog/ai-tools/media-tags-pack.js.map +7 -0
  23. package/dist/modules/catalog/ai-tools/merchandising-pack.js +429 -0
  24. package/dist/modules/catalog/ai-tools/merchandising-pack.js.map +7 -0
  25. package/dist/modules/catalog/ai-tools/mutation-pack.js +576 -0
  26. package/dist/modules/catalog/ai-tools/mutation-pack.js.map +7 -0
  27. package/dist/modules/catalog/ai-tools/prices-offers-pack.js +208 -0
  28. package/dist/modules/catalog/ai-tools/prices-offers-pack.js.map +7 -0
  29. package/dist/modules/catalog/ai-tools/products-pack.js +298 -0
  30. package/dist/modules/catalog/ai-tools/products-pack.js.map +7 -0
  31. package/dist/modules/catalog/ai-tools/stats-pack.js +57 -0
  32. package/dist/modules/catalog/ai-tools/stats-pack.js.map +7 -0
  33. package/dist/modules/catalog/ai-tools/types.js +10 -0
  34. package/dist/modules/catalog/ai-tools/types.js.map +7 -0
  35. package/dist/modules/catalog/ai-tools/variants-pack.js +75 -0
  36. package/dist/modules/catalog/ai-tools/variants-pack.js.map +7 -0
  37. package/dist/modules/catalog/ai-tools.js +28 -0
  38. package/dist/modules/catalog/ai-tools.js.map +7 -0
  39. package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +466 -0
  40. package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +7 -0
  41. package/dist/modules/catalog/backend/catalog/products/page.js +7 -1
  42. package/dist/modules/catalog/backend/catalog/products/page.js.map +2 -2
  43. package/dist/modules/catalog/components/CatalogStatsCard.js +91 -0
  44. package/dist/modules/catalog/components/CatalogStatsCard.js.map +7 -0
  45. package/dist/modules/catalog/components/products/ProductsDataTable.js +23 -3
  46. package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
  47. package/dist/modules/catalog/events.js +7 -4
  48. package/dist/modules/catalog/events.js.map +2 -2
  49. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js +59 -0
  50. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js.map +7 -0
  51. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js +17 -0
  52. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js.map +7 -0
  53. package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js +1 -1
  54. package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js.map +2 -2
  55. package/dist/modules/catalog/widgets/injection-table.js +13 -1
  56. package/dist/modules/catalog/widgets/injection-table.js.map +2 -2
  57. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js +94 -0
  58. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js.map +7 -0
  59. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js +17 -0
  60. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js.map +7 -0
  61. package/dist/modules/customer_accounts/widgets/injection-table.js +9 -0
  62. package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
  63. package/dist/modules/customers/ai-agents-context.js +96 -0
  64. package/dist/modules/customers/ai-agents-context.js.map +7 -0
  65. package/dist/modules/customers/ai-agents.js +244 -0
  66. package/dist/modules/customers/ai-agents.js.map +7 -0
  67. package/dist/modules/customers/ai-tools/activities-tasks-pack.js +1015 -0
  68. package/dist/modules/customers/ai-tools/activities-tasks-pack.js.map +7 -0
  69. package/dist/modules/customers/ai-tools/addresses-tags-pack.js +134 -0
  70. package/dist/modules/customers/ai-tools/addresses-tags-pack.js.map +7 -0
  71. package/dist/modules/customers/ai-tools/companies-pack.js +249 -0
  72. package/dist/modules/customers/ai-tools/companies-pack.js.map +7 -0
  73. package/dist/modules/customers/ai-tools/deals-pack.js +348 -0
  74. package/dist/modules/customers/ai-tools/deals-pack.js.map +7 -0
  75. package/dist/modules/customers/ai-tools/people-pack.js +261 -0
  76. package/dist/modules/customers/ai-tools/people-pack.js.map +7 -0
  77. package/dist/modules/customers/ai-tools/settings-pack.js +102 -0
  78. package/dist/modules/customers/ai-tools/settings-pack.js.map +7 -0
  79. package/dist/modules/customers/ai-tools/types.js +10 -0
  80. package/dist/modules/customers/ai-tools/types.js.map +7 -0
  81. package/dist/modules/customers/ai-tools.js +20 -0
  82. package/dist/modules/customers/ai-tools.js.map +7 -0
  83. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +469 -0
  84. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +7 -0
  85. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +17 -0
  86. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +7 -0
  87. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js +117 -0
  88. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js.map +7 -0
  89. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js +17 -0
  90. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js.map +7 -0
  91. package/dist/modules/customers/widgets/injection-table.js +26 -0
  92. package/dist/modules/customers/widgets/injection-table.js.map +7 -0
  93. package/dist/modules/inbox_ops/ai-tools.js +4 -0
  94. package/dist/modules/inbox_ops/ai-tools.js.map +2 -2
  95. package/dist/modules/inbox_ops/lib/llmProvider.js +52 -7
  96. package/dist/modules/inbox_ops/lib/llmProvider.js.map +2 -2
  97. package/dist/modules/notifications/setup.js +13 -0
  98. package/dist/modules/notifications/setup.js.map +7 -0
  99. package/jest.config.cjs +1 -0
  100. package/jest.setup.ts +18 -0
  101. package/package.json +5 -3
  102. package/src/helpers/integration/api.ts +38 -16
  103. package/src/helpers/integration/auth.ts +13 -6
  104. package/src/modules/auth/commands/roles.ts +10 -12
  105. package/src/modules/catalog/AGENTS.md +11 -0
  106. package/src/modules/catalog/ai-agents-context.ts +239 -0
  107. package/src/modules/catalog/ai-agents.ts +525 -0
  108. package/src/modules/catalog/ai-tools/_shared.ts +487 -0
  109. package/src/modules/catalog/ai-tools/authoring-pack.ts +600 -0
  110. package/src/modules/catalog/ai-tools/categories-pack.ts +192 -0
  111. package/src/modules/catalog/ai-tools/configuration-pack.ts +218 -0
  112. package/src/modules/catalog/ai-tools/media-tags-pack.ts +127 -0
  113. package/src/modules/catalog/ai-tools/merchandising-pack.ts +608 -0
  114. package/src/modules/catalog/ai-tools/mutation-pack.ts +761 -0
  115. package/src/modules/catalog/ai-tools/prices-offers-pack.ts +376 -0
  116. package/src/modules/catalog/ai-tools/products-pack.ts +387 -0
  117. package/src/modules/catalog/ai-tools/stats-pack.ts +84 -0
  118. package/src/modules/catalog/ai-tools/types.ts +81 -0
  119. package/src/modules/catalog/ai-tools/variants-pack.ts +147 -0
  120. package/src/modules/catalog/ai-tools.ts +78 -0
  121. package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +597 -0
  122. package/src/modules/catalog/backend/catalog/products/page.tsx +23 -2
  123. package/src/modules/catalog/components/CatalogStatsCard.tsx +118 -0
  124. package/src/modules/catalog/components/products/ProductsDataTable.tsx +54 -6
  125. package/src/modules/catalog/events.ts +7 -4
  126. package/src/modules/catalog/i18n/de.json +17 -0
  127. package/src/modules/catalog/i18n/en.json +17 -0
  128. package/src/modules/catalog/i18n/es.json +17 -0
  129. package/src/modules/catalog/i18n/pl.json +17 -0
  130. package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.tsx +109 -0
  131. package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.ts +29 -0
  132. package/src/modules/catalog/widgets/injection/product-seo/widget.client.tsx +1 -1
  133. package/src/modules/catalog/widgets/injection-table.ts +12 -0
  134. package/src/modules/customer_accounts/i18n/de.json +5 -0
  135. package/src/modules/customer_accounts/i18n/en.json +5 -0
  136. package/src/modules/customer_accounts/i18n/es.json +5 -0
  137. package/src/modules/customer_accounts/i18n/pl.json +5 -0
  138. package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.tsx +136 -0
  139. package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.ts +43 -0
  140. package/src/modules/customer_accounts/widgets/injection-table.ts +9 -0
  141. package/src/modules/customers/AGENTS.md +13 -0
  142. package/src/modules/customers/ai-agents-context.ts +150 -0
  143. package/src/modules/customers/ai-agents.ts +355 -0
  144. package/src/modules/customers/ai-tools/activities-tasks-pack.ts +1248 -0
  145. package/src/modules/customers/ai-tools/addresses-tags-pack.ts +145 -0
  146. package/src/modules/customers/ai-tools/companies-pack.ts +362 -0
  147. package/src/modules/customers/ai-tools/deals-pack.ts +505 -0
  148. package/src/modules/customers/ai-tools/people-pack.ts +369 -0
  149. package/src/modules/customers/ai-tools/settings-pack.ts +121 -0
  150. package/src/modules/customers/ai-tools/types.ts +76 -0
  151. package/src/modules/customers/ai-tools.ts +34 -0
  152. package/src/modules/customers/i18n/de.json +25 -0
  153. package/src/modules/customers/i18n/en.json +25 -0
  154. package/src/modules/customers/i18n/es.json +25 -0
  155. package/src/modules/customers/i18n/pl.json +25 -0
  156. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +580 -0
  157. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +36 -0
  158. package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.tsx +191 -0
  159. package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.ts +37 -0
  160. package/src/modules/customers/widgets/injection-table.ts +41 -0
  161. package/src/modules/inbox_ops/ai-tools.ts +4 -0
  162. package/src/modules/inbox_ops/lib/llmProvider.ts +83 -7
  163. 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