@interware/content-generator 0.6.1 → 0.7.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/_chunks/App-Cwr5ra5m.js +1380 -0
- package/dist/_chunks/App-h4_txTsI.mjs +1362 -0
- package/dist/_chunks/{ImageStudioTabs-BLAH3KYv.mjs → ImageStudioTabs-B-mOwDfY.mjs} +687 -682
- package/dist/_chunks/{ImageStudioTabs-DtusqpQO.js → ImageStudioTabs-CG4KTn1S.js} +683 -678
- package/dist/_chunks/{Settings-o2GngLaW.mjs → Settings-BT-EqvXs.mjs} +2 -2
- package/dist/_chunks/{Settings-BQXxV3LO.js → Settings-D4NGBTk1.js} +4 -4
- package/dist/_chunks/{StudioApp-BaySMwrk.js → StudioApp-_ViSAwCm.js} +2 -2
- package/dist/_chunks/{StudioApp-C_znlQRo.mjs → StudioApp-xaEy-uIk.mjs} +2 -2
- package/dist/_chunks/{index-BKigRXm3.mjs → index-D67xWUPz.mjs} +3 -28
- package/dist/_chunks/{index-BKhe-Jyx.js → index-NA9OuMGs.js} +3 -28
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/src/features/articles/ArticlePage.d.ts +5 -0
- package/dist/admin/src/features/articles/ArticlesPage.d.ts +7 -0
- package/dist/admin/src/features/articles/NewArticlePage.d.ts +6 -0
- package/dist/admin/src/features/articles/SharePreview.d.ts +19 -0
- package/dist/admin/src/features/articles/articlesApi.d.ts +94 -0
- package/dist/admin/src/features/articles/model.d.ts +24 -0
- package/dist/admin/src/features/articles/useArticlesApi.d.ts +19 -0
- package/dist/admin/src/features/library/LibraryImagePicker.d.ts +6 -1
- package/dist/admin/src/lib/postText.d.ts +25 -0
- package/dist/server/index.js +514 -320
- package/dist/server/index.mjs +514 -320
- package/dist/server/src/articles/generation.d.ts +135 -0
- package/dist/server/src/articles/schedule.d.ts +12 -0
- package/dist/server/src/articles/sources.d.ts +12 -0
- package/dist/server/src/bootstrap.d.ts +2 -0
- package/dist/server/src/controllers/articles.d.ts +30 -0
- package/dist/server/src/controllers/controller.d.ts +0 -4
- package/dist/server/src/controllers/index.d.ts +92 -4
- package/dist/server/src/index.d.ts +122 -67
- package/dist/server/src/services/articles.d.ts +126 -0
- package/dist/server/src/services/image-studio.d.ts +0 -10
- package/dist/server/src/services/index.d.ts +30 -63
- package/dist/server/src/services/service.d.ts +1 -62
- package/package.json +1 -1
- package/dist/_chunks/App-CJxSC8yt.js +0 -1105
- package/dist/_chunks/App-qbgUXlOZ.mjs +0 -1087
- package/dist/_chunks/getTranslation-B6Bsu-uq.mjs +0 -5
- package/dist/_chunks/getTranslation-BRVMWSf8.js +0 -4
- package/dist/admin/src/components/commons/dialog/index.d.ts +0 -12
- package/dist/admin/src/components/commons/generate-image-dialog/index.d.ts +0 -5
- package/dist/admin/src/components/generate-content/Form.d.ts +0 -15
- package/dist/admin/src/components/home/table.d.ts +0 -13
- package/dist/admin/src/components/index.d.ts +0 -12
- package/dist/admin/src/containers/generate-content/index.d.ts +0 -2
- package/dist/admin/src/containers/home/index.d.ts +0 -2
- package/dist/admin/src/hooks/useDialog.d.ts +0 -11
- package/dist/admin/src/lib/use-cases/articles/index.d.ts +0 -16
- package/dist/admin/src/lib/use-cases/categories/index.d.ts +0 -16
- package/dist/admin/src/lib/use-cases/facebook/index.d.ts +0 -5
- package/dist/admin/src/lib/use-cases/generate-content/index.d.ts +0 -6
- package/dist/admin/src/lib/use-cases/linkedin/index.d.ts +0 -5
- package/dist/admin/src/lib/use-cases/users/index.d.ts +0 -16
- package/dist/admin/src/pages/GenerateContent.d.ts +0 -2
- package/dist/admin/src/pages/HomePage.d.ts +0 -2
- package/dist/admin/src/reducers/dialog/index.d.ts +0 -6
- package/dist/admin/src/types/reducers.d.ts +0 -4
|
@@ -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-NA9OuMGs.js");
|
|
6
6
|
const react = require("react");
|
|
7
7
|
const icons = require("@strapi/icons");
|
|
8
8
|
const styledComponents = require("styled-components");
|
|
@@ -11,645 +11,735 @@ const htmlToImage = require("html-to-image");
|
|
|
11
11
|
const qrcode = require("qrcode-generator");
|
|
12
12
|
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
13
13
|
const qrcode__default = /* @__PURE__ */ _interopDefault(qrcode);
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
role: "radio",
|
|
22
|
-
"aria-checked": on,
|
|
23
|
-
onClick: () => onChange(option),
|
|
24
|
-
color: on ? "neutral800" : "neutral600",
|
|
25
|
-
background: on ? "neutral0" : "transparent",
|
|
26
|
-
shadow: on ? "filterShadow" : void 0,
|
|
27
|
-
hasRadius: true,
|
|
28
|
-
paddingLeft: 4,
|
|
29
|
-
paddingRight: 4,
|
|
30
|
-
style: { height: 36, border: 0, cursor: "pointer" },
|
|
31
|
-
children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
|
|
32
|
-
/* @__PURE__ */ jsxRuntime.jsx(Icon, { width: 16, height: 16 }),
|
|
33
|
-
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { fontWeight: on ? "bold" : "regular", textColor: on ? "neutral800" : "neutral600", children: text })
|
|
34
|
-
] })
|
|
35
|
-
},
|
|
36
|
-
option
|
|
37
|
-
);
|
|
38
|
-
}) });
|
|
39
|
-
const CHECKERBOARD = "repeating-conic-gradient(#e6e6ec 0% 25%, #ffffff 0% 50%) 50% / 16px 16px";
|
|
40
|
-
const useThemeColors = () => {
|
|
41
|
-
const theme = styledComponents.useTheme();
|
|
42
|
-
return theme?.colors ?? designSystem.lightTheme.colors;
|
|
43
|
-
};
|
|
44
|
-
const RangeField = ({ label, value, min, max, step = 1, onChange, format = String }) => {
|
|
45
|
-
const c = useThemeColors();
|
|
46
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 1, children: [
|
|
47
|
-
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", children: [
|
|
48
|
-
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", children: label }),
|
|
49
|
-
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: format(value) })
|
|
50
|
-
] }),
|
|
51
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
52
|
-
"input",
|
|
53
|
-
{
|
|
54
|
-
type: "range",
|
|
55
|
-
"aria-label": label,
|
|
56
|
-
min,
|
|
57
|
-
max,
|
|
58
|
-
step,
|
|
59
|
-
value,
|
|
60
|
-
onChange: (e) => onChange(Number(e.target.value)),
|
|
61
|
-
style: { width: "100%", accentColor: c.primary600 }
|
|
62
|
-
}
|
|
63
|
-
)
|
|
64
|
-
] });
|
|
65
|
-
};
|
|
66
|
-
const SOFTNESS = 0.06;
|
|
67
|
-
const MAX_DISTANCE = Math.sqrt(3) * 255;
|
|
68
|
-
const colorKeyAlpha = (rgba, width, height, points, tolerance, { contiguous = true } = {}) => {
|
|
69
|
-
const alpha = new Uint8ClampedArray(width * height).fill(255);
|
|
70
|
-
const toAlpha = (d) => d <= tolerance ? 0 : d <= tolerance + SOFTNESS ? Math.round(255 * (d - tolerance) / SOFTNESS) : 255;
|
|
71
|
-
const inside = points.filter(({ x, y }) => x >= 0 && y >= 0 && x < width && y < height);
|
|
72
|
-
if (!contiguous) {
|
|
73
|
-
const seeds = inside.map(({ x, y }) => 4 * (Math.floor(y) * width + Math.floor(x))).map((s) => [rgba[s], rgba[s + 1], rgba[s + 2]]);
|
|
74
|
-
for (let i = 0; i < alpha.length; i++) {
|
|
75
|
-
for (const [r, g, b] of seeds) {
|
|
76
|
-
const dr = rgba[4 * i] - r;
|
|
77
|
-
const dg = rgba[4 * i + 1] - g;
|
|
78
|
-
const db = rgba[4 * i + 2] - b;
|
|
79
|
-
alpha[i] = Math.min(alpha[i], toAlpha(Math.sqrt(dr * dr + dg * dg + db * db) / MAX_DISTANCE));
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return alpha;
|
|
14
|
+
const flattenTree = (tree, parentId = null, depth = 0) => tree.flatMap((f) => [{ id: f.id, name: f.name, parentId, depth }, ...flattenTree(f.children ?? [], f.id, depth + 1)]);
|
|
15
|
+
const pathTo = (tree, id) => {
|
|
16
|
+
if (id === null) return [];
|
|
17
|
+
for (const f of tree) {
|
|
18
|
+
if (f.id === id) return [f];
|
|
19
|
+
const sub = pathTo(f.children ?? [], id);
|
|
20
|
+
if (sub.length) return [f, ...sub];
|
|
83
21
|
}
|
|
84
|
-
|
|
85
|
-
const stack = [];
|
|
86
|
-
inside.forEach(({ x, y }, n) => {
|
|
87
|
-
const mark = n + 1;
|
|
88
|
-
const start = Math.floor(y) * width + Math.floor(x);
|
|
89
|
-
const [r, g, b] = [rgba[4 * start], rgba[4 * start + 1], rgba[4 * start + 2]];
|
|
90
|
-
stack.push(start);
|
|
91
|
-
seen[start] = mark;
|
|
92
|
-
while (stack.length) {
|
|
93
|
-
const i = stack.pop();
|
|
94
|
-
const dr = rgba[4 * i] - r;
|
|
95
|
-
const dg = rgba[4 * i + 1] - g;
|
|
96
|
-
const db = rgba[4 * i + 2] - b;
|
|
97
|
-
const d = Math.sqrt(dr * dr + dg * dg + db * db) / MAX_DISTANCE;
|
|
98
|
-
if (d > tolerance + SOFTNESS) continue;
|
|
99
|
-
alpha[i] = Math.min(alpha[i], toAlpha(d));
|
|
100
|
-
if (d > tolerance) continue;
|
|
101
|
-
const px = i % width;
|
|
102
|
-
const visit = (j) => {
|
|
103
|
-
if (j < 0 || j >= alpha.length || seen[j] === mark) return;
|
|
104
|
-
seen[j] = mark;
|
|
105
|
-
stack.push(j);
|
|
106
|
-
};
|
|
107
|
-
if (px > 0) visit(i - 1);
|
|
108
|
-
if (px < width - 1) visit(i + 1);
|
|
109
|
-
visit(i - width);
|
|
110
|
-
visit(i + width);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
return alpha;
|
|
114
|
-
};
|
|
115
|
-
const applyKeyAlpha = (rgba, alpha) => {
|
|
116
|
-
const out = new Uint8ClampedArray(rgba);
|
|
117
|
-
for (let i = 0; i < alpha.length; i++) out[4 * i + 3] = Math.min(out[4 * i + 3], alpha[i]);
|
|
118
|
-
return out;
|
|
119
|
-
};
|
|
120
|
-
const canvas = (width, height) => {
|
|
121
|
-
const c = document.createElement("canvas");
|
|
122
|
-
c.width = width;
|
|
123
|
-
c.height = height;
|
|
124
|
-
const ctx = c.getContext("2d", { willReadFrequently: true });
|
|
125
|
-
if (!ctx) throw new Error("Canvas 2D no disponible");
|
|
126
|
-
ctx.imageSmoothingQuality = "high";
|
|
127
|
-
return { c, ctx };
|
|
22
|
+
return [];
|
|
128
23
|
};
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
});
|
|
135
|
-
const loadPixels = async (src) => {
|
|
136
|
-
const img = await loadImage(src);
|
|
137
|
-
const { ctx } = canvas(img.naturalWidth, img.naturalHeight);
|
|
138
|
-
ctx.drawImage(img, 0, 0);
|
|
139
|
-
return { width: img.naturalWidth, height: img.naturalHeight, data: ctx.getImageData(0, 0, img.naturalWidth, img.naturalHeight).data };
|
|
24
|
+
const childrenOf = (tree, id) => id === null ? tree : pathTo(tree, id).at(-1)?.children ?? [];
|
|
25
|
+
const cleanFolderName = (name) => name.trim().replace(/\s*\/\s*/g, " - ");
|
|
26
|
+
const nameTaken = (tree, parentId, name) => {
|
|
27
|
+
const target = cleanFolderName(name).toLocaleLowerCase("es");
|
|
28
|
+
return childrenOf(tree, parentId).some((f) => f.name.toLocaleLowerCase("es") === target);
|
|
140
29
|
};
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
30
|
+
const safeDeletePlan = (tree, folderId, fileIds) => {
|
|
31
|
+
const path = pathTo(tree, folderId);
|
|
32
|
+
const folder = path.at(-1);
|
|
33
|
+
if (!folder) return null;
|
|
34
|
+
const folderIds = (folder.children ?? []).map((c) => c.id);
|
|
35
|
+
const move = fileIds.length || folderIds.length ? { destinationFolderId: path.length > 1 ? path[path.length - 2].id : null, fileIds, folderIds } : null;
|
|
36
|
+
return { move, remove: [folderId] };
|
|
145
37
|
};
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
38
|
+
const noBackgroundName = (name) => `${name.replace(/\.[a-z0-9]+$/i, "")} (sin fondo).png`;
|
|
39
|
+
const IMAGE_MIMES = ["image/png", "image/jpeg", "image/webp"];
|
|
40
|
+
const fileForm = (fileInfo, file) => {
|
|
41
|
+
const fd = new FormData();
|
|
42
|
+
if (file) fd.append("files", file);
|
|
43
|
+
fd.append("fileInfo", JSON.stringify(fileInfo));
|
|
44
|
+
return fd;
|
|
150
45
|
};
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
46
|
+
const createUploadApi = (client) => {
|
|
47
|
+
const api = {
|
|
48
|
+
async getTree() {
|
|
49
|
+
const { data } = await client.get("/upload/folder-structure");
|
|
50
|
+
return data?.data ?? [];
|
|
51
|
+
},
|
|
52
|
+
async getCounts() {
|
|
53
|
+
const { data } = await client.get("/upload/folders", {
|
|
54
|
+
params: { populate: { files: { count: true }, children: { count: true } } }
|
|
55
|
+
});
|
|
56
|
+
return Object.fromEntries(
|
|
57
|
+
(data?.data ?? []).map((f) => [
|
|
58
|
+
f.id,
|
|
59
|
+
{ files: f.files?.count ?? 0, children: f.children?.count ?? 0 }
|
|
60
|
+
])
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
/** Archivos de una carpeta; con `mimes`, solo esos tipos (el selector de imágenes, S4.13). */
|
|
64
|
+
async listFiles(folderId, { mimes } = {}) {
|
|
65
|
+
const byFolder = { folder: { id: folderId === null ? { $null: true } : { $eq: folderId } } };
|
|
66
|
+
const { data } = await client.get("/upload/files", {
|
|
67
|
+
params: {
|
|
68
|
+
page: 1,
|
|
69
|
+
pageSize: 100,
|
|
70
|
+
sort: "updatedAt:DESC",
|
|
71
|
+
filters: { $and: mimes?.length ? [byFolder, { mime: { $in: mimes } }] : [byFolder] }
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return data?.results ?? [];
|
|
75
|
+
},
|
|
76
|
+
/** Las más recientes de varias carpetas a la vez («Recientes» del Generador, S4.14). */
|
|
77
|
+
async listRecent({ folderIds, mimes, limit }) {
|
|
78
|
+
const inFolders = { folder: { id: { $in: folderIds } } };
|
|
79
|
+
const { data } = await client.get("/upload/files", {
|
|
80
|
+
params: {
|
|
81
|
+
page: 1,
|
|
82
|
+
pageSize: limit,
|
|
83
|
+
sort: "updatedAt:DESC",
|
|
84
|
+
filters: { $and: mimes?.length ? [inFolders, { mime: { $in: mimes } }] : [inFolders] }
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return data?.results ?? [];
|
|
88
|
+
},
|
|
89
|
+
async createFolder(name, parent) {
|
|
90
|
+
const { data } = await client.post("/upload/folders", { name, parent });
|
|
91
|
+
return data?.data;
|
|
92
|
+
},
|
|
93
|
+
async renameFolder(id, name, parent) {
|
|
94
|
+
await client.put(`/upload/folders/${id}`, { name, parent });
|
|
95
|
+
},
|
|
96
|
+
async uploadFiles(files, folderId) {
|
|
97
|
+
const uploaded = [];
|
|
98
|
+
for (const file of files) {
|
|
99
|
+
const { data } = await client.post("/upload", fileForm({ name: file.name, folder: folderId }, file));
|
|
100
|
+
uploaded.push(...Array.isArray(data) ? data : [data]);
|
|
101
|
+
}
|
|
102
|
+
return uploaded;
|
|
103
|
+
},
|
|
104
|
+
async renameFile(file, name) {
|
|
105
|
+
await client.post(
|
|
106
|
+
`/upload?id=${file.id}`,
|
|
107
|
+
fileForm({ name, folder: file.folder?.id ?? null, alternativeText: file.alternativeText, caption: file.caption })
|
|
108
|
+
);
|
|
109
|
+
},
|
|
110
|
+
async replaceFile(id, file) {
|
|
111
|
+
await client.post(`/upload?id=${id}`, fileForm({ name: file.name }, file));
|
|
112
|
+
},
|
|
113
|
+
async move({ fileIds = [], folderIds = [] }, destinationFolderId) {
|
|
114
|
+
await client.post("/upload/actions/bulk-move", { destinationFolderId, fileIds, folderIds });
|
|
115
|
+
},
|
|
116
|
+
async remove({ fileIds = [], folderIds = [] }) {
|
|
117
|
+
await client.post("/upload/actions/bulk-delete", { fileIds, folderIds });
|
|
118
|
+
},
|
|
119
|
+
/** Elimina una carpeta dejando sus archivos y subcarpetas en la carpeta superior (D1). */
|
|
120
|
+
deleteFolderSafely: (tree, folderId) => deleteFolderSafely(api, tree, folderId)
|
|
121
|
+
};
|
|
122
|
+
return api;
|
|
160
123
|
};
|
|
161
|
-
const
|
|
162
|
-
|
|
124
|
+
const deleteFolderSafely = async (api, tree, folderId) => {
|
|
125
|
+
const files = await api.listFiles(folderId);
|
|
126
|
+
const plan = safeDeletePlan(tree, folderId, files.map((f) => f.id));
|
|
127
|
+
if (!plan) return;
|
|
128
|
+
if (plan.move) await api.move(plan.move, plan.move.destinationFolderId);
|
|
129
|
+
await api.remove({ folderIds: plan.remove });
|
|
163
130
|
};
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const removeBackgroundError = (err) => err instanceof RemoveBackgroundTimeout ? TIMEOUT_MESSAGE : "No se pudo quitar el fondo.";
|
|
173
|
-
let loading = null;
|
|
174
|
-
const loadModel = (onStatus) => {
|
|
175
|
-
if (!loading) {
|
|
176
|
-
const current = (async () => {
|
|
177
|
-
const worker = new Worker(WORKER_URL, { type: "module" });
|
|
178
|
-
const pending = /* @__PURE__ */ new Map();
|
|
179
|
-
let seq2 = 0;
|
|
180
|
-
let status2 = onStatus;
|
|
181
|
-
const discard = (err) => {
|
|
182
|
-
worker.terminate();
|
|
183
|
-
if (loading === current) loading = null;
|
|
184
|
-
pending.forEach(({ reject }) => reject(err));
|
|
185
|
-
pending.clear();
|
|
186
|
-
};
|
|
187
|
-
await new Promise((resolve, reject) => {
|
|
188
|
-
worker.onmessage = (e) => {
|
|
189
|
-
const m = e.data;
|
|
190
|
-
if (m.type === "status") status2?.(m.status);
|
|
191
|
-
else if (m.type === "ready") resolve();
|
|
192
|
-
else if (m.type === "mask") {
|
|
193
|
-
pending.get(m.id)?.resolve(new Float32Array(m.mask));
|
|
194
|
-
pending.delete(m.id);
|
|
195
|
-
} else if (m.type === "error") {
|
|
196
|
-
const err = new Error(m.message ?? "Error del modelo");
|
|
197
|
-
if (m.id === void 0) reject(err);
|
|
198
|
-
else {
|
|
199
|
-
pending.get(m.id)?.reject(err);
|
|
200
|
-
pending.delete(m.id);
|
|
201
|
-
discard(err);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
};
|
|
205
|
-
worker.onerror = () => reject(new Error("El worker de «Quitar fondo» no cargó."));
|
|
206
|
-
worker.postMessage({ type: "load" });
|
|
207
|
-
}).catch((err) => {
|
|
208
|
-
discard(err);
|
|
209
|
-
throw err;
|
|
210
|
-
});
|
|
211
|
-
status2 = void 0;
|
|
212
|
-
return {
|
|
213
|
-
run: (rgba) => new Promise((resolve, reject) => {
|
|
214
|
-
const id = ++seq2;
|
|
215
|
-
const timer = setTimeout(() => {
|
|
216
|
-
pending.get(id)?.reject(new RemoveBackgroundTimeout());
|
|
217
|
-
pending.delete(id);
|
|
218
|
-
discard(new RemoveBackgroundTimeout());
|
|
219
|
-
}, RUN_TIMEOUT_MS);
|
|
220
|
-
pending.set(id, {
|
|
221
|
-
resolve: (m) => {
|
|
222
|
-
clearTimeout(timer);
|
|
223
|
-
resolve(m);
|
|
224
|
-
},
|
|
225
|
-
reject: (e) => {
|
|
226
|
-
clearTimeout(timer);
|
|
227
|
-
reject(e);
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
worker.postMessage({ type: "run", id, rgba: rgba.buffer }, [rgba.buffer]);
|
|
231
|
-
})
|
|
232
|
-
};
|
|
233
|
-
})();
|
|
234
|
-
loading = current;
|
|
235
|
-
current.catch(() => {
|
|
236
|
-
if (loading === current) loading = null;
|
|
131
|
+
const STUDIO_ROOT = "Generated";
|
|
132
|
+
const ROOT_LABEL = "/";
|
|
133
|
+
const scopeToFolder = (base, rootName) => {
|
|
134
|
+
let root = null;
|
|
135
|
+
const rootId = () => {
|
|
136
|
+
root ??= base.getTree().then(async (tree) => tree.find((f) => f.name === rootName)?.id ?? (await base.createFolder(rootName, null)).id);
|
|
137
|
+
root.catch(() => {
|
|
138
|
+
root = null;
|
|
237
139
|
});
|
|
238
|
-
|
|
239
|
-
|
|
140
|
+
return root;
|
|
141
|
+
};
|
|
142
|
+
const at = async (id) => id ?? rootId();
|
|
143
|
+
const scoped = {
|
|
144
|
+
...base,
|
|
145
|
+
async getTree() {
|
|
146
|
+
const id = await rootId();
|
|
147
|
+
return (await base.getTree()).find((f) => f.id === id)?.children ?? [];
|
|
148
|
+
},
|
|
149
|
+
listFiles: async (folderId, options) => base.listFiles(await at(folderId), options),
|
|
150
|
+
// Toda la biblioteca del Estudio: la raíz y sus subcarpetas; las carpetas pedidas se ignoran.
|
|
151
|
+
listRecent: async (options) => {
|
|
152
|
+
const id = await rootId();
|
|
153
|
+
const children = (await base.getTree()).find((f) => f.id === id)?.children ?? [];
|
|
154
|
+
return base.listRecent({ ...options, folderIds: [id, ...flattenTree(children).map((f) => f.id)] });
|
|
155
|
+
},
|
|
156
|
+
createFolder: async (name, parent) => base.createFolder(name, await at(parent)),
|
|
157
|
+
renameFolder: async (id, name, parent) => base.renameFolder(id, name, await at(parent)),
|
|
158
|
+
uploadFiles: async (files, folderId) => base.uploadFiles(files, await at(folderId)),
|
|
159
|
+
move: async (selection, destination) => base.move(selection, await at(destination)),
|
|
160
|
+
deleteFolderSafely: (tree, folderId) => deleteFolderSafely(scoped, tree, folderId)
|
|
161
|
+
};
|
|
162
|
+
return scoped;
|
|
240
163
|
};
|
|
241
|
-
const
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const h = INPUT_SIZE;
|
|
248
|
-
const small = canvas(w, h);
|
|
249
|
-
small.ctx.drawImage(img, 0, 0, w, h);
|
|
250
|
-
const mask = await model.run(small.ctx.getImageData(0, 0, w, h).data);
|
|
251
|
-
const maskSmall = canvas(w, h);
|
|
252
|
-
maskSmall.ctx.putImageData(new ImageData(maskToRgba(mask, w, h), w, h), 0, 0);
|
|
253
|
-
const maskFull = canvas(W, H);
|
|
254
|
-
maskFull.ctx.drawImage(maskSmall.c, 0, 0, W, H);
|
|
255
|
-
const out = canvas(W, H);
|
|
256
|
-
out.ctx.drawImage(img, 0, 0);
|
|
257
|
-
const pixels = out.ctx.getImageData(0, 0, W, H);
|
|
258
|
-
applyAlpha(pixels.data, maskFull.ctx.getImageData(0, 0, W, H).data);
|
|
259
|
-
out.ctx.putImageData(pixels, 0, 0);
|
|
260
|
-
return out.c.toDataURL("image/png");
|
|
164
|
+
const dataUrlToBytes = (dataUrl) => {
|
|
165
|
+
const comma = dataUrl.indexOf(",");
|
|
166
|
+
const meta = dataUrl.slice(0, comma);
|
|
167
|
+
const payload = dataUrl.slice(comma + 1);
|
|
168
|
+
if (meta.endsWith(";base64")) return Uint8Array.from(atob(payload), (c) => c.charCodeAt(0));
|
|
169
|
+
return new TextEncoder().encode(decodeURIComponent(payload));
|
|
261
170
|
};
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
171
|
+
const dataUrlToBlob = (dataUrl) => {
|
|
172
|
+
const type = dataUrl.slice(5, dataUrl.indexOf(",")).split(";")[0];
|
|
173
|
+
return new Blob([dataUrlToBytes(dataUrl)], { type });
|
|
174
|
+
};
|
|
175
|
+
const DOWNLOAD_PROXY$1 = "/strapi-content-generator-gemini/download-file";
|
|
176
|
+
const useLibraryApi = () => {
|
|
177
|
+
const { get, post, put } = admin.useFetchClient();
|
|
178
|
+
const { toggleNotification } = admin.useNotification();
|
|
179
|
+
return react.useMemo(() => {
|
|
180
|
+
const base = scopeToFolder(createUploadApi({ get, post, put }), STUDIO_ROOT);
|
|
181
|
+
const dataUrlOf = async (file) => {
|
|
182
|
+
if (!file.url) throw new Error("El archivo no tiene URL");
|
|
183
|
+
const url = file.url.startsWith("/") ? `${window.location.origin}${file.url}` : file.url;
|
|
184
|
+
const { data } = await post(DOWNLOAD_PROXY$1, { url });
|
|
185
|
+
return `data:${data.contentType};base64,${data.base64}`;
|
|
186
|
+
};
|
|
187
|
+
const download = async (file) => {
|
|
188
|
+
if (!file.url) return;
|
|
189
|
+
const href = URL.createObjectURL(dataUrlToBlob(await dataUrlOf(file)));
|
|
190
|
+
const a = document.createElement("a");
|
|
191
|
+
a.href = href;
|
|
192
|
+
a.download = file.name;
|
|
193
|
+
a.click();
|
|
194
|
+
setTimeout(() => URL.revokeObjectURL(href), 1e3);
|
|
195
|
+
};
|
|
196
|
+
const notify = (type, message) => toggleNotification({ type, message });
|
|
197
|
+
const saveBlob = (blob, name, folderId) => base.uploadFiles([new File([blob], name, { type: blob.type })], folderId);
|
|
198
|
+
return { api: { ...base, dataUrlOf, download, saveBlob }, notify };
|
|
199
|
+
}, [get, post, put, toggleNotification]);
|
|
200
|
+
};
|
|
201
|
+
const thumb$1 = (f) => f.formats?.thumbnail?.url ?? f.formats?.small?.url ?? f.url;
|
|
202
|
+
const LibraryImagePicker = ({ open, max, onClose, onPick, title = "Elegir de la biblioteca", footerStart }) => {
|
|
203
|
+
const { api, notify } = useLibraryApi();
|
|
204
|
+
const [tree, setTree] = react.useState([]);
|
|
205
|
+
const [current, setCurrent] = react.useState(null);
|
|
206
|
+
const [files, setFiles] = react.useState([]);
|
|
207
|
+
const [loading2, setLoading] = react.useState(true);
|
|
208
|
+
const [picked, setPicked] = react.useState([]);
|
|
278
209
|
react.useEffect(() => {
|
|
279
210
|
if (!open) return;
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
setWholeImage(false);
|
|
285
|
-
setAuto({ status: "Preparando…" });
|
|
286
|
-
let live = true;
|
|
287
|
-
removeBackground(src, (status2) => live && setAuto({ status: status2 })).then((result2) => live && setAuto({ result: result2 })).catch((err) => live && setAuto({ error: removeBackgroundError(err) }));
|
|
288
|
-
return () => {
|
|
289
|
-
live = false;
|
|
290
|
-
};
|
|
291
|
-
}, [open, src]);
|
|
211
|
+
setPicked([]);
|
|
212
|
+
setCurrent(null);
|
|
213
|
+
api.getTree().then(setTree, () => notify("danger", "No se pudieron cargar las carpetas."));
|
|
214
|
+
}, [open, api, notify]);
|
|
292
215
|
react.useEffect(() => {
|
|
293
|
-
if (!open
|
|
294
|
-
let
|
|
295
|
-
|
|
216
|
+
if (!open) return;
|
|
217
|
+
let alive = true;
|
|
218
|
+
setLoading(true);
|
|
219
|
+
api.listFiles(current, { mimes: IMAGE_MIMES }).then((list) => alive && setFiles(list), () => notify("danger", "No se pudieron cargar las imágenes.")).finally(() => alive && setLoading(false));
|
|
296
220
|
return () => {
|
|
297
|
-
|
|
221
|
+
alive = false;
|
|
298
222
|
};
|
|
299
|
-
}, [open,
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}, [mode, pixels, keyed]);
|
|
307
|
-
const pick = (e) => {
|
|
308
|
-
if (!pixels) return;
|
|
309
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
310
|
-
const x = Math.floor((e.clientX - rect.left) / rect.width * pixels.width);
|
|
311
|
-
const y = Math.floor((e.clientY - rect.top) / rect.height * pixels.height);
|
|
312
|
-
setPoints((p) => [...p, { x, y }]);
|
|
313
|
-
};
|
|
314
|
-
const result = mode === "auto" ? auto.result : keyed && pixels ? "color" : void 0;
|
|
315
|
-
const apply = async () => {
|
|
316
|
-
const dataUrl = mode === "auto" ? auto.result : keyed && pixels ? pixelsToDataUrl(pixels.width, pixels.height, keyed) : void 0;
|
|
317
|
-
if (!dataUrl) return;
|
|
318
|
-
setApplying(true);
|
|
319
|
-
try {
|
|
320
|
-
await onApply(dataUrl);
|
|
321
|
-
} finally {
|
|
322
|
-
setApplying(false);
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
const frame = { maxWidth: "100%", maxHeight: "45vh", display: "block" };
|
|
326
|
-
return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Root, { open, onOpenChange: (o) => !o && onClose(), children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Modal.Content, { children: [
|
|
327
|
-
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Header, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Title, { children: "Quitar fondo" }) }),
|
|
223
|
+
}, [open, current, api, notify]);
|
|
224
|
+
const isPicked = (f) => picked.some((p) => p.id === f.id);
|
|
225
|
+
const toggle = (f) => setPicked((list) => list.some((p) => p.id === f.id) ? list.filter((p) => p.id !== f.id) : max === 1 ? [f] : [...list, f]);
|
|
226
|
+
const full = max > 1 && picked.length >= max;
|
|
227
|
+
const path = pathTo(tree, current);
|
|
228
|
+
return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Root, { open, onOpenChange: (o) => !o && onClose(), children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Modal.Content, { style: { width: "min(880px, 92vw)" }, children: [
|
|
229
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Header, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Title, { children: title }) }),
|
|
328
230
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Body, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 4, children: [
|
|
329
|
-
/* @__PURE__ */ jsxRuntime.
|
|
330
|
-
|
|
331
|
-
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
] })
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
border: `2px solid ${c.neutral0}`,
|
|
362
|
-
pointerEvents: "none"
|
|
363
|
-
}
|
|
364
|
-
},
|
|
365
|
-
i
|
|
366
|
-
))
|
|
367
|
-
] }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { small: true }))
|
|
368
|
-
] })
|
|
231
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, wrap: "wrap", alignItems: "center", children: [
|
|
232
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "ghost", size: "S", onClick: () => setCurrent(null), children: ROOT_LABEL }),
|
|
233
|
+
path.map((f) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 1, alignItems: "center", children: [
|
|
234
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.ChevronRight, { width: 12, height: 12 }),
|
|
235
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "ghost", size: "S", onClick: () => setCurrent(f.id), children: f.name })
|
|
236
|
+
] }, f.id))
|
|
237
|
+
] }),
|
|
238
|
+
childrenOf(tree, current).length > 0 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { gap: 2, wrap: "wrap", children: childrenOf(tree, current).map((f) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Folder, {}), "aria-label": `Abrir carpeta ${f.name}`, onClick: () => setCurrent(f.id), children: f.name }, f.id)) }),
|
|
239
|
+
loading2 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 6, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { small: true, children: "Cargando imágenes…" }) }) : files.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: "No hay imágenes en esta carpeta." }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Root, { gap: 3, children: files.map((f) => {
|
|
240
|
+
const on = isPicked(f);
|
|
241
|
+
return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 2, s: 4, xs: 6, direction: "column", alignItems: "stretch", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
242
|
+
designSystem.Box,
|
|
243
|
+
{
|
|
244
|
+
tag: "button",
|
|
245
|
+
type: "button",
|
|
246
|
+
color: "neutral800",
|
|
247
|
+
"aria-label": f.name,
|
|
248
|
+
"aria-pressed": on,
|
|
249
|
+
disabled: !on && full,
|
|
250
|
+
onClick: () => toggle(f),
|
|
251
|
+
padding: 1,
|
|
252
|
+
hasRadius: true,
|
|
253
|
+
background: on ? "primary100" : "neutral0",
|
|
254
|
+
borderColor: on ? "primary600" : "neutral200",
|
|
255
|
+
style: { cursor: !on && full ? "not-allowed" : "pointer", opacity: !on && full ? 0.5 : 1 },
|
|
256
|
+
children: [
|
|
257
|
+
/* @__PURE__ */ jsxRuntime.jsx("img", { src: thumb$1(f), alt: "", style: { width: "100%", aspectRatio: "1", objectFit: "cover", display: "block", borderRadius: 2 } }),
|
|
258
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", ellipsis: true, tag: "span", style: { display: "block", marginTop: 4 }, children: f.name })
|
|
259
|
+
]
|
|
260
|
+
}
|
|
261
|
+
) }, f.id);
|
|
262
|
+
}) })
|
|
369
263
|
] }) }),
|
|
370
264
|
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Modal.Footer, { children: [
|
|
371
|
-
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", onClick: onClose, children: "Cancelar" }),
|
|
265
|
+
footerStart ?? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", onClick: onClose, children: "Cancelar" }),
|
|
372
266
|
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
|
|
373
|
-
|
|
374
|
-
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, {
|
|
267
|
+
footerStart && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", onClick: onClose, children: "Cancelar" }),
|
|
268
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { disabled: picked.length === 0, onClick: () => onPick(picked), children: `Usar ${picked.length} ${picked.length === 1 ? "imagen" : "imágenes"}` })
|
|
375
269
|
] })
|
|
376
270
|
] })
|
|
377
271
|
] }) });
|
|
378
272
|
};
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
273
|
+
const PROMPT_MAX = 2e3;
|
|
274
|
+
const CONTROL = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g;
|
|
275
|
+
const INVISIBLE = /[----]/g;
|
|
276
|
+
const TAG = /<\/?[a-z][^>]*>/gi;
|
|
277
|
+
const sanitizePrompt = (text) => text.normalize("NFC").replace(CONTROL, "").replace(INVISIBLE, "").replace(TAG, "").replace(/\s+/g, " ").trim();
|
|
278
|
+
const PROMPT_LABELS = { label: "El prompt", empty: "Escribe un prompt." };
|
|
279
|
+
const INSTRUCTION_LABELS = { label: "La instrucción", empty: "Describe el cambio que quieres." };
|
|
280
|
+
const checkPrompt = (text, { label, empty } = PROMPT_LABELS) => {
|
|
281
|
+
const clean = sanitizePrompt(text);
|
|
282
|
+
if (!clean) return { ok: false, error: empty };
|
|
283
|
+
if ([...clean].length > PROMPT_MAX) return { ok: false, error: `${label} puede tener hasta ${PROMPT_MAX} caracteres.` };
|
|
284
|
+
return { ok: true, text: clean };
|
|
285
|
+
};
|
|
286
|
+
const useThemeColors = () => {
|
|
287
|
+
const theme = styledComponents.useTheme();
|
|
288
|
+
return theme?.colors ?? designSystem.lightTheme.colors;
|
|
289
|
+
};
|
|
290
|
+
const PromptComposer = (p) => {
|
|
291
|
+
const c = useThemeColors();
|
|
292
|
+
const id = react.useId();
|
|
293
|
+
const [focused, setFocused] = react.useState(false);
|
|
294
|
+
const check = checkPrompt(p.value, p.labels);
|
|
295
|
+
const error = !check.ok && p.value.trim() ? check.error : null;
|
|
296
|
+
const count = [...p.value].length;
|
|
297
|
+
const submit = () => {
|
|
298
|
+
if (check.ok && !p.disabled && !p.loading) p.onSubmit(check.text);
|
|
299
|
+
};
|
|
300
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 1, role: "group", "aria-labelledby": `${id}-label`, children: [
|
|
301
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { tag: "label", id: `${id}-label`, htmlFor: id, variant: "pi", fontWeight: "bold", children: p.label }),
|
|
302
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
303
|
+
designSystem.Box,
|
|
304
|
+
{
|
|
305
|
+
hasRadius: true,
|
|
306
|
+
background: "neutral0",
|
|
307
|
+
style: {
|
|
308
|
+
border: `1px solid ${error ? c.danger600 : focused ? c.primary600 : c.neutral200}`,
|
|
309
|
+
boxShadow: focused && !error ? `${c.primary600} 0 0 0 2px` : void 0
|
|
310
|
+
},
|
|
311
|
+
children: [
|
|
312
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
313
|
+
"textarea",
|
|
314
|
+
{
|
|
315
|
+
id,
|
|
316
|
+
value: p.value,
|
|
317
|
+
placeholder: p.placeholder,
|
|
318
|
+
"aria-invalid": Boolean(error),
|
|
319
|
+
"aria-describedby": `${id}-help`,
|
|
320
|
+
onChange: (e) => p.onChange(e.target.value),
|
|
321
|
+
onFocus: () => setFocused(true),
|
|
322
|
+
onBlur: () => setFocused(false),
|
|
323
|
+
onKeyDown: (e) => {
|
|
324
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
325
|
+
e.preventDefault();
|
|
326
|
+
submit();
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
rows: 4,
|
|
330
|
+
style: {
|
|
331
|
+
display: "block",
|
|
332
|
+
width: "100%",
|
|
333
|
+
boxSizing: "border-box",
|
|
334
|
+
resize: "vertical",
|
|
335
|
+
minHeight: 96,
|
|
336
|
+
padding: "12px 16px 4px",
|
|
337
|
+
border: 0,
|
|
338
|
+
outline: "none",
|
|
339
|
+
background: "transparent",
|
|
340
|
+
color: c.neutral800,
|
|
341
|
+
font: "inherit",
|
|
342
|
+
fontSize: 14,
|
|
343
|
+
lineHeight: 1.5
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
),
|
|
347
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", gap: 2, paddingLeft: 3, paddingRight: 2, paddingBottom: 2, children: [
|
|
348
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
|
|
349
|
+
p.tools,
|
|
350
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: count > PROMPT_MAX ? "danger600" : "neutral500", children: [
|
|
351
|
+
count,
|
|
352
|
+
"/",
|
|
353
|
+
PROMPT_MAX
|
|
354
|
+
] })
|
|
355
|
+
] }),
|
|
356
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { startIcon: p.actionIcon, loading: p.loading, disabled: p.disabled || !check.ok, onClick: submit, children: p.actionLabel })
|
|
357
|
+
] })
|
|
358
|
+
]
|
|
359
|
+
}
|
|
360
|
+
),
|
|
361
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { id: `${id}-help`, variant: "pi", textColor: error ? "danger600" : "neutral600", children: error ?? p.hint ?? "⌘/Ctrl + Enter también envía." })
|
|
362
|
+
] });
|
|
363
|
+
};
|
|
364
|
+
const SegmentedSwitch = ({ label, options, value, onChange }) => /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { role: "radiogroup", "aria-label": label, gap: 1, padding: 1, background: "neutral150", hasRadius: true, children: options.map(({ value: option, label: text, Icon }) => {
|
|
365
|
+
const on = value === option;
|
|
366
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
367
|
+
designSystem.Box,
|
|
368
|
+
{
|
|
369
|
+
tag: "button",
|
|
370
|
+
type: "button",
|
|
371
|
+
role: "radio",
|
|
372
|
+
"aria-checked": on,
|
|
373
|
+
onClick: () => onChange(option),
|
|
374
|
+
color: on ? "neutral800" : "neutral600",
|
|
375
|
+
background: on ? "neutral0" : "transparent",
|
|
376
|
+
shadow: on ? "filterShadow" : void 0,
|
|
377
|
+
hasRadius: true,
|
|
378
|
+
paddingLeft: 4,
|
|
379
|
+
paddingRight: 4,
|
|
380
|
+
style: { height: 36, border: 0, cursor: "pointer" },
|
|
381
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
|
|
382
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { width: 16, height: 16 }),
|
|
383
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { fontWeight: on ? "bold" : "regular", textColor: on ? "neutral800" : "neutral600", children: text })
|
|
384
|
+
] })
|
|
385
|
+
},
|
|
386
|
+
option
|
|
387
|
+
);
|
|
388
|
+
}) });
|
|
389
|
+
const CHECKERBOARD = "repeating-conic-gradient(#e6e6ec 0% 25%, #ffffff 0% 50%) 50% / 16px 16px";
|
|
390
|
+
const RangeField = ({ label, value, min, max, step = 1, onChange, format = String }) => {
|
|
391
|
+
const c = useThemeColors();
|
|
392
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 1, children: [
|
|
393
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", children: [
|
|
394
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", children: label }),
|
|
395
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: format(value) })
|
|
396
|
+
] }),
|
|
383
397
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
384
|
-
|
|
398
|
+
"input",
|
|
385
399
|
{
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
400
|
+
type: "range",
|
|
401
|
+
"aria-label": label,
|
|
402
|
+
min,
|
|
403
|
+
max,
|
|
404
|
+
step,
|
|
405
|
+
value,
|
|
406
|
+
onChange: (e) => onChange(Number(e.target.value)),
|
|
407
|
+
style: { width: "100%", accentColor: c.primary600 }
|
|
393
408
|
}
|
|
394
409
|
)
|
|
395
410
|
] });
|
|
396
411
|
};
|
|
397
|
-
const
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
412
|
+
const SOFTNESS = 0.06;
|
|
413
|
+
const MAX_DISTANCE = Math.sqrt(3) * 255;
|
|
414
|
+
const colorKeyAlpha = (rgba, width, height, points, tolerance, { contiguous = true } = {}) => {
|
|
415
|
+
const alpha = new Uint8ClampedArray(width * height).fill(255);
|
|
416
|
+
const toAlpha = (d) => d <= tolerance ? 0 : d <= tolerance + SOFTNESS ? Math.round(255 * (d - tolerance) / SOFTNESS) : 255;
|
|
417
|
+
const inside = points.filter(({ x, y }) => x >= 0 && y >= 0 && x < width && y < height);
|
|
418
|
+
if (!contiguous) {
|
|
419
|
+
const seeds = inside.map(({ x, y }) => 4 * (Math.floor(y) * width + Math.floor(x))).map((s) => [rgba[s], rgba[s + 1], rgba[s + 2]]);
|
|
420
|
+
for (let i = 0; i < alpha.length; i++) {
|
|
421
|
+
for (const [r, g, b] of seeds) {
|
|
422
|
+
const dr = rgba[4 * i] - r;
|
|
423
|
+
const dg = rgba[4 * i + 1] - g;
|
|
424
|
+
const db = rgba[4 * i + 2] - b;
|
|
425
|
+
alpha[i] = Math.min(alpha[i], toAlpha(Math.sqrt(dr * dr + dg * dg + db * db) / MAX_DISTANCE));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return alpha;
|
|
404
429
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
async getCounts() {
|
|
436
|
-
const { data } = await client.get("/upload/folders", {
|
|
437
|
-
params: { populate: { files: { count: true }, children: { count: true } } }
|
|
438
|
-
});
|
|
439
|
-
return Object.fromEntries(
|
|
440
|
-
(data?.data ?? []).map((f) => [
|
|
441
|
-
f.id,
|
|
442
|
-
{ files: f.files?.count ?? 0, children: f.children?.count ?? 0 }
|
|
443
|
-
])
|
|
444
|
-
);
|
|
445
|
-
},
|
|
446
|
-
/** Archivos de una carpeta; con `mimes`, solo esos tipos (el selector de imágenes, S4.13). */
|
|
447
|
-
async listFiles(folderId, { mimes } = {}) {
|
|
448
|
-
const byFolder = { folder: { id: folderId === null ? { $null: true } : { $eq: folderId } } };
|
|
449
|
-
const { data } = await client.get("/upload/files", {
|
|
450
|
-
params: {
|
|
451
|
-
page: 1,
|
|
452
|
-
pageSize: 100,
|
|
453
|
-
sort: "updatedAt:DESC",
|
|
454
|
-
filters: { $and: mimes?.length ? [byFolder, { mime: { $in: mimes } }] : [byFolder] }
|
|
455
|
-
}
|
|
456
|
-
});
|
|
457
|
-
return data?.results ?? [];
|
|
458
|
-
},
|
|
459
|
-
/** Las más recientes de varias carpetas a la vez («Recientes» del Generador, S4.14). */
|
|
460
|
-
async listRecent({ folderIds, mimes, limit }) {
|
|
461
|
-
const inFolders = { folder: { id: { $in: folderIds } } };
|
|
462
|
-
const { data } = await client.get("/upload/files", {
|
|
463
|
-
params: {
|
|
464
|
-
page: 1,
|
|
465
|
-
pageSize: limit,
|
|
466
|
-
sort: "updatedAt:DESC",
|
|
467
|
-
filters: { $and: mimes?.length ? [inFolders, { mime: { $in: mimes } }] : [inFolders] }
|
|
468
|
-
}
|
|
469
|
-
});
|
|
470
|
-
return data?.results ?? [];
|
|
471
|
-
},
|
|
472
|
-
async createFolder(name, parent) {
|
|
473
|
-
const { data } = await client.post("/upload/folders", { name, parent });
|
|
474
|
-
return data?.data;
|
|
475
|
-
},
|
|
476
|
-
async renameFolder(id, name, parent) {
|
|
477
|
-
await client.put(`/upload/folders/${id}`, { name, parent });
|
|
478
|
-
},
|
|
479
|
-
async uploadFiles(files, folderId) {
|
|
480
|
-
const uploaded = [];
|
|
481
|
-
for (const file of files) {
|
|
482
|
-
const { data } = await client.post("/upload", fileForm({ name: file.name, folder: folderId }, file));
|
|
483
|
-
uploaded.push(...Array.isArray(data) ? data : [data]);
|
|
484
|
-
}
|
|
485
|
-
return uploaded;
|
|
486
|
-
},
|
|
487
|
-
async renameFile(file, name) {
|
|
488
|
-
await client.post(
|
|
489
|
-
`/upload?id=${file.id}`,
|
|
490
|
-
fileForm({ name, folder: file.folder?.id ?? null, alternativeText: file.alternativeText, caption: file.caption })
|
|
491
|
-
);
|
|
492
|
-
},
|
|
493
|
-
async replaceFile(id, file) {
|
|
494
|
-
await client.post(`/upload?id=${id}`, fileForm({ name: file.name }, file));
|
|
495
|
-
},
|
|
496
|
-
async move({ fileIds = [], folderIds = [] }, destinationFolderId) {
|
|
497
|
-
await client.post("/upload/actions/bulk-move", { destinationFolderId, fileIds, folderIds });
|
|
498
|
-
},
|
|
499
|
-
async remove({ fileIds = [], folderIds = [] }) {
|
|
500
|
-
await client.post("/upload/actions/bulk-delete", { fileIds, folderIds });
|
|
501
|
-
},
|
|
502
|
-
/** Elimina una carpeta dejando sus archivos y subcarpetas en la carpeta superior (D1). */
|
|
503
|
-
deleteFolderSafely: (tree, folderId) => deleteFolderSafely(api, tree, folderId)
|
|
504
|
-
};
|
|
505
|
-
return api;
|
|
430
|
+
const seen = new Int32Array(width * height);
|
|
431
|
+
const stack = [];
|
|
432
|
+
inside.forEach(({ x, y }, n) => {
|
|
433
|
+
const mark = n + 1;
|
|
434
|
+
const start = Math.floor(y) * width + Math.floor(x);
|
|
435
|
+
const [r, g, b] = [rgba[4 * start], rgba[4 * start + 1], rgba[4 * start + 2]];
|
|
436
|
+
stack.push(start);
|
|
437
|
+
seen[start] = mark;
|
|
438
|
+
while (stack.length) {
|
|
439
|
+
const i = stack.pop();
|
|
440
|
+
const dr = rgba[4 * i] - r;
|
|
441
|
+
const dg = rgba[4 * i + 1] - g;
|
|
442
|
+
const db = rgba[4 * i + 2] - b;
|
|
443
|
+
const d = Math.sqrt(dr * dr + dg * dg + db * db) / MAX_DISTANCE;
|
|
444
|
+
if (d > tolerance + SOFTNESS) continue;
|
|
445
|
+
alpha[i] = Math.min(alpha[i], toAlpha(d));
|
|
446
|
+
if (d > tolerance) continue;
|
|
447
|
+
const px = i % width;
|
|
448
|
+
const visit = (j) => {
|
|
449
|
+
if (j < 0 || j >= alpha.length || seen[j] === mark) return;
|
|
450
|
+
seen[j] = mark;
|
|
451
|
+
stack.push(j);
|
|
452
|
+
};
|
|
453
|
+
if (px > 0) visit(i - 1);
|
|
454
|
+
if (px < width - 1) visit(i + 1);
|
|
455
|
+
visit(i - width);
|
|
456
|
+
visit(i + width);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
return alpha;
|
|
506
460
|
};
|
|
507
|
-
const
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
if (plan.move) await api.move(plan.move, plan.move.destinationFolderId);
|
|
512
|
-
await api.remove({ folderIds: plan.remove });
|
|
461
|
+
const applyKeyAlpha = (rgba, alpha) => {
|
|
462
|
+
const out = new Uint8ClampedArray(rgba);
|
|
463
|
+
for (let i = 0; i < alpha.length; i++) out[4 * i + 3] = Math.min(out[4 * i + 3], alpha[i]);
|
|
464
|
+
return out;
|
|
513
465
|
};
|
|
514
|
-
const
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
});
|
|
523
|
-
return root;
|
|
524
|
-
};
|
|
525
|
-
const at = async (id) => id ?? rootId();
|
|
526
|
-
const scoped = {
|
|
527
|
-
...base,
|
|
528
|
-
async getTree() {
|
|
529
|
-
const id = await rootId();
|
|
530
|
-
return (await base.getTree()).find((f) => f.id === id)?.children ?? [];
|
|
531
|
-
},
|
|
532
|
-
listFiles: async (folderId, options) => base.listFiles(await at(folderId), options),
|
|
533
|
-
// Toda la biblioteca del Estudio: la raíz y sus subcarpetas; las carpetas pedidas se ignoran.
|
|
534
|
-
listRecent: async (options) => {
|
|
535
|
-
const id = await rootId();
|
|
536
|
-
const children = (await base.getTree()).find((f) => f.id === id)?.children ?? [];
|
|
537
|
-
return base.listRecent({ ...options, folderIds: [id, ...flattenTree(children).map((f) => f.id)] });
|
|
538
|
-
},
|
|
539
|
-
createFolder: async (name, parent) => base.createFolder(name, await at(parent)),
|
|
540
|
-
renameFolder: async (id, name, parent) => base.renameFolder(id, name, await at(parent)),
|
|
541
|
-
uploadFiles: async (files, folderId) => base.uploadFiles(files, await at(folderId)),
|
|
542
|
-
move: async (selection, destination) => base.move(selection, await at(destination)),
|
|
543
|
-
deleteFolderSafely: (tree, folderId) => deleteFolderSafely(scoped, tree, folderId)
|
|
544
|
-
};
|
|
545
|
-
return scoped;
|
|
466
|
+
const canvas = (width, height) => {
|
|
467
|
+
const c = document.createElement("canvas");
|
|
468
|
+
c.width = width;
|
|
469
|
+
c.height = height;
|
|
470
|
+
const ctx = c.getContext("2d", { willReadFrequently: true });
|
|
471
|
+
if (!ctx) throw new Error("Canvas 2D no disponible");
|
|
472
|
+
ctx.imageSmoothingQuality = "high";
|
|
473
|
+
return { c, ctx };
|
|
546
474
|
};
|
|
547
|
-
const
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
475
|
+
const loadImage = (src) => new Promise((resolve, reject) => {
|
|
476
|
+
const img = new Image();
|
|
477
|
+
img.onload = () => resolve(img);
|
|
478
|
+
img.onerror = () => reject(new Error("No se pudo leer la imagen"));
|
|
479
|
+
img.src = src;
|
|
480
|
+
});
|
|
481
|
+
const loadPixels = async (src) => {
|
|
482
|
+
const img = await loadImage(src);
|
|
483
|
+
const { ctx } = canvas(img.naturalWidth, img.naturalHeight);
|
|
484
|
+
ctx.drawImage(img, 0, 0);
|
|
485
|
+
return { width: img.naturalWidth, height: img.naturalHeight, data: ctx.getImageData(0, 0, img.naturalWidth, img.naturalHeight).data };
|
|
553
486
|
};
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
487
|
+
const drawPixels = (target, width, height, data) => {
|
|
488
|
+
target.width = width;
|
|
489
|
+
target.height = height;
|
|
490
|
+
target.getContext("2d")?.putImageData(new ImageData(new Uint8ClampedArray(data), width, height), 0, 0);
|
|
557
491
|
};
|
|
558
|
-
const
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
return react.useMemo(() => {
|
|
563
|
-
const base = scopeToFolder(createUploadApi({ get, post, put }), STUDIO_ROOT);
|
|
564
|
-
const dataUrlOf = async (file) => {
|
|
565
|
-
if (!file.url) throw new Error("El archivo no tiene URL");
|
|
566
|
-
const url = file.url.startsWith("/") ? `${window.location.origin}${file.url}` : file.url;
|
|
567
|
-
const { data } = await post(DOWNLOAD_PROXY$1, { url });
|
|
568
|
-
return `data:${data.contentType};base64,${data.base64}`;
|
|
569
|
-
};
|
|
570
|
-
const download = async (file) => {
|
|
571
|
-
if (!file.url) return;
|
|
572
|
-
const href = URL.createObjectURL(dataUrlToBlob(await dataUrlOf(file)));
|
|
573
|
-
const a = document.createElement("a");
|
|
574
|
-
a.href = href;
|
|
575
|
-
a.download = file.name;
|
|
576
|
-
a.click();
|
|
577
|
-
setTimeout(() => URL.revokeObjectURL(href), 1e3);
|
|
578
|
-
};
|
|
579
|
-
const notify = (type, message) => toggleNotification({ type, message });
|
|
580
|
-
const saveBlob = (blob, name, folderId) => base.uploadFiles([new File([blob], name, { type: blob.type })], folderId);
|
|
581
|
-
return { api: { ...base, dataUrlOf, download, saveBlob }, notify };
|
|
582
|
-
}, [get, post, put, toggleNotification]);
|
|
492
|
+
const pixelsToDataUrl = (width, height, data) => {
|
|
493
|
+
const { c, ctx } = canvas(width, height);
|
|
494
|
+
ctx.putImageData(new ImageData(new Uint8ClampedArray(data), width, height), 0, 0);
|
|
495
|
+
return c.toDataURL("image/png");
|
|
583
496
|
};
|
|
584
|
-
const
|
|
585
|
-
const
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
497
|
+
const INPUT_SIZE = 1024;
|
|
498
|
+
const maskToRgba = (mask, width, height) => {
|
|
499
|
+
const out = new Uint8ClampedArray(4 * width * height);
|
|
500
|
+
for (let i = 0; i < width * height; i++) {
|
|
501
|
+
const v = mask[i] * 255;
|
|
502
|
+
out[4 * i] = out[4 * i + 1] = out[4 * i + 2] = v;
|
|
503
|
+
out[4 * i + 3] = 255;
|
|
504
|
+
}
|
|
505
|
+
return out;
|
|
506
|
+
};
|
|
507
|
+
const applyAlpha = (rgba, maskRgba) => {
|
|
508
|
+
for (let i = 3; i < rgba.length; i += 4) rgba[i] = maskRgba[i - 3];
|
|
509
|
+
};
|
|
510
|
+
const WORKER_URL = "/ort/background-worker.mjs";
|
|
511
|
+
const RUN_TIMEOUT_MS = 12e4;
|
|
512
|
+
const TIMEOUT_MESSAGE = "Quitar fondo tardó demasiado. Intenta de nuevo.";
|
|
513
|
+
class RemoveBackgroundTimeout extends Error {
|
|
514
|
+
constructor() {
|
|
515
|
+
super(TIMEOUT_MESSAGE);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const removeBackgroundError = (err) => err instanceof RemoveBackgroundTimeout ? TIMEOUT_MESSAGE : "No se pudo quitar el fondo.";
|
|
519
|
+
let loading = null;
|
|
520
|
+
const loadModel = (onStatus) => {
|
|
521
|
+
if (!loading) {
|
|
522
|
+
const current = (async () => {
|
|
523
|
+
const worker = new Worker(WORKER_URL, { type: "module" });
|
|
524
|
+
const pending = /* @__PURE__ */ new Map();
|
|
525
|
+
let seq2 = 0;
|
|
526
|
+
let status2 = onStatus;
|
|
527
|
+
const discard = (err) => {
|
|
528
|
+
worker.terminate();
|
|
529
|
+
if (loading === current) loading = null;
|
|
530
|
+
pending.forEach(({ reject }) => reject(err));
|
|
531
|
+
pending.clear();
|
|
532
|
+
};
|
|
533
|
+
await new Promise((resolve, reject) => {
|
|
534
|
+
worker.onmessage = (e) => {
|
|
535
|
+
const m = e.data;
|
|
536
|
+
if (m.type === "status") status2?.(m.status);
|
|
537
|
+
else if (m.type === "ready") resolve();
|
|
538
|
+
else if (m.type === "mask") {
|
|
539
|
+
pending.get(m.id)?.resolve(new Float32Array(m.mask));
|
|
540
|
+
pending.delete(m.id);
|
|
541
|
+
} else if (m.type === "error") {
|
|
542
|
+
const err = new Error(m.message ?? "Error del modelo");
|
|
543
|
+
if (m.id === void 0) reject(err);
|
|
544
|
+
else {
|
|
545
|
+
pending.get(m.id)?.reject(err);
|
|
546
|
+
pending.delete(m.id);
|
|
547
|
+
discard(err);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
worker.onerror = () => reject(new Error("El worker de «Quitar fondo» no cargó."));
|
|
552
|
+
worker.postMessage({ type: "load" });
|
|
553
|
+
}).catch((err) => {
|
|
554
|
+
discard(err);
|
|
555
|
+
throw err;
|
|
556
|
+
});
|
|
557
|
+
status2 = void 0;
|
|
558
|
+
return {
|
|
559
|
+
run: (rgba) => new Promise((resolve, reject) => {
|
|
560
|
+
const id = ++seq2;
|
|
561
|
+
const timer = setTimeout(() => {
|
|
562
|
+
pending.get(id)?.reject(new RemoveBackgroundTimeout());
|
|
563
|
+
pending.delete(id);
|
|
564
|
+
discard(new RemoveBackgroundTimeout());
|
|
565
|
+
}, RUN_TIMEOUT_MS);
|
|
566
|
+
pending.set(id, {
|
|
567
|
+
resolve: (m) => {
|
|
568
|
+
clearTimeout(timer);
|
|
569
|
+
resolve(m);
|
|
570
|
+
},
|
|
571
|
+
reject: (e) => {
|
|
572
|
+
clearTimeout(timer);
|
|
573
|
+
reject(e);
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
worker.postMessage({ type: "run", id, rgba: rgba.buffer }, [rgba.buffer]);
|
|
577
|
+
})
|
|
578
|
+
};
|
|
579
|
+
})();
|
|
580
|
+
loading = current;
|
|
581
|
+
current.catch(() => {
|
|
582
|
+
if (loading === current) loading = null;
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
return loading;
|
|
586
|
+
};
|
|
587
|
+
const removeBackground = async (src, onStatus) => {
|
|
588
|
+
const [model, img] = await Promise.all([loadModel(onStatus), loadImage(src)]);
|
|
589
|
+
onStatus?.("Quitando fondo…");
|
|
590
|
+
const W = img.naturalWidth;
|
|
591
|
+
const H = img.naturalHeight;
|
|
592
|
+
const w = INPUT_SIZE;
|
|
593
|
+
const h = INPUT_SIZE;
|
|
594
|
+
const small = canvas(w, h);
|
|
595
|
+
small.ctx.drawImage(img, 0, 0, w, h);
|
|
596
|
+
const mask = await model.run(small.ctx.getImageData(0, 0, w, h).data);
|
|
597
|
+
const maskSmall = canvas(w, h);
|
|
598
|
+
maskSmall.ctx.putImageData(new ImageData(maskToRgba(mask, w, h), w, h), 0, 0);
|
|
599
|
+
const maskFull = canvas(W, H);
|
|
600
|
+
maskFull.ctx.drawImage(maskSmall.c, 0, 0, W, H);
|
|
601
|
+
const out = canvas(W, H);
|
|
602
|
+
out.ctx.drawImage(img, 0, 0);
|
|
603
|
+
const pixels = out.ctx.getImageData(0, 0, W, H);
|
|
604
|
+
applyAlpha(pixels.data, maskFull.ctx.getImageData(0, 0, W, H).data);
|
|
605
|
+
out.ctx.putImageData(pixels, 0, 0);
|
|
606
|
+
return out.c.toDataURL("image/png");
|
|
607
|
+
};
|
|
608
|
+
const MODES$1 = [
|
|
609
|
+
{ value: "auto", label: "Automático", Icon: icons.Magic },
|
|
610
|
+
{ value: "color", label: "Por color", Icon: icons.Pencil }
|
|
611
|
+
];
|
|
612
|
+
const DEFAULT_TOLERANCE = 15;
|
|
613
|
+
const PICK_LABEL = "Da clic en el fondo que quieres quitar";
|
|
614
|
+
const RemoveBackgroundDialog = ({ open, src, onClose, onApply }) => {
|
|
615
|
+
const c = useThemeColors();
|
|
616
|
+
const [mode, setMode] = react.useState("auto");
|
|
617
|
+
const [auto, setAuto] = react.useState({});
|
|
618
|
+
const [pixels, setPixels] = react.useState(null);
|
|
619
|
+
const [points, setPoints] = react.useState([]);
|
|
620
|
+
const [tolerance, setTolerance] = react.useState(DEFAULT_TOLERANCE);
|
|
621
|
+
const [wholeImage, setWholeImage] = react.useState(false);
|
|
622
|
+
const [applying, setApplying] = react.useState(false);
|
|
623
|
+
const preview = react.useRef(null);
|
|
592
624
|
react.useEffect(() => {
|
|
593
625
|
if (!open) return;
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
626
|
+
setMode("auto");
|
|
627
|
+
setPixels(null);
|
|
628
|
+
setPoints([]);
|
|
629
|
+
setTolerance(DEFAULT_TOLERANCE);
|
|
630
|
+
setWholeImage(false);
|
|
631
|
+
setAuto({ status: "Preparando…" });
|
|
632
|
+
let live = true;
|
|
633
|
+
removeBackground(src, (status2) => live && setAuto({ status: status2 })).then((result2) => live && setAuto({ result: result2 })).catch((err) => live && setAuto({ error: removeBackgroundError(err) }));
|
|
634
|
+
return () => {
|
|
635
|
+
live = false;
|
|
636
|
+
};
|
|
637
|
+
}, [open, src]);
|
|
598
638
|
react.useEffect(() => {
|
|
599
|
-
if (!open) return;
|
|
600
|
-
let
|
|
601
|
-
|
|
602
|
-
api.listFiles(current, { mimes: IMAGE_MIMES }).then((list) => alive && setFiles(list), () => notify("danger", "No se pudieron cargar las imágenes.")).finally(() => alive && setLoading(false));
|
|
639
|
+
if (!open || mode !== "color" || pixels) return;
|
|
640
|
+
let live = true;
|
|
641
|
+
loadPixels(src).then((p) => live && setPixels(p)).catch(() => live && setAuto((a) => ({ ...a, error: "No se pudo leer la imagen." })));
|
|
603
642
|
return () => {
|
|
604
|
-
|
|
643
|
+
live = false;
|
|
605
644
|
};
|
|
606
|
-
}, [open,
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
645
|
+
}, [open, mode, src, pixels]);
|
|
646
|
+
const keyed = react.useMemo(
|
|
647
|
+
() => pixels && points.length ? applyKeyAlpha(pixels.data, colorKeyAlpha(pixels.data, pixels.width, pixels.height, points, tolerance / 100, { contiguous: !wholeImage })) : null,
|
|
648
|
+
[pixels, points, tolerance, wholeImage]
|
|
649
|
+
);
|
|
650
|
+
react.useEffect(() => {
|
|
651
|
+
if (mode === "color" && pixels && preview.current) drawPixels(preview.current, pixels.width, pixels.height, keyed ?? pixels.data);
|
|
652
|
+
}, [mode, pixels, keyed]);
|
|
653
|
+
const pick = (e) => {
|
|
654
|
+
if (!pixels) return;
|
|
655
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
656
|
+
const x = Math.floor((e.clientX - rect.left) / rect.width * pixels.width);
|
|
657
|
+
const y = Math.floor((e.clientY - rect.top) / rect.height * pixels.height);
|
|
658
|
+
setPoints((p) => [...p, { x, y }]);
|
|
659
|
+
};
|
|
660
|
+
const result = mode === "auto" ? auto.result : keyed && pixels ? "color" : void 0;
|
|
661
|
+
const apply = async () => {
|
|
662
|
+
const dataUrl = mode === "auto" ? auto.result : keyed && pixels ? pixelsToDataUrl(pixels.width, pixels.height, keyed) : void 0;
|
|
663
|
+
if (!dataUrl) return;
|
|
664
|
+
setApplying(true);
|
|
665
|
+
try {
|
|
666
|
+
await onApply(dataUrl);
|
|
667
|
+
} finally {
|
|
668
|
+
setApplying(false);
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
const frame = { maxWidth: "100%", maxHeight: "45vh", display: "block" };
|
|
672
|
+
return /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Root, { open, onOpenChange: (o) => !o && onClose(), children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Modal.Content, { children: [
|
|
673
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Header, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Title, { children: "Quitar fondo" }) }),
|
|
613
674
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Modal.Body, { children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 4, children: [
|
|
614
|
-
/* @__PURE__ */ jsxRuntime.
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
] },
|
|
675
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", children: /* @__PURE__ */ jsxRuntime.jsx(SegmentedSwitch, { label: "Modo de quitar fondo", options: MODES$1, value: mode, onChange: setMode }) }),
|
|
676
|
+
mode === "color" && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 3, children: [
|
|
677
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: [
|
|
678
|
+
PICK_LABEL,
|
|
679
|
+
". Cada clic agrega un punto."
|
|
680
|
+
] }),
|
|
681
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 6, alignItems: "end", children: [
|
|
682
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { flex: "1", children: /* @__PURE__ */ jsxRuntime.jsx(RangeField, { label: "Tolerancia", value: tolerance, min: 0, max: 60, onChange: setTolerance, format: (v) => `${v} %` }) }),
|
|
683
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Checkbox, { checked: wholeImage, onCheckedChange: (v) => setWholeImage(v === true), children: "Quitar ese color en toda la imagen" })
|
|
684
|
+
] })
|
|
620
685
|
] }),
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
686
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "center", padding: 2, hasRadius: true, style: { background: CHECKERBOARD, minHeight: 240 }, children: [
|
|
687
|
+
mode === "auto" && (auto.result ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: auto.result, alt: "Vista previa sin fondo", style: frame }) : auto.error ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "danger600", children: auto.error }) : /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 2, children: [
|
|
688
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { small: true }),
|
|
689
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: auto.status })
|
|
690
|
+
] })),
|
|
691
|
+
mode === "color" && (pixels ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { position: "relative", children: [
|
|
692
|
+
/* @__PURE__ */ jsxRuntime.jsx("canvas", { ref: preview, "aria-label": PICK_LABEL, onClick: pick, style: { ...frame, cursor: "crosshair" } }),
|
|
693
|
+
points.map((p, i) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
694
|
+
"span",
|
|
695
|
+
{
|
|
696
|
+
"aria-hidden": "true",
|
|
697
|
+
style: {
|
|
698
|
+
position: "absolute",
|
|
699
|
+
left: `${(p.x + 0.5) / pixels.width * 100}%`,
|
|
700
|
+
top: `${(p.y + 0.5) / pixels.height * 100}%`,
|
|
701
|
+
width: 10,
|
|
702
|
+
height: 10,
|
|
703
|
+
marginLeft: -5,
|
|
704
|
+
marginTop: -5,
|
|
705
|
+
borderRadius: "50%",
|
|
706
|
+
background: c.primary600,
|
|
707
|
+
border: `2px solid ${c.neutral0}`,
|
|
708
|
+
pointerEvents: "none"
|
|
709
|
+
}
|
|
710
|
+
},
|
|
711
|
+
i
|
|
712
|
+
))
|
|
713
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { small: true }))
|
|
714
|
+
] })
|
|
646
715
|
] }) }),
|
|
647
716
|
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Modal.Footer, { children: [
|
|
648
717
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", onClick: onClose, children: "Cancelar" }),
|
|
649
|
-
/* @__PURE__ */ jsxRuntime.
|
|
718
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
|
|
719
|
+
mode === "color" && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "secondary", onClick: () => setPoints([]), disabled: !points.length, children: "Quitar puntos" }),
|
|
720
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: apply, disabled: !result || applying, loading: applying, children: "Aplicar" })
|
|
721
|
+
] })
|
|
650
722
|
] })
|
|
651
723
|
] }) });
|
|
652
724
|
};
|
|
725
|
+
const RemoveBackgroundButton = ({ src, onDone }) => {
|
|
726
|
+
const [open, setOpen] = react.useState(false);
|
|
727
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
728
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "secondary", startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Magic, {}), onClick: () => setOpen(true), fullWidth: true, children: "Quitar fondo" }),
|
|
729
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
730
|
+
RemoveBackgroundDialog,
|
|
731
|
+
{
|
|
732
|
+
open,
|
|
733
|
+
src,
|
|
734
|
+
onClose: () => setOpen(false),
|
|
735
|
+
onApply: (dataUrl) => {
|
|
736
|
+
onDone(dataUrl);
|
|
737
|
+
setOpen(false);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
)
|
|
741
|
+
] });
|
|
742
|
+
};
|
|
653
743
|
const NameDialog = ({ open, title, label, initial = "", confirmLabel, validate, onClose, onConfirm }) => {
|
|
654
744
|
const [name, setName] = react.useState(initial);
|
|
655
745
|
const [error, setError] = react.useState(null);
|
|
@@ -1040,19 +1130,6 @@ const downloadBlob = (blob, name) => {
|
|
|
1040
1130
|
a.click();
|
|
1041
1131
|
setTimeout(() => URL.revokeObjectURL(href), 1e3);
|
|
1042
1132
|
};
|
|
1043
|
-
const PROMPT_MAX = 2e3;
|
|
1044
|
-
const CONTROL = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g;
|
|
1045
|
-
const INVISIBLE = /[----]/g;
|
|
1046
|
-
const TAG = /<\/?[a-z][^>]*>/gi;
|
|
1047
|
-
const sanitizePrompt = (text) => text.normalize("NFC").replace(CONTROL, "").replace(INVISIBLE, "").replace(TAG, "").replace(/\s+/g, " ").trim();
|
|
1048
|
-
const PROMPT_LABELS = { label: "El prompt", empty: "Escribe un prompt." };
|
|
1049
|
-
const INSTRUCTION_LABELS = { label: "La instrucción", empty: "Describe el cambio que quieres." };
|
|
1050
|
-
const checkPrompt = (text, { label, empty } = PROMPT_LABELS) => {
|
|
1051
|
-
const clean = sanitizePrompt(text);
|
|
1052
|
-
if (!clean) return { ok: false, error: empty };
|
|
1053
|
-
if ([...clean].length > PROMPT_MAX) return { ok: false, error: `${label} puede tener hasta ${PROMPT_MAX} caracteres.` };
|
|
1054
|
-
return { ok: true, text: clean };
|
|
1055
|
-
};
|
|
1056
1133
|
const initialGenerator = { versions: [], currentId: null };
|
|
1057
1134
|
const currentVersion = (s) => s.versions.find((v) => v.id === s.currentId) ?? null;
|
|
1058
1135
|
const generatorReducer = (state, action) => {
|
|
@@ -1237,80 +1314,6 @@ const GroundingSources = ({ grounding }) => {
|
|
|
1237
1314
|
)
|
|
1238
1315
|
] }) });
|
|
1239
1316
|
};
|
|
1240
|
-
const PromptComposer = (p) => {
|
|
1241
|
-
const c = useThemeColors();
|
|
1242
|
-
const id = react.useId();
|
|
1243
|
-
const [focused, setFocused] = react.useState(false);
|
|
1244
|
-
const check = checkPrompt(p.value, p.labels);
|
|
1245
|
-
const error = !check.ok && p.value.trim() ? check.error : null;
|
|
1246
|
-
const count = [...p.value].length;
|
|
1247
|
-
const submit = () => {
|
|
1248
|
-
if (check.ok && !p.disabled && !p.loading) p.onSubmit(check.text);
|
|
1249
|
-
};
|
|
1250
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", alignItems: "stretch", gap: 1, role: "group", "aria-labelledby": `${id}-label`, children: [
|
|
1251
|
-
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { tag: "label", id: `${id}-label`, htmlFor: id, variant: "pi", fontWeight: "bold", children: p.label }),
|
|
1252
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1253
|
-
designSystem.Box,
|
|
1254
|
-
{
|
|
1255
|
-
hasRadius: true,
|
|
1256
|
-
background: "neutral0",
|
|
1257
|
-
style: {
|
|
1258
|
-
border: `1px solid ${error ? c.danger600 : focused ? c.primary600 : c.neutral200}`,
|
|
1259
|
-
boxShadow: focused && !error ? `${c.primary600} 0 0 0 2px` : void 0
|
|
1260
|
-
},
|
|
1261
|
-
children: [
|
|
1262
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1263
|
-
"textarea",
|
|
1264
|
-
{
|
|
1265
|
-
id,
|
|
1266
|
-
value: p.value,
|
|
1267
|
-
placeholder: p.placeholder,
|
|
1268
|
-
"aria-invalid": Boolean(error),
|
|
1269
|
-
"aria-describedby": `${id}-help`,
|
|
1270
|
-
onChange: (e) => p.onChange(e.target.value),
|
|
1271
|
-
onFocus: () => setFocused(true),
|
|
1272
|
-
onBlur: () => setFocused(false),
|
|
1273
|
-
onKeyDown: (e) => {
|
|
1274
|
-
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
1275
|
-
e.preventDefault();
|
|
1276
|
-
submit();
|
|
1277
|
-
}
|
|
1278
|
-
},
|
|
1279
|
-
rows: 4,
|
|
1280
|
-
style: {
|
|
1281
|
-
display: "block",
|
|
1282
|
-
width: "100%",
|
|
1283
|
-
boxSizing: "border-box",
|
|
1284
|
-
resize: "vertical",
|
|
1285
|
-
minHeight: 96,
|
|
1286
|
-
padding: "12px 16px 4px",
|
|
1287
|
-
border: 0,
|
|
1288
|
-
outline: "none",
|
|
1289
|
-
background: "transparent",
|
|
1290
|
-
color: c.neutral800,
|
|
1291
|
-
font: "inherit",
|
|
1292
|
-
fontSize: 14,
|
|
1293
|
-
lineHeight: 1.5
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
),
|
|
1297
|
-
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", gap: 2, paddingLeft: 3, paddingRight: 2, paddingBottom: 2, children: [
|
|
1298
|
-
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
|
|
1299
|
-
p.tools,
|
|
1300
|
-
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Typography, { variant: "pi", textColor: count > PROMPT_MAX ? "danger600" : "neutral500", children: [
|
|
1301
|
-
count,
|
|
1302
|
-
"/",
|
|
1303
|
-
PROMPT_MAX
|
|
1304
|
-
] })
|
|
1305
|
-
] }),
|
|
1306
|
-
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { startIcon: p.actionIcon, loading: p.loading, disabled: p.disabled || !check.ok, onClick: submit, children: p.actionLabel })
|
|
1307
|
-
] })
|
|
1308
|
-
]
|
|
1309
|
-
}
|
|
1310
|
-
),
|
|
1311
|
-
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { id: `${id}-help`, variant: "pi", textColor: error ? "danger600" : "neutral600", children: error ?? p.hint ?? "⌘/Ctrl + Enter también envía." })
|
|
1312
|
-
] });
|
|
1313
|
-
};
|
|
1314
1317
|
const MODES = [
|
|
1315
1318
|
{ value: "create", label: "Crear imagen", Icon: icons.Sparkle },
|
|
1316
1319
|
{ value: "edit", label: "Editar imagen", Icon: icons.Pencil }
|
|
@@ -5267,5 +5270,7 @@ const ImageStudioTabs = () => {
|
|
|
5267
5270
|
};
|
|
5268
5271
|
exports.ImageStudioTabs = ImageStudioTabs;
|
|
5269
5272
|
exports.LEGACY_ROUTES = LEGACY_ROUTES;
|
|
5273
|
+
exports.LibraryImagePicker = LibraryImagePicker;
|
|
5274
|
+
exports.PromptComposer = PromptComposer;
|
|
5270
5275
|
exports.StudioRedirect = StudioRedirect;
|
|
5271
5276
|
exports.legacyRedirect = legacyRedirect;
|