@hed-hog/catalog 0.0.293 → 0.0.295

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 (66) hide show
  1. package/README.md +391 -361
  2. package/dist/catalog-resource.config.d.ts.map +1 -1
  3. package/dist/catalog-resource.config.js +51 -24
  4. package/dist/catalog-resource.config.js.map +1 -1
  5. package/dist/catalog.controller.d.ts +420 -0
  6. package/dist/catalog.controller.d.ts.map +1 -1
  7. package/dist/catalog.controller.js +98 -0
  8. package/dist/catalog.controller.js.map +1 -1
  9. package/dist/catalog.module.d.ts.map +1 -1
  10. package/dist/catalog.module.js +5 -1
  11. package/dist/catalog.module.js.map +1 -1
  12. package/dist/catalog.service.d.ts +216 -1
  13. package/dist/catalog.service.d.ts.map +1 -1
  14. package/dist/catalog.service.js +1121 -7
  15. package/dist/catalog.service.js.map +1 -1
  16. package/hedhog/data/catalog_attribute.yaml +202 -0
  17. package/hedhog/data/catalog_attribute_option.yaml +109 -0
  18. package/hedhog/data/catalog_category.yaml +47 -0
  19. package/hedhog/data/catalog_category_attribute.yaml +209 -0
  20. package/hedhog/data/menu.yaml +46 -12
  21. package/hedhog/data/role.yaml +7 -7
  22. package/hedhog/data/route.yaml +64 -0
  23. package/hedhog/frontend/app/[resource]/page.tsx.ejs +358 -0
  24. package/hedhog/frontend/app/_components/catalog-ai-form-assist-dialog.tsx.ejs +340 -0
  25. package/hedhog/frontend/app/_components/catalog-resource-form-sheet.tsx.ejs +815 -0
  26. package/hedhog/frontend/app/_lib/catalog-resources.tsx.ejs +504 -736
  27. package/hedhog/frontend/app/dashboard/page.tsx.ejs +14 -83
  28. package/hedhog/frontend/messages/en.json +150 -60
  29. package/hedhog/frontend/messages/pt.json +185 -95
  30. package/hedhog/table/catalog_affiliate_program.yaml +41 -41
  31. package/hedhog/table/catalog_attribute.yaml +22 -7
  32. package/hedhog/table/catalog_attribute_group.yaml +18 -18
  33. package/hedhog/table/catalog_attribute_option.yaml +40 -0
  34. package/hedhog/table/catalog_brand.yaml +34 -34
  35. package/hedhog/table/catalog_category.yaml +40 -0
  36. package/hedhog/table/catalog_category_attribute.yaml +13 -7
  37. package/hedhog/table/catalog_click_event.yaml +50 -50
  38. package/hedhog/table/catalog_comparison.yaml +3 -6
  39. package/hedhog/table/catalog_comparison_highlight.yaml +39 -39
  40. package/hedhog/table/catalog_comparison_item.yaml +30 -30
  41. package/hedhog/table/catalog_content_relation.yaml +42 -42
  42. package/hedhog/table/catalog_import_run.yaml +33 -33
  43. package/hedhog/table/catalog_import_source.yaml +24 -24
  44. package/hedhog/table/catalog_merchant.yaml +29 -29
  45. package/hedhog/table/catalog_offer.yaml +83 -83
  46. package/hedhog/table/catalog_price_history.yaml +34 -34
  47. package/hedhog/table/catalog_product.yaml +5 -3
  48. package/hedhog/table/catalog_product_attribute_value.yaml +15 -2
  49. package/hedhog/table/catalog_product_category.yaml +3 -3
  50. package/hedhog/table/catalog_product_image.yaml +34 -34
  51. package/hedhog/table/catalog_product_score.yaml +38 -38
  52. package/hedhog/table/catalog_product_site.yaml +47 -47
  53. package/hedhog/table/catalog_product_tag.yaml +19 -19
  54. package/hedhog/table/catalog_score_criterion.yaml +25 -8
  55. package/hedhog/table/catalog_seo_page_rule.yaml +2 -2
  56. package/hedhog/table/catalog_similarity_rule.yaml +19 -6
  57. package/hedhog/table/catalog_site.yaml +8 -0
  58. package/hedhog/table/catalog_site_category.yaml +3 -3
  59. package/package.json +7 -7
  60. package/src/catalog-resource.config.ts +51 -24
  61. package/src/catalog.controller.ts +67 -0
  62. package/src/catalog.module.ts +5 -1
  63. package/src/catalog.service.ts +1531 -6
  64. package/src/index.ts +1 -1
  65. package/src/language/en.json +4 -4
  66. package/src/language/pt.json +4 -4
