@umodeapporg/ui 0.15.0 → 0.16.0
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/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,3 +1,4 @@
|
|
|
1
|
+
import { TImageAiReviewHandler } from '../../UI/ImageAiAdjust/imageAiReview';
|
|
1
2
|
import { TImageAiAdjuster } from '../../UI/ImageAiAdjust/useImageAiAdjust';
|
|
2
3
|
export interface IControlledImageAiAdjust {
|
|
3
4
|
adjustImage: TImageAiAdjuster;
|
|
@@ -5,6 +6,8 @@ export interface IControlledImageAiAdjust {
|
|
|
5
6
|
onAdjusted: (file: File) => Promise<void> | void;
|
|
6
7
|
/** Associates the generation with a product when the surface knows one. */
|
|
7
8
|
productId?: string;
|
|
9
|
+
/** Receives approve/discard feedback when the adjuster returns a generationId. */
|
|
10
|
+
onReviewResult?: TImageAiReviewHandler;
|
|
8
11
|
}
|
|
9
12
|
interface IControlledImageProps {
|
|
10
13
|
src: string | null | undefined;
|
|
@@ -92,7 +92,8 @@ const ControlledImageContent = ({
|
|
|
92
92
|
productId: aiAdjust.productId,
|
|
93
93
|
adjustImage: aiAdjust.adjustImage,
|
|
94
94
|
onClose: () => setAiAdjustOpen(false),
|
|
95
|
-
onSave: aiAdjust.onAdjusted
|
|
95
|
+
onSave: aiAdjust.onAdjusted,
|
|
96
|
+
onReviewResult: aiAdjust.onReviewResult
|
|
96
97
|
}
|
|
97
98
|
)
|
|
98
99
|
] })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../../../src/components/Form/ControlledImage/index.tsx"],"sourcesContent":["import { lazy, Suspense, useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { TImageAiAdjuster } from '@/components/UI/ImageAiAdjust/useImageAiAdjust';\nimport ImagePlaceholder from '@/components/UI/ImagePlaceholder';\n\nimport ControlledButton from '../../Form/ControlledButton';\nimport LoadingSpinner from '../../UI/LoadingSpinner';\nimport { MaterialSymbol } from '../../UI/MaterialSymbol';\n\nimport Img from './Img';\n\n/**\n * Lazy por dois motivos: quebra o ciclo de import (o diálogo de IA renderiza um\n * `ControlledImage` no comparativo) e mantém os dois editores fora do bundle de quem só\n * exibe imagem, que é quase toda a aplicação.\n */\nconst ImageAnnotationDialog = lazy(() => import('@/components/UI/ImageAnnotator/ImageAnnotationDialog'));\nconst ImageAiAdjustDialog = lazy(() => import('@/components/UI/ImageAiAdjust/ImageAiAdjustDialog'));\n\nexport interface IControlledImageAiAdjust {\n adjustImage: TImageAiAdjuster;\n /** Receives the adjusted image; resolving closes the dialog, throwing keeps it open. */\n onAdjusted: (file: File) => Promise<void> | void;\n /** Associates the generation with a product when the surface knows one. */\n productId?: string;\n}\n\ninterface IControlledImageProps {\n src: string | null | undefined;\n fallbackSrc?: string;\n className?: string;\n tooltip?: string;\n onClickHook?: () => void;\n /**\n * Opts into the annotation flow (pencil overlay + editor). Hosts own what happens to the\n * annotated file, so enabling it here never conflicts with other image click behaviors.\n */\n annotatable?: boolean;\n /** Receives the annotated image; resolving closes the editor, throwing keeps it open. */\n onAnnotated?: (file: File) => Promise<void> | void;\n annotationFileName?: string;\n /**\n * Opts into the AI adjustment flow. Sibling of `annotatable`, same contract: the host owns the\n * resulting file, and decides whether the feature is available at all.\n */\n aiAdjust?: IControlledImageAiAdjust;\n}\n\nconst ControlledImageContent: React.FC<IControlledImageProps> = ({\n src,\n fallbackSrc,\n className,\n tooltip,\n onClickHook,\n annotatable = false,\n onAnnotated,\n annotationFileName,\n aiAdjust,\n}) => {\n const { t } = useTranslation('ui');\n const [annotatorOpen, setAnnotatorOpen] = useState(false);\n const [aiAdjustOpen, setAiAdjustOpen] = useState(false);\n\n const renderImage = () => {\n if (!src) return <ImagePlaceholder className={className} />;\n\n return (\n <Img\n onClick={onClickHook}\n key={src}\n src={src}\n fallbackSrc={fallbackSrc}\n className={className}\n loader={\n <div className={classNames('flex size-full items-center justify-center bg-muted', className)}>\n <LoadingSpinner size={6} />\n </div>\n }\n unloader={<ImagePlaceholder variant=\"broken\" className={className} />}\n />\n );\n };\n\n const image = tooltip ? <ControlledTooltip content={tooltip}>{renderImage()}</ControlledTooltip> : renderImage();\n\n const showAnnotate = annotatable && Boolean(onAnnotated) && Boolean(src);\n const showAiAdjust = Boolean(aiAdjust) && Boolean(src);\n\n if (!src || (!showAnnotate && !showAiAdjust)) return image;\n\n const overlayButtonClass =\n 'flex size-8 items-center justify-center rounded-full bg-scrim text-white transition-colors glass-tint hover:bg-scrim';\n\n return (\n <span className=\"relative inline-flex max-w-full\">\n {image}\n\n <span className=\"absolute right-2 top-2 flex items-center gap-x-1\">\n {showAnnotate && (\n <ControlledTooltip content={t('imageAnnotator.annotate')} className=\"z-[9999]\">\n <ControlledButton\n defaultHtmlButton\n onClick={(event: React.MouseEvent) => {\n event.stopPropagation();\n setAnnotatorOpen(true);\n }}\n className={overlayButtonClass}\n >\n <MaterialSymbol icon=\"edit\" size={16} color=\"white\" />\n </ControlledButton>\n </ControlledTooltip>\n )}\n\n {showAiAdjust && (\n <ControlledTooltip content={t('imageAiAdjust.action')} className=\"z-[9999]\">\n <ControlledButton\n defaultHtmlButton\n onClick={(event: React.MouseEvent) => {\n event.stopPropagation();\n setAiAdjustOpen(true);\n }}\n className={overlayButtonClass}\n >\n <MaterialSymbol icon=\"auto_fix_high\" size={16} color=\"white\" />\n </ControlledButton>\n </ControlledTooltip>\n )}\n </span>\n\n <Suspense fallback={null}>\n {annotatorOpen && onAnnotated && (\n <ImageAnnotationDialog\n open={annotatorOpen}\n imageUrl={src}\n imageName={annotationFileName}\n onClose={() => setAnnotatorOpen(false)}\n onSave={onAnnotated}\n />\n )}\n\n {aiAdjustOpen && aiAdjust && (\n <ImageAiAdjustDialog\n open={aiAdjustOpen}\n imageUrl={src}\n imageName={annotationFileName}\n productId={aiAdjust.productId}\n adjustImage={aiAdjust.adjustImage}\n onClose={() => setAiAdjustOpen(false)}\n onSave={aiAdjust.onAdjusted}\n />\n )}\n </Suspense>\n </span>\n );\n};\n\nconst ControlledImage: React.FC<IControlledImageProps> = (props) => {\n return <ControlledImageContent {...props} />;\n};\n\nexport default ControlledImage;\n"],"names":[],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../../src/components/Form/ControlledImage/index.tsx"],"sourcesContent":["import { lazy, Suspense, useState } from 'react';\nimport classNames from 'classnames';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { TImageAiReviewHandler } from '@/components/UI/ImageAiAdjust/imageAiReview';\nimport { TImageAiAdjuster } from '@/components/UI/ImageAiAdjust/useImageAiAdjust';\nimport ImagePlaceholder from '@/components/UI/ImagePlaceholder';\n\nimport ControlledButton from '../../Form/ControlledButton';\nimport LoadingSpinner from '../../UI/LoadingSpinner';\nimport { MaterialSymbol } from '../../UI/MaterialSymbol';\n\nimport Img from './Img';\n\n/**\n * Lazy por dois motivos: quebra o ciclo de import (o diálogo de IA renderiza um\n * `ControlledImage` no comparativo) e mantém os dois editores fora do bundle de quem só\n * exibe imagem, que é quase toda a aplicação.\n */\nconst ImageAnnotationDialog = lazy(() => import('@/components/UI/ImageAnnotator/ImageAnnotationDialog'));\nconst ImageAiAdjustDialog = lazy(() => import('@/components/UI/ImageAiAdjust/ImageAiAdjustDialog'));\n\nexport interface IControlledImageAiAdjust {\n adjustImage: TImageAiAdjuster;\n /** Receives the adjusted image; resolving closes the dialog, throwing keeps it open. */\n onAdjusted: (file: File) => Promise<void> | void;\n /** Associates the generation with a product when the surface knows one. */\n productId?: string;\n /** Receives approve/discard feedback when the adjuster returns a generationId. */\n onReviewResult?: TImageAiReviewHandler;\n}\n\ninterface IControlledImageProps {\n src: string | null | undefined;\n fallbackSrc?: string;\n className?: string;\n tooltip?: string;\n onClickHook?: () => void;\n /**\n * Opts into the annotation flow (pencil overlay + editor). Hosts own what happens to the\n * annotated file, so enabling it here never conflicts with other image click behaviors.\n */\n annotatable?: boolean;\n /** Receives the annotated image; resolving closes the editor, throwing keeps it open. */\n onAnnotated?: (file: File) => Promise<void> | void;\n annotationFileName?: string;\n /**\n * Opts into the AI adjustment flow. Sibling of `annotatable`, same contract: the host owns the\n * resulting file, and decides whether the feature is available at all.\n */\n aiAdjust?: IControlledImageAiAdjust;\n}\n\nconst ControlledImageContent: React.FC<IControlledImageProps> = ({\n src,\n fallbackSrc,\n className,\n tooltip,\n onClickHook,\n annotatable = false,\n onAnnotated,\n annotationFileName,\n aiAdjust,\n}) => {\n const { t } = useTranslation('ui');\n const [annotatorOpen, setAnnotatorOpen] = useState(false);\n const [aiAdjustOpen, setAiAdjustOpen] = useState(false);\n\n const renderImage = () => {\n if (!src) return <ImagePlaceholder className={className} />;\n\n return (\n <Img\n onClick={onClickHook}\n key={src}\n src={src}\n fallbackSrc={fallbackSrc}\n className={className}\n loader={\n <div className={classNames('flex size-full items-center justify-center bg-muted', className)}>\n <LoadingSpinner size={6} />\n </div>\n }\n unloader={<ImagePlaceholder variant=\"broken\" className={className} />}\n />\n );\n };\n\n const image = tooltip ? <ControlledTooltip content={tooltip}>{renderImage()}</ControlledTooltip> : renderImage();\n\n const showAnnotate = annotatable && Boolean(onAnnotated) && Boolean(src);\n const showAiAdjust = Boolean(aiAdjust) && Boolean(src);\n\n if (!src || (!showAnnotate && !showAiAdjust)) return image;\n\n const overlayButtonClass =\n 'flex size-8 items-center justify-center rounded-full bg-scrim text-white transition-colors glass-tint hover:bg-scrim';\n\n return (\n <span className=\"relative inline-flex max-w-full\">\n {image}\n\n <span className=\"absolute right-2 top-2 flex items-center gap-x-1\">\n {showAnnotate && (\n <ControlledTooltip content={t('imageAnnotator.annotate')} className=\"z-[9999]\">\n <ControlledButton\n defaultHtmlButton\n onClick={(event: React.MouseEvent) => {\n event.stopPropagation();\n setAnnotatorOpen(true);\n }}\n className={overlayButtonClass}\n >\n <MaterialSymbol icon=\"edit\" size={16} color=\"white\" />\n </ControlledButton>\n </ControlledTooltip>\n )}\n\n {showAiAdjust && (\n <ControlledTooltip content={t('imageAiAdjust.action')} className=\"z-[9999]\">\n <ControlledButton\n defaultHtmlButton\n onClick={(event: React.MouseEvent) => {\n event.stopPropagation();\n setAiAdjustOpen(true);\n }}\n className={overlayButtonClass}\n >\n <MaterialSymbol icon=\"auto_fix_high\" size={16} color=\"white\" />\n </ControlledButton>\n </ControlledTooltip>\n )}\n </span>\n\n <Suspense fallback={null}>\n {annotatorOpen && onAnnotated && (\n <ImageAnnotationDialog\n open={annotatorOpen}\n imageUrl={src}\n imageName={annotationFileName}\n onClose={() => setAnnotatorOpen(false)}\n onSave={onAnnotated}\n />\n )}\n\n {aiAdjustOpen && aiAdjust && (\n <ImageAiAdjustDialog\n open={aiAdjustOpen}\n imageUrl={src}\n imageName={annotationFileName}\n productId={aiAdjust.productId}\n adjustImage={aiAdjust.adjustImage}\n onClose={() => setAiAdjustOpen(false)}\n onSave={aiAdjust.onAdjusted}\n onReviewResult={aiAdjust.onReviewResult}\n />\n )}\n </Suspense>\n </span>\n );\n};\n\nconst ControlledImage: React.FC<IControlledImageProps> = (props) => {\n return <ControlledImageContent {...props} />;\n};\n\nexport default ControlledImage;\n"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TImageAiReviewHandler } from './imageAiReview';
|
|
1
2
|
import { TImageAiAdjuster } from './useImageAiAdjust';
|
|
2
3
|
interface IImageAiAdjustDialogProps {
|
|
3
4
|
open: boolean;
|
|
@@ -10,6 +11,11 @@ interface IImageAiAdjustDialogProps {
|
|
|
10
11
|
onClose: () => void;
|
|
11
12
|
/** Recebe a imagem ajustada; resolver fecha o diálogo, lançar mantém aberto. */
|
|
12
13
|
onSave: (file: File) => Promise<void> | void;
|
|
14
|
+
/**
|
|
15
|
+
* Optional feedback sink. Only used when the adjuster resolved a `generationId`: using the
|
|
16
|
+
* result approves it, discarding asks for a reason first. Failures never block the flow.
|
|
17
|
+
*/
|
|
18
|
+
onReviewResult?: TImageAiReviewHandler;
|
|
13
19
|
}
|
|
14
20
|
declare const ImageAiAdjustDialog: React.FC<IImageAiAdjustDialogProps>;
|
|
15
21
|
export default ImageAiAdjustDialog;
|
|
@@ -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":";;;;;;;;;;;;;;AA6BA,MAAM,sBAA2D,CAAC;AAAA,EAChE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AACjC,QAAM,EAAE,aAAA,IAAiB,cAAA;AACzB,QAAM,EAAE,OAAA,IAAW,gBAAA;AAEnB,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA,IACE,iBAAiB,EAAE,UAAU,WAAW,WAAW,aAAa;AAEpE,QAAM,aAAa,YAAY;AAC7B,QAAI,CAAC,aAAa,OAAQ;AAE1B,cAAU,IAAI;AACd,QAAI;AACF,YAAM,OAAO,MAAM,iBAAiB;AACpC,cAAA;AAAA,IACF,SAAS,OAAO;AACd,eAAS,MAAM,wCAAwC,KAAK;AAC5D,aAAO,EAAE,SAAS,EAAE,yBAAyB,GAAG,MAAM,SAAS;AAAA,IACjE,UAAA;AACE,gBAAU,KAAK;AAAA,IACjB;AAAA,EACF;AAEA,QAAM,eAAe;AAErB,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA,MAAM,eAAe,QAAQ;AAAA,MAC7B,OAAO,EAAE,qBAAqB;AAAA,MAC9B,UAAU,EAAE,wBAAwB;AAAA,MACpC,UAAU;AAAA,MACV,WAAW;AAAA,MACX,SAAO;AAAA,MAEP,UAAA,qBAAC,OAAA,EAAI,WAAU,sDACb,UAAA;AAAA,QAAA,qBAAC,SAAI,WAAW,WAAW,cAAc,eAAe,aAAa,UAAU,GAC7E,UAAA;AAAA,UAAA,qBAAC,OAAA,EAAI,WAAU,0CACb,UAAA;AAAA,YAAA,oBAAC,QAAA,EAAK,WAAU,yEACb,UAAA,EAAE,wBAAwB,GAC7B;AAAA,YACA,oBAAC,OAAA,EAAI,WAAU,6DACb,UAAA,oBAAC,mBAAgB,KAAK,UAAU,WAAW,aAAA,CAAc,EAAA,CAC3D;AAAA,UAAA,GACF;AAAA,UAEA,qBAAC,OAAA,EAAI,WAAU,0CACb,UAAA;AAAA,YAAA,oBAAC,QAAA,EAAK,WAAU,yEACb,UAAA,EAAE,sBAAsB,GAC3B;AAAA,YACA,qBAAC,OAAA,EAAI,WAAU,sEACZ,UAAA;AAAA,cAAA,gBACC,qBAAC,OAAA,EAAI,WAAU,0CACb,UAAA;AAAA,gBAAA,oBAAC,gBAAA,EAAe,MAAM,GAAG,OAAM,SAAQ;AAAA,oCACtC,QAAA,EAAK,WAAU,kCAAkC,UAAA,EAAE,0BAA0B,EAAA,CAAE;AAAA,cAAA,GAClF;AAAA,cAGD,CAAC,gBAAgB,aAAa,oBAAC,mBAAgB,KAAK,WAAW,WAAW,cAAc;AAAA,cAExF,CAAC,gBAAgB,CAAC,aACjB,oBAAC,UAAK,WAAU,kDAAkD,UAAA,EAAE,0BAA0B,EAAA,CAAE;AAAA,YAAA,EAAA,CAEpG;AAAA,UAAA,EAAA,CACF;AAAA,QAAA,GACF;AAAA,QAEA;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,QAAO;AAAA,YACP,OAAO;AAAA,YACP,MAAM;AAAA,YACN,UAAU,gBAAgB;AAAA,YAC1B,cAAY,EAAE,2BAA2B;AAAA,YACzC,aAAa,EAAE,iCAAiC;AAAA,YAChD,UAAU,CAAC,UAAU,gBAAgB,MAAM,OAAO,KAAK;AAAA,YACvD,WAAU;AAAA,UAAA;AAAA,QAAA;AAAA,4BAGX,OAAA,EAAI,WAAU,iDACZ,UAAA,eACC,oBAAC,oBAAiB,MAAK,MAAK,SAAQ,YAAW,SAAS,QAAQ,WAAU,eACvE,YAAE,gBAAgB,EAAA,CACrB,IAEA,qBAAA,UAAA,EACG,UAAA;AAAA,UAAA,aACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAQ;AAAA,cACR,UAAU;AAAA,cACV,SAAS;AAAA,cACT,WAAU;AAAA,cAEV,UAAA;AAAA,gBAAA,oBAAC,gBAAA,EAAe,MAAK,WAAU,MAAM,IAAI;AAAA,gBACxC,EAAE,wBAAwB;AAAA,cAAA;AAAA,YAAA;AAAA,UAAA;AAAA,UAI/B;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS,YAAY,aAAa;AAAA,cAClC,UAAU,CAAC,eAAe;AAAA,cAC1B,SAAS;AAAA,cACT,WAAU;AAAA,cAEV,UAAA;AAAA,gBAAA,oBAAC,gBAAA,EAAe,MAAK,iBAAgB,MAAM,IAAI;AAAA,gBAC9C,EAAE,wBAAwB;AAAA,cAAA;AAAA,YAAA;AAAA,UAAA;AAAA,UAG5B,aACC;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAK;AAAA,cACL,UAAU;AAAA,cACV,SAAS;AAAA,cACT,WAAU;AAAA,cAET,UAAA;AAAA,gBAAA,SACC,oBAAC,gBAAA,EAAe,MAAM,GAAG,OAAM,SAAQ,IAEvC,oBAAC,gBAAA,EAAe,MAAK,SAAQ,MAAM,IAAI,OAAM,SAAQ;AAAA,gBAEtD,EAAE,yBAAyB;AAAA,cAAA;AAAA,YAAA;AAAA,UAAA;AAAA,QAC9B,EAAA,CAEJ,EAAA,CAEJ;AAAA,MAAA,EAAA,CACF;AAAA,IAAA;AAAA,EAAA;AAGN;"}
|
|
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>;
|