@interware/content-generator 0.7.0 → 0.7.2
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/_chunks/{App-h4_txTsI.mjs → App-CTh6owSM.mjs} +2 -2
- package/dist/_chunks/{App-Cwr5ra5m.js → App-ugsCmlsC.js} +2 -2
- package/dist/_chunks/{ImageStudioTabs-CG4KTn1S.js → ImageStudioTabs-BFEF9ggK.js} +60 -46
- package/dist/_chunks/{ImageStudioTabs-B-mOwDfY.mjs → ImageStudioTabs-Umf3GODW.mjs} +60 -46
- package/dist/_chunks/{Settings-BT-EqvXs.mjs → Settings-CdGW2O8h.mjs} +1 -1
- package/dist/_chunks/{Settings-D4NGBTk1.js → Settings-Sk78JUBS.js} +1 -1
- package/dist/_chunks/{StudioApp-xaEy-uIk.mjs → StudioApp-CrGn-9Gg.mjs} +2 -2
- package/dist/_chunks/{StudioApp-_ViSAwCm.js → StudioApp-DoC0z4eh.js} +2 -2
- package/dist/_chunks/{index-NA9OuMGs.js → index-C2s9Jhcc.js} +3 -3
- package/dist/_chunks/{index-D67xWUPz.mjs → index-D6UAvh53.mjs} +3 -3
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/src/features/templates/editor/ColorField.d.ts +4 -2
- package/dist/admin/src/features/templates/model/palette.d.ts +2 -2
- package/dist/admin/src/lib/imageLimits.d.ts +9 -0
- package/dist/server/index.js +24 -10
- package/dist/server/index.mjs +25 -11
- package/dist/server/src/services/image-studio.d.ts +6 -1
- package/package.json +1 -1
|
@@ -5,8 +5,8 @@ import { P as PluginProviders } from "./PluginProviders-DWMpcAIh.mjs";
|
|
|
5
5
|
import { useMemo, useState, useEffect, useRef, useCallback } from "react";
|
|
6
6
|
import { Box, Flex, Typography, Button, Badge, TextInput, Loader, Table, Thead, Tr, Th, VisuallyHidden, Tbody, Td, TextButton, IconButton, Link, LinkButton, Field, Radio, Modal, Textarea, Combobox, ComboboxOption, Toggle, Grid } from "@strapi/design-system";
|
|
7
7
|
import { Plus, Search, Check, Pencil, ExternalLink, ArrowLeft, Image, Clock, WarningCircle, Trash, Sparkle, PlusCircle } from "@strapi/icons";
|
|
8
|
-
import { P as PLUGIN_ID, S as STUDIO_PATH, p as pluginPermissions } from "./index-
|
|
9
|
-
import { L as LibraryImagePicker, P as PromptComposer, a as LEGACY_ROUTES, S as StudioRedirect, l as legacyRedirect } from "./ImageStudioTabs-
|
|
8
|
+
import { P as PLUGIN_ID, S as STUDIO_PATH, p as pluginPermissions } from "./index-D6UAvh53.mjs";
|
|
9
|
+
import { L as LibraryImagePicker, P as PromptComposer, a as LEGACY_ROUTES, S as StudioRedirect, l as legacyRedirect } from "./ImageStudioTabs-Umf3GODW.mjs";
|
|
10
10
|
import { useIntl } from "react-intl";
|
|
11
11
|
import { useFormik } from "formik";
|
|
12
12
|
import * as Yup from "yup";
|
|
@@ -7,8 +7,8 @@ const PluginProviders = require("./PluginProviders-D79TeGSE.js");
|
|
|
7
7
|
const react = require("react");
|
|
8
8
|
const designSystem = require("@strapi/design-system");
|
|
9
9
|
const icons = require("@strapi/icons");
|
|
10
|
-
const index = require("./index-
|
|
11
|
-
const ImageStudioTabs = require("./ImageStudioTabs-
|
|
10
|
+
const index = require("./index-C2s9Jhcc.js");
|
|
11
|
+
const ImageStudioTabs = require("./ImageStudioTabs-BFEF9ggK.js");
|
|
12
12
|
const reactIntl = require("react-intl");
|
|
13
13
|
const formik = require("formik");
|
|
14
14
|
const Yup = require("yup");
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const jsxRuntime = require("react/jsx-runtime");
|
|
3
3
|
const designSystem = require("@strapi/design-system");
|
|
4
4
|
const reactRouterDom = require("react-router-dom");
|
|
5
|
-
const index = require("./index-
|
|
5
|
+
const index = require("./index-C2s9Jhcc.js");
|
|
6
6
|
const react = require("react");
|
|
7
7
|
const icons = require("@strapi/icons");
|
|
8
8
|
const styledComponents = require("styled-components");
|
|
@@ -740,6 +740,9 @@ const RemoveBackgroundButton = ({ src, onDone }) => {
|
|
|
740
740
|
)
|
|
741
741
|
] });
|
|
742
742
|
};
|
|
743
|
+
const IMAGE_MAX_MB = 30;
|
|
744
|
+
const IMAGE_MAX_BYTES = IMAGE_MAX_MB * 1024 * 1024;
|
|
745
|
+
const IMAGE_FORMATS_HINT = `PNG, JPG o WEBP de hasta ${IMAGE_MAX_MB} MB.`;
|
|
743
746
|
const NameDialog = ({ open, title, label, initial = "", confirmLabel, validate, onClose, onConfirm }) => {
|
|
744
747
|
const [name, setName] = react.useState(initial);
|
|
745
748
|
const [error, setError] = react.useState(null);
|
|
@@ -1253,7 +1256,10 @@ const EditEmpty = ({ disabled, onUpload, onOpenLibrary, onOpenRecent }) => {
|
|
|
1253
1256
|
style: { width: "100%", maxWidth: 600, border: `2px dashed ${over ? c.primary600 : c.neutral200}` },
|
|
1254
1257
|
children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "center", gap: 3, style: { textAlign: "center" }, children: [
|
|
1255
1258
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "beta", tag: "h2", children: "Elige la imagen que quieres editar" }),
|
|
1256
|
-
/* @__PURE__ */ jsxRuntime.
|
|
1259
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { textColor: "neutral600", children: [
|
|
1260
|
+
"Arrástrala aquí, súbela o elígela de la biblioteca. ",
|
|
1261
|
+
IMAGE_FORMATS_HINT
|
|
1262
|
+
] }),
|
|
1257
1263
|
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 3, paddingTop: 2, children: [
|
|
1258
1264
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Upload, {}), disabled, onClick: () => input.current?.click(), children: "Subir imagen" }),
|
|
1259
1265
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Folder, {}), disabled, onClick: onOpenLibrary, children: "Elegir de la biblioteca" })
|
|
@@ -1328,7 +1334,7 @@ const ReferencePicker = ({
|
|
|
1328
1334
|
onChange,
|
|
1329
1335
|
onPickFromLibrary,
|
|
1330
1336
|
label = "Imágenes de referencia",
|
|
1331
|
-
hint =
|
|
1337
|
+
hint = IMAGE_FORMATS_HINT,
|
|
1332
1338
|
noun = REFERENCES
|
|
1333
1339
|
}) => {
|
|
1334
1340
|
const input = react.useRef(null);
|
|
@@ -1522,8 +1528,8 @@ const GeneratorTab = () => {
|
|
|
1522
1528
|
};
|
|
1523
1529
|
const uploadBase = (file) => {
|
|
1524
1530
|
if (!file) return;
|
|
1525
|
-
if (!IMAGE_MIMES.includes(file.type) || file.size >
|
|
1526
|
-
notify("warning",
|
|
1531
|
+
if (!IMAGE_MIMES.includes(file.type) || file.size > IMAGE_MAX_BYTES) {
|
|
1532
|
+
notify("warning", `Usa una imagen ${IMAGE_FORMATS_HINT}`);
|
|
1527
1533
|
return;
|
|
1528
1534
|
}
|
|
1529
1535
|
const reader = new FileReader();
|
|
@@ -1598,7 +1604,7 @@ const GeneratorTab = () => {
|
|
|
1598
1604
|
ReferencePicker,
|
|
1599
1605
|
{
|
|
1600
1606
|
label: "Referencias de estilo",
|
|
1601
|
-
hint:
|
|
1607
|
+
hint: `Guían el estilo, no se copian. ${IMAGE_FORMATS_HINT}`,
|
|
1602
1608
|
files: references,
|
|
1603
1609
|
max: model?.maxReferences ?? 0,
|
|
1604
1610
|
modelLabel: model?.label ?? "Este modelo",
|
|
@@ -2187,17 +2193,6 @@ const LibraryTab = () => {
|
|
|
2187
2193
|
/* @__PURE__ */ jsxRuntime.jsx(RemoveBackgroundDialog, { open: !!bgTarget, src: bgTarget?.src ?? "", onClose: () => setBgTarget(null), onApply: saveNoBackground })
|
|
2188
2194
|
] });
|
|
2189
2195
|
};
|
|
2190
|
-
const PALETTE = [
|
|
2191
|
-
{ label: "Navy", value: "#1F2A44" },
|
|
2192
|
-
{ label: "Rojo", value: "#E4002B" },
|
|
2193
|
-
{ label: "Teal", value: "#00C7B1" },
|
|
2194
|
-
{ label: "Teal suave", value: "#92CFC7" },
|
|
2195
|
-
{ label: "Amarillo", value: "#FFC844" },
|
|
2196
|
-
{ label: "Amarillo suave", value: "#F8DF8D" },
|
|
2197
|
-
{ label: "Gris", value: "#5B6473" },
|
|
2198
|
-
{ label: "Papel", value: "#EEF1F5" },
|
|
2199
|
-
{ label: "Blanco", value: "#FFFFFF" }
|
|
2200
|
-
];
|
|
2201
2196
|
const NAVY = "#1F2A44";
|
|
2202
2197
|
const RED$1 = "#E4002B";
|
|
2203
2198
|
const TEAL$1 = "#00C7B1";
|
|
@@ -3937,44 +3932,63 @@ const SaveTemplateDialog = ({ open, defaultName, defaultFolder, folders, onClose
|
|
|
3937
3932
|
] })
|
|
3938
3933
|
] }) });
|
|
3939
3934
|
};
|
|
3935
|
+
const parseHex = (raw) => {
|
|
3936
|
+
const hex = raw.trim().replace(/^#/, "");
|
|
3937
|
+
if (/^[0-9a-f]{3}$/i.test(hex)) return `#${[...hex].map((c) => c + c).join("")}`.toUpperCase();
|
|
3938
|
+
if (/^[0-9a-f]{6}$/i.test(hex)) return `#${hex}`.toUpperCase();
|
|
3939
|
+
return null;
|
|
3940
|
+
};
|
|
3940
3941
|
const ColorField = ({ label, value, onChange }) => {
|
|
3941
3942
|
const tc = useThemeColors();
|
|
3943
|
+
const current = parseHex(value ?? "") ?? "#000000";
|
|
3944
|
+
const [draft, setDraft] = react.useState(current);
|
|
3945
|
+
react.useEffect(() => setDraft((d) => parseHex(d) === current ? d : current), [current]);
|
|
3946
|
+
const apply = (text) => {
|
|
3947
|
+
const hex = parseHex(text);
|
|
3948
|
+
if (hex && hex !== current) onChange(hex);
|
|
3949
|
+
};
|
|
3950
|
+
const typeHex = (text) => {
|
|
3951
|
+
setDraft(text);
|
|
3952
|
+
if (text.trim().replace(/^#/, "").length === 6) apply(text);
|
|
3953
|
+
};
|
|
3954
|
+
const leave = () => {
|
|
3955
|
+
apply(draft);
|
|
3956
|
+
setDraft(parseHex(draft) ?? current);
|
|
3957
|
+
};
|
|
3942
3958
|
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 1, children: [
|
|
3943
3959
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", children: label }),
|
|
3944
|
-
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap:
|
|
3945
|
-
PALETTE.map((c) => {
|
|
3946
|
-
const active = value?.toUpperCase() === c.value;
|
|
3947
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3948
|
-
designSystem.Box,
|
|
3949
|
-
{
|
|
3950
|
-
tag: "button",
|
|
3951
|
-
color: "neutral800",
|
|
3952
|
-
type: "button",
|
|
3953
|
-
"aria-label": `${label}: ${c.label}`,
|
|
3954
|
-
"aria-pressed": active,
|
|
3955
|
-
title: c.label,
|
|
3956
|
-
onClick: () => onChange(c.value),
|
|
3957
|
-
style: {
|
|
3958
|
-
width: 22,
|
|
3959
|
-
height: 22,
|
|
3960
|
-
borderRadius: 4,
|
|
3961
|
-
cursor: "pointer",
|
|
3962
|
-
background: c.value,
|
|
3963
|
-
border: active ? `2px solid ${tc.primary600}` : `1px solid ${tc.neutral200}`,
|
|
3964
|
-
padding: 0
|
|
3965
|
-
}
|
|
3966
|
-
},
|
|
3967
|
-
c.value
|
|
3968
|
-
);
|
|
3969
|
-
}),
|
|
3960
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
|
|
3970
3961
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3971
3962
|
"input",
|
|
3972
3963
|
{
|
|
3973
3964
|
type: "color",
|
|
3974
|
-
"aria-label": `${label}
|
|
3975
|
-
|
|
3965
|
+
"aria-label": `${label}: selector`,
|
|
3966
|
+
title: "Abrir selector de color",
|
|
3967
|
+
value: current.toLowerCase(),
|
|
3976
3968
|
onChange: (e) => onChange(e.target.value.toUpperCase()),
|
|
3977
|
-
style: {
|
|
3969
|
+
style: {
|
|
3970
|
+
flexShrink: 0,
|
|
3971
|
+
width: 32,
|
|
3972
|
+
height: 32,
|
|
3973
|
+
padding: 2,
|
|
3974
|
+
borderRadius: 4,
|
|
3975
|
+
cursor: "pointer",
|
|
3976
|
+
border: `1px solid ${tc.neutral200}`,
|
|
3977
|
+
background: tc.neutral0
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
),
|
|
3981
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3982
|
+
designSystem.TextInput,
|
|
3983
|
+
{
|
|
3984
|
+
"aria-label": `${label}: hexadecimal`,
|
|
3985
|
+
size: "S",
|
|
3986
|
+
value: draft,
|
|
3987
|
+
placeholder: "#1F2A44",
|
|
3988
|
+
maxLength: 7,
|
|
3989
|
+
spellCheck: false,
|
|
3990
|
+
onChange: (e) => typeHex(e.target.value),
|
|
3991
|
+
onBlur: leave
|
|
3978
3992
|
}
|
|
3979
3993
|
)
|
|
3980
3994
|
] })
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Modal, Flex, Button, Loader, Typography, Grid, Box, lightTheme, Checkbox, Field, TextInput, Dialog, SingleSelect, SingleSelectOption, Link, Badge, IconButtonGroup, IconButton, Toggle, Menu, NumberInput, Divider, Textarea, Tabs } from "@strapi/design-system";
|
|
3
3
|
import { useLocation, Navigate, useSearchParams } from "react-router-dom";
|
|
4
|
-
import { S as STUDIO_PATH } from "./index-
|
|
4
|
+
import { S as STUDIO_PATH } from "./index-D6UAvh53.mjs";
|
|
5
5
|
import { useMemo, useState, useEffect, useId, useRef, useReducer, useCallback, useSyncExternalStore, useLayoutEffect, memo, forwardRef } from "react";
|
|
6
6
|
import { ChevronRight, Folder, Magic, Pencil, Upload, Sparkle, Plus, Cross, Download, ArrowClockwise, House, ChevronDown, FilePdf, File as File$1, More, Trash, User, Star, SquaresFour, Quotes, PriceTag, Paragraph, NumberList, Minus, Italic, Image as Image$1, HeadingTwo, HeadingOne, Hashtag, Feather, Drag, BulletList, ArrowLeft, Check, ArrowsCounterClockwise, ArrowLineLeft, ArrowLineRight, Expand, WarningCircle, Play, Phone, Mail, PinMap, Earth, PresentationChart, ChartPie, ChartBubble, Cog, Database, Cloud, Globe, Server, Lightbulb, Crown, SealCheck, Briefcase, Rocket, Shield, CheckCircle, Lightning, Heart, Collapse, Bold, ArrowUp, ArrowDown, Duplicate, Lock, EyeStriked, Eye } from "@strapi/icons";
|
|
7
7
|
import { useTheme } from "styled-components";
|
|
@@ -737,6 +737,9 @@ const RemoveBackgroundButton = ({ src, onDone }) => {
|
|
|
737
737
|
)
|
|
738
738
|
] });
|
|
739
739
|
};
|
|
740
|
+
const IMAGE_MAX_MB = 30;
|
|
741
|
+
const IMAGE_MAX_BYTES = IMAGE_MAX_MB * 1024 * 1024;
|
|
742
|
+
const IMAGE_FORMATS_HINT = `PNG, JPG o WEBP de hasta ${IMAGE_MAX_MB} MB.`;
|
|
740
743
|
const NameDialog = ({ open, title, label, initial = "", confirmLabel, validate, onClose, onConfirm }) => {
|
|
741
744
|
const [name, setName] = useState(initial);
|
|
742
745
|
const [error, setError] = useState(null);
|
|
@@ -1250,7 +1253,10 @@ const EditEmpty = ({ disabled, onUpload, onOpenLibrary, onOpenRecent }) => {
|
|
|
1250
1253
|
style: { width: "100%", maxWidth: 600, border: `2px dashed ${over ? c.primary600 : c.neutral200}` },
|
|
1251
1254
|
children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "center", gap: 3, style: { textAlign: "center" }, children: [
|
|
1252
1255
|
/* @__PURE__ */ jsx(Typography, { variant: "beta", tag: "h2", children: "Elige la imagen que quieres editar" }),
|
|
1253
|
-
/* @__PURE__ */
|
|
1256
|
+
/* @__PURE__ */ jsxs(Typography, { textColor: "neutral600", children: [
|
|
1257
|
+
"Arrástrala aquí, súbela o elígela de la biblioteca. ",
|
|
1258
|
+
IMAGE_FORMATS_HINT
|
|
1259
|
+
] }),
|
|
1254
1260
|
/* @__PURE__ */ jsxs(Flex, { gap: 3, paddingTop: 2, children: [
|
|
1255
1261
|
/* @__PURE__ */ jsx(Button, { variant: "tertiary", startIcon: /* @__PURE__ */ jsx(Upload, {}), disabled, onClick: () => input.current?.click(), children: "Subir imagen" }),
|
|
1256
1262
|
/* @__PURE__ */ jsx(Button, { startIcon: /* @__PURE__ */ jsx(Folder, {}), disabled, onClick: onOpenLibrary, children: "Elegir de la biblioteca" })
|
|
@@ -1325,7 +1331,7 @@ const ReferencePicker = ({
|
|
|
1325
1331
|
onChange,
|
|
1326
1332
|
onPickFromLibrary,
|
|
1327
1333
|
label = "Imágenes de referencia",
|
|
1328
|
-
hint =
|
|
1334
|
+
hint = IMAGE_FORMATS_HINT,
|
|
1329
1335
|
noun = REFERENCES
|
|
1330
1336
|
}) => {
|
|
1331
1337
|
const input = useRef(null);
|
|
@@ -1519,8 +1525,8 @@ const GeneratorTab = () => {
|
|
|
1519
1525
|
};
|
|
1520
1526
|
const uploadBase = (file) => {
|
|
1521
1527
|
if (!file) return;
|
|
1522
|
-
if (!IMAGE_MIMES.includes(file.type) || file.size >
|
|
1523
|
-
notify("warning",
|
|
1528
|
+
if (!IMAGE_MIMES.includes(file.type) || file.size > IMAGE_MAX_BYTES) {
|
|
1529
|
+
notify("warning", `Usa una imagen ${IMAGE_FORMATS_HINT}`);
|
|
1524
1530
|
return;
|
|
1525
1531
|
}
|
|
1526
1532
|
const reader = new FileReader();
|
|
@@ -1595,7 +1601,7 @@ const GeneratorTab = () => {
|
|
|
1595
1601
|
ReferencePicker,
|
|
1596
1602
|
{
|
|
1597
1603
|
label: "Referencias de estilo",
|
|
1598
|
-
hint:
|
|
1604
|
+
hint: `Guían el estilo, no se copian. ${IMAGE_FORMATS_HINT}`,
|
|
1599
1605
|
files: references,
|
|
1600
1606
|
max: model?.maxReferences ?? 0,
|
|
1601
1607
|
modelLabel: model?.label ?? "Este modelo",
|
|
@@ -2184,17 +2190,6 @@ const LibraryTab = () => {
|
|
|
2184
2190
|
/* @__PURE__ */ jsx(RemoveBackgroundDialog, { open: !!bgTarget, src: bgTarget?.src ?? "", onClose: () => setBgTarget(null), onApply: saveNoBackground })
|
|
2185
2191
|
] });
|
|
2186
2192
|
};
|
|
2187
|
-
const PALETTE = [
|
|
2188
|
-
{ label: "Navy", value: "#1F2A44" },
|
|
2189
|
-
{ label: "Rojo", value: "#E4002B" },
|
|
2190
|
-
{ label: "Teal", value: "#00C7B1" },
|
|
2191
|
-
{ label: "Teal suave", value: "#92CFC7" },
|
|
2192
|
-
{ label: "Amarillo", value: "#FFC844" },
|
|
2193
|
-
{ label: "Amarillo suave", value: "#F8DF8D" },
|
|
2194
|
-
{ label: "Gris", value: "#5B6473" },
|
|
2195
|
-
{ label: "Papel", value: "#EEF1F5" },
|
|
2196
|
-
{ label: "Blanco", value: "#FFFFFF" }
|
|
2197
|
-
];
|
|
2198
2193
|
const NAVY = "#1F2A44";
|
|
2199
2194
|
const RED$1 = "#E4002B";
|
|
2200
2195
|
const TEAL$1 = "#00C7B1";
|
|
@@ -3934,44 +3929,63 @@ const SaveTemplateDialog = ({ open, defaultName, defaultFolder, folders, onClose
|
|
|
3934
3929
|
] })
|
|
3935
3930
|
] }) });
|
|
3936
3931
|
};
|
|
3932
|
+
const parseHex = (raw) => {
|
|
3933
|
+
const hex = raw.trim().replace(/^#/, "");
|
|
3934
|
+
if (/^[0-9a-f]{3}$/i.test(hex)) return `#${[...hex].map((c) => c + c).join("")}`.toUpperCase();
|
|
3935
|
+
if (/^[0-9a-f]{6}$/i.test(hex)) return `#${hex}`.toUpperCase();
|
|
3936
|
+
return null;
|
|
3937
|
+
};
|
|
3937
3938
|
const ColorField = ({ label, value, onChange }) => {
|
|
3938
3939
|
const tc = useThemeColors();
|
|
3940
|
+
const current = parseHex(value ?? "") ?? "#000000";
|
|
3941
|
+
const [draft, setDraft] = useState(current);
|
|
3942
|
+
useEffect(() => setDraft((d) => parseHex(d) === current ? d : current), [current]);
|
|
3943
|
+
const apply = (text) => {
|
|
3944
|
+
const hex = parseHex(text);
|
|
3945
|
+
if (hex && hex !== current) onChange(hex);
|
|
3946
|
+
};
|
|
3947
|
+
const typeHex = (text) => {
|
|
3948
|
+
setDraft(text);
|
|
3949
|
+
if (text.trim().replace(/^#/, "").length === 6) apply(text);
|
|
3950
|
+
};
|
|
3951
|
+
const leave = () => {
|
|
3952
|
+
apply(draft);
|
|
3953
|
+
setDraft(parseHex(draft) ?? current);
|
|
3954
|
+
};
|
|
3939
3955
|
return /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 1, children: [
|
|
3940
3956
|
/* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", children: label }),
|
|
3941
|
-
/* @__PURE__ */ jsxs(Flex, { gap:
|
|
3942
|
-
PALETTE.map((c) => {
|
|
3943
|
-
const active = value?.toUpperCase() === c.value;
|
|
3944
|
-
return /* @__PURE__ */ jsx(
|
|
3945
|
-
Box,
|
|
3946
|
-
{
|
|
3947
|
-
tag: "button",
|
|
3948
|
-
color: "neutral800",
|
|
3949
|
-
type: "button",
|
|
3950
|
-
"aria-label": `${label}: ${c.label}`,
|
|
3951
|
-
"aria-pressed": active,
|
|
3952
|
-
title: c.label,
|
|
3953
|
-
onClick: () => onChange(c.value),
|
|
3954
|
-
style: {
|
|
3955
|
-
width: 22,
|
|
3956
|
-
height: 22,
|
|
3957
|
-
borderRadius: 4,
|
|
3958
|
-
cursor: "pointer",
|
|
3959
|
-
background: c.value,
|
|
3960
|
-
border: active ? `2px solid ${tc.primary600}` : `1px solid ${tc.neutral200}`,
|
|
3961
|
-
padding: 0
|
|
3962
|
-
}
|
|
3963
|
-
},
|
|
3964
|
-
c.value
|
|
3965
|
-
);
|
|
3966
|
-
}),
|
|
3957
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
|
|
3967
3958
|
/* @__PURE__ */ jsx(
|
|
3968
3959
|
"input",
|
|
3969
3960
|
{
|
|
3970
3961
|
type: "color",
|
|
3971
|
-
"aria-label": `${label}
|
|
3972
|
-
|
|
3962
|
+
"aria-label": `${label}: selector`,
|
|
3963
|
+
title: "Abrir selector de color",
|
|
3964
|
+
value: current.toLowerCase(),
|
|
3973
3965
|
onChange: (e) => onChange(e.target.value.toUpperCase()),
|
|
3974
|
-
style: {
|
|
3966
|
+
style: {
|
|
3967
|
+
flexShrink: 0,
|
|
3968
|
+
width: 32,
|
|
3969
|
+
height: 32,
|
|
3970
|
+
padding: 2,
|
|
3971
|
+
borderRadius: 4,
|
|
3972
|
+
cursor: "pointer",
|
|
3973
|
+
border: `1px solid ${tc.neutral200}`,
|
|
3974
|
+
background: tc.neutral0
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
),
|
|
3978
|
+
/* @__PURE__ */ jsx(
|
|
3979
|
+
TextInput,
|
|
3980
|
+
{
|
|
3981
|
+
"aria-label": `${label}: hexadecimal`,
|
|
3982
|
+
size: "S",
|
|
3983
|
+
value: draft,
|
|
3984
|
+
placeholder: "#1F2A44",
|
|
3985
|
+
maxLength: 7,
|
|
3986
|
+
spellCheck: false,
|
|
3987
|
+
onChange: (e) => typeHex(e.target.value),
|
|
3988
|
+
onBlur: leave
|
|
3975
3989
|
}
|
|
3976
3990
|
)
|
|
3977
3991
|
] })
|
|
@@ -5,7 +5,7 @@ import { useCallback, useState, useEffect } from "react";
|
|
|
5
5
|
import { Box, Grid, Field, Flex, Button, Typography, Textarea } from "@strapi/design-system";
|
|
6
6
|
import { Formik } from "formik";
|
|
7
7
|
import { P as PluginProviders } from "./PluginProviders-DWMpcAIh.mjs";
|
|
8
|
-
import { P as PLUGIN_ID } from "./index-
|
|
8
|
+
import { P as PLUGIN_ID } from "./index-D6UAvh53.mjs";
|
|
9
9
|
const useSettings = () => {
|
|
10
10
|
const { get, post, del } = useFetchClient();
|
|
11
11
|
const getSettings = useCallback(
|
|
@@ -7,7 +7,7 @@ const react = require("react");
|
|
|
7
7
|
const designSystem = require("@strapi/design-system");
|
|
8
8
|
const formik = require("formik");
|
|
9
9
|
const PluginProviders = require("./PluginProviders-D79TeGSE.js");
|
|
10
|
-
const index = require("./index-
|
|
10
|
+
const index = require("./index-C2s9Jhcc.js");
|
|
11
11
|
const useSettings = () => {
|
|
12
12
|
const { get, post, del } = admin.useFetchClient();
|
|
13
13
|
const getSettings = react.useCallback(
|
|
@@ -2,9 +2,9 @@ import { jsxs, jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Page } from "@strapi/strapi/admin";
|
|
3
3
|
import { Routes, Route } from "react-router-dom";
|
|
4
4
|
import { P as PluginProviders } from "./PluginProviders-DWMpcAIh.mjs";
|
|
5
|
-
import { p as pluginPermissions } from "./index-
|
|
5
|
+
import { p as pluginPermissions } from "./index-D6UAvh53.mjs";
|
|
6
6
|
import { Box } from "@strapi/design-system";
|
|
7
|
-
import { I as ImageStudioTabs } from "./ImageStudioTabs-
|
|
7
|
+
import { I as ImageStudioTabs } from "./ImageStudioTabs-Umf3GODW.mjs";
|
|
8
8
|
const ImageStudio = () => /* @__PURE__ */ jsxs(Page.Main, { children: [
|
|
9
9
|
/* @__PURE__ */ jsx(Page.Title, { children: "Estudio de imágenes" }),
|
|
10
10
|
/* @__PURE__ */ jsx(Box, { style: { height: "100vh" }, children: /* @__PURE__ */ jsx(ImageStudioTabs, {}) })
|
|
@@ -4,9 +4,9 @@ const jsxRuntime = require("react/jsx-runtime");
|
|
|
4
4
|
const admin = require("@strapi/strapi/admin");
|
|
5
5
|
const reactRouterDom = require("react-router-dom");
|
|
6
6
|
const PluginProviders = require("./PluginProviders-D79TeGSE.js");
|
|
7
|
-
const index = require("./index-
|
|
7
|
+
const index = require("./index-C2s9Jhcc.js");
|
|
8
8
|
const designSystem = require("@strapi/design-system");
|
|
9
|
-
const ImageStudioTabs = require("./ImageStudioTabs-
|
|
9
|
+
const ImageStudioTabs = require("./ImageStudioTabs-BFEF9ggK.js");
|
|
10
10
|
const ImageStudio = () => /* @__PURE__ */ jsxRuntime.jsxs(admin.Page.Main, { children: [
|
|
11
11
|
/* @__PURE__ */ jsxRuntime.jsx(admin.Page.Title, { children: "Estudio de imágenes" }),
|
|
12
12
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { height: "100vh" }, children: /* @__PURE__ */ jsxRuntime.jsx(ImageStudioTabs.ImageStudioTabs, {}) })
|
|
@@ -57,7 +57,7 @@ const index = {
|
|
|
57
57
|
defaultMessage: "Generador de contenido"
|
|
58
58
|
},
|
|
59
59
|
Component: async () => {
|
|
60
|
-
const { App } = await Promise.resolve().then(() => require("./App-
|
|
60
|
+
const { App } = await Promise.resolve().then(() => require("./App-ugsCmlsC.js"));
|
|
61
61
|
return App;
|
|
62
62
|
},
|
|
63
63
|
permissions: [
|
|
@@ -72,7 +72,7 @@ const index = {
|
|
|
72
72
|
defaultMessage: "Estudio de imágenes"
|
|
73
73
|
},
|
|
74
74
|
Component: async () => {
|
|
75
|
-
const { StudioApp } = await Promise.resolve().then(() => require("./StudioApp-
|
|
75
|
+
const { StudioApp } = await Promise.resolve().then(() => require("./StudioApp-DoC0z4eh.js"));
|
|
76
76
|
return StudioApp;
|
|
77
77
|
},
|
|
78
78
|
permissions: [
|
|
@@ -92,7 +92,7 @@ const index = {
|
|
|
92
92
|
defaultMessage: PLUGIN_ID
|
|
93
93
|
},
|
|
94
94
|
Component: async () => {
|
|
95
|
-
const component = await Promise.resolve().then(() => require("./Settings-
|
|
95
|
+
const component = await Promise.resolve().then(() => require("./Settings-Sk78JUBS.js"));
|
|
96
96
|
return component.default;
|
|
97
97
|
},
|
|
98
98
|
to: `plugins/${PLUGIN_ID}/settings`,
|
|
@@ -56,7 +56,7 @@ const index = {
|
|
|
56
56
|
defaultMessage: "Generador de contenido"
|
|
57
57
|
},
|
|
58
58
|
Component: async () => {
|
|
59
|
-
const { App } = await import("./App-
|
|
59
|
+
const { App } = await import("./App-CTh6owSM.mjs");
|
|
60
60
|
return App;
|
|
61
61
|
},
|
|
62
62
|
permissions: [
|
|
@@ -71,7 +71,7 @@ const index = {
|
|
|
71
71
|
defaultMessage: "Estudio de imágenes"
|
|
72
72
|
},
|
|
73
73
|
Component: async () => {
|
|
74
|
-
const { StudioApp } = await import("./StudioApp-
|
|
74
|
+
const { StudioApp } = await import("./StudioApp-CrGn-9Gg.mjs");
|
|
75
75
|
return StudioApp;
|
|
76
76
|
},
|
|
77
77
|
permissions: [
|
|
@@ -91,7 +91,7 @@ const index = {
|
|
|
91
91
|
defaultMessage: PLUGIN_ID
|
|
92
92
|
},
|
|
93
93
|
Component: async () => {
|
|
94
|
-
const component = await import("./Settings-
|
|
94
|
+
const component = await import("./Settings-CdGW2O8h.mjs");
|
|
95
95
|
return component.default;
|
|
96
96
|
},
|
|
97
97
|
to: `plugins/${PLUGIN_ID}/settings`,
|
package/dist/admin/index.js
CHANGED
package/dist/admin/index.mjs
CHANGED
|
@@ -3,9 +3,11 @@ interface ColorFieldProps {
|
|
|
3
3
|
value: string;
|
|
4
4
|
onChange: (value: string) => void;
|
|
5
5
|
}
|
|
6
|
+
/** «#0af», «0AF», «00aaff» o «#00AAFF» → «#00AAFF». Cualquier otra cosa → null. */
|
|
7
|
+
export declare const parseHex: (raw: string) => string | null;
|
|
6
8
|
/**
|
|
7
|
-
* Selector de color (snowflake: el DS no tiene uno).
|
|
8
|
-
*
|
|
9
|
+
* Selector de color (snowflake: el DS no tiene uno). La muestra abre el selector de color del
|
|
10
|
+
* navegador y el campo acepta un hexadecimal escrito o pegado; solo se aplica cuando es válido.
|
|
9
11
|
*/
|
|
10
12
|
export declare const ColorField: ({ label, value, onChange }: ColorFieldProps) => import("react/jsx-runtime").JSX.Element;
|
|
11
13
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Paleta InterWare (paleta canónica del brochure). Es la única que
|
|
3
|
-
* editor
|
|
2
|
+
* Paleta InterWare (paleta canónica del brochure). Es la única que usan las plantillas base; en el
|
|
3
|
+
* editor el color es libre (selector o hexadecimal).
|
|
4
4
|
*/
|
|
5
5
|
export declare const PALETTE: readonly [{
|
|
6
6
|
readonly label: "Navy";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tope por imagen de referencia o a editar; lo comparten el panel y el servidor. Medido en Vertex
|
|
3
|
+
* (2026-10-07, `scripts/vertex-diag-size.mjs`): una imagen generada en 2K pesa ~8 MB y en 4K hasta
|
|
4
|
+
* ~24 MB, y Google aceptó editarla en línea pese a los 7 MB que dice su documentación. 30 MB deja
|
|
5
|
+
* iterar sobre una 4K con margen.
|
|
6
|
+
*/
|
|
7
|
+
export declare const IMAGE_MAX_MB = 30;
|
|
8
|
+
export declare const IMAGE_MAX_BYTES: number;
|
|
9
|
+
export declare const IMAGE_FORMATS_HINT = "PNG, JPG o WEBP de hasta 30 MB.";
|
package/dist/server/index.js
CHANGED
|
@@ -266,14 +266,11 @@ const safetySettings = (safety) => HARM_CATEGORIES.map((category) => ({
|
|
|
266
266
|
category,
|
|
267
267
|
threshold: safety ? genai.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE : genai.HarmBlockThreshold.BLOCK_ONLY_HIGH
|
|
268
268
|
}));
|
|
269
|
-
const
|
|
270
|
-
none: "ALLOW_NONE",
|
|
271
|
-
adults: genai.PersonGeneration.ALLOW_ADULT
|
|
272
|
-
};
|
|
269
|
+
const NO_PEOPLE = "ALLOW_NONE";
|
|
273
270
|
const imageConfig = ({ aspectRatio, imageSize, allowPeople }, backend = "gemini-api") => ({
|
|
274
271
|
...aspectRatio ? { aspectRatio } : {},
|
|
275
272
|
...imageSize ? { imageSize } : {},
|
|
276
|
-
...backend === "vertex"
|
|
273
|
+
...backend === "vertex" && !allowPeople ? { personGeneration: NO_PEOPLE } : {}
|
|
277
274
|
});
|
|
278
275
|
const promptPart = (text2, allowPeople, backend = "gemini-api") => ({
|
|
279
276
|
text: backend === "gemini-api" && !allowPeople ? `${text2} ${PEOPLE_CLAUSE}` : text2
|
|
@@ -339,6 +336,10 @@ const EXPAND_INSTRUCTION = (concept) => [
|
|
|
339
336
|
`Concepto: ${concept}`
|
|
340
337
|
].join("\n");
|
|
341
338
|
class BlockedError extends Error {
|
|
339
|
+
constructor(message, detail) {
|
|
340
|
+
super(message);
|
|
341
|
+
this.detail = detail;
|
|
342
|
+
}
|
|
342
343
|
}
|
|
343
344
|
const sourceOf = (chunk) => {
|
|
344
345
|
if (chunk.web?.uri) return { title: chunk.web.title || new URL(chunk.web.uri).hostname, uri: chunk.web.uri };
|
|
@@ -360,12 +361,21 @@ const BLOCK_REASONS = {
|
|
|
360
361
|
PROHIBITED_CONTENT: "El modelo bloqueó la solicitud por contenido no permitido.",
|
|
361
362
|
IMAGE_SAFETY: "El modelo bloqueó la imagen resultante por seguridad."
|
|
362
363
|
};
|
|
364
|
+
const blockMessage = (reason, googleMessage) => {
|
|
365
|
+
const code = googleMessage.match(/Support code:\s*(\d+)/i)?.[1];
|
|
366
|
+
const suffix = code ? ` (Código de Google: ${code})` : "";
|
|
367
|
+
if (/PersonGeneration/i.test(googleMessage)) {
|
|
368
|
+
return /input image/i.test(googleMessage) ? `Google rechazó la imagen de referencia por la configuración de «Personas». Prueba con otra imagen o cambia «Personas».${suffix}` : `Google filtró la imagen generada por la configuración de «Personas». Cambia el prompt o «Personas».${suffix}`;
|
|
369
|
+
}
|
|
370
|
+
return `${BLOCK_REASONS[reason] ?? "El modelo no devolvió una imagen. Intenta con otro prompt."}${suffix}`;
|
|
371
|
+
};
|
|
363
372
|
const imageFrom = (res, model) => {
|
|
364
373
|
const parts = res.candidates?.[0]?.content?.parts ?? [];
|
|
365
374
|
const part = parts.find((p) => p.inlineData?.data && (p.inlineData.mimeType ?? "").startsWith("image/"));
|
|
366
375
|
if (part?.inlineData?.data) return { image: part.inlineData.data, mimeType: part.inlineData.mimeType ?? "image/png", model };
|
|
367
|
-
const reason = res.promptFeedback?.blockReason ?? res.candidates?.[0]?.finishReason;
|
|
368
|
-
|
|
376
|
+
const reason = String(res.promptFeedback?.blockReason ?? res.candidates?.[0]?.finishReason ?? "");
|
|
377
|
+
const googleMessage = [res.candidates?.[0]?.finishMessage, res.promptFeedback?.blockReasonMessage].filter(Boolean).join(" ");
|
|
378
|
+
throw new BlockedError(blockMessage(reason, googleMessage), [reason, googleMessage].filter(Boolean).join(": ") || void 0);
|
|
369
379
|
};
|
|
370
380
|
const inline = async (file, read) => inlinePart(await read(file.filepath), file.mimetype ?? "image/png");
|
|
371
381
|
const userTurn = (parts) => [{ role: "user", parts }];
|
|
@@ -418,10 +428,11 @@ const imageStudio$1 = ({ strapi }) => createImageStudio(async () => {
|
|
|
418
428
|
const { ai, backend } = await strapi.plugin("strapi-content-generator-gemini").service("service").geminiClient();
|
|
419
429
|
return { models: ai.models, backend };
|
|
420
430
|
});
|
|
431
|
+
const IMAGE_MAX_MB = 30;
|
|
432
|
+
const IMAGE_MAX_BYTES = IMAGE_MAX_MB * 1024 * 1024;
|
|
421
433
|
class InputError extends Error {
|
|
422
434
|
}
|
|
423
435
|
const ALLOWED = ["image/png", "image/jpeg", "image/webp"];
|
|
424
|
-
const MAX_BYTES = 10 * 1024 * 1024;
|
|
425
436
|
const str = (v) => typeof v === "string" ? v : "";
|
|
426
437
|
const bool = (v) => v === true || v === "true";
|
|
427
438
|
const list = (v) => v ? Array.isArray(v) ? v : [v] : [];
|
|
@@ -432,7 +443,7 @@ const clean = (text2, labels) => {
|
|
|
432
443
|
};
|
|
433
444
|
const checkImage = (f) => {
|
|
434
445
|
if (!ALLOWED.includes(f.mimetype ?? "")) throw new InputError("Las imágenes deben ser PNG, JPG o WEBP.");
|
|
435
|
-
if (f.size >
|
|
446
|
+
if (f.size > IMAGE_MAX_BYTES) throw new InputError(`Cada imagen puede pesar hasta ${IMAGE_MAX_MB} MB.`);
|
|
436
447
|
return f;
|
|
437
448
|
};
|
|
438
449
|
const parseGenerateInput = (fields, files) => {
|
|
@@ -486,7 +497,10 @@ const imageStudio = ({ strapi }) => {
|
|
|
486
497
|
ctx.body = await work();
|
|
487
498
|
} catch (err) {
|
|
488
499
|
if (err instanceof InputError) return fail$3(ctx, 400, "BadRequestError", err.message);
|
|
489
|
-
if (err instanceof BlockedError)
|
|
500
|
+
if (err instanceof BlockedError) {
|
|
501
|
+
strapi.log.warn(`[image-studio] bloqueo: ${err.detail ?? err.message}`);
|
|
502
|
+
return fail$3(ctx, 422, "UnprocessableEntityError", err.message);
|
|
503
|
+
}
|
|
490
504
|
strapi.log.error(`[image-studio] ${err instanceof Error ? err.message : String(err)}`);
|
|
491
505
|
return fail$3(ctx, 502, "BadGatewayError", "El servicio de imágenes no respondió. Intenta de nuevo en un momento.");
|
|
492
506
|
}
|
package/dist/server/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import { promises } from "fs";
|
|
3
|
-
import { HarmCategory,
|
|
3
|
+
import { HarmCategory, Modality, HarmBlockThreshold, GoogleGenAI, VideoGenerationReferenceType } from "@google/genai";
|
|
4
4
|
const PLUGIN_ID = "strapi-content-generator-gemini";
|
|
5
5
|
const FOLDER_UID = "plugin::upload.folder";
|
|
6
6
|
const hasValidPath = (path) => /^(\/\d+)+$/.test(path);
|
|
@@ -248,14 +248,11 @@ const safetySettings = (safety) => HARM_CATEGORIES.map((category) => ({
|
|
|
248
248
|
category,
|
|
249
249
|
threshold: safety ? HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE : HarmBlockThreshold.BLOCK_ONLY_HIGH
|
|
250
250
|
}));
|
|
251
|
-
const
|
|
252
|
-
none: "ALLOW_NONE",
|
|
253
|
-
adults: PersonGeneration.ALLOW_ADULT
|
|
254
|
-
};
|
|
251
|
+
const NO_PEOPLE = "ALLOW_NONE";
|
|
255
252
|
const imageConfig = ({ aspectRatio, imageSize, allowPeople }, backend = "gemini-api") => ({
|
|
256
253
|
...aspectRatio ? { aspectRatio } : {},
|
|
257
254
|
...imageSize ? { imageSize } : {},
|
|
258
|
-
...backend === "vertex"
|
|
255
|
+
...backend === "vertex" && !allowPeople ? { personGeneration: NO_PEOPLE } : {}
|
|
259
256
|
});
|
|
260
257
|
const promptPart = (text2, allowPeople, backend = "gemini-api") => ({
|
|
261
258
|
text: backend === "gemini-api" && !allowPeople ? `${text2} ${PEOPLE_CLAUSE}` : text2
|
|
@@ -321,6 +318,10 @@ const EXPAND_INSTRUCTION = (concept) => [
|
|
|
321
318
|
`Concepto: ${concept}`
|
|
322
319
|
].join("\n");
|
|
323
320
|
class BlockedError extends Error {
|
|
321
|
+
constructor(message, detail) {
|
|
322
|
+
super(message);
|
|
323
|
+
this.detail = detail;
|
|
324
|
+
}
|
|
324
325
|
}
|
|
325
326
|
const sourceOf = (chunk) => {
|
|
326
327
|
if (chunk.web?.uri) return { title: chunk.web.title || new URL(chunk.web.uri).hostname, uri: chunk.web.uri };
|
|
@@ -342,12 +343,21 @@ const BLOCK_REASONS = {
|
|
|
342
343
|
PROHIBITED_CONTENT: "El modelo bloqueó la solicitud por contenido no permitido.",
|
|
343
344
|
IMAGE_SAFETY: "El modelo bloqueó la imagen resultante por seguridad."
|
|
344
345
|
};
|
|
346
|
+
const blockMessage = (reason, googleMessage) => {
|
|
347
|
+
const code = googleMessage.match(/Support code:\s*(\d+)/i)?.[1];
|
|
348
|
+
const suffix = code ? ` (Código de Google: ${code})` : "";
|
|
349
|
+
if (/PersonGeneration/i.test(googleMessage)) {
|
|
350
|
+
return /input image/i.test(googleMessage) ? `Google rechazó la imagen de referencia por la configuración de «Personas». Prueba con otra imagen o cambia «Personas».${suffix}` : `Google filtró la imagen generada por la configuración de «Personas». Cambia el prompt o «Personas».${suffix}`;
|
|
351
|
+
}
|
|
352
|
+
return `${BLOCK_REASONS[reason] ?? "El modelo no devolvió una imagen. Intenta con otro prompt."}${suffix}`;
|
|
353
|
+
};
|
|
345
354
|
const imageFrom = (res, model) => {
|
|
346
355
|
const parts = res.candidates?.[0]?.content?.parts ?? [];
|
|
347
356
|
const part = parts.find((p) => p.inlineData?.data && (p.inlineData.mimeType ?? "").startsWith("image/"));
|
|
348
357
|
if (part?.inlineData?.data) return { image: part.inlineData.data, mimeType: part.inlineData.mimeType ?? "image/png", model };
|
|
349
|
-
const reason = res.promptFeedback?.blockReason ?? res.candidates?.[0]?.finishReason;
|
|
350
|
-
|
|
358
|
+
const reason = String(res.promptFeedback?.blockReason ?? res.candidates?.[0]?.finishReason ?? "");
|
|
359
|
+
const googleMessage = [res.candidates?.[0]?.finishMessage, res.promptFeedback?.blockReasonMessage].filter(Boolean).join(" ");
|
|
360
|
+
throw new BlockedError(blockMessage(reason, googleMessage), [reason, googleMessage].filter(Boolean).join(": ") || void 0);
|
|
351
361
|
};
|
|
352
362
|
const inline = async (file, read) => inlinePart(await read(file.filepath), file.mimetype ?? "image/png");
|
|
353
363
|
const userTurn = (parts) => [{ role: "user", parts }];
|
|
@@ -400,10 +410,11 @@ const imageStudio$1 = ({ strapi }) => createImageStudio(async () => {
|
|
|
400
410
|
const { ai, backend } = await strapi.plugin("strapi-content-generator-gemini").service("service").geminiClient();
|
|
401
411
|
return { models: ai.models, backend };
|
|
402
412
|
});
|
|
413
|
+
const IMAGE_MAX_MB = 30;
|
|
414
|
+
const IMAGE_MAX_BYTES = IMAGE_MAX_MB * 1024 * 1024;
|
|
403
415
|
class InputError extends Error {
|
|
404
416
|
}
|
|
405
417
|
const ALLOWED = ["image/png", "image/jpeg", "image/webp"];
|
|
406
|
-
const MAX_BYTES = 10 * 1024 * 1024;
|
|
407
418
|
const str = (v) => typeof v === "string" ? v : "";
|
|
408
419
|
const bool = (v) => v === true || v === "true";
|
|
409
420
|
const list = (v) => v ? Array.isArray(v) ? v : [v] : [];
|
|
@@ -414,7 +425,7 @@ const clean = (text2, labels) => {
|
|
|
414
425
|
};
|
|
415
426
|
const checkImage = (f) => {
|
|
416
427
|
if (!ALLOWED.includes(f.mimetype ?? "")) throw new InputError("Las imágenes deben ser PNG, JPG o WEBP.");
|
|
417
|
-
if (f.size >
|
|
428
|
+
if (f.size > IMAGE_MAX_BYTES) throw new InputError(`Cada imagen puede pesar hasta ${IMAGE_MAX_MB} MB.`);
|
|
418
429
|
return f;
|
|
419
430
|
};
|
|
420
431
|
const parseGenerateInput = (fields, files) => {
|
|
@@ -468,7 +479,10 @@ const imageStudio = ({ strapi }) => {
|
|
|
468
479
|
ctx.body = await work();
|
|
469
480
|
} catch (err) {
|
|
470
481
|
if (err instanceof InputError) return fail$3(ctx, 400, "BadRequestError", err.message);
|
|
471
|
-
if (err instanceof BlockedError)
|
|
482
|
+
if (err instanceof BlockedError) {
|
|
483
|
+
strapi.log.warn(`[image-studio] bloqueo: ${err.detail ?? err.message}`);
|
|
484
|
+
return fail$3(ctx, 422, "UnprocessableEntityError", err.message);
|
|
485
|
+
}
|
|
472
486
|
strapi.log.error(`[image-studio] ${err instanceof Error ? err.message : String(err)}`);
|
|
473
487
|
return fail$3(ctx, 502, "BadGatewayError", "El servicio de imágenes no respondió. Intenta de nuevo en un momento.");
|
|
474
488
|
}
|
|
@@ -4,8 +4,13 @@ import type { Core } from '@strapi/strapi';
|
|
|
4
4
|
import type { GoogleGenAI } from '@google/genai';
|
|
5
5
|
import type { Backend } from '../image-studio/geminiConfig';
|
|
6
6
|
import type { EditInput, GenerateInput } from '../image-studio/validate';
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* El modelo no devolvió imagen (bloqueo de seguridad u otra causa): el controlador responde 422 con
|
|
9
|
+
* `message` y registra `detail` (motivo y mensaje de Google, con su código de soporte).
|
|
10
|
+
*/
|
|
8
11
|
export declare class BlockedError extends Error {
|
|
12
|
+
readonly detail?: string;
|
|
13
|
+
constructor(message: string, detail?: string);
|
|
9
14
|
}
|
|
10
15
|
type ModelsApi = Pick<GoogleGenAI['models'], 'generateContent' | 'list'>;
|
|
11
16
|
type ReadFile = (path: string) => Promise<Buffer>;
|
package/package.json
CHANGED