@@ -0,0 +1,340 @@
1
+ 'use client';
2
+
3
+ import { Badge } from '@/components/ui/badge';
4
+ import { Button } from '@/components/ui/button';
5
+ import {
6
+ Dialog,
7
+ DialogContent,
8
+ DialogDescription,
9
+ DialogFooter,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ DialogTrigger,
13
+ } from '@/components/ui/dialog';
14
+ import { ScrollArea } from '@/components/ui/scroll-area';
15
+ import { Textarea } from '@/components/ui/textarea';
16
+ import { useApp } from '@hed-hog/next-app-provider';
17
+ import { Loader2, Sparkles } from 'lucide-react';
18
+ import { useTranslations } from 'next-intl';
19
+ import { useMemo, useState } from 'react';
20
+ import { toast } from 'sonner';
21
+ import {
22
+ getCatalogLocalizedText,
23
+ type CatalogFormFieldDefinition,
24
+ type CatalogResourceDefinition,
25
+ } from '../_lib/catalog-resources';
26
+
27
+ type CatalogRecord = Record<string, unknown>;
28
+
29
+ type CatalogAiSuggestionResponse = {
30
+ fields?: Record<string, unknown>;
31
+ applied_fields?: Array<{
32
+ key: string;
33
+ label: string;
34
+ type: string;
35
+ value: unknown;
36
+ }>;
37
+ product_attributes?: Array<Record<string, unknown>>;
38
+ warnings?: string[];
39
+ };
40
+
41
+ type CatalogAiFormAssistDialogProps = {
42
+ resource: string;
43
+ resourceConfig: CatalogResourceDefinition;
44
+ disabled?: boolean;
45
+ getCurrentValues: () => Record<string, unknown>;
46
+ getCurrentAttributeValues?: () => Array<Record<string, unknown>>;
47
+ onApply: (payload: CatalogAiSuggestionResponse) => void;
48
+ };
49
+
50
+ function serializeFieldDefinition(
51
+ field: CatalogFormFieldDefinition,
52
+ localeCode?: string | null,
53
+ ) {
54
+ return {
55
+ key: field.key,
56
+ label: getCatalogLocalizedText(field.label, localeCode),
57
+ type: field.type,
58
+ required: Boolean(field.required),
59
+ options: (field.options ?? []).map((option) => ({
60
+ value: option.value,
61
+ label: getCatalogLocalizedText(option.label, localeCode),
62
+ })),
63
+ relation: field.relation
64
+ ? {
65
+ endpoint: field.relation.endpoint,
66
+ resource: field.relation.resource,
67
+ labelKeys: field.relation.labelKeys,
68
+ }
69
+ : null,
70
+ };
71
+ }
72
+
73
+ function renderSuggestionValue(value: unknown) {
74
+ if (value === null || value === undefined || value === '') {
75
+ return '-';
76
+ }
77
+
78
+ if (typeof value === 'object') {
79
+ return JSON.stringify(value);
80
+ }
81
+
82
+ return String(value);
83
+ }
84
+
85
+ export function CatalogAiFormAssistDialog({
86
+ resource,
87
+ resourceConfig,
88
+ disabled,
89
+ getCurrentValues,
90
+ getCurrentAttributeValues,
91
+ onApply,
92
+ }: CatalogAiFormAssistDialogProps) {
93
+ const { request, currentLocaleCode } = useApp();
94
+ const t = useTranslations('catalog');
95
+ const [open, setOpen] = useState(false);
96
+ const [prompt, setPrompt] = useState('');
97
+ const [isGenerating, setIsGenerating] = useState(false);
98
+ const [suggestion, setSuggestion] = useState<CatalogAiSuggestionResponse | null>(
99
+ null,
100
+ );
101
+ const [generationError, setGenerationError] = useState<string | null>(null);
102
+ const fieldContext = useMemo(
103
+ () =>
104
+ resourceConfig.formSections.flatMap((section) =>
105
+ section.fields.map((field) =>
106
+ serializeFieldDefinition(field, currentLocaleCode),
107
+ ),
108
+ ),
109
+ [currentLocaleCode, resourceConfig],
110
+ );
111
+
112
+ const handleGenerate = async () => {
113
+ const trimmedPrompt = prompt.trim();
114
+
115
+ if (!trimmedPrompt) {
116
+ toast.error(t('aiAssist.errors.promptRequired'));
117
+ return;
118
+ }
119
+
120
+ setIsGenerating(true);
121
+ setGenerationError(null);
122
+
123
+ try {
124
+ const response = await request<CatalogAiSuggestionResponse>({
125
+ url: `/catalog/forms/${resource}/ai-assist`,
126
+ method: 'POST',
127
+ data: {
128
+ prompt: trimmedPrompt,
129
+ fields: fieldContext,
130
+ current_values: getCurrentValues(),
131
+ current_attribute_values: getCurrentAttributeValues?.() ?? [],
132
+ },
133
+ });
134
+
135
+ setSuggestion((response.data ?? null) as CatalogAiSuggestionResponse | null);
136
+ } catch (error) {
137
+ const message =
138
+ error instanceof Error ? error.message : t('aiAssist.errors.generate');
139
+ setGenerationError(message);
140
+ toast.error(message);
141
+ } finally {
142
+ setIsGenerating(false);
143
+ }
144
+ };
145
+
146
+ const handleApply = () => {
147
+ if (!suggestion) {
148
+ return;
149
+ }
150
+
151
+ onApply(suggestion);
152
+ setOpen(false);
153
+ toast.success(t('aiAssist.toasts.applied'));
154
+ };
155
+
156
+ return (
157
+ <Dialog
158
+ open={open}
159
+ onOpenChange={(nextOpen) => {
160
+ setOpen(nextOpen);
161
+
162
+ if (!nextOpen) {
163
+ setGenerationError(null);
164
+ }
165
+ }}
166
+ >
167
+ <DialogTrigger asChild>
168
+ <Button
169
+ type="button"
170
+ variant="outline"
171
+ size="sm"
172
+ disabled={disabled}
173
+ >
174
+ <Sparkles className="mr-2 h-4 w-4" />
175
+ {t('aiAssist.button')}
176
+ </Button>
177
+ </DialogTrigger>
178
+ <DialogContent className="sm:max-w-3xl">
179
+ <DialogHeader>
180
+ <DialogTitle>{t('aiAssist.title')}</DialogTitle>
181
+ <DialogDescription>
182
+ {t('aiAssist.description', {
183
+ resource: getCatalogLocalizedText(
184
+ resourceConfig.singularLabel,
185
+ currentLocaleCode,
186
+ ),
187
+ })}
188
+ </DialogDescription>
189
+ </DialogHeader>
190
+
191
+ <div className="space-y-4">
192
+ <div className="space-y-2">
193
+ <div className="text-sm font-medium">{t('aiAssist.promptLabel')}</div>
194
+ <Textarea
195
+ value={prompt}
196
+ onChange={(event) => setPrompt(event.target.value)}
197
+ placeholder={t('aiAssist.promptPlaceholder')}
198
+ className="min-h-32"
199
+ />
200
+ <p className="text-xs text-muted-foreground">
201
+ {t('aiAssist.promptHint')}
202
+ </p>
203
+ </div>
204
+
205
+ {generationError ? (
206
+ <div className="rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
207
+ {generationError}
208
+ </div>
209
+ ) : null}
210
+
211
+ {suggestion ? (
212
+ <div className="space-y-4 rounded-lg border bg-muted/20 p-4">
213
+ <div className="space-y-1">
214
+ <div className="text-sm font-medium">
215
+ {t('aiAssist.previewTitle')}
216
+ </div>
217
+ <p className="text-xs text-muted-foreground">
218
+ {t('aiAssist.previewDescription')}
219
+ </p>
220
+ </div>
221
+
222
+ <ScrollArea className="max-h-72 pr-4">
223
+ <div className="space-y-4">
224
+ {suggestion.applied_fields?.length ? (
225
+ <div className="space-y-2">
226
+ <div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
227
+ {t('aiAssist.sections.fields')}
228
+ </div>
229
+ <div className="space-y-2">
230
+ {suggestion.applied_fields.map((field) => (
231
+ <div
232
+ key={field.key}
233
+ className="rounded-md border bg-background p-3"
234
+ >
235
+ <div className="text-sm font-medium">{field.label}</div>
236
+ <div className="text-xs text-muted-foreground">
237
+ {field.key}
238
+ </div>
239
+ <div className="mt-2 text-sm">
240
+ {renderSuggestionValue(field.value)}
241
+ </div>
242
+ </div>
243
+ ))}
244
+ </div>
245
+ </div>
246
+ ) : null}
247
+
248
+ {suggestion.product_attributes?.length ? (
249
+ <div className="space-y-2">
250
+ <div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
251
+ {t('aiAssist.sections.productAttributes')}
252
+ </div>
253
+ <div className="space-y-2">
254
+ {suggestion.product_attributes.map((attribute) => (
255
+ <div
256
+ key={String(attribute.attribute_id ?? attribute.attribute_slug)}
257
+ className="rounded-md border bg-background p-3"
258
+ >
259
+ <div className="flex items-center justify-between gap-2">
260
+ <div className="text-sm font-medium">
261
+ {String(
262
+ attribute.attribute_name ??
263
+ attribute.attribute_slug ??
264
+ attribute.attribute_id,
265
+ )}
266
+ </div>
267
+ {attribute.group_name ? (
268
+ <Badge variant="secondary">
269
+ {String(attribute.group_name)}
270
+ </Badge>
271
+ ) : null}
272
+ </div>
273
+ <div className="mt-2 text-sm">
274
+ {renderSuggestionValue(
275
+ attribute.value_text ??
276
+ attribute.value_number ??
277
+ attribute.value_boolean ??
278
+ attribute.attribute_option_id ??
279
+ null,
280
+ )}
281
+ </div>
282
+ </div>
283
+ ))}
284
+ </div>
285
+ </div>
286
+ ) : null}
287
+
288
+ {suggestion.warnings?.length ? (
289
+ <div className="space-y-2">
290
+ <div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
291
+ {t('aiAssist.sections.warnings')}
292
+ </div>
293
+ <div className="space-y-2">
294
+ {suggestion.warnings.map((warning, index) => (
295
+ <div
296
+ key={`${warning}-${index}`}
297
+ className="rounded-md border border-amber-300/40 bg-amber-50 p-3 text-sm text-amber-900"
298
+ >
299
+ {warning}
300
+ </div>
301
+ ))}
302
+ </div>
303
+ </div>
304
+ ) : null}
305
+ </div>
306
+ </ScrollArea>
307
+ </div>
308
+ ) : null}
309
+ </div>
310
+
311
+ <DialogFooter className="gap-2 sm:justify-between">
312
+ <Button
313
+ type="button"
314
+ variant="outline"
315
+ onClick={handleGenerate}
316
+ disabled={isGenerating || disabled}
317
+ >
318
+ {isGenerating ? (
319
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
320
+ ) : (
321
+ <Sparkles className="mr-2 h-4 w-4" />
322
+ )}
323
+ {t('aiAssist.generate')}
324
+ </Button>
325
+ <Button
326
+ type="button"
327
+ onClick={handleApply}
328
+ disabled={
329
+ !suggestion ||
330
+ (!suggestion.applied_fields?.length &&
331
+ !suggestion.product_attributes?.length)
332
+ }
333
+ >
334
+ {t('aiAssist.apply')}
335
+ </Button>
336
+ </DialogFooter>
337
+ </DialogContent>
338
+ </Dialog>
339
+ );
340
+ }