@hed-hog/catalog 0.0.297 → 0.0.299
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/hedhog/frontend/app/[resource]/page.tsx.ejs +391 -391
- package/hedhog/frontend/app/_components/catalog-ai-form-assist-dialog.tsx.ejs +340 -340
- package/hedhog/frontend/app/_components/catalog-resource-form-sheet.tsx.ejs +907 -907
- package/hedhog/frontend/app/_lib/catalog-resources.tsx.ejs +929 -929
- package/hedhog/frontend/messages/en.json +389 -389
- package/hedhog/frontend/messages/pt.json +389 -389
- package/package.json +6 -6
|
@@ -1,340 +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
|
-
}
|
|
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
|
+
}
|