@interware/content-generator 0.6.0 → 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 +515 -321
  23. package/dist/server/index.mjs +515 -321
  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
@@ -0,0 +1,1362 @@
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { useFetchClient, useNotification, Page, useRBAC } from "@strapi/strapi/admin";
3
+ import { useNavigate, useParams, Routes, Route } from "react-router-dom";
4
+ import { P as PluginProviders } from "./PluginProviders-DWMpcAIh.mjs";
5
+ import { useMemo, useState, useEffect, useRef, useCallback } from "react";
6
+ import { Box, Flex, Typography, Button, Badge, TextInput, Loader, Table, Thead, Tr, Th, VisuallyHidden, Tbody, Td, TextButton, IconButton, Link, LinkButton, Field, Radio, Modal, Textarea, Combobox, ComboboxOption, Toggle, Grid } from "@strapi/design-system";
7
+ import { Plus, Search, Check, Pencil, ExternalLink, ArrowLeft, Image, Clock, WarningCircle, Trash, Sparkle, PlusCircle } from "@strapi/icons";
8
+ import { P as PLUGIN_ID, S as STUDIO_PATH, p as pluginPermissions } from "./index-D67xWUPz.mjs";
9
+ import { L as LibraryImagePicker, P as PromptComposer, a as LEGACY_ROUTES, S as StudioRedirect, l as legacyRedirect } from "./ImageStudioTabs-B-mOwDfY.mjs";
10
+ import { useIntl } from "react-intl";
11
+ import { useFormik } from "formik";
12
+ import * as Yup from "yup";
13
+ const OFFSET = "-06:00";
14
+ const ZONA = "America/Mexico_City";
15
+ const nextAction = (r) => {
16
+ if (r.estado === "programado") return { label: "Ver programación", variant: "tertiary" };
17
+ if (r.estado === "publicado") return r.linkedInPostId && r.facebookPostId ? { label: "Ver", variant: "tertiary" } : { label: "Compartir", variant: "tertiary" };
18
+ return r.imagen ? { label: "Publicar", variant: "secondary" } : { label: "Elegir imagen", variant: "secondary" };
19
+ };
20
+ const FILTROS = [
21
+ { id: "todos", label: "Todos", test: () => true },
22
+ { id: "borradores", label: "Borradores", test: (r) => r.estado === "borrador" },
23
+ { id: "programados", label: "Programados", test: (r) => r.estado === "programado" },
24
+ { id: "sin-imagen", label: "Sin imagen", test: (r) => !r.imagen },
25
+ { id: "publicados", label: "Publicados", test: (r) => r.estado === "publicado" }
26
+ ];
27
+ const MESES = ["ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic"];
28
+ const formatFecha = (fecha) => {
29
+ if (!fecha) return "Sin fecha";
30
+ const [y, m, d] = fecha.split("-").map(Number);
31
+ return `${d} ${MESES[m - 1]} ${y}`;
32
+ };
33
+ const partes = (iso) => {
34
+ const p = new Intl.DateTimeFormat("en-CA", { timeZone: ZONA, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).formatToParts(new Date(iso)).reduce((acc, x) => ({ ...acc, [x.type]: x.value }), {});
35
+ return { fecha: `${p.year}-${p.month}-${p.day}`, hora: `${p.hour}:${p.minute}` };
36
+ };
37
+ const formatFechaHora = (iso) => {
38
+ const { fecha, hora } = partes(iso);
39
+ return `${formatFecha(fecha)} · ${hora.replace(/^0/, "")}`;
40
+ };
41
+ const fromMexicoLocal = (fecha, hora) => (/* @__PURE__ */ new Date(`${fecha}T${hora}:00${OFFSET}`)).toISOString();
42
+ const toMexicoLocal = (iso) => partes(iso);
43
+ const BASE = "/strapi-content-generator-gemini";
44
+ const GENERATE_TIMEOUT_MS = 24e4;
45
+ const errorMessage = (err, fallback) => {
46
+ const message = err?.response?.data?.error?.message;
47
+ return typeof message === "string" && message ? message : fallback;
48
+ };
49
+ const createArticlesApi = (client) => ({
50
+ async list() {
51
+ const { data } = await client.get(`${BASE}/articles`);
52
+ return data.articles;
53
+ },
54
+ async detail(documentId) {
55
+ const { data } = await client.get(`${BASE}/articles/${documentId}`);
56
+ return data;
57
+ },
58
+ async generate(body) {
59
+ const signal = AbortSignal.timeout(GENERATE_TIMEOUT_MS);
60
+ const { data } = await client.post(`${BASE}/articles/generate`, body, { signal });
61
+ return data.documentId;
62
+ },
63
+ async enrich(texto) {
64
+ const { data } = await client.post(`${BASE}/articles/enrich`, { texto });
65
+ return data.texto;
66
+ },
67
+ async basePrompt() {
68
+ const { data } = await client.get(`${BASE}/articles/base-prompt`);
69
+ return data;
70
+ },
71
+ async saveBasePrompt(prompt) {
72
+ const { data } = await client.put(`${BASE}/articles/base-prompt`, { prompt });
73
+ return data;
74
+ },
75
+ async resetBasePrompt() {
76
+ const { data } = await client.del(`${BASE}/articles/base-prompt`);
77
+ return data;
78
+ },
79
+ async setImage(documentId, fileId) {
80
+ const { data } = await client.put(`${BASE}/articles/${documentId}/image`, { fileId });
81
+ return data;
82
+ },
83
+ async publish(documentId) {
84
+ const { data } = await client.post(`${BASE}/articles/${documentId}/publish`, {});
85
+ return data;
86
+ },
87
+ async schedule(documentId, at) {
88
+ const { data } = await client.post(`${BASE}/articles/${documentId}/schedule`, { at });
89
+ return data;
90
+ },
91
+ async unschedule(documentId) {
92
+ const { data } = await client.del(`${BASE}/articles/${documentId}/schedule`);
93
+ return data;
94
+ },
95
+ async categories() {
96
+ const { data } = await client.get(`${BASE}/categories`);
97
+ return (data ?? []).map((c) => ({ id: c.documentId, nombre: c.categoria }));
98
+ },
99
+ /** Compartir en LinkedIn o Facebook con el texto escrito en el panel (vacío → el predeterminado). */
100
+ async share(documentId, red, mensaje) {
101
+ await client.post(`${BASE}/publish-${red}`, { articleId: documentId, mensaje });
102
+ }
103
+ });
104
+ const useArticlesApi = () => {
105
+ const { get, post, put, del } = useFetchClient();
106
+ const { toggleNotification } = useNotification();
107
+ return useMemo(() => {
108
+ const api = createArticlesApi({ get, post, put, del });
109
+ const notify = (type, message) => toggleNotification({ type, message });
110
+ return { api, notify };
111
+ }, [get, post, put, del, toggleNotification]);
112
+ };
113
+ const ESTADO$1 = {
114
+ borrador: { label: "Borrador", variant: "warning" },
115
+ programado: { label: "Programado", variant: "secondary" },
116
+ publicado: { label: "Publicado", variant: "success" }
117
+ };
118
+ const articlePath = (documentId) => `/plugins/${PLUGIN_ID}/articles/${documentId}`;
119
+ const NEW_ARTICLE_PATH = `/plugins/${PLUGIN_ID}/generate-content`;
120
+ const contentManagerUrl$1 = (documentId) => `/admin/content-manager/collection-types/api::articulo.articulo/${documentId}`;
121
+ const fechaDe = (r) => r.programadaPara && r.estado === "programado" ? formatFechaHora(r.programadaPara) : formatFecha(r.fechaPublicacion);
122
+ const Redes = ({ r }) => {
123
+ if (!r.linkedInPostId && !r.facebookPostId) return /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: "—" });
124
+ return /* @__PURE__ */ jsxs(Flex, { gap: 3, wrap: "wrap", children: [
125
+ r.linkedInPostId && /* @__PURE__ */ jsx(Link, { href: `https://www.linkedin.com/feed/update/${r.linkedInPostId}`, isExternal: true, endIcon: /* @__PURE__ */ jsx(ExternalLink, {}), children: "LinkedIn" }),
126
+ r.facebookPostId && /* @__PURE__ */ jsx(Link, { href: `https://www.facebook.com/${r.facebookPostId}`, isExternal: true, endIcon: /* @__PURE__ */ jsx(ExternalLink, {}), children: "Facebook" })
127
+ ] });
128
+ };
129
+ const ArticlesPage = () => {
130
+ const { api, notify } = useArticlesApi();
131
+ const navigate = useNavigate();
132
+ const [rows, setRows] = useState(null);
133
+ const [filtro, setFiltro] = useState("todos");
134
+ const [busqueda, setBusqueda] = useState("");
135
+ useEffect(() => {
136
+ let alive = true;
137
+ api.list().then(
138
+ (list) => alive && setRows(list),
139
+ () => {
140
+ if (!alive) return;
141
+ setRows([]);
142
+ notify("danger", "No se pudieron cargar los artículos.");
143
+ }
144
+ );
145
+ return () => {
146
+ alive = false;
147
+ };
148
+ }, [api, notify]);
149
+ const visibles = useMemo(() => {
150
+ const test = FILTROS.find((f) => f.id === filtro)?.test ?? (() => true);
151
+ const q = busqueda.trim().toLowerCase();
152
+ return (rows ?? []).filter((r) => test(r) && (!q || r.titulo.toLowerCase().includes(q)));
153
+ }, [rows, filtro, busqueda]);
154
+ const abrir = (r) => navigate(articlePath(r.documentId));
155
+ return /* @__PURE__ */ jsxs(Box, { padding: 8, paddingTop: 10, children: [
156
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "flex-start", gap: 4, wrap: "wrap", marginBottom: 8, children: [
157
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
158
+ /* @__PURE__ */ jsx(Typography, { variant: "alpha", tag: "h1", children: "Generador de contenido" }),
159
+ /* @__PURE__ */ jsx(Typography, { variant: "epsilon", textColor: "neutral600", children: "Artículos del blog y qué le falta a cada uno para publicarse." })
160
+ ] }),
161
+ /* @__PURE__ */ jsx(Button, { size: "L", startIcon: /* @__PURE__ */ jsx(Plus, {}), onClick: () => navigate(NEW_ARTICLE_PATH), children: "Generar artículo" })
162
+ ] }),
163
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", gap: 4, wrap: "wrap", marginBottom: 4, children: [
164
+ /* @__PURE__ */ jsx(Flex, { gap: 2, wrap: "wrap", role: "group", "aria-label": "Filtrar artículos", children: FILTROS.map((f) => {
165
+ const on = f.id === filtro;
166
+ return /* @__PURE__ */ jsx(Button, { variant: on ? "secondary" : "tertiary", "aria-pressed": on, onClick: () => setFiltro(f.id), children: /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
167
+ f.label,
168
+ /* @__PURE__ */ jsx(Badge, { active: on, children: (rows ?? []).filter(f.test).length })
169
+ ] }) }, f.id);
170
+ }) }),
171
+ /* @__PURE__ */ jsx(Box, { style: { flex: "0 1 320px", minWidth: 220 }, children: /* @__PURE__ */ jsx(TextInput, { "aria-label": "Buscar por título", placeholder: "Buscar por título", startAction: /* @__PURE__ */ jsx(Search, {}), value: busqueda, onChange: (e) => setBusqueda(e.target.value) }) })
172
+ ] }),
173
+ rows === null ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 10, children: /* @__PURE__ */ jsx(Loader, { children: "Cargando artículos…" }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
174
+ /* @__PURE__ */ jsxs(Table, { colCount: 7, rowCount: visibles.length + 1, children: [
175
+ /* @__PURE__ */ jsx(Thead, { children: /* @__PURE__ */ jsxs(Tr, { children: [
176
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Artículo" }) }),
177
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Estado" }) }),
178
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Imagen" }) }),
179
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Publicación" }) }),
180
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", children: "Compartido en" }) }),
181
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(VisuallyHidden, { children: "Acción" }) }),
182
+ /* @__PURE__ */ jsx(Th, { children: /* @__PURE__ */ jsx(VisuallyHidden, { children: "Editar" }) })
183
+ ] }) }),
184
+ /* @__PURE__ */ jsx(Tbody, { children: visibles.map((r) => {
185
+ const accion = nextAction(r);
186
+ return /* @__PURE__ */ jsxs(Tr, { children: [
187
+ /* @__PURE__ */ jsx(Td, { style: { maxWidth: 420, whiteSpace: "normal" }, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
188
+ /* @__PURE__ */ jsx(TextButton, { onClick: () => abrir(r), style: { textAlign: "left", whiteSpace: "normal", height: "auto" }, children: /* @__PURE__ */ jsx(Typography, { fontWeight: "semiBold", textColor: "neutral800", style: { whiteSpace: "normal", overflowWrap: "anywhere" }, children: r.titulo }) }),
189
+ r.categoria && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: r.categoria })
190
+ ] }) }),
191
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Badge, { variant: ESTADO$1[r.estado].variant, children: ESTADO$1[r.estado].label }) }),
192
+ /* @__PURE__ */ jsx(Td, { children: r.imagen ? /* @__PURE__ */ jsx(Badge, { variant: "success", children: /* @__PURE__ */ jsxs(Flex, { gap: 1, alignItems: "center", children: [
193
+ /* @__PURE__ */ jsx(Check, { width: 10, height: 10 }),
194
+ "Lista"
195
+ ] }) }) : /* @__PURE__ */ jsx(Badge, { children: "Falta" }) }),
196
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral700", style: { whiteSpace: "nowrap" }, children: fechaDe(r) }) }),
197
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Redes, { r }) }),
198
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Flex, { justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Button, { variant: accion.variant, onClick: () => abrir(r), children: accion.label }) }) }),
199
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(IconButton, { label: `Editar «${r.titulo}» en el Content Manager`, variant: "ghost", tag: "a", href: contentManagerUrl$1(r.documentId), children: /* @__PURE__ */ jsx(Pencil, {}) }) })
200
+ ] }, r.documentId);
201
+ }) })
202
+ ] }),
203
+ visibles.length === 0 && /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "center", gap: 2, padding: 10, background: "neutral0", children: [
204
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "p", children: "Nada por aquí" }),
205
+ /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: rows.length ? "Ningún artículo cumple este filtro." : "Aún no hay artículos. Genera el primero." })
206
+ ] }),
207
+ /* @__PURE__ */ jsx(Box, { paddingTop: 3, children: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: `${visibles.length} de ${rows.length} artículos` }) })
208
+ ] })
209
+ ] });
210
+ };
211
+ const POST_MAX = 3e3;
212
+ const hashtags = (keywords) => keywords.map((k) => k.trim().replace(/\s+/g, "").replace(/[^\p{L}\p{N}_]/gu, "")).filter(Boolean).map((k) => `#${k}`).join(" ");
213
+ const defaultPostText = ({ titulo, descripcion, keywords }, red) => [red === "facebook" ? titulo : "", descripcion, hashtags(keywords)].filter(Boolean).join("\n\n");
214
+ const DOMINIO = "interware.com.mx";
215
+ const Texto = ({ mensaje }) => /* @__PURE__ */ jsx(
216
+ Typography,
217
+ {
218
+ tag: "p",
219
+ textColor: "neutral800",
220
+ style: { whiteSpace: "pre-wrap", overflowWrap: "anywhere", lineHeight: 1.5 },
221
+ children: mensaje.split(/(#[\p{L}\p{N}_]+)/u).map(
222
+ (parte, i) => parte.startsWith("#") ? /* @__PURE__ */ jsx(Typography, { tag: "span", textColor: "primary600", fontWeight: "semiBold", "data-hashtag": "", children: parte }, i) : parte
223
+ )
224
+ }
225
+ );
226
+ const SharePreview = ({ red, mensaje, article }) => {
227
+ const nombre = red === "linkedin" ? "LinkedIn" : "Facebook";
228
+ const vacio = !mensaje.trim();
229
+ return /* @__PURE__ */ jsxs(
230
+ Box,
231
+ {
232
+ tag: "section",
233
+ "aria-label": `Vista previa en ${nombre}`,
234
+ background: "neutral0",
235
+ hasRadius: true,
236
+ borderColor: "neutral200",
237
+ borderStyle: "solid",
238
+ borderWidth: "1px",
239
+ overflow: "hidden",
240
+ children: [
241
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "center", padding: 4, paddingBottom: 3, children: [
242
+ /* @__PURE__ */ jsx(
243
+ Flex,
244
+ {
245
+ justifyContent: "center",
246
+ alignItems: "center",
247
+ background: "neutral800",
248
+ hasRadius: true,
249
+ style: { width: 40, height: 40, flex: "none" },
250
+ children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral0", fontWeight: "bold", children: "IW" })
251
+ }
252
+ ),
253
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", children: [
254
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", textColor: "neutral800", children: "InterWare" }),
255
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Ahora · Público" })
256
+ ] })
257
+ ] }),
258
+ /* @__PURE__ */ jsx(Box, { paddingLeft: 4, paddingRight: 4, paddingBottom: 3, children: vacio ? /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "danger600", children: "Escribe el texto de la publicación." }) : /* @__PURE__ */ jsx(Texto, { mensaje }) }),
259
+ /* @__PURE__ */ jsxs(
260
+ Box,
261
+ {
262
+ background: "neutral100",
263
+ borderColor: "neutral200",
264
+ borderStyle: "solid",
265
+ borderWidth: "1px 0 0",
266
+ children: [
267
+ article.imageUrl && /* @__PURE__ */ jsx(
268
+ "img",
269
+ {
270
+ src: article.imageUrl,
271
+ alt: article.imageName ?? "",
272
+ style: { display: "block", width: "100%", aspectRatio: "1.91 / 1", maxHeight: 200, objectFit: "cover" }
273
+ }
274
+ ),
275
+ /* @__PURE__ */ jsx(Flex, { direction: "column", alignItems: "flex-start", gap: 1, padding: 3, children: red === "facebook" ? /* @__PURE__ */ jsxs(Fragment, { children: [
276
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: DOMINIO.toUpperCase() }),
277
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", textColor: "neutral800", children: article.titulo }),
278
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", ellipsis: true, style: { maxWidth: "100%" }, children: article.descripcion })
279
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
280
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", textColor: "neutral800", children: article.titulo }),
281
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: DOMINIO })
282
+ ] }) })
283
+ ]
284
+ }
285
+ )
286
+ ]
287
+ }
288
+ );
289
+ };
290
+ const ESTADO = {
291
+ borrador: { label: "Borrador", variant: "warning" },
292
+ programado: { label: "Programado", variant: "secondary" },
293
+ publicado: { label: "Publicado", variant: "success" }
294
+ };
295
+ const REDES = [
296
+ {
297
+ id: "linkedin",
298
+ nombre: "LinkedIn",
299
+ campo: "linkedInPostId",
300
+ url: (id) => `https://www.linkedin.com/feed/update/${id}`
301
+ },
302
+ {
303
+ id: "facebook",
304
+ nombre: "Facebook",
305
+ campo: "facebookPostId",
306
+ url: (id) => `https://www.facebook.com/${id}`
307
+ }
308
+ ];
309
+ const SITE_URL = "https://www.interware.com.mx/blog";
310
+ const contentManagerUrl = (documentId) => `/admin/content-manager/collection-types/api::articulo.articulo/${documentId}`;
311
+ const extracto = (html, max = 700) => {
312
+ const text = (new DOMParser().parseFromString(html, "text/html").body.textContent ?? "").replace(/\s+/g, " ").trim();
313
+ return text.length > max ? `${text.slice(0, max)}…` : text;
314
+ };
315
+ const manana = () => toMexicoLocal(new Date(Date.now() + 24 * 60 * 60 * 1e3).toISOString()).fecha;
316
+ const Tarjeta = ({
317
+ titulo,
318
+ extra,
319
+ children
320
+ }) => /* @__PURE__ */ jsx(Box, { background: "neutral0", padding: 6, hasRadius: true, shadow: "tableShadow", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 3, children: [
321
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", gap: 2, children: [
322
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", children: titulo }),
323
+ extra
324
+ ] }),
325
+ children
326
+ ] }) });
327
+ const Requisito = ({
328
+ ok,
329
+ children,
330
+ accion
331
+ }) => /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", gap: 3, style: { minHeight: 44 }, children: [
332
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
333
+ /* @__PURE__ */ jsx(Box, { color: ok ? "success600" : "warning600", children: ok ? /* @__PURE__ */ jsx(Check, { width: 16, height: 16 }) : /* @__PURE__ */ jsx(WarningCircle, { width: 16, height: 16 }) }),
334
+ /* @__PURE__ */ jsx(Typography, { children })
335
+ ] }),
336
+ accion
337
+ ] });
338
+ const ArticlePage = () => {
339
+ const { documentId = "" } = useParams();
340
+ const navigate = useNavigate();
341
+ const { api, notify } = useArticlesApi();
342
+ const [d, setD] = useState(null);
343
+ const [picker, setPicker] = useState(false);
344
+ const [cuando, setCuando] = useState("ahora");
345
+ const [fecha, setFecha] = useState(manana);
346
+ const [hora, setHora] = useState("09:00");
347
+ const [busy, setBusy] = useState(null);
348
+ const [confirmar, setConfirmar] = useState(null);
349
+ const [mensaje, setMensaje] = useState("");
350
+ useEffect(() => {
351
+ let alive = true;
352
+ api.detail(documentId).then(
353
+ (x) => alive && setD(x),
354
+ (err) => notify("danger", errorMessage(err, "No se pudo cargar el artículo."))
355
+ );
356
+ return () => {
357
+ alive = false;
358
+ };
359
+ }, [api, notify, documentId]);
360
+ const texto = useMemo(() => d ? extracto(d.content) : "", [d]);
361
+ const run = async (id, work, ok, fallback) => {
362
+ setBusy(id);
363
+ try {
364
+ setD(await work());
365
+ notify("success", ok);
366
+ } catch (err) {
367
+ notify("danger", errorMessage(err, fallback));
368
+ } finally {
369
+ setBusy(null);
370
+ }
371
+ };
372
+ const elegirImagen = (files) => {
373
+ setPicker(false);
374
+ if (files[0])
375
+ void run(
376
+ "imagen",
377
+ () => api.setImage(documentId, files[0].id),
378
+ "Imagen asignada.",
379
+ "No se pudo asignar la imagen."
380
+ );
381
+ };
382
+ const publicar = () => {
383
+ if (cuando === "ahora")
384
+ return run(
385
+ "publicar",
386
+ () => api.publish(documentId),
387
+ "Artículo publicado en el sitio.",
388
+ "No se pudo publicar."
389
+ );
390
+ return run(
391
+ "publicar",
392
+ () => api.schedule(documentId, fromMexicoLocal(fecha, hora)),
393
+ "Publicación programada.",
394
+ "No se pudo programar."
395
+ );
396
+ };
397
+ const abrirCompartir = (red) => {
398
+ if (!d) return;
399
+ setMensaje(
400
+ defaultPostText({ titulo: d.titulo, descripcion: d.descripcion, keywords: d.keywords }, red)
401
+ );
402
+ setConfirmar(red);
403
+ };
404
+ const compartir = async (red) => {
405
+ setConfirmar(null);
406
+ setBusy(red);
407
+ try {
408
+ await api.share(documentId, red, mensaje.trim());
409
+ setD(await api.detail(documentId));
410
+ notify("success", `Compartido en ${red === "linkedin" ? "LinkedIn" : "Facebook"}.`);
411
+ } catch (err) {
412
+ notify("danger", errorMessage(err, "No se pudo compartir."));
413
+ } finally {
414
+ setBusy(null);
415
+ }
416
+ };
417
+ if (!d) {
418
+ return /* @__PURE__ */ jsx(Flex, { justifyContent: "center", padding: 10, children: /* @__PURE__ */ jsx(Loader, { children: "Cargando artículo…" }) });
419
+ }
420
+ const referencias = d.fuentes.filter((f) => f.tipo === "referencia");
421
+ const encontradas = d.fuentes.filter((f) => f.tipo === "busqueda");
422
+ return /* @__PURE__ */ jsxs(Box, { padding: 8, paddingTop: 6, children: [
423
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 4, marginBottom: 6, children: [
424
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
425
+ Button,
426
+ {
427
+ variant: "ghost",
428
+ startIcon: /* @__PURE__ */ jsx(ArrowLeft, {}),
429
+ onClick: () => navigate(`/plugins/${PLUGIN_ID}`),
430
+ children: "Volver a artículos"
431
+ }
432
+ ) }),
433
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "flex-end", gap: 4, wrap: "wrap", children: [
434
+ /* @__PURE__ */ jsxs(
435
+ Flex,
436
+ {
437
+ direction: "column",
438
+ alignItems: "flex-start",
439
+ gap: 2,
440
+ style: { flex: "1 1 520px", minWidth: 0 },
441
+ children: [
442
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, wrap: "wrap", children: [
443
+ /* @__PURE__ */ jsx(Badge, { variant: ESTADO[d.estado].variant, children: ESTADO[d.estado].label }),
444
+ d.categoria && /* @__PURE__ */ jsx(Badge, { children: d.categoria }),
445
+ d.minuteRead && /* @__PURE__ */ jsx(Badge, { children: `${d.minuteRead} min de lectura` })
446
+ ] }),
447
+ /* @__PURE__ */ jsx(Typography, { variant: "alpha", tag: "h1", children: d.titulo })
448
+ ]
449
+ }
450
+ ),
451
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, wrap: "wrap", children: [
452
+ /* @__PURE__ */ jsx(
453
+ LinkButton,
454
+ {
455
+ variant: "tertiary",
456
+ size: "L",
457
+ startIcon: /* @__PURE__ */ jsx(Pencil, {}),
458
+ href: contentManagerUrl(d.documentId),
459
+ children: "Editar en el Content Manager"
460
+ }
461
+ ),
462
+ d.estado === "publicado" && d.slug && /* @__PURE__ */ jsx(
463
+ LinkButton,
464
+ {
465
+ variant: "tertiary",
466
+ size: "L",
467
+ startIcon: /* @__PURE__ */ jsx(ExternalLink, {}),
468
+ href: `${SITE_URL}/${d.slug}`,
469
+ isExternal: true,
470
+ children: "Ver en el sitio"
471
+ }
472
+ )
473
+ ] })
474
+ ] })
475
+ ] }),
476
+ /* @__PURE__ */ jsxs(Flex, { gap: 6, alignItems: "flex-start", wrap: "wrap", children: [
477
+ /* @__PURE__ */ jsx(
478
+ Box,
479
+ {
480
+ background: "neutral0",
481
+ padding: 6,
482
+ hasRadius: true,
483
+ shadow: "tableShadow",
484
+ style: { flex: "999 1 560px", minWidth: 0 },
485
+ children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 6, children: [
486
+ d.imageUrl ? /* @__PURE__ */ jsxs(Box, { position: "relative", children: [
487
+ /* @__PURE__ */ jsx(
488
+ "img",
489
+ {
490
+ src: d.imageUrl,
491
+ alt: d.imageName ?? "",
492
+ style: {
493
+ display: "block",
494
+ width: "100%",
495
+ aspectRatio: "16 / 9",
496
+ objectFit: "cover",
497
+ borderRadius: 4
498
+ }
499
+ }
500
+ ),
501
+ /* @__PURE__ */ jsx(Box, { position: "absolute", right: 4, bottom: 4, children: /* @__PURE__ */ jsx(
502
+ Button,
503
+ {
504
+ variant: "tertiary",
505
+ onClick: () => setPicker(true),
506
+ loading: busy === "imagen",
507
+ children: "Cambiar imagen"
508
+ }
509
+ ) })
510
+ ] }) : /* @__PURE__ */ jsxs(
511
+ Flex,
512
+ {
513
+ direction: "column",
514
+ gap: 3,
515
+ justifyContent: "center",
516
+ hasRadius: true,
517
+ background: "neutral100",
518
+ borderColor: "neutral200",
519
+ borderStyle: "dashed",
520
+ borderWidth: "1px",
521
+ style: { aspectRatio: "16 / 9" },
522
+ children: [
523
+ /* @__PURE__ */ jsx(Box, { color: "neutral500", children: /* @__PURE__ */ jsx(Image, { width: 32, height: 32 }) }),
524
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", children: "Sin imagen principal" }),
525
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Elige una de la biblioteca de Image Studio." }),
526
+ /* @__PURE__ */ jsx(
527
+ Button,
528
+ {
529
+ variant: "secondary",
530
+ size: "L",
531
+ onClick: () => setPicker(true),
532
+ loading: busy === "imagen",
533
+ children: "Elegir imagen"
534
+ }
535
+ )
536
+ ]
537
+ }
538
+ ),
539
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 4, paddingLeft: 2, paddingRight: 2, children: [
540
+ /* @__PURE__ */ jsx(Typography, { variant: "epsilon", textColor: "neutral700", children: d.descripcion }),
541
+ /* @__PURE__ */ jsx(Typography, { textColor: "neutral700", style: { lineHeight: 1.65 }, children: texto }),
542
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Vista previa del inicio del contenido. El texto completo se edita en el Content Manager." })
543
+ ] })
544
+ ] })
545
+ }
546
+ ),
547
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 4, style: { flex: "1 1 340px" }, children: [
548
+ d.estado === "borrador" && /* @__PURE__ */ jsxs(Tarjeta, { titulo: "Para publicar en el sitio", children: [
549
+ /* @__PURE__ */ jsx(Requisito, { ok: true, children: "Contenido y SEO" }),
550
+ /* @__PURE__ */ jsx(
551
+ Requisito,
552
+ {
553
+ ok: d.imagen,
554
+ accion: /* @__PURE__ */ jsx(
555
+ Button,
556
+ {
557
+ variant: d.imagen ? "tertiary" : "secondary",
558
+ onClick: () => setPicker(true),
559
+ children: d.imagen ? "Cambiar" : "Elegir imagen"
560
+ }
561
+ ),
562
+ children: "Imagen principal"
563
+ }
564
+ ),
565
+ /* @__PURE__ */ jsx(Box, { paddingTop: 2, children: /* @__PURE__ */ jsxs(Field.Root, { name: "cuando", children: [
566
+ /* @__PURE__ */ jsx(Field.Label, { children: "¿Cuándo?" }),
567
+ /* @__PURE__ */ jsxs(
568
+ Radio.Group,
569
+ {
570
+ value: cuando,
571
+ onValueChange: (v) => setCuando(v),
572
+ "aria-label": "¿Cuándo?",
573
+ children: [
574
+ /* @__PURE__ */ jsx(Radio.Item, { value: "ahora", children: "Publicar ahora" }),
575
+ /* @__PURE__ */ jsx(Radio.Item, { value: "programar", children: "Programar" })
576
+ ]
577
+ }
578
+ )
579
+ ] }) }),
580
+ cuando === "programar" ? /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
581
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
582
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1 }, children: /* @__PURE__ */ jsxs(Field.Root, { name: "fecha", children: [
583
+ /* @__PURE__ */ jsx(Field.Label, { children: "Fecha" }),
584
+ /* @__PURE__ */ jsx(
585
+ TextInput,
586
+ {
587
+ type: "date",
588
+ value: fecha,
589
+ onChange: (e) => setFecha(e.target.value)
590
+ }
591
+ )
592
+ ] }) }),
593
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1 }, children: /* @__PURE__ */ jsxs(Field.Root, { name: "hora", children: [
594
+ /* @__PURE__ */ jsx(Field.Label, { children: "Hora" }),
595
+ /* @__PURE__ */ jsx(
596
+ TextInput,
597
+ {
598
+ type: "time",
599
+ value: hora,
600
+ onChange: (e) => setHora(e.target.value)
601
+ }
602
+ )
603
+ ] }) })
604
+ ] }),
605
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Hora del centro de México. La fecha de publicación del artículo será la programada." })
606
+ ] }) : /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "La fecha de publicación del artículo será hoy." }),
607
+ /* @__PURE__ */ jsx(
608
+ Button,
609
+ {
610
+ size: "L",
611
+ fullWidth: true,
612
+ disabled: !d.imagen || cuando === "programar" && (!fecha || !hora),
613
+ loading: busy === "publicar",
614
+ onClick: publicar,
615
+ children: cuando === "programar" ? "Programar publicación" : "Publicar en el sitio"
616
+ }
617
+ ),
618
+ !d.imagen && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", textAlign: "center", children: "Elige una imagen para poder publicar." })
619
+ ] }),
620
+ d.estado === "programado" && d.programadaPara && /* @__PURE__ */ jsxs(Tarjeta, { titulo: "Publicación programada", children: [
621
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
622
+ /* @__PURE__ */ jsx(Box, { color: "secondary600", children: /* @__PURE__ */ jsx(Clock, { width: 16, height: 16 }) }),
623
+ /* @__PURE__ */ jsx(Typography, { children: `Se publicará el ${formatFechaHora(d.programadaPara)}` })
624
+ ] }),
625
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, wrap: "wrap", paddingTop: 2, children: [
626
+ /* @__PURE__ */ jsx(
627
+ Button,
628
+ {
629
+ variant: "secondary",
630
+ loading: busy === "publicar",
631
+ onClick: () => run(
632
+ "publicar",
633
+ () => api.publish(documentId),
634
+ "Artículo publicado en el sitio.",
635
+ "No se pudo publicar."
636
+ ),
637
+ children: "Publicar ahora"
638
+ }
639
+ ),
640
+ /* @__PURE__ */ jsx(
641
+ Button,
642
+ {
643
+ variant: "tertiary",
644
+ loading: busy === "cancelar",
645
+ onClick: () => run(
646
+ "cancelar",
647
+ () => api.unschedule(documentId),
648
+ "Programación cancelada.",
649
+ "No se pudo cancelar."
650
+ ),
651
+ children: "Cancelar programación"
652
+ }
653
+ )
654
+ ] })
655
+ ] }),
656
+ d.estado === "publicado" && /* @__PURE__ */ jsxs(Fragment, { children: [
657
+ /* @__PURE__ */ jsx(Tarjeta, { titulo: "Publicado en el sitio", children: /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
658
+ /* @__PURE__ */ jsx(Box, { color: "success600", children: /* @__PURE__ */ jsx(Check, { width: 16, height: 16 }) }),
659
+ /* @__PURE__ */ jsx(Typography, { children: `Fecha de publicación: ${formatFecha(d.fechaPublicacion)}` })
660
+ ] }) }),
661
+ /* @__PURE__ */ jsx(Tarjeta, { titulo: "Compartir en redes", extra: /* @__PURE__ */ jsx(Badge, { children: "Opcional" }), children: REDES.map((red) => {
662
+ const postId = d[red.campo];
663
+ return /* @__PURE__ */ jsxs(
664
+ Flex,
665
+ {
666
+ justifyContent: "space-between",
667
+ alignItems: "center",
668
+ gap: 3,
669
+ paddingTop: 2,
670
+ style: { minHeight: 44 },
671
+ children: [
672
+ /* @__PURE__ */ jsx(Typography, { fontWeight: "bold", children: red.nombre }),
673
+ postId ? /* @__PURE__ */ jsx(Link, { href: red.url(postId), isExternal: true, endIcon: /* @__PURE__ */ jsx(ExternalLink, {}), children: `Ver en ${red.nombre}` }) : /* @__PURE__ */ jsx(
674
+ Button,
675
+ {
676
+ variant: "tertiary",
677
+ "aria-label": `Compartir en ${red.nombre}`,
678
+ loading: busy === red.id,
679
+ onClick: () => abrirCompartir(red.id),
680
+ children: "Compartir"
681
+ }
682
+ )
683
+ ]
684
+ },
685
+ red.id
686
+ );
687
+ }) })
688
+ ] }),
689
+ d.fuentes.length > 0 && /* @__PURE__ */ jsxs(
690
+ Tarjeta,
691
+ {
692
+ titulo: "Fuentes",
693
+ extra: /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Verifícalas antes de publicar" }),
694
+ children: [
695
+ referencias.length > 0 && /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
696
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: "Tus referencias" }),
697
+ referencias.map((f) => /* @__PURE__ */ jsx(Link, { href: f.url, isExternal: true, children: f.titulo }, f.url))
698
+ ] }),
699
+ encontradas.length > 0 && /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 2, paddingTop: 2, children: [
700
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: "Encontradas en Google" }),
701
+ encontradas.map((f) => /* @__PURE__ */ jsx(Link, { href: f.url, isExternal: true, children: f.titulo }, f.url))
702
+ ] })
703
+ ]
704
+ }
705
+ ),
706
+ /* @__PURE__ */ jsxs(Tarjeta, { titulo: "SEO", children: [
707
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 1, children: [
708
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Meta título" }),
709
+ /* @__PURE__ */ jsx(Typography, { children: d.seo?.metaTitle || "—" })
710
+ ] }),
711
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 1, children: [
712
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Meta descripción" }),
713
+ /* @__PURE__ */ jsx(Typography, { children: d.seo?.metaDescription || "—" })
714
+ ] }),
715
+ d.keywords.length > 0 && /* @__PURE__ */ jsx(Flex, { gap: 2, wrap: "wrap", children: d.keywords.map((k) => /* @__PURE__ */ jsx(Badge, { children: k }, k)) })
716
+ ] })
717
+ ] })
718
+ ] }),
719
+ /* @__PURE__ */ jsx(
720
+ LibraryImagePicker,
721
+ {
722
+ open: picker,
723
+ max: 1,
724
+ title: "Elegir imagen principal",
725
+ onClose: () => setPicker(false),
726
+ onPick: elegirImagen,
727
+ footerStart: /* @__PURE__ */ jsx(Link, { href: `/admin/${STUDIO_PATH}`, isExternal: true, endIcon: /* @__PURE__ */ jsx(ExternalLink, {}), children: "¿No está la que buscas? Créala en Image Studio" })
728
+ }
729
+ ),
730
+ /* @__PURE__ */ jsx(
731
+ Modal.Root,
732
+ {
733
+ open: confirmar !== null,
734
+ onOpenChange: (open) => !open && setConfirmar(null),
735
+ children: /* @__PURE__ */ jsxs(Modal.Content, { style: { width: "min(1040px, 94vw)", maxWidth: "min(1040px, 94vw)" }, children: [
736
+ /* @__PURE__ */ jsx(Modal.Header, { children: /* @__PURE__ */ jsx(Modal.Title, { children: `Compartir en ${confirmar === "facebook" ? "Facebook" : "LinkedIn"}` }) }),
737
+ /* @__PURE__ */ jsx(Modal.Body, { children: /* @__PURE__ */ jsxs(Flex, { gap: 6, alignItems: "flex-start", wrap: "wrap", children: [
738
+ /* @__PURE__ */ jsx(Box, { style: { flex: "1 1 340px", minWidth: 0 }, children: /* @__PURE__ */ jsxs(
739
+ Field.Root,
740
+ {
741
+ name: "mensaje",
742
+ hint: "Se publica con el enlace al artículo y su imagen. Una vez publicado no se puede borrar desde aquí.",
743
+ children: [
744
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "baseline", children: [
745
+ /* @__PURE__ */ jsx(Field.Label, { children: "Texto de la publicación" }),
746
+ /* @__PURE__ */ jsx(
747
+ Typography,
748
+ {
749
+ variant: "pi",
750
+ textColor: [...mensaje].length > POST_MAX ? "danger600" : "neutral500",
751
+ children: `${[...mensaje].length}/${POST_MAX}`
752
+ }
753
+ )
754
+ ] }),
755
+ /* @__PURE__ */ jsx(
756
+ Textarea,
757
+ {
758
+ value: mensaje,
759
+ onChange: (e) => setMensaje(e.target.value),
760
+ rows: 10
761
+ }
762
+ ),
763
+ /* @__PURE__ */ jsx(Field.Hint, {})
764
+ ]
765
+ }
766
+ ) }),
767
+ /* @__PURE__ */ jsx(Box, { style: { flex: "1 1 340px", minWidth: 0 }, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
768
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", children: "Vista previa" }),
769
+ confirmar && /* @__PURE__ */ jsx(SharePreview, { red: confirmar, mensaje, article: d }),
770
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Aproximada: cada red ajusta el diseño y corta el texto largo con «ver más»." })
771
+ ] }) })
772
+ ] }) }),
773
+ /* @__PURE__ */ jsxs(Modal.Footer, { children: [
774
+ /* @__PURE__ */ jsx(Button, { variant: "tertiary", onClick: () => setConfirmar(null), children: "Cancelar" }),
775
+ /* @__PURE__ */ jsx(
776
+ Button,
777
+ {
778
+ disabled: !mensaje.trim() || [...mensaje].length > POST_MAX,
779
+ onClick: () => confirmar && compartir(confirmar),
780
+ children: "Compartir"
781
+ }
782
+ )
783
+ ] })
784
+ ] })
785
+ }
786
+ )
787
+ ] });
788
+ };
789
+ const MAX_URLS = 3;
790
+ const NUEVA = "nueva:";
791
+ const LABELS = { label: "Las instrucciones", empty: "Escribe las instrucciones del artículo." };
792
+ const Panel = ({ children }) => /* @__PURE__ */ jsx(Box, { background: "neutral0", padding: 6, hasRadius: true, shadow: "tableShadow", children: /* @__PURE__ */ jsx(Flex, { direction: "column", alignItems: "stretch", gap: 6, children }) });
793
+ const Punto = ({ children }) => /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "flex-start", children: [
794
+ /* @__PURE__ */ jsx(Box, { color: "success600", paddingTop: 1, children: /* @__PURE__ */ jsx(Check, { width: 14, height: 14 }) }),
795
+ /* @__PURE__ */ jsx(Typography, { children })
796
+ ] });
797
+ const useElapsed = (running) => {
798
+ const [seconds, setSeconds] = useState(0);
799
+ useEffect(() => {
800
+ if (!running) return setSeconds(0);
801
+ const t = setInterval(() => setSeconds((s) => s + 1), 1e3);
802
+ return () => clearInterval(t);
803
+ }, [running]);
804
+ return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`;
805
+ };
806
+ const NewArticlePage = () => {
807
+ const { api, notify } = useArticlesApi();
808
+ const navigate = useNavigate();
809
+ const [categorias, setCategorias] = useState([]);
810
+ const [categoria, setCategoria] = useState();
811
+ const [titulo, setTitulo] = useState("");
812
+ const [temperatura, setTemperatura] = useState(0.5);
813
+ const [urls, setUrls] = useState([]);
814
+ const [busqueda, setBusqueda] = useState(false);
815
+ const [instrucciones, setInstrucciones] = useState("");
816
+ const [usarBase, setUsarBase] = useState(true);
817
+ const [guardado, setGuardado] = useState(null);
818
+ const [base, setBase] = useState("");
819
+ const [busy, setBusy] = useState(null);
820
+ const elapsed = useElapsed(busy === "generate");
821
+ const alive = useRef(true);
822
+ useEffect(() => {
823
+ alive.current = true;
824
+ api.categories().then(
825
+ (list) => alive.current && setCategorias(list),
826
+ () => notify("danger", "No se pudieron cargar las categorías.")
827
+ );
828
+ api.basePrompt().then(
829
+ (b) => {
830
+ if (!alive.current) return;
831
+ setGuardado(b);
832
+ setBase(b.prompt);
833
+ },
834
+ () => notify("danger", "No se pudo cargar el prompt base.")
835
+ );
836
+ return () => {
837
+ alive.current = false;
838
+ };
839
+ }, [api, notify]);
840
+ const baseEditado = Boolean(guardado) && base.trim() !== guardado?.prompt;
841
+ const estadoBase = !usarBase ? "sin prompt base (instrucciones tal cual)" : baseEditado ? "editado para este artículo" : guardado?.personalizado ? "personalizado" : "predeterminado";
842
+ const cambiarBase = async (work, ok) => {
843
+ setBusy("base");
844
+ try {
845
+ const b = await work();
846
+ if (!alive.current) return;
847
+ setGuardado(b);
848
+ setBase(b.prompt);
849
+ notify("success", ok);
850
+ } catch (err) {
851
+ notify("danger", errorMessage(err, "No se pudo actualizar el prompt base."));
852
+ } finally {
853
+ if (alive.current) setBusy(null);
854
+ }
855
+ };
856
+ const nueva = categoria?.startsWith(NUEVA) ? categoria.slice(NUEVA.length) : null;
857
+ const nombreCategoria = nueva ?? categorias.find((c) => c.id === categoria)?.nombre ?? "";
858
+ const crearCategoria = (value) => {
859
+ const nombre = value?.trim();
860
+ if (!nombre) return;
861
+ const existente = categorias.find((c) => c.nombre.toLowerCase() === nombre.toLowerCase());
862
+ setCategoria(existente ? existente.id : `${NUEVA}${nombre}`);
863
+ };
864
+ const elegirCategoria = (v) => {
865
+ if (!v) return setCategoria(void 0);
866
+ if (v.startsWith(NUEVA) || categorias.some((c) => c.id === v)) return setCategoria(v);
867
+ crearCategoria(v);
868
+ };
869
+ const enriquecer = async () => {
870
+ setBusy("enrich");
871
+ try {
872
+ const texto = await api.enrich(instrucciones.trim());
873
+ if (alive.current) setInstrucciones(texto);
874
+ } catch (err) {
875
+ notify("danger", errorMessage(err, "No se pudieron enriquecer las instrucciones."));
876
+ } finally {
877
+ if (alive.current) setBusy(null);
878
+ }
879
+ };
880
+ const generar = async (texto) => {
881
+ if (!categoria) return;
882
+ const body = {
883
+ titulo: titulo.trim(),
884
+ categoria: nueva ? { nueva } : { id: categoria },
885
+ temperatura,
886
+ instrucciones: texto,
887
+ urls: urls.map((u) => u.trim()).filter(Boolean),
888
+ busqueda,
889
+ usarPromptBase: usarBase,
890
+ ...usarBase && baseEditado && base.trim() ? { promptBase: base.trim() } : {}
891
+ };
892
+ setBusy("generate");
893
+ try {
894
+ const documentId = await api.generate(body);
895
+ notify("success", "Borrador generado. Elige la imagen y publícalo.");
896
+ navigate(articlePath(documentId));
897
+ } catch (err) {
898
+ notify("danger", errorMessage(err, "No se pudo generar el artículo. Intenta de nuevo."));
899
+ if (alive.current) setBusy(null);
900
+ }
901
+ };
902
+ return /* @__PURE__ */ jsxs(Box, { padding: 8, paddingTop: 6, children: [
903
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 4, marginBottom: 6, children: [
904
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", startIcon: /* @__PURE__ */ jsx(ArrowLeft, {}), onClick: () => navigate(".."), children: "Volver a artículos" }),
905
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "baseline", gap: 4, wrap: "wrap", width: "100%", children: [
906
+ /* @__PURE__ */ jsx(Typography, { variant: "alpha", tag: "h1", children: "Nuevo artículo" }),
907
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Se guarda como borrador. Nada se publica sin tu revisión." })
908
+ ] })
909
+ ] }),
910
+ busy === "generate" ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", children: /* @__PURE__ */ jsx(Box, { background: "neutral0", padding: 8, hasRadius: true, shadow: "tableShadow", style: { width: "100%", maxWidth: 720 }, "aria-live": "polite", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 6, children: [
911
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 2, children: [
912
+ /* @__PURE__ */ jsx(Typography, { variant: "beta", tag: "h2", children: "Redactando el artículo" }),
913
+ /* @__PURE__ */ jsx(Typography, { textColor: "neutral600", children: busqueda || urls.some(Boolean) ? "Gemini 2.5 Pro está investigando y escribiendo el borrador." : "Gemini 2.5 Pro está escribiendo el borrador con tus datos." })
914
+ ] }),
915
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "center", children: [
916
+ /* @__PURE__ */ jsx(Loader, { small: true, children: "Generando" }),
917
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: `${elapsed} transcurridos` })
918
+ ] }),
919
+ /* @__PURE__ */ jsx(Box, { padding: 5, hasRadius: true, borderColor: "neutral150", borderStyle: "solid", borderWidth: "1px", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 3, children: [
920
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: "Lo que pediste" }),
921
+ /* @__PURE__ */ jsx(Typography, { children: `Categoría: ${nombreCategoria}${nueva ? " (nueva)" : ""}` }),
922
+ /* @__PURE__ */ jsx(Typography, { children: `Creatividad: ${temperatura}` }),
923
+ /* @__PURE__ */ jsx(Typography, { children: `Búsqueda de Google: ${busqueda ? "activada" : "no"} · URLs de referencia: ${urls.filter(Boolean).length}` }),
924
+ /* @__PURE__ */ jsx(Typography, { children: `Prompt base: ${estadoBase}` })
925
+ ] }) }),
926
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Si sales de esta pantalla, el borrador aparecerá en Artículos al terminar." })
927
+ ] }) }) }) : /* @__PURE__ */ jsxs(Flex, { gap: 6, alignItems: "flex-start", wrap: "wrap", children: [
928
+ /* @__PURE__ */ jsx(Box, { style: { flex: "999 1 560px", minWidth: 0 }, children: /* @__PURE__ */ jsxs(Panel, { children: [
929
+ /* @__PURE__ */ jsx(Typography, { variant: "delta", tag: "h2", children: "¿De qué trata el artículo?" }),
930
+ /* @__PURE__ */ jsxs(Flex, { gap: 6, alignItems: "flex-start", wrap: "wrap", children: [
931
+ /* @__PURE__ */ jsx(Box, { style: { flex: "1 1 280px" }, children: /* @__PURE__ */ jsxs(Field.Root, { name: "titulo", hint: "Opcional. Si lo dejas vacío, Gemini propone uno.", children: [
932
+ /* @__PURE__ */ jsx(Field.Label, { children: "Título" }),
933
+ /* @__PURE__ */ jsx(TextInput, { placeholder: "Ej: Las 10 tendencias en IA para 2026", value: titulo, onChange: (e) => setTitulo(e.target.value) }),
934
+ /* @__PURE__ */ jsx(Field.Hint, {})
935
+ ] }) }),
936
+ /* @__PURE__ */ jsx(Box, { style: { flex: "1 1 280px" }, children: /* @__PURE__ */ jsxs(Field.Root, { name: "categoria", required: true, hint: nueva ? void 0 : "Elige una o escribe para crear una nueva.", children: [
937
+ /* @__PURE__ */ jsx(Field.Label, { children: "Categoría" }),
938
+ /* @__PURE__ */ jsxs(
939
+ Combobox,
940
+ {
941
+ placeholder: "Busca o escribe una nueva",
942
+ value: categoria,
943
+ onChange: elegirCategoria,
944
+ creatable: true,
945
+ createMessage: (v) => `Crear categoría «${v}»`,
946
+ onCreateOption: crearCategoria,
947
+ onClear: () => setCategoria(void 0),
948
+ children: [
949
+ categorias.map((c) => /* @__PURE__ */ jsx(ComboboxOption, { value: c.id, children: c.nombre }, c.id)),
950
+ nueva && /* @__PURE__ */ jsx(ComboboxOption, { value: categoria, children: nueva })
951
+ ]
952
+ }
953
+ ),
954
+ nueva ? /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", paddingTop: 1, children: [
955
+ /* @__PURE__ */ jsx(Badge, { variant: "primary", children: "Nueva" }),
956
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Se crea en Categorías al generar el artículo." })
957
+ ] }) : /* @__PURE__ */ jsx(Field.Hint, {})
958
+ ] }) })
959
+ ] }),
960
+ /* @__PURE__ */ jsxs(Field.Root, { name: "temperatura", children: [
961
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "baseline", children: [
962
+ /* @__PURE__ */ jsx(Field.Label, { children: "Creatividad" }),
963
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", children: temperatura })
964
+ ] }),
965
+ /* @__PURE__ */ jsx(
966
+ "input",
967
+ {
968
+ type: "range",
969
+ "aria-label": "Creatividad",
970
+ min: 0,
971
+ max: 2,
972
+ step: 0.05,
973
+ value: temperatura,
974
+ onChange: (e) => setTemperatura(Number(e.target.value)),
975
+ style: { width: "100%" }
976
+ }
977
+ ),
978
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", children: [
979
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Más precisa y sobria" }),
980
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Más creativa y variada" })
981
+ ] })
982
+ ] }),
983
+ /* @__PURE__ */ jsx(Box, { padding: 5, hasRadius: true, borderColor: "neutral150", borderStyle: "solid", borderWidth: "1px", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 5, children: [
984
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", tag: "h3", children: "Fuentes" }),
985
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 2, children: [
986
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", children: "URLs de referencia" }),
987
+ urls.map((url, i) => /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
988
+ /* @__PURE__ */ jsx(Box, { style: { flex: 1 }, children: /* @__PURE__ */ jsx(
989
+ TextInput,
990
+ {
991
+ "aria-label": `URL de referencia ${i + 1}`,
992
+ type: "url",
993
+ placeholder: "https://",
994
+ value: url,
995
+ onChange: (e) => setUrls((list) => list.map((u, j) => j === i ? e.target.value : u))
996
+ }
997
+ ) }),
998
+ /* @__PURE__ */ jsx(IconButton, { label: `Quitar URL ${i + 1}`, variant: "tertiary", onClick: () => setUrls((list) => list.filter((_, j) => j !== i)), children: /* @__PURE__ */ jsx(Trash, {}) })
999
+ ] }, i)),
1000
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", gap: 2, wrap: "wrap", children: [
1001
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: "Hasta 3 páginas: una nota, un caso, una página de servicio. Gemini escribe a partir de ellas." }),
1002
+ urls.length < MAX_URLS && /* @__PURE__ */ jsx(Button, { variant: "tertiary", size: "S", startIcon: /* @__PURE__ */ jsx(Plus, {}), onClick: () => setUrls((list) => [...list, ""]), children: "Agregar URL" })
1003
+ ] })
1004
+ ] }),
1005
+ /* @__PURE__ */ jsxs(Field.Root, { name: "busqueda", hint: "Usa información actual y muestra las fuentes en la revisión. Tiene un costo adicional por artículo.", children: [
1006
+ /* @__PURE__ */ jsx(Field.Label, { children: "Buscar en Google" }),
1007
+ /* @__PURE__ */ jsx(Toggle, { onLabel: "Sí", offLabel: "No", checked: busqueda, onChange: (e) => setBusqueda(e.target.checked) }),
1008
+ /* @__PURE__ */ jsx(Field.Hint, {})
1009
+ ] })
1010
+ ] }) }),
1011
+ /* @__PURE__ */ jsx(Box, { padding: 5, hasRadius: true, borderColor: "neutral150", borderStyle: "solid", borderWidth: "1px", children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 4, children: [
1012
+ /* @__PURE__ */ jsxs(
1013
+ Field.Root,
1014
+ {
1015
+ name: "usarPromptBase",
1016
+ hint: usarBase ? "Persona, tono, reglas y arco narrativo de InterWare. Lo que edites se usa en este artículo." : "Se mandan tus instrucciones tal cual, sin el prompt base.",
1017
+ children: [
1018
+ /* @__PURE__ */ jsx(Field.Label, { children: "Usar prompt base" }),
1019
+ /* @__PURE__ */ jsx(Toggle, { onLabel: "Sí", offLabel: "No", checked: usarBase, onChange: (e) => setUsarBase(e.target.checked) }),
1020
+ /* @__PURE__ */ jsx(Field.Hint, {})
1021
+ ]
1022
+ }
1023
+ ),
1024
+ usarBase && /* @__PURE__ */ jsxs(Fragment, { children: [
1025
+ /* @__PURE__ */ jsxs(Field.Root, { name: "promptBase", children: [
1026
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", gap: 2, children: [
1027
+ /* @__PURE__ */ jsx(Field.Label, { children: "Prompt base" }),
1028
+ baseEditado ? /* @__PURE__ */ jsx(Badge, { variant: "warning", children: "Con cambios sin guardar" }) : guardado?.personalizado && /* @__PURE__ */ jsx(Badge, { variant: "primary", children: "Personalizado" })
1029
+ ] }),
1030
+ /* @__PURE__ */ jsx(Textarea, { value: base, onChange: (e) => setBase(e.target.value), rows: 14, disabled: !guardado })
1031
+ ] }),
1032
+ /* @__PURE__ */ jsxs(Flex, { gap: 2, wrap: "wrap", justifyContent: "flex-end", children: [
1033
+ baseEditado && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "S", disabled: busy !== null, onClick: () => setBase(guardado?.prompt ?? ""), children: "Descartar cambios" }),
1034
+ guardado?.personalizado && !baseEditado && /* @__PURE__ */ jsx(Button, { variant: "tertiary", size: "S", loading: busy === "base", disabled: busy !== null, onClick: () => cambiarBase(() => api.resetBasePrompt(), "Se restauró el prompt base original."), children: "Restaurar el original" }),
1035
+ /* @__PURE__ */ jsx(
1036
+ Button,
1037
+ {
1038
+ variant: "tertiary",
1039
+ size: "S",
1040
+ loading: busy === "base",
1041
+ disabled: busy !== null || !baseEditado || !base.trim(),
1042
+ onClick: () => cambiarBase(() => api.saveBasePrompt(base.trim()), "Prompt base guardado como predeterminado."),
1043
+ children: "Guardar como predeterminado"
1044
+ }
1045
+ )
1046
+ ] })
1047
+ ] })
1048
+ ] }) }),
1049
+ /* @__PURE__ */ jsx(
1050
+ PromptComposer,
1051
+ {
1052
+ label: "Instrucciones",
1053
+ value: instrucciones,
1054
+ onChange: setInstrucciones,
1055
+ onSubmit: generar,
1056
+ labels: LABELS,
1057
+ placeholder: "Tema, público, puntos que debe cubrir, tono, llamada a la acción…",
1058
+ actionLabel: "Generar artículo",
1059
+ actionIcon: /* @__PURE__ */ jsx(Sparkle, {}),
1060
+ loading: false,
1061
+ disabled: !categoria || busy !== null,
1062
+ hint: categoria ? "«Enriquecer» convierte una idea corta en instrucciones completas. ⌘/Ctrl + Enter también genera." : "Elige una categoría para generar.",
1063
+ tools: /* @__PURE__ */ jsx(Button, { variant: "tertiary", size: "S", startIcon: /* @__PURE__ */ jsx(Sparkle, {}), loading: busy === "enrich", disabled: busy !== null || !instrucciones.trim(), onClick: enriquecer, children: "Enriquecer instrucciones" })
1064
+ }
1065
+ )
1066
+ ] }) }),
1067
+ /* @__PURE__ */ jsx(Box, { style: { flex: "1 1 300px" }, children: /* @__PURE__ */ jsxs(Panel, { children: [
1068
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", tag: "h2", children: "Qué vas a obtener" }),
1069
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "stretch", gap: 3, children: [
1070
+ /* @__PURE__ */ jsx(Punto, { children: "Título, descripción y contenido completo" }),
1071
+ /* @__PURE__ */ jsx(Punto, { children: "Datos SEO y palabras clave" }),
1072
+ /* @__PURE__ */ jsx(Punto, { children: "Las fuentes consultadas, para verificarlas antes de publicar" }),
1073
+ /* @__PURE__ */ jsx(Punto, { children: "Un borrador listo para elegir imagen y publicar" })
1074
+ ] }),
1075
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 1, children: [
1076
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: "Modelo" }),
1077
+ /* @__PURE__ */ jsx(Typography, { children: "Gemini 2.5 Pro · Vertex AI" })
1078
+ ] })
1079
+ ] }) })
1080
+ ] })
1081
+ ] });
1082
+ };
1083
+ const sceneSchema = Yup.object().shape({
1084
+ prompt: Yup.string().trim().required("El prompt es obligatorio para esta escena"),
1085
+ durationSeconds: Yup.number().min(1, "La duración mínima es 1 segundo").max(90, "La duración máxima es 90 segundos").required("La duración es obligatoria"),
1086
+ referenceImages: Yup.array().of(
1087
+ Yup.object().shape({
1088
+ id: Yup.number().required(),
1089
+ file: Yup.mixed().optional(),
1090
+ previewUrl: Yup.string().optional()
1091
+ })
1092
+ ).max(3, "Se permiten máximo 3 imágenes de referencia por escena").optional()
1093
+ });
1094
+ const validationSchema = Yup.object().shape({
1095
+ aspectRatio: Yup.string().oneOf(["16:9", "9:16", "1:1"], "Aspect ratio no válido").required("El aspect ratio es obligatorio"),
1096
+ scenes: Yup.array().of(sceneSchema).min(1, "Debe haber al menos una escena").max(20, "No se permiten más de 20 escenas")
1097
+ });
1098
+ const initialValues = {
1099
+ aspectRatio: "16:9",
1100
+ scenes: [
1101
+ {
1102
+ prompt: ``,
1103
+ durationSeconds: 8,
1104
+ narrationText: ``,
1105
+ referenceImages: []
1106
+ }
1107
+ ]
1108
+ };
1109
+ const Form = ({ onSubmit }) => {
1110
+ const formik = useFormik({
1111
+ initialValues,
1112
+ validationSchema,
1113
+ validateOnMount: true,
1114
+ onSubmit
1115
+ });
1116
+ console.log("🚀 ~ Form ~ formik:", formik.errors);
1117
+ return /* @__PURE__ */ jsx(Box, { background: "neutral0", padding: 6, hasRadius: true, width: "100%", children: /* @__PURE__ */ jsx("form", { onSubmit: formik.handleSubmit, children: /* @__PURE__ */ jsxs(Grid.Root, { gap: 4, children: [
1118
+ /* @__PURE__ */ jsx(Grid.Item, { m: 12, children: /* @__PURE__ */ jsxs(Field.Root, { id: "aspectRatio", width: "100%", required: true, children: [
1119
+ /* @__PURE__ */ jsx(Field.Label, { children: "Relación de aspecto" }),
1120
+ /* @__PURE__ */ jsxs(
1121
+ Combobox,
1122
+ {
1123
+ name: "aspectRatio",
1124
+ onChange: (value) => formik.setFieldValue("aspectRatio", value),
1125
+ onBlur: () => formik.setFieldTouched("aspectRatio", true),
1126
+ value: formik.values.aspectRatio,
1127
+ onClear: () => formik.setFieldValue("aspectRatio", ""),
1128
+ children: [
1129
+ /* @__PURE__ */ jsx(ComboboxOption, { value: "16:9", children: "16:9" }),
1130
+ /* @__PURE__ */ jsx(ComboboxOption, { value: "9:16", children: "9:16" }),
1131
+ /* @__PURE__ */ jsx(ComboboxOption, { value: "1:1", children: "1:1" })
1132
+ ]
1133
+ }
1134
+ ),
1135
+ /* @__PURE__ */ jsx(Field.Error, { children: formik.errors.aspectRatio })
1136
+ ] }) }),
1137
+ formik.values.scenes.map((scene, index) => /* @__PURE__ */ jsx(Grid.Item, { children: /* @__PURE__ */ jsxs(Box, { background: "neutral100", padding: 4, hasRadius: true, width: "100%", children: [
1138
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", marginBottom: 3, children: [
1139
+ /* @__PURE__ */ jsxs("strong", { children: [
1140
+ "Escena ",
1141
+ index + 1
1142
+ ] }),
1143
+ formik.values.scenes.length > 1 && /* @__PURE__ */ jsx(
1144
+ Button,
1145
+ {
1146
+ variant: "danger-light",
1147
+ onClick: () => {
1148
+ const updatedScenes = [...formik.values.scenes];
1149
+ updatedScenes.splice(index, 1);
1150
+ formik.setFieldValue("scenes", updatedScenes);
1151
+ },
1152
+ children: "Eliminar Escena"
1153
+ }
1154
+ )
1155
+ ] }),
1156
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, children: [
1157
+ /* @__PURE__ */ jsxs(Field.Root, { id: `scenes.${index}.prompt`, width: "100%", required: true, children: [
1158
+ /* @__PURE__ */ jsx(Field.Label, { children: "Prompt de la escena" }),
1159
+ /* @__PURE__ */ jsx(
1160
+ Textarea,
1161
+ {
1162
+ name: `scenes.${index}.prompt`,
1163
+ placeholder: "Describe visualmente esta escena",
1164
+ onChange: formik.handleChange,
1165
+ onBlur: formik.handleBlur,
1166
+ value: formik.values.scenes[index].prompt,
1167
+ rows: 6
1168
+ }
1169
+ )
1170
+ ] }),
1171
+ /* @__PURE__ */ jsxs(Field.Root, { id: `scenes.${index}.durationSeconds`, width: "100%", required: true, children: [
1172
+ /* @__PURE__ */ jsx(Field.Label, { children: "Duración (segundos)" }),
1173
+ /* @__PURE__ */ jsxs(
1174
+ Combobox,
1175
+ {
1176
+ name: `scenes.${index}.durationSeconds`,
1177
+ onChange: (value) => formik.setFieldValue(`scenes.${index}.durationSeconds`, value),
1178
+ onBlur: formik.handleBlur,
1179
+ value: formik.values.scenes[index].durationSeconds,
1180
+ onClear: () => formik.setFieldValue(`scenes.${index}.durationSeconds`, ""),
1181
+ children: [
1182
+ /* @__PURE__ */ jsx(ComboboxOption, { value: 4, children: "4" }),
1183
+ /* @__PURE__ */ jsx(ComboboxOption, { value: 6, children: "6" }),
1184
+ /* @__PURE__ */ jsx(ComboboxOption, { value: 8, children: "8" })
1185
+ ]
1186
+ }
1187
+ )
1188
+ ] }),
1189
+ /* @__PURE__ */ jsxs(Field.Root, { id: `scenes.${index}.referenceImages`, width: "100%", children: [
1190
+ /* @__PURE__ */ jsx(Field.Label, { children: "Imagen(es) de referencia" }),
1191
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, width: "100%", children: [
1192
+ scene.referenceImages.map((image, imgIndex) => /* @__PURE__ */ jsx(Box, { background: "neutral200", padding: 2, hasRadius: true, width: "100%", children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
1193
+ image.previewUrl ? /* @__PURE__ */ jsx("img", { src: image.previewUrl, alt: "preview", style: { width: 50, height: 50, objectFit: "cover" } }) : /* @__PURE__ */ jsx("div", { style: { width: 50, height: 50, backgroundColor: "#ccc" } }),
1194
+ /* @__PURE__ */ jsx(
1195
+ IconButton,
1196
+ {
1197
+ onClick: () => {
1198
+ const imageToRemove = scene.referenceImages[imgIndex];
1199
+ if (imageToRemove.previewUrl) {
1200
+ URL.revokeObjectURL(imageToRemove.previewUrl);
1201
+ }
1202
+ const updatedImages = [...scene.referenceImages];
1203
+ updatedImages.splice(imgIndex, 1);
1204
+ formik.setFieldValue(`scenes.${index}.referenceImages`, updatedImages);
1205
+ },
1206
+ label: "Eliminar imagen",
1207
+ children: /* @__PURE__ */ jsx(Trash, {})
1208
+ }
1209
+ )
1210
+ ] }) }, image.id)),
1211
+ /* @__PURE__ */ jsx(
1212
+ Button,
1213
+ {
1214
+ type: "button",
1215
+ startIcon: /* @__PURE__ */ jsx(PlusCircle, {}),
1216
+ variant: "secondary",
1217
+ fullWidth: true,
1218
+ onClick: () => {
1219
+ const input = document.createElement("input");
1220
+ input.type = "file";
1221
+ input.accept = "image/*";
1222
+ input.onchange = (e) => {
1223
+ const selectedFile = e.target.files?.[0];
1224
+ if (!selectedFile) return;
1225
+ const newImage = {
1226
+ id: Date.now(),
1227
+ file: selectedFile,
1228
+ previewUrl: URL.createObjectURL(selectedFile)
1229
+ };
1230
+ const newImages = [...scene.referenceImages, newImage];
1231
+ formik.setFieldValue(`scenes.${index}.referenceImages`, newImages.slice(0, 5));
1232
+ };
1233
+ input.click();
1234
+ },
1235
+ disabled: scene.referenceImages.length >= 5,
1236
+ children: "Añadir imagen de referencia"
1237
+ }
1238
+ )
1239
+ ] })
1240
+ ] })
1241
+ ] })
1242
+ ] }, index) }, `scene-${index}`)),
1243
+ /* @__PURE__ */ jsx(Grid.Item, { m: 12, children: /* @__PURE__ */ jsx(
1244
+ Button,
1245
+ {
1246
+ type: "button",
1247
+ startIcon: /* @__PURE__ */ jsx(PlusCircle, {}),
1248
+ variant: "secondary",
1249
+ onClick: () => {
1250
+ formik.setFieldValue("scenes", [
1251
+ ...formik.values.scenes,
1252
+ { prompt: "", durationSeconds: 6, narrationText: "", referenceImages: [] }
1253
+ ]);
1254
+ },
1255
+ fullWidth: true,
1256
+ children: "Añadir Escena"
1257
+ }
1258
+ ) }),
1259
+ /* @__PURE__ */ jsx(Grid.Item, { col: 12, children: /* @__PURE__ */ jsx(Flex, { width: "100%", justifyContent: "end", alignItems: "end", children: /* @__PURE__ */ jsx(
1260
+ Button,
1261
+ {
1262
+ type: "submit",
1263
+ fullWidth: true,
1264
+ disabled: !(formik.isValid && formik.dirty) || formik.isSubmitting,
1265
+ loading: formik.isSubmitting,
1266
+ children: "Generar Video"
1267
+ }
1268
+ ) }) })
1269
+ ] }) }) });
1270
+ };
1271
+ const useImages = () => {
1272
+ const { post } = useFetchClient();
1273
+ const generateVideoWithScenes = useCallback(
1274
+ async (data, config) => {
1275
+ return await post("/strapi-content-generator-gemini/generate-video-with-scenes", data, config);
1276
+ },
1277
+ [post]
1278
+ );
1279
+ return {
1280
+ generateVideoWithScenes
1281
+ };
1282
+ };
1283
+ const GenerateVideoContainer = () => {
1284
+ const { toggleNotification } = useNotification();
1285
+ const { generateVideoWithScenes } = useImages();
1286
+ const { formatMessage } = useIntl();
1287
+ const handleSubmit = async (values, { setSubmitting }) => {
1288
+ try {
1289
+ const formData = new FormData();
1290
+ formData.append("aspectRatio", values.aspectRatio);
1291
+ values.scenes.forEach((scene, index) => {
1292
+ formData.append(`scenes[${index}][prompt]`, scene.prompt);
1293
+ formData.append(`scenes[${index}][durationSeconds]`, scene.durationSeconds.toString());
1294
+ formData.append(`scenes[${index}][narrationText]`, scene.narrationText || "");
1295
+ scene.referenceImages.forEach((image, imgIndex) => {
1296
+ if (image.file) {
1297
+ formData.append(`scenes[${index}][referenceImages][${imgIndex}]`, image.file);
1298
+ }
1299
+ });
1300
+ });
1301
+ const response = await generateVideoWithScenes(formData);
1302
+ console.log("🚀 ~ handleSubmit ~ response:", response);
1303
+ toggleNotification({
1304
+ type: "success",
1305
+ message: formatMessage({
1306
+ id: `${PLUGIN_ID}.config.load.success`,
1307
+ defaultMessage: "Video generado con éxito"
1308
+ })
1309
+ });
1310
+ } catch (error) {
1311
+ toggleNotification({
1312
+ type: "danger",
1313
+ message: formatMessage({
1314
+ id: `${PLUGIN_ID}.config.load.error`,
1315
+ defaultMessage: "Ocurrió un error al generar el video"
1316
+ })
1317
+ });
1318
+ } finally {
1319
+ setSubmitting(false);
1320
+ }
1321
+ };
1322
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1323
+ /* @__PURE__ */ jsx(Grid.Root, { padding: 6, children: /* @__PURE__ */ jsx(Grid.Item, { children: /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "start", children: [
1324
+ /* @__PURE__ */ jsx(Typography, { variant: "alpha", children: "Generar video con IA" }),
1325
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", children: "Para uso exclusivo de InterWare de México" })
1326
+ ] }) }) }),
1327
+ /* @__PURE__ */ jsxs(Grid.Root, { padding: 6, children: [
1328
+ /* @__PURE__ */ jsx(Grid.Item, { m: 2 }),
1329
+ /* @__PURE__ */ jsx(Grid.Item, { m: 8, children: /* @__PURE__ */ jsx(Form, { onSubmit: handleSubmit }) })
1330
+ ] })
1331
+ ] });
1332
+ };
1333
+ const GenerateVideo = () => {
1334
+ return /* @__PURE__ */ jsxs(Page.Main, { children: [
1335
+ /* @__PURE__ */ jsx(Page.Title, { children: "Generar video con IA" }),
1336
+ /* @__PURE__ */ jsx(GenerateVideoContainer, {})
1337
+ ] });
1338
+ };
1339
+ const Screen = ({ title, children }) => /* @__PURE__ */ jsxs(Page.Main, { children: [
1340
+ /* @__PURE__ */ jsx(Page.Title, { children: title }),
1341
+ children
1342
+ ] });
1343
+ const App = () => {
1344
+ const { allowedActions, isLoading } = useRBAC(pluginPermissions);
1345
+ if (isLoading) {
1346
+ return /* @__PURE__ */ jsx(Page.Loading, {});
1347
+ }
1348
+ return /* @__PURE__ */ jsx(Page.Protect, { permissions: [...pluginPermissions.access], children: /* @__PURE__ */ jsx(PluginProviders, { children: /* @__PURE__ */ jsxs(Routes, { children: [
1349
+ allowedActions.canAccess && /* @__PURE__ */ jsx(Route, { index: true, element: /* @__PURE__ */ jsx(Screen, { title: "Generador de contenido", children: /* @__PURE__ */ jsx(ArticlesPage, {}) }) }),
1350
+ allowedActions.canAccess && /* @__PURE__ */ jsx(Route, { path: "/generate-content", element: /* @__PURE__ */ jsx(Screen, { title: "Nuevo artículo", children: /* @__PURE__ */ jsx(NewArticlePage, {}) }) }),
1351
+ allowedActions.canAccess && /* @__PURE__ */ jsx(Route, { path: "/articles/:documentId", element: /* @__PURE__ */ jsx(Screen, { title: "Artículo", children: /* @__PURE__ */ jsx(ArticlePage, {}) }) }),
1352
+ allowedActions.canAccess && /* @__PURE__ */ jsxs(Fragment, { children: [
1353
+ /* @__PURE__ */ jsx(Route, { path: "/image-studio", element: /* @__PURE__ */ jsx(StudioRedirect, {}) }),
1354
+ LEGACY_ROUTES.map((r) => /* @__PURE__ */ jsx(Route, { path: r.path, element: legacyRedirect(r.tab) }, r.path)),
1355
+ /* @__PURE__ */ jsx(Route, { path: "/generate-video", element: /* @__PURE__ */ jsx(GenerateVideo, {}) })
1356
+ ] }),
1357
+ /* @__PURE__ */ jsx(Route, { path: "*", element: /* @__PURE__ */ jsx(Page.Error, {}) })
1358
+ ] }) }) });
1359
+ };
1360
+ export {
1361
+ App
1362
+ };