@umodeapporg/ui 0.15.0 → 0.16.1
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/dist/components/Form/ControlledImage/index.d.ts +3 -0
- package/dist/components/Form/ControlledImage/index.js +2 -1
- package/dist/components/Form/ControlledImage/index.js.map +1 -1
- package/dist/components/Form/ControlledSelect.js +11 -4
- package/dist/components/Form/ControlledSelect.js.map +1 -1
- package/dist/components/UI/ImageAiAdjust/ImageAiAdjustDialog.d.ts +6 -0
- package/dist/components/UI/ImageAiAdjust/ImageAiAdjustDialog.js +164 -74
- package/dist/components/UI/ImageAiAdjust/ImageAiAdjustDialog.js.map +1 -1
- package/dist/components/UI/ImageAiAdjust/ImageAiDiscardReasonDialog.d.ts +8 -0
- package/dist/components/UI/ImageAiAdjust/ImageAiDiscardReasonDialog.js +67 -0
- package/dist/components/UI/ImageAiAdjust/ImageAiDiscardReasonDialog.js.map +1 -0
- package/dist/components/UI/ImageAiAdjust/ImageAiVerificationNotice.d.ts +8 -0
- package/dist/components/UI/ImageAiAdjust/ImageAiVerificationNotice.js +39 -0
- package/dist/components/UI/ImageAiAdjust/ImageAiVerificationNotice.js.map +1 -0
- package/dist/components/UI/ImageAiAdjust/imageAiReview.d.ts +22 -0
- package/dist/components/UI/ImageAiAdjust/imageAiReview.js +37 -0
- package/dist/components/UI/ImageAiAdjust/imageAiReview.js.map +1 -0
- package/dist/components/UI/ImageAiAdjust/imageAiVerification.d.ts +19 -0
- package/dist/components/UI/ImageAiAdjust/imageAiVerification.js +25 -0
- package/dist/components/UI/ImageAiAdjust/imageAiVerification.js.map +1 -0
- package/dist/components/UI/ImageAiAdjust/useImageAiAdjust.d.ts +25 -2
- package/dist/components/UI/ImageAiAdjust/useImageAiAdjust.js +44 -19
- package/dist/components/UI/ImageAiAdjust/useImageAiAdjust.js.map +1 -1
- package/dist/components/UI/ImageMaskPainter/index.d.ts +19 -0
- package/dist/components/UI/ImageMaskPainter/index.js +267 -0
- package/dist/components/UI/ImageMaskPainter/index.js.map +1 -0
- package/dist/components/UI/ImageMaskPainter/maskStrokes.d.ts +19 -0
- package/dist/components/UI/ImageMaskPainter/maskStrokes.js +71 -0
- package/dist/components/UI/ImageMaskPainter/maskStrokes.js.map +1 -0
- package/dist/helpers/imageFile.d.ts +16 -0
- package/dist/helpers/imageFile.js +59 -10
- package/dist/helpers/imageFile.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +12 -10
- package/dist/index.js.map +1 -1
- package/dist/locales/en/ui.json.d.ts +36 -1
- package/dist/locales/en/ui.json.js +7 -1
- package/dist/locales/en/ui.json.js.map +1 -1
- package/dist/locales/index.d.ts +70 -0
- package/dist/locales/pt/ui.json.d.ts +36 -1
- package/dist/locales/pt/ui.json.js +7 -1
- package/dist/locales/pt/ui.json.js.map +1 -1
- package/package.json +5 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
import classNames from "classnames";
|
|
4
4
|
import { useTranslation } from "react-i18next";
|
|
@@ -6,12 +6,16 @@ import ControlledButton from "../../Form/ControlledButton.js";
|
|
|
6
6
|
import ControlledDialog from "../../Form/ControlledDialog.js";
|
|
7
7
|
import ControlledImage from "../../Form/ControlledImage/index.js";
|
|
8
8
|
import ControlledTextarea from "../../Form/ControlledTextarea.js";
|
|
9
|
+
import ImageMaskPainter from "../ImageMaskPainter/index.js";
|
|
9
10
|
import LoadingSpinner from "../LoadingSpinner.js";
|
|
10
11
|
import { MaterialSymbol } from "../MaterialSymbol/index.js";
|
|
11
12
|
import { uiLogger } from "../../../helpers/uiLogger.js";
|
|
12
13
|
import { useBreakpoint } from "../../../hooks/useBreakpoint.js";
|
|
13
14
|
import { useNotification } from "../../../hooks/useNotification.js";
|
|
14
|
-
import
|
|
15
|
+
import ImageAiDiscardReasonDialog from "./ImageAiDiscardReasonDialog.js";
|
|
16
|
+
import { submitImageAiReview } from "./imageAiReview.js";
|
|
17
|
+
import ImageAiVerificationNotice from "./ImageAiVerificationNotice.js";
|
|
18
|
+
import { useImageAiAdjust, IMAGE_AI_ADJUST_MAX_INSTRUCTIONS } from "./useImageAiAdjust.js";
|
|
15
19
|
const ImageAiAdjustDialog = ({
|
|
16
20
|
open,
|
|
17
21
|
imageUrl,
|
|
@@ -19,28 +23,42 @@ const ImageAiAdjustDialog = ({
|
|
|
19
23
|
productId,
|
|
20
24
|
adjustImage,
|
|
21
25
|
onClose,
|
|
22
|
-
onSave
|
|
26
|
+
onSave,
|
|
27
|
+
onReviewResult
|
|
23
28
|
}) => {
|
|
24
29
|
const { t } = useTranslation("ui");
|
|
25
30
|
const { isTabletDown } = useBreakpoint();
|
|
26
31
|
const { notify } = useNotification();
|
|
27
32
|
const [saving, setSaving] = useState(false);
|
|
33
|
+
const [pendingDiscard, setPendingDiscard] = useState(null);
|
|
34
|
+
const [markingArea, setMarkingArea] = useState(false);
|
|
35
|
+
const [mask, setMask] = useState(null);
|
|
28
36
|
const {
|
|
29
37
|
instructions,
|
|
30
38
|
setInstructions,
|
|
31
39
|
resultUrl,
|
|
40
|
+
resultGenerationId,
|
|
41
|
+
resultVerification,
|
|
32
42
|
isGenerating,
|
|
33
43
|
canGenerate,
|
|
34
44
|
generate,
|
|
35
45
|
cancel,
|
|
36
46
|
discardResult,
|
|
37
47
|
buildResultFile
|
|
38
|
-
} = useImageAiAdjust({ imageUrl, imageName, productId, adjustImage });
|
|
48
|
+
} = useImageAiAdjust({ imageUrl, imageName, productId, adjustImage, mask: markingArea ? mask : null });
|
|
49
|
+
const toggleMarkingArea = () => {
|
|
50
|
+
if (markingArea) setMask(null);
|
|
51
|
+
setMarkingArea(!markingArea);
|
|
52
|
+
};
|
|
53
|
+
const reviewEnabled = Boolean(onReviewResult && resultGenerationId);
|
|
39
54
|
const handleSave = async () => {
|
|
40
55
|
if (!resultUrl || saving) return;
|
|
41
56
|
setSaving(true);
|
|
42
57
|
try {
|
|
43
58
|
await onSave(await buildResultFile());
|
|
59
|
+
if (reviewEnabled && resultGenerationId) {
|
|
60
|
+
void submitImageAiReview(onReviewResult, { generationId: resultGenerationId, decision: "approved" });
|
|
61
|
+
}
|
|
44
62
|
onClose();
|
|
45
63
|
} catch (error) {
|
|
46
64
|
uiLogger.error("Failed to save the AI adjusted image", error);
|
|
@@ -49,8 +67,39 @@ const ImageAiAdjustDialog = ({
|
|
|
49
67
|
setSaving(false);
|
|
50
68
|
}
|
|
51
69
|
};
|
|
70
|
+
const handleTryAgain = () => {
|
|
71
|
+
if (reviewEnabled) {
|
|
72
|
+
setPendingDiscard("tryAgain");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
discardResult();
|
|
76
|
+
};
|
|
77
|
+
const handleGenerate = () => {
|
|
78
|
+
if (reviewEnabled && resultUrl) {
|
|
79
|
+
setPendingDiscard("regenerate");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
void generate();
|
|
83
|
+
};
|
|
84
|
+
const handleDiscardConfirmed = ({ reasonCode, reason }) => {
|
|
85
|
+
const action = pendingDiscard;
|
|
86
|
+
setPendingDiscard(null);
|
|
87
|
+
if (resultGenerationId) {
|
|
88
|
+
void submitImageAiReview(onReviewResult, {
|
|
89
|
+
generationId: resultGenerationId,
|
|
90
|
+
decision: "discarded",
|
|
91
|
+
reasonCode,
|
|
92
|
+
reason
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (action === "regenerate") {
|
|
96
|
+
void generate();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
discardResult();
|
|
100
|
+
};
|
|
52
101
|
const previewClass = "max-h-[38vh] w-auto max-w-full rounded-lg object-contain";
|
|
53
|
-
return /* @__PURE__ */
|
|
102
|
+
return /* @__PURE__ */ jsxs(
|
|
54
103
|
ControlledDialog,
|
|
55
104
|
{
|
|
56
105
|
open,
|
|
@@ -60,81 +109,122 @@ const ImageAiAdjustDialog = ({
|
|
|
60
109
|
onCancel: onClose,
|
|
61
110
|
useFooter: false,
|
|
62
111
|
fitBody: true,
|
|
63
|
-
children:
|
|
64
|
-
/* @__PURE__ */ jsxs("div", { className:
|
|
65
|
-
/* @__PURE__ */ jsxs("div", { className: "flex
|
|
66
|
-
/* @__PURE__ */
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
112
|
+
children: [
|
|
113
|
+
/* @__PURE__ */ jsxs("div", { className: "flex max-h-[76vh] flex-col gap-y-3 overflow-y-auto", children: [
|
|
114
|
+
/* @__PURE__ */ jsxs("div", { className: classNames("flex gap-3", isTabletDown ? "flex-col" : "flex-row"), children: [
|
|
115
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-y-1.5", children: [
|
|
116
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2", children: [
|
|
117
|
+
/* @__PURE__ */ jsx("span", { className: "text-xxs font-semibold uppercase tracking-wide text-foreground-subtle", children: t("imageAiAdjust.original") }),
|
|
118
|
+
/* @__PURE__ */ jsxs(
|
|
119
|
+
ControlledButton,
|
|
120
|
+
{
|
|
121
|
+
size: "sm",
|
|
122
|
+
variant: "text",
|
|
123
|
+
tone: markingArea ? "brand" : "neutral",
|
|
124
|
+
"aria-pressed": markingArea,
|
|
125
|
+
disabled: isGenerating || saving,
|
|
126
|
+
onClick: toggleMarkingArea,
|
|
127
|
+
className: "flex items-center gap-x-1.5 px-2 py-1 normal-case",
|
|
128
|
+
children: [
|
|
129
|
+
/* @__PURE__ */ jsx(MaterialSymbol, { "aria-hidden": true, icon: markingArea ? "close" : "brush", size: 14 }),
|
|
130
|
+
markingArea ? t("imageAiAdjust.mask.stop") : t("imageAiAdjust.mask.start")
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
)
|
|
75
134
|
] }),
|
|
76
|
-
|
|
77
|
-
|
|
135
|
+
markingArea ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
136
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-foreground-muted", children: t("imageAiAdjust.mask.hint") }),
|
|
137
|
+
/* @__PURE__ */ jsx(
|
|
138
|
+
ImageMaskPainter,
|
|
139
|
+
{
|
|
140
|
+
imageUrl,
|
|
141
|
+
disabled: isGenerating || saving,
|
|
142
|
+
onMaskChange: setMask,
|
|
143
|
+
imageClassName: previewClass
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
] }) : /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center rounded-xl bg-ink/95 p-2", children: /* @__PURE__ */ jsx(ControlledImage, { src: imageUrl, className: previewClass }) })
|
|
147
|
+
] }),
|
|
148
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-y-1.5", children: [
|
|
149
|
+
/* @__PURE__ */ jsx("span", { className: "text-xxs font-semibold uppercase tracking-wide text-foreground-subtle", children: t("imageAiAdjust.result") }),
|
|
150
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-h-40 items-center justify-center rounded-xl bg-ink/95 p-2", children: [
|
|
151
|
+
isGenerating && /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-y-2 p-4", children: [
|
|
152
|
+
/* @__PURE__ */ jsx(LoadingSpinner, { size: 8, color: "white" }),
|
|
153
|
+
/* @__PURE__ */ jsx("span", { className: "text-center text-xs text-white", children: t("imageAiAdjust.generating") })
|
|
154
|
+
] }),
|
|
155
|
+
!isGenerating && resultUrl && /* @__PURE__ */ jsx(ControlledImage, { src: resultUrl, className: previewClass }),
|
|
156
|
+
!isGenerating && !resultUrl && /* @__PURE__ */ jsx("span", { className: "max-w-64 p-4 text-center text-xs text-white/70", children: t("imageAiAdjust.resultHint") })
|
|
157
|
+
] }),
|
|
158
|
+
!isGenerating && resultUrl && /* @__PURE__ */ jsx(ImageAiVerificationNotice, { verification: resultVerification })
|
|
78
159
|
] })
|
|
79
|
-
] })
|
|
160
|
+
] }),
|
|
161
|
+
/* @__PURE__ */ jsx(
|
|
162
|
+
ControlledTextarea,
|
|
163
|
+
{
|
|
164
|
+
resize: "none",
|
|
165
|
+
value: instructions,
|
|
166
|
+
rows: 3,
|
|
167
|
+
maxLength: IMAGE_AI_ADJUST_MAX_INSTRUCTIONS,
|
|
168
|
+
disabled: isGenerating || saving,
|
|
169
|
+
"aria-label": t("imageAiAdjust.promptLabel"),
|
|
170
|
+
placeholder: t("imageAiAdjust.promptPlaceholder"),
|
|
171
|
+
onChange: (event) => setInstructions(event.target.value),
|
|
172
|
+
className: "rounded-xl"
|
|
173
|
+
}
|
|
174
|
+
),
|
|
175
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center justify-end gap-2", children: isGenerating ? /* @__PURE__ */ jsx(ControlledButton, { size: "sm", variant: "outlined", onClick: cancel, className: "normal-case", children: t("actions.cancel") }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
176
|
+
resultUrl && /* @__PURE__ */ jsxs(
|
|
177
|
+
ControlledButton,
|
|
178
|
+
{
|
|
179
|
+
size: "sm",
|
|
180
|
+
variant: "outlined",
|
|
181
|
+
disabled: saving,
|
|
182
|
+
onClick: handleTryAgain,
|
|
183
|
+
className: "flex items-center gap-x-1.5 normal-case",
|
|
184
|
+
children: [
|
|
185
|
+
/* @__PURE__ */ jsx(MaterialSymbol, { icon: "refresh", size: 14 }),
|
|
186
|
+
t("imageAiAdjust.tryAgain")
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
),
|
|
190
|
+
/* @__PURE__ */ jsxs(
|
|
191
|
+
ControlledButton,
|
|
192
|
+
{
|
|
193
|
+
size: "sm",
|
|
194
|
+
variant: resultUrl ? "outlined" : "filled",
|
|
195
|
+
disabled: !canGenerate || saving,
|
|
196
|
+
onClick: handleGenerate,
|
|
197
|
+
className: "flex items-center gap-x-1.5 normal-case",
|
|
198
|
+
children: [
|
|
199
|
+
/* @__PURE__ */ jsx(MaterialSymbol, { icon: "auto_fix_high", size: 14 }),
|
|
200
|
+
t("imageAiAdjust.generate")
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
),
|
|
204
|
+
resultUrl && /* @__PURE__ */ jsxs(
|
|
205
|
+
ControlledButton,
|
|
206
|
+
{
|
|
207
|
+
size: "sm",
|
|
208
|
+
disabled: saving,
|
|
209
|
+
onClick: handleSave,
|
|
210
|
+
className: "flex items-center gap-x-1.5 normal-case",
|
|
211
|
+
children: [
|
|
212
|
+
saving ? /* @__PURE__ */ jsx(LoadingSpinner, { size: 4, color: "white" }) : /* @__PURE__ */ jsx(MaterialSymbol, { icon: "check", size: 14, color: "white" }),
|
|
213
|
+
t("imageAiAdjust.useResult")
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
] }) })
|
|
80
218
|
] }),
|
|
81
219
|
/* @__PURE__ */ jsx(
|
|
82
|
-
|
|
220
|
+
ImageAiDiscardReasonDialog,
|
|
83
221
|
{
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
disabled: isGenerating || saving,
|
|
88
|
-
"aria-label": t("imageAiAdjust.promptLabel"),
|
|
89
|
-
placeholder: t("imageAiAdjust.promptPlaceholder"),
|
|
90
|
-
onChange: (event) => setInstructions(event.target.value),
|
|
91
|
-
className: "rounded-xl"
|
|
222
|
+
open: pendingDiscard !== null,
|
|
223
|
+
onCancel: () => setPendingDiscard(null),
|
|
224
|
+
onConfirm: handleDiscardConfirmed
|
|
92
225
|
}
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
resultUrl && /* @__PURE__ */ jsxs(
|
|
96
|
-
ControlledButton,
|
|
97
|
-
{
|
|
98
|
-
size: "sm",
|
|
99
|
-
variant: "outlined",
|
|
100
|
-
disabled: saving,
|
|
101
|
-
onClick: discardResult,
|
|
102
|
-
className: "flex items-center gap-x-1.5 normal-case",
|
|
103
|
-
children: [
|
|
104
|
-
/* @__PURE__ */ jsx(MaterialSymbol, { icon: "refresh", size: 14 }),
|
|
105
|
-
t("imageAiAdjust.tryAgain")
|
|
106
|
-
]
|
|
107
|
-
}
|
|
108
|
-
),
|
|
109
|
-
/* @__PURE__ */ jsxs(
|
|
110
|
-
ControlledButton,
|
|
111
|
-
{
|
|
112
|
-
size: "sm",
|
|
113
|
-
variant: resultUrl ? "outlined" : "filled",
|
|
114
|
-
disabled: !canGenerate || saving,
|
|
115
|
-
onClick: generate,
|
|
116
|
-
className: "flex items-center gap-x-1.5 normal-case",
|
|
117
|
-
children: [
|
|
118
|
-
/* @__PURE__ */ jsx(MaterialSymbol, { icon: "auto_fix_high", size: 14 }),
|
|
119
|
-
t("imageAiAdjust.generate")
|
|
120
|
-
]
|
|
121
|
-
}
|
|
122
|
-
),
|
|
123
|
-
resultUrl && /* @__PURE__ */ jsxs(
|
|
124
|
-
ControlledButton,
|
|
125
|
-
{
|
|
126
|
-
size: "sm",
|
|
127
|
-
disabled: saving,
|
|
128
|
-
onClick: handleSave,
|
|
129
|
-
className: "flex items-center gap-x-1.5 normal-case",
|
|
130
|
-
children: [
|
|
131
|
-
saving ? /* @__PURE__ */ jsx(LoadingSpinner, { size: 4, color: "white" }) : /* @__PURE__ */ jsx(MaterialSymbol, { icon: "check", size: 14, color: "white" }),
|
|
132
|
-
t("imageAiAdjust.useResult")
|
|
133
|
-
]
|
|
134
|
-
}
|
|
135
|
-
)
|
|
136
|
-
] }) })
|
|
137
|
-
] })
|
|
226
|
+
)
|
|
227
|
+
]
|
|
138
228
|
}
|
|
139
229
|
);
|
|
140
230
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageAiAdjustDialog.js","sources":["../../../../src/components/UI/ImageAiAdjust/ImageAiAdjustDialog.tsx"],"sourcesContent":["import { useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledDialog from '@/components/Form/ControlledDialog';\nimport ControlledImage from '@/components/Form/ControlledImage';\nimport ControlledTextarea from '@/components/Form/ControlledTextarea';\nimport LoadingSpinner from '@/components/UI/LoadingSpinner';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport { uiLogger } from '@/helpers/uiLogger';\nimport { useBreakpoint } from '@/hooks/useBreakpoint';\nimport { useNotification } from '@/hooks/useNotification';\n\nimport { TImageAiAdjuster, useImageAiAdjust } from './useImageAiAdjust';\n\ninterface IImageAiAdjustDialogProps {\n open: boolean;\n imageUrl: string;\n imageName?: string;\n /** Opcional: quando existe, a geração fica associada ao produto no criAI. */\n productId?: string;\n /** Runs the generation; the host decides which AI backend serves it. */\n adjustImage: TImageAiAdjuster;\n onClose: () => void;\n /** Recebe a imagem ajustada; resolver fecha o diálogo, lançar mantém aberto. */\n onSave: (file: File) => Promise<void> | void;\n}\n\nconst ImageAiAdjustDialog: React.FC<IImageAiAdjustDialogProps> = ({\n open,\n imageUrl,\n imageName,\n productId,\n adjustImage,\n onClose,\n onSave,\n}) => {\n const { t } = useTranslation('ui');\n const { isTabletDown } = useBreakpoint();\n const { notify } = useNotification();\n\n const [saving, setSaving] = useState(false);\n const {\n instructions,\n setInstructions,\n resultUrl,\n isGenerating,\n canGenerate,\n generate,\n cancel,\n discardResult,\n buildResultFile,\n } = useImageAiAdjust({ imageUrl, imageName, productId, adjustImage });\n\n const handleSave = async () => {\n if (!resultUrl || saving) return;\n\n setSaving(true);\n try {\n await onSave(await buildResultFile());\n onClose();\n } catch (error) {\n uiLogger.error('Failed to save the AI adjusted image', error);\n notify({ message: t('imageAiAdjust.errorSave'), type: 'error' });\n } finally {\n setSaving(false);\n }\n };\n\n const previewClass = 'max-h-[38vh] w-auto max-w-full rounded-lg object-contain';\n\n return (\n <ControlledDialog\n open={open}\n size={isTabletDown ? 'xxl' : 'lg'}\n title={t('imageAiAdjust.title')}\n subtitle={t('imageAiAdjust.subtitle')}\n onCancel={onClose}\n useFooter={false}\n fitBody\n >\n <div className=\"flex max-h-[76vh] flex-col gap-y-3 overflow-y-auto\">\n <div className={classNames('flex gap-3', isTabletDown ? 'flex-col' : 'flex-row')}>\n <div className=\"flex min-w-0 flex-1 flex-col gap-y-1.5\">\n <span className=\"text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {t('imageAiAdjust.original')}\n </span>\n <div className=\"flex items-center justify-center rounded-xl bg-ink/95 p-2\">\n <ControlledImage src={imageUrl} className={previewClass} />\n </div>\n </div>\n\n <div className=\"flex min-w-0 flex-1 flex-col gap-y-1.5\">\n <span className=\"text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {t('imageAiAdjust.result')}\n </span>\n <div className=\"flex min-h-40 items-center justify-center rounded-xl bg-ink/95 p-2\">\n {isGenerating && (\n <div className=\"flex flex-col items-center gap-y-2 p-4\">\n <LoadingSpinner size={8} color=\"white\" />\n <span className=\"text-center text-xs text-white\">{t('imageAiAdjust.generating')}</span>\n </div>\n )}\n\n {!isGenerating && resultUrl && <ControlledImage src={resultUrl} className={previewClass} />}\n\n {!isGenerating && !resultUrl && (\n <span className=\"max-w-64 p-4 text-center text-xs text-white/70\">{t('imageAiAdjust.resultHint')}</span>\n )}\n </div>\n </div>\n </div>\n\n <ControlledTextarea\n resize=\"none\"\n value={instructions}\n rows={3}\n disabled={isGenerating || saving}\n aria-label={t('imageAiAdjust.promptLabel')}\n placeholder={t('imageAiAdjust.promptPlaceholder')}\n onChange={(event) => setInstructions(event.target.value)}\n className=\"rounded-xl\"\n />\n\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n {isGenerating ? (\n <ControlledButton size=\"sm\" variant=\"outlined\" onClick={cancel} className=\"normal-case\">\n {t('actions.cancel')}\n </ControlledButton>\n ) : (\n <>\n {resultUrl && (\n <ControlledButton\n size=\"sm\"\n variant=\"outlined\"\n disabled={saving}\n onClick={discardResult}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n <MaterialSymbol icon=\"refresh\" size={14} />\n {t('imageAiAdjust.tryAgain')}\n </ControlledButton>\n )}\n\n <ControlledButton\n size=\"sm\"\n variant={resultUrl ? 'outlined' : 'filled'}\n disabled={!canGenerate || saving}\n onClick={generate}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n <MaterialSymbol icon=\"auto_fix_high\" size={14} />\n {t('imageAiAdjust.generate')}\n </ControlledButton>\n\n {resultUrl && (\n <ControlledButton\n size=\"sm\"\n disabled={saving}\n onClick={handleSave}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n {saving ? (\n <LoadingSpinner size={4} color=\"white\" />\n ) : (\n <MaterialSymbol icon=\"check\" size={14} color=\"white\" />\n )}\n {t('imageAiAdjust.useResult')}\n </ControlledButton>\n )}\n </>\n )}\n </div>\n </div>\n </ControlledDialog>\n );\n};\n\nexport default ImageAiAdjustDialog;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ImageAiAdjustDialog.js","sources":["../../../../src/components/UI/ImageAiAdjust/ImageAiAdjustDialog.tsx"],"sourcesContent":["import { useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledDialog from '@/components/Form/ControlledDialog';\nimport ControlledImage from '@/components/Form/ControlledImage';\nimport ControlledTextarea from '@/components/Form/ControlledTextarea';\nimport ImageMaskPainter from '@/components/UI/ImageMaskPainter';\nimport LoadingSpinner from '@/components/UI/LoadingSpinner';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport { uiLogger } from '@/helpers/uiLogger';\nimport { useBreakpoint } from '@/hooks/useBreakpoint';\nimport { useNotification } from '@/hooks/useNotification';\n\nimport ImageAiDiscardReasonDialog from './ImageAiDiscardReasonDialog';\nimport { IImageAiDiscardReason, submitImageAiReview, TImageAiReviewHandler } from './imageAiReview';\nimport ImageAiVerificationNotice from './ImageAiVerificationNotice';\nimport { IMAGE_AI_ADJUST_MAX_INSTRUCTIONS, TImageAiAdjuster, useImageAiAdjust } from './useImageAiAdjust';\n\ntype TPendingDiscard = 'tryAgain' | 'regenerate';\n\ninterface IImageAiAdjustDialogProps {\n open: boolean;\n imageUrl: string;\n imageName?: string;\n /** Opcional: quando existe, a geração fica associada ao produto no criAI. */\n productId?: string;\n /** Runs the generation; the host decides which AI backend serves it. */\n adjustImage: TImageAiAdjuster;\n onClose: () => void;\n /** Recebe a imagem ajustada; resolver fecha o diálogo, lançar mantém aberto. */\n onSave: (file: File) => Promise<void> | void;\n /**\n * Optional feedback sink. Only used when the adjuster resolved a `generationId`: using the\n * result approves it, discarding asks for a reason first. Failures never block the flow.\n */\n onReviewResult?: TImageAiReviewHandler;\n}\n\nconst ImageAiAdjustDialog: React.FC<IImageAiAdjustDialogProps> = ({\n open,\n imageUrl,\n imageName,\n productId,\n adjustImage,\n onClose,\n onSave,\n onReviewResult,\n}) => {\n const { t } = useTranslation('ui');\n const { isTabletDown } = useBreakpoint();\n const { notify } = useNotification();\n\n const [saving, setSaving] = useState(false);\n const [pendingDiscard, setPendingDiscard] = useState<TPendingDiscard | null>(null);\n const [markingArea, setMarkingArea] = useState(false);\n const [mask, setMask] = useState<File | null>(null);\n const {\n instructions,\n setInstructions,\n resultUrl,\n resultGenerationId,\n resultVerification,\n isGenerating,\n canGenerate,\n generate,\n cancel,\n discardResult,\n buildResultFile,\n } = useImageAiAdjust({ imageUrl, imageName, productId, adjustImage, mask: markingArea ? mask : null });\n\n const toggleMarkingArea = () => {\n if (markingArea) setMask(null);\n setMarkingArea(!markingArea);\n };\n\n const reviewEnabled = Boolean(onReviewResult && resultGenerationId);\n\n const handleSave = async () => {\n if (!resultUrl || saving) return;\n\n setSaving(true);\n try {\n await onSave(await buildResultFile());\n if (reviewEnabled && resultGenerationId) {\n void submitImageAiReview(onReviewResult, { generationId: resultGenerationId, decision: 'approved' });\n }\n onClose();\n } catch (error) {\n uiLogger.error('Failed to save the AI adjusted image', error);\n notify({ message: t('imageAiAdjust.errorSave'), type: 'error' });\n } finally {\n setSaving(false);\n }\n };\n\n const handleTryAgain = () => {\n if (reviewEnabled) {\n setPendingDiscard('tryAgain');\n return;\n }\n discardResult();\n };\n\n const handleGenerate = () => {\n if (reviewEnabled && resultUrl) {\n setPendingDiscard('regenerate');\n return;\n }\n void generate();\n };\n\n const handleDiscardConfirmed = ({ reasonCode, reason }: IImageAiDiscardReason) => {\n const action = pendingDiscard;\n setPendingDiscard(null);\n if (resultGenerationId) {\n void submitImageAiReview(onReviewResult, {\n generationId: resultGenerationId,\n decision: 'discarded',\n reasonCode,\n reason,\n });\n }\n if (action === 'regenerate') {\n void generate();\n return;\n }\n discardResult();\n };\n\n const previewClass = 'max-h-[38vh] w-auto max-w-full rounded-lg object-contain';\n\n return (\n <ControlledDialog\n open={open}\n size={isTabletDown ? 'xxl' : 'lg'}\n title={t('imageAiAdjust.title')}\n subtitle={t('imageAiAdjust.subtitle')}\n onCancel={onClose}\n useFooter={false}\n fitBody\n >\n <div className=\"flex max-h-[76vh] flex-col gap-y-3 overflow-y-auto\">\n <div className={classNames('flex gap-3', isTabletDown ? 'flex-col' : 'flex-row')}>\n <div className=\"flex min-w-0 flex-1 flex-col gap-y-1.5\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <span className=\"text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {t('imageAiAdjust.original')}\n </span>\n <ControlledButton\n size=\"sm\"\n variant=\"text\"\n tone={markingArea ? 'brand' : 'neutral'}\n aria-pressed={markingArea}\n disabled={isGenerating || saving}\n onClick={toggleMarkingArea}\n className=\"flex items-center gap-x-1.5 px-2 py-1 normal-case\"\n >\n <MaterialSymbol aria-hidden icon={markingArea ? 'close' : 'brush'} size={14} />\n {markingArea ? t('imageAiAdjust.mask.stop') : t('imageAiAdjust.mask.start')}\n </ControlledButton>\n </div>\n {markingArea ? (\n <>\n <span className=\"text-xs text-foreground-muted\">{t('imageAiAdjust.mask.hint')}</span>\n <ImageMaskPainter\n imageUrl={imageUrl}\n disabled={isGenerating || saving}\n onMaskChange={setMask}\n imageClassName={previewClass}\n />\n </>\n ) : (\n <div className=\"flex items-center justify-center rounded-xl bg-ink/95 p-2\">\n <ControlledImage src={imageUrl} className={previewClass} />\n </div>\n )}\n </div>\n\n <div className=\"flex min-w-0 flex-1 flex-col gap-y-1.5\">\n <span className=\"text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {t('imageAiAdjust.result')}\n </span>\n <div className=\"flex min-h-40 items-center justify-center rounded-xl bg-ink/95 p-2\">\n {isGenerating && (\n <div className=\"flex flex-col items-center gap-y-2 p-4\">\n <LoadingSpinner size={8} color=\"white\" />\n <span className=\"text-center text-xs text-white\">{t('imageAiAdjust.generating')}</span>\n </div>\n )}\n\n {!isGenerating && resultUrl && <ControlledImage src={resultUrl} className={previewClass} />}\n\n {!isGenerating && !resultUrl && (\n <span className=\"max-w-64 p-4 text-center text-xs text-white/70\">{t('imageAiAdjust.resultHint')}</span>\n )}\n </div>\n {!isGenerating && resultUrl && <ImageAiVerificationNotice verification={resultVerification} />}\n </div>\n </div>\n\n <ControlledTextarea\n resize=\"none\"\n value={instructions}\n rows={3}\n maxLength={IMAGE_AI_ADJUST_MAX_INSTRUCTIONS}\n disabled={isGenerating || saving}\n aria-label={t('imageAiAdjust.promptLabel')}\n placeholder={t('imageAiAdjust.promptPlaceholder')}\n onChange={(event) => setInstructions(event.target.value)}\n className=\"rounded-xl\"\n />\n\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n {isGenerating ? (\n <ControlledButton size=\"sm\" variant=\"outlined\" onClick={cancel} className=\"normal-case\">\n {t('actions.cancel')}\n </ControlledButton>\n ) : (\n <>\n {resultUrl && (\n <ControlledButton\n size=\"sm\"\n variant=\"outlined\"\n disabled={saving}\n onClick={handleTryAgain}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n <MaterialSymbol icon=\"refresh\" size={14} />\n {t('imageAiAdjust.tryAgain')}\n </ControlledButton>\n )}\n\n <ControlledButton\n size=\"sm\"\n variant={resultUrl ? 'outlined' : 'filled'}\n disabled={!canGenerate || saving}\n onClick={handleGenerate}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n <MaterialSymbol icon=\"auto_fix_high\" size={14} />\n {t('imageAiAdjust.generate')}\n </ControlledButton>\n\n {resultUrl && (\n <ControlledButton\n size=\"sm\"\n disabled={saving}\n onClick={handleSave}\n className=\"flex items-center gap-x-1.5 normal-case\"\n >\n {saving ? (\n <LoadingSpinner size={4} color=\"white\" />\n ) : (\n <MaterialSymbol icon=\"check\" size={14} color=\"white\" />\n )}\n {t('imageAiAdjust.useResult')}\n </ControlledButton>\n )}\n </>\n )}\n </div>\n </div>\n\n <ImageAiDiscardReasonDialog\n open={pendingDiscard !== null}\n onCancel={() => setPendingDiscard(null)}\n onConfirm={handleDiscardConfirmed}\n />\n </ControlledDialog>\n );\n};\n\nexport default ImageAiAdjustDialog;\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { IImageAiDiscardReason } from './imageAiReview';
|
|
2
|
+
interface IImageAiDiscardReasonDialogProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onCancel: () => void;
|
|
5
|
+
onConfirm: (reason: IImageAiDiscardReason) => void;
|
|
6
|
+
}
|
|
7
|
+
declare const ImageAiDiscardReasonDialog: React.FC<IImageAiDiscardReasonDialogProps>;
|
|
8
|
+
export default ImageAiDiscardReasonDialog;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from "react";
|
|
3
|
+
import { useTranslation } from "react-i18next";
|
|
4
|
+
import ControlledDialog from "../../Form/ControlledDialog.js";
|
|
5
|
+
import ControlledRadio from "../../Form/ControlledRadio.js";
|
|
6
|
+
import ControlledTextarea from "../../Form/ControlledTextarea.js";
|
|
7
|
+
import { IMAGE_AI_REVIEW_REASON_CODES, IMAGE_AI_REVIEW_REASON_LABEL_KEYS, IMAGE_AI_REVIEW_REASON_MAX } from "./imageAiReview.js";
|
|
8
|
+
const ImageAiDiscardReasonDialog = ({ open, onCancel, onConfirm }) => {
|
|
9
|
+
const { t } = useTranslation("ui");
|
|
10
|
+
const [reasonCode, setReasonCode] = useState(null);
|
|
11
|
+
const [reason, setReason] = useState("");
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!open) return;
|
|
14
|
+
setReasonCode(null);
|
|
15
|
+
setReason("");
|
|
16
|
+
}, [open]);
|
|
17
|
+
const handleConfirm = () => {
|
|
18
|
+
if (!reasonCode) return;
|
|
19
|
+
const trimmed = reason.trim().slice(0, IMAGE_AI_REVIEW_REASON_MAX);
|
|
20
|
+
onConfirm({ reasonCode, reason: trimmed || void 0 });
|
|
21
|
+
};
|
|
22
|
+
return /* @__PURE__ */ jsx(
|
|
23
|
+
ControlledDialog,
|
|
24
|
+
{
|
|
25
|
+
open,
|
|
26
|
+
size: "sm",
|
|
27
|
+
title: t("imageAiReview.discardTitle"),
|
|
28
|
+
subtitle: t("imageAiReview.discardSubtitle"),
|
|
29
|
+
onCancel,
|
|
30
|
+
onConfirm: handleConfirm,
|
|
31
|
+
cancelLabel: t("actions.cancel"),
|
|
32
|
+
confirmLabel: t("imageAiReview.discardConfirm"),
|
|
33
|
+
disableConfirm: !reasonCode,
|
|
34
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-3", children: [
|
|
35
|
+
/* @__PURE__ */ jsx("div", { role: "radiogroup", "aria-label": t("imageAiReview.reasonLabel"), className: "flex flex-col gap-y-1.5", children: IMAGE_AI_REVIEW_REASON_CODES.map((code) => /* @__PURE__ */ jsx(
|
|
36
|
+
ControlledRadio,
|
|
37
|
+
{
|
|
38
|
+
id: `image-ai-discard-reason-${code}`,
|
|
39
|
+
name: "image-ai-discard-reason",
|
|
40
|
+
size: "sm",
|
|
41
|
+
checked: reasonCode === code,
|
|
42
|
+
onChange: () => setReasonCode(code),
|
|
43
|
+
label: t(IMAGE_AI_REVIEW_REASON_LABEL_KEYS[code])
|
|
44
|
+
},
|
|
45
|
+
code
|
|
46
|
+
)) }),
|
|
47
|
+
/* @__PURE__ */ jsx(
|
|
48
|
+
ControlledTextarea,
|
|
49
|
+
{
|
|
50
|
+
resize: "none",
|
|
51
|
+
rows: 3,
|
|
52
|
+
value: reason,
|
|
53
|
+
maxLength: IMAGE_AI_REVIEW_REASON_MAX,
|
|
54
|
+
"aria-label": t("imageAiReview.detailsLabel"),
|
|
55
|
+
placeholder: t("imageAiReview.detailsPlaceholder"),
|
|
56
|
+
onChange: (event) => setReason(event.target.value),
|
|
57
|
+
className: "rounded-xl"
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
] })
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
export {
|
|
65
|
+
ImageAiDiscardReasonDialog as default
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=ImageAiDiscardReasonDialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageAiDiscardReasonDialog.js","sources":["../../../../src/components/UI/ImageAiAdjust/ImageAiDiscardReasonDialog.tsx"],"sourcesContent":["import { useEffect, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledDialog from '@/components/Form/ControlledDialog';\nimport ControlledRadio from '@/components/Form/ControlledRadio';\nimport ControlledTextarea from '@/components/Form/ControlledTextarea';\n\nimport {\n IImageAiDiscardReason,\n IMAGE_AI_REVIEW_REASON_CODES,\n IMAGE_AI_REVIEW_REASON_LABEL_KEYS,\n IMAGE_AI_REVIEW_REASON_MAX,\n TImageAiReviewReasonCode,\n} from './imageAiReview';\n\ninterface IImageAiDiscardReasonDialogProps {\n open: boolean;\n onCancel: () => void;\n onConfirm: (reason: IImageAiDiscardReason) => void;\n}\n\nconst ImageAiDiscardReasonDialog: React.FC<IImageAiDiscardReasonDialogProps> = ({ open, onCancel, onConfirm }) => {\n const { t } = useTranslation('ui');\n const [reasonCode, setReasonCode] = useState<TImageAiReviewReasonCode | null>(null);\n const [reason, setReason] = useState('');\n\n useEffect(() => {\n if (!open) return;\n setReasonCode(null);\n setReason('');\n }, [open]);\n\n const handleConfirm = () => {\n if (!reasonCode) return;\n const trimmed = reason.trim().slice(0, IMAGE_AI_REVIEW_REASON_MAX);\n onConfirm({ reasonCode, reason: trimmed || undefined });\n };\n\n return (\n <ControlledDialog\n open={open}\n size=\"sm\"\n title={t('imageAiReview.discardTitle')}\n subtitle={t('imageAiReview.discardSubtitle')}\n onCancel={onCancel}\n onConfirm={handleConfirm}\n cancelLabel={t('actions.cancel')}\n confirmLabel={t('imageAiReview.discardConfirm')}\n disableConfirm={!reasonCode}\n >\n <div className=\"flex flex-col gap-y-3\">\n <div role=\"radiogroup\" aria-label={t('imageAiReview.reasonLabel')} className=\"flex flex-col gap-y-1.5\">\n {IMAGE_AI_REVIEW_REASON_CODES.map((code) => (\n <ControlledRadio\n key={code}\n id={`image-ai-discard-reason-${code}`}\n name=\"image-ai-discard-reason\"\n size=\"sm\"\n checked={reasonCode === code}\n onChange={() => setReasonCode(code)}\n label={t(IMAGE_AI_REVIEW_REASON_LABEL_KEYS[code])}\n />\n ))}\n </div>\n\n <ControlledTextarea\n resize=\"none\"\n rows={3}\n value={reason}\n maxLength={IMAGE_AI_REVIEW_REASON_MAX}\n aria-label={t('imageAiReview.detailsLabel')}\n placeholder={t('imageAiReview.detailsPlaceholder')}\n onChange={(event) => setReason(event.target.value)}\n className=\"rounded-xl\"\n />\n </div>\n </ControlledDialog>\n );\n};\n\nexport default ImageAiDiscardReasonDialog;\n"],"names":[],"mappings":";;;;;;;AAqBA,MAAM,6BAAyE,CAAC,EAAE,MAAM,UAAU,gBAAgB;AAChH,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AACjC,QAAM,CAAC,YAAY,aAAa,IAAI,SAA0C,IAAI;AAClF,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,EAAE;AAEvC,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AACX,kBAAc,IAAI;AAClB,cAAU,EAAE;AAAA,EACd,GAAG,CAAC,IAAI,CAAC;AAET,QAAM,gBAAgB,MAAM;AAC1B,QAAI,CAAC,WAAY;AACjB,UAAM,UAAU,OAAO,KAAA,EAAO,MAAM,GAAG,0BAA0B;AACjE,cAAU,EAAE,YAAY,QAAQ,WAAW,QAAW;AAAA,EACxD;AAEA,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,MAAK;AAAA,MACL,OAAO,EAAE,4BAA4B;AAAA,MACrC,UAAU,EAAE,+BAA+B;AAAA,MAC3C;AAAA,MACA,WAAW;AAAA,MACX,aAAa,EAAE,gBAAgB;AAAA,MAC/B,cAAc,EAAE,8BAA8B;AAAA,MAC9C,gBAAgB,CAAC;AAAA,MAEjB,UAAA,qBAAC,OAAA,EAAI,WAAU,yBACb,UAAA;AAAA,QAAA,oBAAC,OAAA,EAAI,MAAK,cAAa,cAAY,EAAE,2BAA2B,GAAG,WAAU,2BAC1E,UAAA,6BAA6B,IAAI,CAAC,SACjC;AAAA,UAAC;AAAA,UAAA;AAAA,YAEC,IAAI,2BAA2B,IAAI;AAAA,YACnC,MAAK;AAAA,YACL,MAAK;AAAA,YACL,SAAS,eAAe;AAAA,YACxB,UAAU,MAAM,cAAc,IAAI;AAAA,YAClC,OAAO,EAAE,kCAAkC,IAAI,CAAC;AAAA,UAAA;AAAA,UAN3C;AAAA,QAAA,CAQR,GACH;AAAA,QAEA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,QAAO;AAAA,YACP,MAAM;AAAA,YACN,OAAO;AAAA,YACP,WAAW;AAAA,YACX,cAAY,EAAE,4BAA4B;AAAA,YAC1C,aAAa,EAAE,kCAAkC;AAAA,YACjD,UAAU,CAAC,UAAU,UAAU,MAAM,OAAO,KAAK;AAAA,YACjD,WAAU;AAAA,UAAA;AAAA,QAAA;AAAA,MACZ,EAAA,CACF;AAAA,IAAA;AAAA,EAAA;AAGN;"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { IImageAiVerification } from './imageAiVerification';
|
|
2
|
+
interface IImageAiVerificationNoticeProps {
|
|
3
|
+
verification: IImageAiVerification | null | undefined;
|
|
4
|
+
className?: string;
|
|
5
|
+
}
|
|
6
|
+
/** Renders nothing unless the backend flagged the result; the flags are advisory only. */
|
|
7
|
+
declare const ImageAiVerificationNotice: React.FC<IImageAiVerificationNoticeProps>;
|
|
8
|
+
export default ImageAiVerificationNotice;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
import { useTranslation } from "react-i18next";
|
|
4
|
+
import { MaterialSymbol } from "../MaterialSymbol/index.js";
|
|
5
|
+
const ImageAiVerificationNotice = ({ verification, className }) => {
|
|
6
|
+
const { t } = useTranslation("ui");
|
|
7
|
+
const changedTooMuch = verification?.changedMoreThanRequested === true;
|
|
8
|
+
const colorCheck = verification?.colorCheck ?? null;
|
|
9
|
+
const colorFailed = colorCheck?.passed === false;
|
|
10
|
+
if (!changedTooMuch && !colorFailed) return null;
|
|
11
|
+
return /* @__PURE__ */ jsxs(
|
|
12
|
+
"div",
|
|
13
|
+
{
|
|
14
|
+
role: "status",
|
|
15
|
+
className: classNames(
|
|
16
|
+
"flex flex-col gap-y-1 rounded-xl border border-warning-border bg-warning-soft px-3 py-2 text-xs",
|
|
17
|
+
"text-warning-emphasis",
|
|
18
|
+
className
|
|
19
|
+
),
|
|
20
|
+
children: [
|
|
21
|
+
changedTooMuch && /* @__PURE__ */ jsxs("span", { className: "flex items-start gap-x-1.5", children: [
|
|
22
|
+
/* @__PURE__ */ jsx(MaterialSymbol, { "aria-hidden": true, icon: "warning", size: 14, className: "mt-px shrink-0" }),
|
|
23
|
+
t("imageAiAdjust.verification.changedMoreThanRequested")
|
|
24
|
+
] }),
|
|
25
|
+
colorFailed && colorCheck && /* @__PURE__ */ jsxs("span", { className: "flex items-start gap-x-1.5", children: [
|
|
26
|
+
/* @__PURE__ */ jsx(MaterialSymbol, { "aria-hidden": true, icon: "warning", size: 14, className: "mt-px shrink-0" }),
|
|
27
|
+
t("imageAiAdjust.verification.colorMismatch", {
|
|
28
|
+
dominant: colorCheck.dominantHex,
|
|
29
|
+
target: colorCheck.targetHex
|
|
30
|
+
})
|
|
31
|
+
] })
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
export {
|
|
37
|
+
ImageAiVerificationNotice as default
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=ImageAiVerificationNotice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageAiVerificationNotice.js","sources":["../../../../src/components/UI/ImageAiAdjust/ImageAiVerificationNotice.tsx"],"sourcesContent":["import classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\n\nimport { IImageAiVerification } from './imageAiVerification';\n\ninterface IImageAiVerificationNoticeProps {\n verification: IImageAiVerification | null | undefined;\n className?: string;\n}\n\n/** Renders nothing unless the backend flagged the result; the flags are advisory only. */\nconst ImageAiVerificationNotice: React.FC<IImageAiVerificationNoticeProps> = ({ verification, className }) => {\n const { t } = useTranslation('ui');\n\n const changedTooMuch = verification?.changedMoreThanRequested === true;\n const colorCheck = verification?.colorCheck ?? null;\n const colorFailed = colorCheck?.passed === false;\n if (!changedTooMuch && !colorFailed) return null;\n\n return (\n <div\n role=\"status\"\n className={classNames(\n 'flex flex-col gap-y-1 rounded-xl border border-warning-border bg-warning-soft px-3 py-2 text-xs',\n 'text-warning-emphasis',\n className,\n )}\n >\n {changedTooMuch && (\n <span className=\"flex items-start gap-x-1.5\">\n <MaterialSymbol aria-hidden icon=\"warning\" size={14} className=\"mt-px shrink-0\" />\n {t('imageAiAdjust.verification.changedMoreThanRequested')}\n </span>\n )}\n {colorFailed && colorCheck && (\n <span className=\"flex items-start gap-x-1.5\">\n <MaterialSymbol aria-hidden icon=\"warning\" size={14} className=\"mt-px shrink-0\" />\n {t('imageAiAdjust.verification.colorMismatch', {\n dominant: colorCheck.dominantHex,\n target: colorCheck.targetHex,\n })}\n </span>\n )}\n </div>\n );\n};\n\nexport default ImageAiVerificationNotice;\n"],"names":[],"mappings":";;;;AAaA,MAAM,4BAAuE,CAAC,EAAE,cAAc,gBAAgB;AAC5G,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AAEjC,QAAM,iBAAiB,cAAc,6BAA6B;AAClE,QAAM,aAAa,cAAc,cAAc;AAC/C,QAAM,cAAc,YAAY,WAAW;AAC3C,MAAI,CAAC,kBAAkB,CAAC,YAAa,QAAO;AAE5C,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAGD,UAAA;AAAA,QAAA,kBACC,qBAAC,QAAA,EAAK,WAAU,8BACd,UAAA;AAAA,UAAA,oBAAC,gBAAA,EAAe,eAAW,MAAC,MAAK,WAAU,MAAM,IAAI,WAAU,iBAAA,CAAiB;AAAA,UAC/E,EAAE,qDAAqD;AAAA,QAAA,GAC1D;AAAA,QAED,eAAe,cACd,qBAAC,QAAA,EAAK,WAAU,8BACd,UAAA;AAAA,UAAA,oBAAC,gBAAA,EAAe,eAAW,MAAC,MAAK,WAAU,MAAM,IAAI,WAAU,iBAAA,CAAiB;AAAA,UAC/E,EAAE,4CAA4C;AAAA,YAC7C,UAAU,WAAW;AAAA,YACrB,QAAQ,WAAW;AAAA,UAAA,CACpB;AAAA,QAAA,EAAA,CACH;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAIR;"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Why a user discarded an AI result; mirrors the backend ReviewReasonCode. */
|
|
2
|
+
export declare const IMAGE_AI_REVIEW_REASON_CODES: readonly ["changed_the_product", "wrong_color", "bad_quality", "person_changed", "text_or_logo", "not_what_i_asked", "other"];
|
|
3
|
+
export type TImageAiReviewReasonCode = (typeof IMAGE_AI_REVIEW_REASON_CODES)[number];
|
|
4
|
+
export type TImageAiReviewDecision = 'approved' | 'discarded';
|
|
5
|
+
/** Matches the backend limit for the free-text review reason. */
|
|
6
|
+
export declare const IMAGE_AI_REVIEW_REASON_MAX = 500;
|
|
7
|
+
/** Keys in the `ui` namespace, so hosts can render the same labels. */
|
|
8
|
+
export declare const IMAGE_AI_REVIEW_REASON_LABEL_KEYS: Record<TImageAiReviewReasonCode, string>;
|
|
9
|
+
export declare const isImageAiReviewReasonCode: (value: unknown) => value is TImageAiReviewReasonCode;
|
|
10
|
+
export interface IImageAiDiscardReason {
|
|
11
|
+
reasonCode: TImageAiReviewReasonCode;
|
|
12
|
+
reason?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface IImageAiReview {
|
|
15
|
+
generationId: string;
|
|
16
|
+
decision: TImageAiReviewDecision;
|
|
17
|
+
reasonCode?: TImageAiReviewReasonCode;
|
|
18
|
+
reason?: string;
|
|
19
|
+
}
|
|
20
|
+
export type TImageAiReviewHandler = (review: IImageAiReview) => Promise<void> | void;
|
|
21
|
+
/** A review is feedback, never a gate: failures are logged and swallowed. */
|
|
22
|
+
export declare const submitImageAiReview: (handler: TImageAiReviewHandler | undefined, review: IImageAiReview) => Promise<void>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { uiLogger } from "../../../helpers/uiLogger.js";
|
|
2
|
+
const IMAGE_AI_REVIEW_REASON_CODES = [
|
|
3
|
+
"changed_the_product",
|
|
4
|
+
"wrong_color",
|
|
5
|
+
"bad_quality",
|
|
6
|
+
"person_changed",
|
|
7
|
+
"text_or_logo",
|
|
8
|
+
"not_what_i_asked",
|
|
9
|
+
"other"
|
|
10
|
+
];
|
|
11
|
+
const IMAGE_AI_REVIEW_REASON_MAX = 500;
|
|
12
|
+
const IMAGE_AI_REVIEW_REASON_LABEL_KEYS = {
|
|
13
|
+
changed_the_product: "imageAiReview.reasons.changed_the_product",
|
|
14
|
+
wrong_color: "imageAiReview.reasons.wrong_color",
|
|
15
|
+
bad_quality: "imageAiReview.reasons.bad_quality",
|
|
16
|
+
person_changed: "imageAiReview.reasons.person_changed",
|
|
17
|
+
text_or_logo: "imageAiReview.reasons.text_or_logo",
|
|
18
|
+
not_what_i_asked: "imageAiReview.reasons.not_what_i_asked",
|
|
19
|
+
other: "imageAiReview.reasons.other"
|
|
20
|
+
};
|
|
21
|
+
const isImageAiReviewReasonCode = (value) => typeof value === "string" && IMAGE_AI_REVIEW_REASON_CODES.includes(value);
|
|
22
|
+
const submitImageAiReview = async (handler, review) => {
|
|
23
|
+
if (!handler || !review.generationId) return;
|
|
24
|
+
try {
|
|
25
|
+
await handler(review);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
uiLogger.warn("Failed to submit the AI image review", error);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
export {
|
|
31
|
+
IMAGE_AI_REVIEW_REASON_CODES,
|
|
32
|
+
IMAGE_AI_REVIEW_REASON_LABEL_KEYS,
|
|
33
|
+
IMAGE_AI_REVIEW_REASON_MAX,
|
|
34
|
+
isImageAiReviewReasonCode,
|
|
35
|
+
submitImageAiReview
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=imageAiReview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imageAiReview.js","sources":["../../../../src/components/UI/ImageAiAdjust/imageAiReview.ts"],"sourcesContent":["import { uiLogger } from '@/helpers/uiLogger';\n\n/** Why a user discarded an AI result; mirrors the backend ReviewReasonCode. */\nexport const IMAGE_AI_REVIEW_REASON_CODES = [\n 'changed_the_product',\n 'wrong_color',\n 'bad_quality',\n 'person_changed',\n 'text_or_logo',\n 'not_what_i_asked',\n 'other',\n] as const;\n\nexport type TImageAiReviewReasonCode = (typeof IMAGE_AI_REVIEW_REASON_CODES)[number];\n\nexport type TImageAiReviewDecision = 'approved' | 'discarded';\n\n/** Matches the backend limit for the free-text review reason. */\nexport const IMAGE_AI_REVIEW_REASON_MAX = 500;\n\n/** Keys in the `ui` namespace, so hosts can render the same labels. */\nexport const IMAGE_AI_REVIEW_REASON_LABEL_KEYS: Record<TImageAiReviewReasonCode, string> = {\n changed_the_product: 'imageAiReview.reasons.changed_the_product',\n wrong_color: 'imageAiReview.reasons.wrong_color',\n bad_quality: 'imageAiReview.reasons.bad_quality',\n person_changed: 'imageAiReview.reasons.person_changed',\n text_or_logo: 'imageAiReview.reasons.text_or_logo',\n not_what_i_asked: 'imageAiReview.reasons.not_what_i_asked',\n other: 'imageAiReview.reasons.other',\n};\n\nexport const isImageAiReviewReasonCode = (value: unknown): value is TImageAiReviewReasonCode =>\n typeof value === 'string' && (IMAGE_AI_REVIEW_REASON_CODES as ReadonlyArray<string>).includes(value);\n\nexport interface IImageAiDiscardReason {\n reasonCode: TImageAiReviewReasonCode;\n reason?: string;\n}\n\nexport interface IImageAiReview {\n generationId: string;\n decision: TImageAiReviewDecision;\n reasonCode?: TImageAiReviewReasonCode;\n reason?: string;\n}\n\nexport type TImageAiReviewHandler = (review: IImageAiReview) => Promise<void> | void;\n\n/** A review is feedback, never a gate: failures are logged and swallowed. */\nexport const submitImageAiReview = async (\n handler: TImageAiReviewHandler | undefined,\n review: IImageAiReview,\n): Promise<void> => {\n if (!handler || !review.generationId) return;\n\n try {\n await handler(review);\n } catch (error) {\n uiLogger.warn('Failed to submit the AI image review', error);\n }\n};\n"],"names":[],"mappings":";AAGO,MAAM,+BAA+B;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAOO,MAAM,6BAA6B;AAGnC,MAAM,oCAA8E;AAAA,EACzF,qBAAqB;AAAA,EACrB,aAAa;AAAA,EACb,aAAa;AAAA,EACb,gBAAgB;AAAA,EAChB,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,OAAO;AACT;AAEO,MAAM,4BAA4B,CAAC,UACxC,OAAO,UAAU,YAAa,6BAAuD,SAAS,KAAK;AAiB9F,MAAM,sBAAsB,OACjC,SACA,WACkB;AAClB,MAAI,CAAC,WAAW,CAAC,OAAO,aAAc;AAEtC,MAAI;AACF,UAAM,QAAQ,MAAM;AAAA,EACtB,SAAS,OAAO;AACd,aAAS,KAAK,wCAAwC,KAAK;AAAA,EAC7D;AACF;"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Backend check of the dominant garment colour against the requested one (CIEDE2000). */
|
|
2
|
+
export interface IImageAiColorCheck {
|
|
3
|
+
targetHex: string;
|
|
4
|
+
dominantHex: string;
|
|
5
|
+
deltaE: number;
|
|
6
|
+
passed: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** What the backend measured on an AI result; every field is advisory, never blocking. */
|
|
9
|
+
export interface IImageAiVerification {
|
|
10
|
+
/** Mean normalized difference outside the painted area (whole image without a mask), 0..1. */
|
|
11
|
+
changeScore: number | null;
|
|
12
|
+
threshold: number;
|
|
13
|
+
changedMoreThanRequested: boolean;
|
|
14
|
+
/** True when pixels outside the mask were restored from the source. */
|
|
15
|
+
composited: boolean;
|
|
16
|
+
colorCheck: IImageAiColorCheck | null;
|
|
17
|
+
}
|
|
18
|
+
/** Coerces whatever the host received into the verification shape, or null when absent/garbled. */
|
|
19
|
+
export declare const normalizeImageAiVerification: (value: unknown) => IImageAiVerification | null;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
2
|
+
const finiteOr = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
3
|
+
const normalizeColorCheck = (value) => {
|
|
4
|
+
if (!isRecord(value) || typeof value.passed !== "boolean") return null;
|
|
5
|
+
return {
|
|
6
|
+
targetHex: typeof value.targetHex === "string" ? value.targetHex : "",
|
|
7
|
+
dominantHex: typeof value.dominantHex === "string" ? value.dominantHex : "",
|
|
8
|
+
deltaE: finiteOr(value.deltaE, 0),
|
|
9
|
+
passed: value.passed
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
const normalizeImageAiVerification = (value) => {
|
|
13
|
+
if (!isRecord(value)) return null;
|
|
14
|
+
return {
|
|
15
|
+
changeScore: finiteOr(value.changeScore, null),
|
|
16
|
+
threshold: finiteOr(value.threshold, 0),
|
|
17
|
+
changedMoreThanRequested: value.changedMoreThanRequested === true,
|
|
18
|
+
composited: value.composited === true,
|
|
19
|
+
colorCheck: normalizeColorCheck(value.colorCheck)
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export {
|
|
23
|
+
normalizeImageAiVerification
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=imageAiVerification.js.map
|