@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
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Module-root AI tool contribution for the catalog module
3
+ * (Phase 1 WS-C, Steps 3.10 + 3.11 + 3.12 — read-only Phase 1 coverage;
4
+ * Phase 3 WS-C, Step 5.14 — D18 mutation pack under the pending-action
5
+ * approval contract).
6
+ *
7
+ * The generator walks every module for a top-level `ai-tools.ts` and takes
8
+ * the default/`aiTools` export as the contribution. This file aggregates the
9
+ * eight catalog packs (products, categories, variants, prices + offers,
10
+ * media + tags, product configuration, D18 merchandising, D18 authoring)
11
+ * so they all flow through the existing `ai-tools.generated.ts` pipeline
12
+ * without any generator changes.
13
+ *
14
+ * Step 5.14 (D18) adds the four mutation tools in `mutation-pack.ts`:
15
+ * - `catalog.update_product`
16
+ * - `catalog.bulk_update_products`
17
+ * - `catalog.apply_attribute_extraction`
18
+ * - `catalog.update_product_media_descriptions`
19
+ *
20
+ * These route through the Step 5.6 `prepareMutation` wrapper and the
21
+ * Step 5.8 confirm route; all other tools remain read-only and enforce
22
+ * tenant + organization scoping via the existing encryption helpers. The Step 3.12 authoring tools are
23
+ * structured-output helpers: they NEVER write and never open a second model
24
+ * call from inside the handler — the surrounding agent turn performs
25
+ * structured output against the handler's `outputSchemaDescriptor`.
26
+ *
27
+ * Step 3.11 (D18) adds the seven spec-named read tools in
28
+ * `merchandising-pack.ts`:
29
+ * - `catalog.search_products`
30
+ * - `catalog.get_product_bundle`
31
+ * - `catalog.list_selected_products`
32
+ * - `catalog.get_product_media`
33
+ * - `catalog.get_attribute_schema`
34
+ * - `catalog.get_category_brief`
35
+ * - `catalog.list_price_kinds`
36
+ *
37
+ * Step 3.12 (D18) adds the five spec-named AI-authoring tools in
38
+ * `authoring-pack.ts`:
39
+ * - `catalog.draft_description_from_attributes`
40
+ * - `catalog.extract_attributes_from_description`
41
+ * - `catalog.draft_description_from_media`
42
+ * - `catalog.suggest_title_variants`
43
+ * - `catalog.suggest_price_adjustment`
44
+ *
45
+ * `catalog.list_price_kinds` (D18) and `catalog.list_price_kinds_base` (base)
46
+ * coexist as distinct tools — both route through the shared `listPriceKindsCore`
47
+ * helper in `ai-tools/_shared.ts` so they cannot drift.
48
+ *
49
+ * See `.ai/runs/2026-04-18-ai-framework-unification/step-3.10-checks.md`,
50
+ * `step-3.11-checks.md`, and `step-3.12-checks.md` for the matrix of
51
+ * required features and decisions.
52
+ */
53
+ import productsAiTools from './ai-tools/products-pack'
54
+ import categoriesAiTools from './ai-tools/categories-pack'
55
+ import variantsAiTools from './ai-tools/variants-pack'
56
+ import pricesOffersAiTools from './ai-tools/prices-offers-pack'
57
+ import mediaTagsAiTools from './ai-tools/media-tags-pack'
58
+ import configurationAiTools from './ai-tools/configuration-pack'
59
+ import merchandisingAiTools from './ai-tools/merchandising-pack'
60
+ import authoringAiTools from './ai-tools/authoring-pack'
61
+ import mutationAiTools from './ai-tools/mutation-pack'
62
+ import statsAiTools from './ai-tools/stats-pack'
63
+ import type { CatalogAiToolDefinition } from './ai-tools/types'
64
+
65
+ export const aiTools: CatalogAiToolDefinition[] = [
66
+ ...productsAiTools,
67
+ ...categoriesAiTools,
68
+ ...variantsAiTools,
69
+ ...pricesOffersAiTools,
70
+ ...mediaTagsAiTools,
71
+ ...configurationAiTools,
72
+ ...merchandisingAiTools,
73
+ ...authoringAiTools,
74
+ ...mutationAiTools,
75
+ ...statsAiTools,
76
+ ]
77
+
78
+ export default aiTools
@@ -0,0 +1,597 @@
1
+ "use client"
2
+
3
+ /**
4
+ * MerchandisingAssistantSheet — Step 4.9 (Spec §10 D18).
5
+ *
6
+ * Embeds `<AiChat agent="catalog.merchandising_assistant" pageContext={...} />`
7
+ * in a right-side sheet (built on the shared Dialog primitive because
8
+ * `packages/ui` does not ship a dedicated Sheet/Drawer primitive in
9
+ * Phase 2). The trigger is a button rendered in the products-list page
10
+ * header.
11
+ *
12
+ * Phase 2 is strictly read-only: the sheet shows proposals (structured
13
+ * output), but the mutation tools (`catalog.update_product`,
14
+ * `catalog.bulk_update_products`, `catalog.apply_attribute_extraction`,
15
+ * `catalog.update_product_media_descriptions`) are intentionally NOT in
16
+ * the agent whitelist. Phase 5.14 introduces those via the pending-action
17
+ * contract.
18
+ *
19
+ * pageContext follows spec §10.1 exactly:
20
+ *
21
+ * {
22
+ * view: 'catalog.products.list',
23
+ * recordType: null,
24
+ * recordId: string, // "" or comma-separated UUIDs
25
+ * extra: {
26
+ * filter: { categoryId, priceRange, tags, status },
27
+ * totalMatching: number,
28
+ * selectedCount: number,
29
+ * }
30
+ * }
31
+ */
32
+
33
+ import * as React from 'react'
34
+ import { Boxes, ChevronDown, FileText, Package, PanelRightOpen, PenLine, Sparkles, Tags, TrendingUp } from 'lucide-react'
35
+ import { AiChat, type AiChatSuggestion, type AiChatContextItem } from '@open-mercato/ui/ai/AiChat'
36
+ import { useAiDock } from '@open-mercato/ui/ai/AiDock'
37
+ import { useAiChatSessions } from '@open-mercato/ui/ai/AiChatSessions'
38
+ import { ChatPaneTabs } from '@open-mercato/ui/ai/ChatPaneTabs'
39
+ // Side-effect import: registers the `catalog.stats-card` UI part on the
40
+ // global registry the first time this client bundle loads. Tools that
41
+ // emit `{ uiPart: { componentId: 'catalog.stats-card' } }` envelopes
42
+ // (catalog.show_stats today; user-defined tools tomorrow) automatically
43
+ // resolve to the card without dispatcher changes.
44
+ import '../../../components/CatalogStatsCard'
45
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
46
+ import { Button } from '@open-mercato/ui/primitives/button'
47
+ import { IconButton } from '@open-mercato/ui/primitives/icon-button'
48
+ import {
49
+ Dialog,
50
+ DialogContent,
51
+ DialogDescription,
52
+ DialogHeader,
53
+ DialogTitle,
54
+ } from '@open-mercato/ui/primitives/dialog'
55
+ import { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'
56
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
57
+ import { cn } from '@open-mercato/shared/lib/utils'
58
+
59
+ export interface MerchandisingPageContextFilter {
60
+ categoryId: string | null
61
+ priceRange: { min?: number; max?: number } | null
62
+ tags: string[]
63
+ status: string | null
64
+ }
65
+
66
+ export interface MerchandisingPageContext {
67
+ view: 'catalog.products.list'
68
+ entityType?: 'catalog.products.list'
69
+ recordType: null
70
+ recordId: string
71
+ extra: {
72
+ filter: MerchandisingPageContextFilter
73
+ totalMatching: number
74
+ selectedCount: number
75
+ }
76
+ }
77
+
78
+ export interface MerchandisingAssistantSheetProps {
79
+ /** Selection-aware page context, built by the products list host. */
80
+ pageContext: MerchandisingPageContext
81
+ /** When false (feature-gated by the host), the sheet renders nothing. */
82
+ enabled?: boolean
83
+ className?: string
84
+ }
85
+
86
+ export const MERCHANDISING_AGENT_ID = 'catalog.merchandising_assistant'
87
+
88
+ function useMerchandisingSuggestions(
89
+ hasSelection: boolean,
90
+ selectedCount: number,
91
+ ): AiChatSuggestion[] {
92
+ const t = useT()
93
+ return React.useMemo(() => {
94
+ if (hasSelection) {
95
+ return [
96
+ {
97
+ label: t(
98
+ 'catalog.merchandising_assistant.suggestions.draftDescriptions',
99
+ 'Draft product descriptions for selected items',
100
+ ),
101
+ prompt: `Draft compelling product descriptions for my ${selectedCount} selected products`,
102
+ icon: <PenLine className="size-4" />,
103
+ },
104
+ {
105
+ label: t(
106
+ 'catalog.merchandising_assistant.suggestions.extractAttributes',
107
+ 'Extract attributes from descriptions',
108
+ ),
109
+ prompt: `Extract structured attributes from the descriptions of my ${selectedCount} selected products`,
110
+ icon: <Tags className="size-4" />,
111
+ },
112
+ {
113
+ label: t(
114
+ 'catalog.merchandising_assistant.suggestions.titleVariants',
115
+ 'Generate title variants for SEO',
116
+ ),
117
+ prompt: `Generate SEO-optimized title variants for my ${selectedCount} selected products`,
118
+ icon: <FileText className="size-4" />,
119
+ },
120
+ {
121
+ label: t(
122
+ 'catalog.merchandising_assistant.suggestions.priceAdjustments',
123
+ 'Suggest price adjustments',
124
+ ),
125
+ prompt: `Analyze and suggest price adjustments for my ${selectedCount} selected products`,
126
+ icon: <TrendingUp className="size-4" />,
127
+ },
128
+ ]
129
+ }
130
+ return [
131
+ {
132
+ label: t(
133
+ 'catalog.merchandising_assistant.suggestions.showStats',
134
+ 'Show catalog overview',
135
+ ),
136
+ // Triggers the `catalog.show_stats` tool, which returns the inline
137
+ // catalog-stats UI part (live counts of products, active products,
138
+ // categories, tags). Demo entry-point for the dynamic UI-part path.
139
+ prompt: 'Show me a quick catalog overview using the stats card.',
140
+ icon: <Boxes className="size-4" />,
141
+ },
142
+ {
143
+ label: t(
144
+ 'catalog.merchandising_assistant.suggestions.browseProducts',
145
+ 'Show me an overview of my product catalog',
146
+ ),
147
+ prompt: 'Give me an overview of my product catalog — categories, total products, and pricing ranges',
148
+ icon: <Package className="size-4" />,
149
+ },
150
+ {
151
+ label: t(
152
+ 'catalog.merchandising_assistant.suggestions.findMissingDescriptions',
153
+ 'Find products with missing descriptions',
154
+ ),
155
+ prompt: 'Find products that are missing descriptions or have very short descriptions',
156
+ icon: <PenLine className="size-4" />,
157
+ },
158
+ {
159
+ label: t(
160
+ 'catalog.merchandising_assistant.suggestions.analyzeAttributes',
161
+ 'Analyze attribute coverage',
162
+ ),
163
+ prompt: 'Analyze which products have incomplete attribute data',
164
+ icon: <Tags className="size-4" />,
165
+ },
166
+ {
167
+ label: t(
168
+ 'catalog.merchandising_assistant.suggestions.pricingOverview',
169
+ 'Show pricing distribution',
170
+ ),
171
+ prompt: 'Show me the pricing distribution across categories',
172
+ icon: <TrendingUp className="size-4" />,
173
+ },
174
+ ]
175
+ }, [hasSelection, selectedCount, t])
176
+ }
177
+
178
+ function useContextItems(pageContext: MerchandisingPageContext): AiChatContextItem[] {
179
+ const t = useT()
180
+ return React.useMemo(() => {
181
+ const items: AiChatContextItem[] = []
182
+ const { selectedCount, totalMatching, filter } = pageContext.extra
183
+ if (selectedCount > 0) {
184
+ items.push({
185
+ label: t(
186
+ 'catalog.merchandising_assistant.context.selectedProducts',
187
+ '{count} products selected',
188
+ ).replace('{count}', String(selectedCount)),
189
+ })
190
+ } else if (totalMatching > 0) {
191
+ items.push({
192
+ label: t(
193
+ 'catalog.merchandising_assistant.context.matchingProducts',
194
+ '{count} products in view',
195
+ ).replace('{count}', String(totalMatching)),
196
+ })
197
+ }
198
+ if (filter.categoryId) {
199
+ items.push({
200
+ label: t('catalog.merchandising_assistant.context.filteredByCategory', 'Filtered by category'),
201
+ detail: filter.categoryId,
202
+ })
203
+ }
204
+ if (filter.status) {
205
+ items.push({ label: filter.status })
206
+ }
207
+ if (filter.tags.length > 0) {
208
+ items.push({
209
+ label: t('catalog.merchandising_assistant.context.tags', '{count} tags').replace(
210
+ '{count}',
211
+ String(filter.tags.length),
212
+ ),
213
+ })
214
+ }
215
+ return items
216
+ }, [pageContext, t])
217
+ }
218
+
219
+ interface MerchandisingAgentDescriptor {
220
+ id: string
221
+ label: string
222
+ description: string
223
+ icon: React.ReactNode
224
+ }
225
+
226
+ interface AgentsResponse {
227
+ agents?: Array<{
228
+ id?: string | null
229
+ }>
230
+ }
231
+
232
+ interface MerchandisingAgentsState {
233
+ agents: MerchandisingAgentDescriptor[]
234
+ loaded: boolean
235
+ }
236
+
237
+ function useMerchandisingAgents(): MerchandisingAgentsState {
238
+ const t = useT()
239
+ const [accessibleAgentIds, setAccessibleAgentIds] = React.useState<Set<string> | null>(null)
240
+ const declaredAgents = React.useMemo(
241
+ () => [
242
+ {
243
+ id: MERCHANDISING_AGENT_ID,
244
+ label: t(
245
+ 'catalog.merchandising_assistant.agents.merchandising.label',
246
+ 'Merchandising Assistant',
247
+ ),
248
+ description: t(
249
+ 'catalog.merchandising_assistant.agents.merchandising.description',
250
+ 'Draft copy, normalize attributes, and propose price changes for the current selection.',
251
+ ),
252
+ icon: <Sparkles className="size-4" />,
253
+ },
254
+ ],
255
+ [t],
256
+ )
257
+
258
+ React.useEffect(() => {
259
+ let cancelled = false
260
+ apiCall<AgentsResponse>('/api/ai_assistant/ai/agents', {
261
+ credentials: 'same-origin',
262
+ headers: { 'x-om-forbidden-redirect': '0', 'x-om-unauthorized-redirect': '0' },
263
+ })
264
+ .then((call) => {
265
+ if (cancelled) return
266
+ if (!call.ok || !call.result || !Array.isArray(call.result.agents)) {
267
+ setAccessibleAgentIds(new Set())
268
+ return
269
+ }
270
+ setAccessibleAgentIds(
271
+ new Set(
272
+ call.result.agents
273
+ .map((agent) => agent?.id)
274
+ .filter((id): id is string => typeof id === 'string' && id.length > 0),
275
+ ),
276
+ )
277
+ })
278
+ .catch(() => {
279
+ if (!cancelled) setAccessibleAgentIds(new Set())
280
+ })
281
+ return () => {
282
+ cancelled = true
283
+ }
284
+ }, [])
285
+
286
+ return React.useMemo(
287
+ () => ({
288
+ agents:
289
+ accessibleAgentIds === null
290
+ ? []
291
+ : declaredAgents.filter((agent) => accessibleAgentIds.has(agent.id)),
292
+ loaded: accessibleAgentIds !== null,
293
+ }),
294
+ [accessibleAgentIds, declaredAgents],
295
+ )
296
+ }
297
+
298
+ export function MerchandisingAssistantSheet({
299
+ pageContext,
300
+ enabled = true,
301
+ className,
302
+ }: MerchandisingAssistantSheetProps): React.ReactElement | null {
303
+ const t = useT()
304
+ const dock = useAiDock()
305
+ const [open, setOpen] = React.useState(false)
306
+ const [popoverOpen, setPopoverOpen] = React.useState(false)
307
+ const [activeAgent, setActiveAgent] = React.useState<string>(MERCHANDISING_AGENT_ID)
308
+ const [lastAgent, setLastAgent] = React.useState<string | null>(null)
309
+
310
+ const selectedCount = pageContext.extra.selectedCount
311
+ const hasSelection = selectedCount > 0
312
+ const suggestions = useMerchandisingSuggestions(hasSelection, selectedCount)
313
+ const contextItems = useContextItems(pageContext)
314
+ const { agents, loaded: agentsLoaded } = useMerchandisingAgents()
315
+
316
+ if (!enabled || !agentsLoaded || agents.length === 0) return null
317
+
318
+ const openAgent = (agentId: string) => {
319
+ setActiveAgent(agentId)
320
+ setLastAgent(agentId)
321
+ setPopoverOpen(false)
322
+ if (dock.state.assistant?.agent === agentId) {
323
+ dock.dock(dock.state.assistant)
324
+ setOpen(false)
325
+ return
326
+ }
327
+ setOpen(true)
328
+ }
329
+
330
+ const handleSelectAgent = (agentId: string) => {
331
+ openAgent(agentId)
332
+ }
333
+
334
+ const handleMainTriggerClick = () => {
335
+ if (agents.length === 1) {
336
+ openAgent(agents[0].id)
337
+ return
338
+ }
339
+ if (lastAgent && agents.some((a) => a.id === lastAgent)) {
340
+ openAgent(lastAgent)
341
+ return
342
+ }
343
+ setPopoverOpen(true)
344
+ }
345
+
346
+ const handleDock = () => {
347
+ const agent = agents.find((a) => a.id === activeAgent) ?? agents[0]
348
+ if (!agent) return
349
+ dock.dock({
350
+ agent: agent.id,
351
+ label: agent.label,
352
+ description: t('catalog.merchandising_assistant.dock.subtitle', 'Catalog'),
353
+ pageContext: pageContext as unknown as Record<string, unknown>,
354
+ placeholder: t(
355
+ 'catalog.merchandising_assistant.sheet.composerPlaceholder',
356
+ 'Ask for descriptions, attributes, titles, or price ideas...',
357
+ ),
358
+ suggestions,
359
+ contextItems,
360
+ welcomeTitle: t(
361
+ 'catalog.merchandising_assistant.sheet.welcomeTitle',
362
+ 'Merchandising Assistant',
363
+ ),
364
+ welcomeDescription: hasSelection
365
+ ? t(
366
+ 'catalog.merchandising_assistant.sheet.welcomeDescriptionSelection',
367
+ 'Ready to work with your {count} selected products. Try one of these:',
368
+ ).replace('{count}', String(selectedCount))
369
+ : t(
370
+ 'catalog.merchandising_assistant.sheet.welcomeDescriptionAll',
371
+ 'Select products for targeted actions, or explore your catalog:',
372
+ ),
373
+ })
374
+ setOpen(false)
375
+ }
376
+
377
+ const triggerLabel = t(
378
+ 'catalog.merchandising_assistant.trigger.ariaLabel',
379
+ 'Open AI merchandising assistant',
380
+ )
381
+ const labelText = t('catalog.merchandising_assistant.trigger.label', 'AI')
382
+ const moreAgentsLabel = t(
383
+ 'catalog.merchandising_assistant.trigger.moreAgentsAriaLabel',
384
+ 'Choose an AI assistant',
385
+ )
386
+
387
+ return (
388
+ <>
389
+ <div className={cn('inline-flex items-center', className)}>
390
+ <Button
391
+ type="button"
392
+ variant="outline"
393
+ onClick={handleMainTriggerClick}
394
+ data-ai-merchandising-trigger=""
395
+ aria-label={triggerLabel}
396
+ title={triggerLabel}
397
+ className={cn(
398
+ 'relative',
399
+ agents.length > 1 && 'rounded-r-none border-r-0',
400
+ )}
401
+ >
402
+ <Sparkles className="size-4" aria-hidden />
403
+ <span>{labelText}</span>
404
+ {hasSelection ? (
405
+ <span
406
+ 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"
407
+ data-ai-merchandising-selected-count={selectedCount}
408
+ >
409
+ {selectedCount}
410
+ </span>
411
+ ) : null}
412
+ </Button>
413
+ {agents.length > 1 ? (
414
+ <Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
415
+ <PopoverTrigger asChild>
416
+ <IconButton
417
+ type="button"
418
+ variant="outline"
419
+ size="lg"
420
+ aria-label={moreAgentsLabel}
421
+ title={moreAgentsLabel}
422
+ className="rounded-l-none"
423
+ data-ai-merchandising-picker=""
424
+ >
425
+ <ChevronDown className="size-4" aria-hidden />
426
+ </IconButton>
427
+ </PopoverTrigger>
428
+ <PopoverContent align="end" className="w-72 p-1">
429
+ <div className="px-3 pt-2 pb-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">
430
+ {t('catalog.merchandising_assistant.popover.heading', 'AI assistants')}
431
+ </div>
432
+ <div className="flex flex-col gap-0.5">
433
+ {agents.map((agent) => (
434
+ <button
435
+ key={agent.id}
436
+ type="button"
437
+ onClick={() => handleSelectAgent(agent.id)}
438
+ data-ai-merchandising-agent-option={agent.id}
439
+ 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"
440
+ >
441
+ <span className="mt-0.5 inline-flex size-6 items-center justify-center rounded-full bg-secondary text-secondary-foreground">
442
+ {agent.icon}
443
+ </span>
444
+ <span className="flex-1 min-w-0">
445
+ <span className="block font-medium leading-tight">{agent.label}</span>
446
+ <span className="block text-xs text-muted-foreground leading-snug">
447
+ {agent.description}
448
+ </span>
449
+ </span>
450
+ </button>
451
+ ))}
452
+ </div>
453
+ </PopoverContent>
454
+ </Popover>
455
+ ) : null}
456
+ </div>
457
+ <Dialog open={open} onOpenChange={setOpen}>
458
+ <DialogContent
459
+ className={cn(
460
+ // Mobile: full-screen sheet. Desktop (≥sm): right-anchored side sheet.
461
+ // The Dialog primitive applies a centering transform at the
462
+ // sm breakpoint; each piece (`top`, `left`, transform, inset)
463
+ // must be overridden at the same breakpoint or the panel
464
+ // renders half off the viewport.
465
+ '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',
466
+ 'sm:top-0 sm:bottom-0 sm:right-0 sm:left-auto sm:translate-x-0 sm:translate-y-0',
467
+ 'sm:max-w-xl sm:w-[36rem] sm:rounded-l-2xl sm:h-screen sm:max-h-screen',
468
+ 'flex flex-col gap-3 p-4 z-[70]',
469
+ )}
470
+ data-ai-merchandising-sheet=""
471
+ >
472
+ <DialogHeader>
473
+ <div className="flex items-center gap-3 pr-8">
474
+ {/* Dock button lives on the LEFT — the Dialog primitive
475
+ auto-renders an X close button absolutely positioned in
476
+ the top-right corner, so anything we drop in the header's
477
+ right side visually collides with it. Mobile hides the
478
+ dock entirely (the side panel is desktop-only). */}
479
+ <IconButton
480
+ type="button"
481
+ variant="ghost"
482
+ size="sm"
483
+ aria-label={t('catalog.merchandising_assistant.sheet.dock', 'Dock to side')}
484
+ title={t('catalog.merchandising_assistant.sheet.dock', 'Dock to side')}
485
+ onClick={handleDock}
486
+ data-ai-merchandising-dock=""
487
+ className="hidden lg:inline-flex shrink-0"
488
+ >
489
+ <PanelRightOpen className="size-4" aria-hidden />
490
+ </IconButton>
491
+ <DialogTitle className="flex-1 min-w-0 truncate">
492
+ {t('catalog.merchandising_assistant.sheet.title', 'Catalog merchandising assistant')}
493
+ </DialogTitle>
494
+ {hasSelection ? (
495
+ <span
496
+ className="shrink-0 inline-flex items-center rounded-full border border-border bg-secondary px-2 py-0.5 text-xs text-secondary-foreground"
497
+ data-ai-merchandising-selection-pill=""
498
+ data-ai-merchandising-selected-count={selectedCount}
499
+ >
500
+ {t(
501
+ 'catalog.merchandising_assistant.sheet.selectionPill',
502
+ 'Acting on {count} products',
503
+ ).replace('{count}', String(selectedCount))}
504
+ </span>
505
+ ) : null}
506
+ </div>
507
+ <DialogDescription>
508
+ {hasSelection
509
+ ? t(
510
+ 'catalog.merchandising_assistant.sheet.descriptionWithSelection',
511
+ 'Working with {count} selected products. Ask for descriptions, attribute extraction, title suggestions, or pricing analysis.',
512
+ ).replace('{count}', String(selectedCount))
513
+ : t(
514
+ 'catalog.merchandising_assistant.sheet.description',
515
+ 'Your AI merchandising copilot. Select products from the list for targeted actions, or explore your full catalog.',
516
+ )}
517
+ </DialogDescription>
518
+ </DialogHeader>
519
+ <MerchandisingChatBody
520
+ activeAgent={activeAgent}
521
+ pageContext={pageContext}
522
+ suggestions={suggestions}
523
+ contextItems={contextItems}
524
+ hasSelection={hasSelection}
525
+ selectedCount={selectedCount}
526
+ />
527
+ </DialogContent>
528
+ </Dialog>
529
+ </>
530
+ )
531
+ }
532
+
533
+ interface MerchandisingChatBodyProps {
534
+ activeAgent: string
535
+ pageContext: MerchandisingPageContext
536
+ suggestions: AiChatSuggestion[]
537
+ contextItems: AiChatContextItem[]
538
+ hasSelection: boolean
539
+ selectedCount: number
540
+ }
541
+
542
+ function MerchandisingChatBody({
543
+ activeAgent,
544
+ pageContext,
545
+ suggestions,
546
+ contextItems,
547
+ hasSelection,
548
+ selectedCount,
549
+ }: MerchandisingChatBodyProps) {
550
+ const t = useT()
551
+ const sessions = useAiChatSessions()
552
+ const session = sessions.getActiveSession(activeAgent)
553
+
554
+ React.useEffect(() => {
555
+ if (!session) sessions.ensureSession(activeAgent)
556
+ }, [activeAgent, session, sessions])
557
+
558
+ return (
559
+ <>
560
+ <ChatPaneTabs agentId={activeAgent} className="border-b" />
561
+ <div className="min-h-0 flex-1" data-ai-merchandising-chat-container="">
562
+ {session ? (
563
+ <AiChat
564
+ key={session.id}
565
+ agent={activeAgent}
566
+ conversationId={session.conversationId}
567
+ pageContext={pageContext as unknown as Record<string, unknown>}
568
+ className="h-full"
569
+ placeholder={t(
570
+ 'catalog.merchandising_assistant.sheet.composerPlaceholder',
571
+ 'Ask for descriptions, attributes, titles, or price ideas...',
572
+ )}
573
+ suggestions={suggestions}
574
+ contextItems={contextItems}
575
+ welcomeTitle={t(
576
+ 'catalog.merchandising_assistant.sheet.welcomeTitle',
577
+ 'Merchandising Assistant',
578
+ )}
579
+ welcomeDescription={
580
+ hasSelection
581
+ ? t(
582
+ 'catalog.merchandising_assistant.sheet.welcomeDescriptionSelection',
583
+ 'Ready to work with your {count} selected products. Try one of these:',
584
+ ).replace('{count}', String(selectedCount))
585
+ : t(
586
+ 'catalog.merchandising_assistant.sheet.welcomeDescriptionAll',
587
+ 'Select products for targeted actions, or explore your catalog:',
588
+ )
589
+ }
590
+ />
591
+ ) : null}
592
+ </div>
593
+ </>
594
+ )
595
+ }
596
+
597
+ export default MerchandisingAssistantSheet