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