Compare commits

...

1 Commits

Author SHA1 Message Date
Flatlogic Bot
45ba0624f1 Auto commit: 2026-06-01T16:57:26.745Z 2026-06-01 16:57:26 +00:00
6 changed files with 677 additions and 149 deletions

View File

@ -1,6 +1,5 @@
import React, {useEffect, useRef} from 'react'
import React, { useEffect, useRef, useState } from 'react'
import Link from 'next/link'
import { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon'

View File

@ -1,5 +1,4 @@
import React, { ReactNode, useEffect } from 'react'
import { useState } from 'react'
import React, { ReactNode, useEffect, useState } from 'react'
import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import menuAside from '../menuAside'

View File

@ -8,6 +8,12 @@ const menuAside: MenuAsideItem[] = [
label: 'Dashboard',
},
{
href: '/tools-studio',
icon: icon.mdiRobotExcitedOutline,
label: 'DarkFactory Studio',
},
{
href: '/users/users-list',
label: 'Users',

View File

@ -105,6 +105,19 @@ const Dashboard = () => {
main>
{''}
</SectionTitleLineWithButton>
<div className='mb-6 overflow-hidden rounded-3xl border border-fuchsia-500/20 bg-[#080A18] p-6 text-white shadow-xl shadow-fuchsia-500/10'>
<div className='flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between'>
<div>
<p className='text-sm font-bold uppercase tracking-[0.25em] text-cyan-300'>DarkFactory.ai member cockpit</p>
<h2 className='mt-2 text-2xl font-black'>Crea paquetes dark y revisa tu historial de producción.</h2>
<p className='mt-2 max-w-3xl text-sm text-slate-300'>Accede rápido a generadores de ideas, guiones, títulos, CTAs, thumbnails, prompts MGX y voces IA. Los resultados se guardan en Tool runs cuando tu rol lo permite.</p>
</div>
<Link href='/tools-studio' className='rounded-full bg-gradient-to-r from-fuchsia-500 to-cyan-400 px-6 py-3 text-center font-black text-white shadow-lg shadow-cyan-500/20 transition hover:-translate-y-0.5'>
Abrir Tools Studio
</Link>
</div>
</div>
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
currentUser={currentUser}

View File

@ -1,161 +1,253 @@
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import React, { ReactElement } from 'react';
import LayoutGuest from '../layouts/Guest';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
const scriptExamples = [
{
label: 'YouTube Dark · Finanzas',
title: '“7 hábitos silenciosos que te hacen perder dinero”',
copy: 'Hook: Si hoy ganas bien pero sigues sin avanzar, probablemente estás repitiendo uno de estos hábitos invisibles...',
},
{
label: 'TikTok · Curiosidades',
title: '“La regla de 10 segundos que usan los creadores virales”',
copy: 'Hook: Antes de grabar otro video, prueba esto: los primeros 10 segundos deciden si te ignoran o te siguen...',
},
{
label: 'Reels · Motivación',
title: '“Nadie habla del lado oscuro de empezar desde cero”',
copy: 'Hook: Empezar sin mostrar tu rostro no es esconderte: es construir un sistema que trabaje por ti todos los días...',
},
];
const plans = [
{
name: 'Free',
price: 'R$0',
badge: 'Para probar',
benefits: ['5 generaciones demo', 'Ideas para Shorts', 'Plantillas básicas'],
},
{
name: 'Pro',
price: 'R$29/mes',
badge: 'Más popular',
highlighted: true,
benefits: ['Guiones ilimitados', 'Títulos + CTAs virales', 'Historial de creaciones', 'Paquetes semanales'],
},
{
name: 'Premium',
price: 'R$49/mes',
badge: 'Escala total',
benefits: ['Prompts MGX avanzados', 'Ideas por nicho', 'Sistema de thumbnails', 'Prioridad en nuevas herramientas'],
},
];
const creatorResults = [
['+38%', 'retención media al usar hooks listos'],
['12h', 'ahorradas por semana en guiones y títulos'],
['3x', 'más consistencia publicando Shorts/Reels'],
];
export default function Starter() {
const [illustrationImage, setIllustrationImage] = useState({
src: undefined,
photographer: undefined,
photographer_url: undefined,
})
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
const [contentType, setContentType] = useState('video');
const [contentPosition, setContentPosition] = useState('left');
const textColor = useAppSelector((state) => state.style.linkColor);
const title = 'DarkFactory.ai'
// Fetch Pexels image/video
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
const imageBlock = (image) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
);
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
};
return (
<div
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<div className="min-h-screen bg-[#070814] text-white selection:bg-fuchsia-500/40">
<Head>
<title>{getPageTitle('Starter Page')}</title>
<title>{getPageTitle('DarkFactory.ai')}</title>
<meta
name="description"
content="DarkFactory.ai genera ideas, guiones, títulos, CTAs y prompts para canales dark con IA."
/>
</Head>
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: null}
{contentType === 'video' && contentPosition !== 'background'
? videoBlock(illustrationVideo)
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your DarkFactory.ai app!"/>
<div className="space-y-3">
<p className='text-center '>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center '>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
<header className="sticky top-0 z-30 border-b border-white/10 bg-[#070814]/85 backdrop-blur-xl">
<nav className="mx-auto flex max-w-7xl items-center justify-between px-5 py-4">
<Link href="/" className="group flex items-center gap-3">
<span className="grid h-10 w-10 place-items-center rounded-2xl bg-gradient-to-br from-fuchsia-500 via-violet-500 to-cyan-400 shadow-lg shadow-fuchsia-500/30">
</span>
<span className="text-lg font-black tracking-tight">
DarkFactory<span className="text-cyan-300">.ai</span>
</span>
</Link>
<div className="hidden items-center gap-6 text-sm text-slate-300 md:flex">
<a href="#herramientas" className="hover:text-white">Herramientas</a>
<a href="#precios" className="hover:text-white">Planes</a>
<a href="#proyecto" className="hover:text-white">Sobre</a>
</div>
<div className="flex items-center gap-3">
<Link href="/login" className="text-sm font-semibold text-slate-300 hover:text-white">
Login
</Link>
<Link
href="/register"
className="rounded-full bg-white px-4 py-2 text-sm font-bold text-[#090a18] shadow-lg shadow-cyan-500/20 transition hover:-translate-y-0.5 hover:bg-cyan-100"
>
Probar gratis
</Link>
</div>
</nav>
</header>
<main>
<section className="relative overflow-hidden px-5 py-20 sm:py-28">
<div className="absolute left-1/2 top-10 h-72 w-72 -translate-x-1/2 rounded-full bg-fuchsia-500/25 blur-3xl" />
<div className="absolute right-0 top-40 h-96 w-96 rounded-full bg-cyan-500/20 blur-3xl" />
<div className="relative mx-auto grid max-w-7xl items-center gap-12 lg:grid-cols-[1.05fr_0.95fr]">
<div>
<div className="mb-6 inline-flex items-center gap-2 rounded-full border border-cyan-300/30 bg-cyan-300/10 px-4 py-2 text-sm font-semibold text-cyan-100">
🤖 SaaS de contenido dark para monetizar sin aparecer
</div>
<h1 className="max-w-4xl text-5xl font-black leading-tight tracking-tight sm:text-6xl lg:text-7xl">
Crea videos que pueden generar dinero{' '}
<span className="bg-gradient-to-r from-fuchsia-300 via-violet-300 to-cyan-300 bg-clip-text text-transparent">
{}sin mostrar tu rostro.
</span>
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-slate-300">
DarkFactory.ai usa inteligencia artificial para generar ideas, guiones, títulos,
CTAs, prompts MGX y paquetes de publicación para YouTube, TikTok y Reels en minutos.
</p>
<div className="mt-9 flex flex-col gap-4 sm:flex-row">
<Link
href="/tools-studio"
className="rounded-full bg-gradient-to-r from-fuchsia-500 to-cyan-400 px-8 py-4 text-center font-black text-white shadow-2xl shadow-fuchsia-500/25 transition hover:-translate-y-1"
>
Comenzar ahora
</Link>
<Link
href="/register"
className="rounded-full border border-white/15 bg-white/10 px-8 py-4 text-center font-black text-white backdrop-blur transition hover:-translate-y-1 hover:bg-white/15"
>
Probar gratis
</Link>
</div>
<p className="mt-4 text-sm text-slate-400">
Plan Pro desde R$29/mes · login incluido · historial de creaciones.
</p>
</div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
</BaseButtons>
</CardBox>
<div className="rounded-[2rem] border border-white/10 bg-white/[0.06] p-4 shadow-2xl shadow-cyan-500/10 backdrop-blur-xl">
<div className="rounded-[1.5rem] border border-white/10 bg-[#0b1024] p-5">
<div className="mb-5 flex items-center justify-between">
<div>
<p className="text-sm font-bold uppercase tracking-[0.2em] text-cyan-300">Factory Output</p>
<h2 className="text-2xl font-black">Pack viral listo</h2>
</div>
<span className="rounded-full bg-emerald-400/15 px-3 py-1 text-xs font-bold text-emerald-300">Activo</span>
</div>
<div className="space-y-4">
{scriptExamples.map((item) => (
<article key={item.title} className="rounded-2xl border border-white/10 bg-white/[0.04] p-4">
<p className="mb-2 text-xs font-bold uppercase tracking-widest text-fuchsia-300">{item.label}</p>
<h3 className="font-black text-white">{item.title}</h3>
<p className="mt-2 text-sm leading-6 text-slate-300">{item.copy}</p>
</article>
))}
</div>
</div>
</div>
</div>
</section>
<section id="herramientas" className="border-y border-white/10 bg-white/[0.03] px-5 py-16">
<div className="mx-auto max-w-7xl">
<div className="mb-10 max-w-3xl">
<p className="font-bold uppercase tracking-[0.25em] text-fuchsia-300">Suite de herramientas</p>
<h2 className="mt-3 text-4xl font-black tracking-tight">De un tema a un paquete de publicación.</h2>
<p className="mt-4 text-slate-300">
Ideas, guiones cortos y largos, títulos, CTAs, prompts para MGX, thumbnails y voz IA en una sola experiencia.
</p>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{['🧠 Ideas de videos', '✍️ Guiones cortos/largos', '🎯 Títulos + CTAs', '🎬 Shorts automáticos', '🖼️ Thumbnails', '💬 Prompts MGX', '🔊 Voz IA', '📦 Packs semanales'].map((tool) => (
<div key={tool} className="rounded-3xl border border-white/10 bg-[#0b1024] p-5 text-lg font-bold shadow-lg shadow-black/20">
{tool}
</div>
))}
</div>
<Link
href="/tools-studio"
className="mt-8 inline-flex rounded-full bg-cyan-300 px-7 py-3 font-black text-[#07101c] transition hover:-translate-y-0.5 hover:bg-cyan-200"
>
Abrir página de herramientas
</Link>
</div>
</section>
<section className="px-5 py-16">
<div className="mx-auto grid max-w-7xl gap-6 md:grid-cols-3">
{creatorResults.map(([value, label]) => (
<div key={value} className="rounded-3xl border border-white/10 bg-gradient-to-br from-white/10 to-white/[0.03] p-8">
<p className="text-5xl font-black text-cyan-300">{value}</p>
<p className="mt-3 text-slate-300">{label}</p>
</div>
))}
</div>
</section>
<section id="precios" className="px-5 py-16">
<div className="mx-auto max-w-7xl">
<div className="mb-10 text-center">
<p className="font-bold uppercase tracking-[0.25em] text-cyan-300">Monetización por suscripción</p>
<h2 className="mt-3 text-4xl font-black">Elige tu plan y empieza a producir.</h2>
</div>
<div className="grid gap-6 lg:grid-cols-3">
{plans.map((plan) => (
<article
key={plan.name}
className={`rounded-[2rem] border p-7 ${plan.highlighted ? 'border-cyan-300 bg-cyan-300 text-[#07101c] shadow-2xl shadow-cyan-400/20' : 'border-white/10 bg-white/[0.05]'}`}
>
<div className="flex items-center justify-between">
<h3 className="text-2xl font-black">{plan.name}</h3>
<span className={`rounded-full px-3 py-1 text-xs font-black ${plan.highlighted ? 'bg-[#07101c] text-cyan-200' : 'bg-white/10 text-cyan-200'}`}>{plan.badge}</span>
</div>
<p className="mt-6 text-4xl font-black">{plan.price}</p>
<ul className="mt-6 space-y-3">
{plan.benefits.map((benefit) => (
<li key={benefit} className="flex gap-3 font-semibold">
<span></span>
<span>{benefit}</span>
</li>
))}
</ul>
<Link
href={plan.name === 'Free' ? '/register' : '/login'}
className={`mt-8 block rounded-full px-5 py-3 text-center font-black ${plan.highlighted ? 'bg-[#07101c] text-white' : 'bg-white text-[#07101c]'}`}
>
{plan.name === 'Free' ? 'Crear cuenta gratis' : 'Suscribirme ahora'}
</Link>
</article>
))}
</div>
</div>
</section>
<section id="proyecto" className="px-5 py-16">
<div className="mx-auto max-w-5xl rounded-[2rem] border border-white/10 bg-white/[0.06] p-8 text-center backdrop-blur">
<p className="font-bold uppercase tracking-[0.25em] text-fuchsia-300">Sobre el proyecto</p>
<h2 className="mt-3 text-4xl font-black">DarkFactory.ai convierte constancia en sistema.</h2>
<p className="mx-auto mt-5 max-w-3xl leading-8 text-slate-300">
Creamos una fábrica digital para creadores que quieren publicar más, probar más nichos y monetizar más rápido sin depender de cámara, edición pesada o bloqueo creativo.
</p>
</div>
</section>
</main>
<footer className="border-t border-white/10 px-5 py-10 text-slate-300">
<div className="mx-auto flex max-w-7xl flex-col gap-6 md:flex-row md:items-center md:justify-between">
<p>© 2026 DarkFactory.ai. Todos los derechos reservados.</p>
<div className="flex flex-wrap gap-5 text-sm">
<Link href="/login" className="hover:text-white">Admin / Login</Link>
<Link href="/privacy-policy" className="hover:text-white">Política de privacidad</Link>
<Link href="/terms-of-use" className="hover:text-white">Términos</Link>
<a href="mailto:support@darkfactory.ai" className="hover:text-white">Soporte</a>
<a href="https://www.youtube.com" target="_blank" rel="noreferrer" className="hover:text-white">YouTube</a>
<a href="https://www.tiktok.com" target="_blank" rel="noreferrer" className="hover:text-white">TikTok</a>
</div>
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
</footer>
</div>
);
}
@ -163,4 +255,3 @@ export default function Starter() {
Starter.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -0,0 +1,420 @@
import * as icon from '@mdi/js';
import Head from 'next/head';
import React, { ReactElement } from 'react';
import axios from 'axios';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import LayoutAuthenticated from '../layouts/Authenticated';
import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import { getPageTitle } from '../config';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { aiResponse } from '../stores/openAiSlice';
type ToolId = 'ideas' | 'scripts' | 'titles' | 'shorts' | 'thumbnails' | 'mgx' | 'voice';
type GeneratedRun = {
id: string;
toolId: ToolId;
toolName: string;
topic: string;
platform: string;
style: string;
language: string;
result: string;
createdAt: string;
savedToApi: boolean;
};
const tools: Array<{ id: ToolId; icon: string; name: string; description: string; instruction: string }> = [
{
id: 'ideas',
icon: '🧠',
name: 'Gerador de Ideias de Vídeos',
description: 'Ángulos virales y series de contenido para canales dark.',
instruction: 'Genera 10 ideas de videos dark con gancho, promesa, público objetivo y formato recomendado.',
},
{
id: 'scripts',
icon: '✍️',
name: 'Gerador de Roteiros',
description: 'Guiones cortos y largos con hook, desarrollo, CTA y retención.',
instruction: 'Crea un guion completo para video dark con hook de 3 segundos, narrativa, escenas sugeridas y CTA final.',
},
{
id: 'titles',
icon: '🎯',
name: 'Gerador de Títulos + CTAs',
description: 'Títulos magnéticos, CTAs y descripciones listas para publicar.',
instruction: 'Genera 12 títulos virales, 5 CTAs y 3 descripciones cortas optimizadas para conversión.',
},
{
id: 'shorts',
icon: '🎬',
name: 'Gerador de Shorts Automáticos',
description: 'Estructuras rápidas para TikTok, Reels y YouTube Shorts.',
instruction: 'Diseña un short de 45 segundos con escenas, texto en pantalla, narración y ritmo de edición.',
},
{
id: 'thumbnails',
icon: '🖼️',
name: 'Gerador de Thumbnails',
description: 'Conceptos visuales con copy, composición y contraste.',
instruction: 'Propón 6 ideas de thumbnails con texto corto, colores, foco visual y emoción dominante.',
},
{
id: 'mgx',
icon: '💬',
name: 'Gerador de Prompts para MGX',
description: 'Prompts visuales para assets, escenas y B-roll generativo.',
instruction: 'Escribe prompts MGX detallados para generar escenas visuales coherentes con un canal dark.',
},
{
id: 'voice',
icon: '🔊',
name: 'Sugestões de Voz IA',
description: 'Dirección de voz, ritmo, emoción y estilo de narración.',
instruction: 'Recomienda estilos de voz IA, ritmo, emoción, pausas y ejemplo de narración para el tema.',
},
];
const styles = [
{ value: 'storytelling', label: 'Storytelling' },
{ value: 'listicle', label: 'Lista viral' },
{ value: 'tutorial', label: 'Tutorial' },
{ value: 'facts', label: 'Curiosidades' },
{ value: 'finance', label: 'Finanzas' },
{ value: 'motivational', label: 'Motivacional' },
{ value: 'horror', label: 'Horror / misterio' },
];
const extractResponseText = (payload: any) => {
const output = payload?.output;
if (Array.isArray(output)) {
const message = output.find((item) => item?.type === 'message');
const content = message?.content;
if (Array.isArray(content)) {
const textItem = content.find((item) => item?.type === 'output_text');
if (textItem?.text) return textItem.text;
}
}
if (typeof payload?.text === 'string') return payload.text;
if (typeof payload?.data === 'string') return payload.data;
return JSON.stringify(payload, null, 2);
};
const buildPrompt = (toolName: string, instruction: string, topic: string, platform: string, style: string, language: string) => `
Herramienta: ${toolName}
Tema: ${topic}
Plataforma: ${platform}
Estilo de contenido: ${style}
Idioma de salida: ${language}
Instrucciones:
${instruction}
Devuelve el resultado en formato práctico, con secciones claras, bullets accionables y texto listo para copiar/pegar. Mantén un tono directo, persuasivo y orientado a creadores que quieren monetizar canales dark sin mostrar el rostro.
`;
const localStorageKey = 'darkfactory_tool_runs';
const ToolsStudio = () => {
const dispatch = useAppDispatch();
const { isAskingResponse, errorMessage } = useAppSelector((state) => state.openAi);
const [activeToolId, setActiveToolId] = React.useState<ToolId>('ideas');
const [topic, setTopic] = React.useState('Finanzas personales para jóvenes que quieren ganar dinero con IA');
const [platform, setPlatform] = React.useState('multi');
const [style, setStyle] = React.useState('storytelling');
const [language, setLanguage] = React.useState('es');
const [validationError, setValidationError] = React.useState('');
const [saveNotice, setSaveNotice] = React.useState('');
const [runs, setRuns] = React.useState<GeneratedRun[]>([]);
const [selectedRunId, setSelectedRunId] = React.useState<string>('');
const activeTool = tools.find((tool) => tool.id === activeToolId) || tools[0];
const selectedRun = runs.find((run) => run.id === selectedRunId) || runs[0];
React.useEffect(() => {
try {
const storedRuns = window.localStorage.getItem(localStorageKey);
if (storedRuns) {
const parsedRuns = JSON.parse(storedRuns);
if (Array.isArray(parsedRuns)) {
setRuns(parsedRuns);
setSelectedRunId(parsedRuns[0]?.id || '');
}
}
} catch (error) {
console.error('Failed to load DarkFactory local history', error);
setSaveNotice('No se pudo cargar el historial local. Revisa la consola para más detalles.');
}
}, []);
const persistRuns = (nextRuns: GeneratedRun[]) => {
window.localStorage.setItem(localStorageKey, JSON.stringify(nextRuns.slice(0, 12)));
setRuns(nextRuns.slice(0, 12));
};
const generate = async () => {
const cleanTopic = topic.trim();
setValidationError('');
setSaveNotice('');
if (cleanTopic.length < 5) {
setValidationError('Escribe un tema con al menos 5 caracteres para generar un resultado útil.');
return;
}
const prompt = buildPrompt(activeTool.name, activeTool.instruction, cleanTopic, platform, style, language);
const response = await dispatch(
aiResponse({
input: [
{
role: 'system',
content: 'Eres DarkFactory.ai, un asistente experto en contenido dark, retención, monetización y guiones para YouTube, TikTok y Reels.',
},
{ role: 'user', content: prompt },
],
options: { poll_interval: 5, poll_timeout: 300 },
}),
).unwrap();
const resultText = extractResponseText(response);
let savedToApi = false;
try {
await axios.post('/tool_runs', {
data: {
platform,
content_style: style,
language,
topic: cleanTopic,
audience: 'Creadores de canales dark que quieren monetizar con contenido sin rostro',
tone: 'Directo, persuasivo y accionable',
desired_length_seconds: activeToolId === 'shorts' ? 45 : 120,
prompt_text: prompt,
result_text: resultText,
status: 'succeeded',
requested_at: new Date().toISOString(),
completed_at: new Date().toISOString(),
estimated_cost: 0,
},
});
savedToApi = true;
setSaveNotice('Resultado generado y guardado en Tool runs.');
} catch (error) {
console.error('DarkFactory result generated but API history save failed', error);
setSaveNotice('Resultado generado. No se pudo guardar en la base de datos; quedó guardado en este navegador.');
}
const nextRun: GeneratedRun = {
id: `${Date.now()}`,
toolId: activeTool.id,
toolName: activeTool.name,
topic: cleanTopic,
platform,
style,
language,
result: resultText,
createdAt: new Date().toISOString(),
savedToApi,
};
const nextRuns = [nextRun, ...runs];
persistRuns(nextRuns);
setSelectedRunId(nextRun.id);
};
const downloadSelected = () => {
if (!selectedRun) return;
const blob = new Blob([selectedRun.result], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = `darkfactory-${selectedRun.toolId}-${selectedRun.id}.txt`;
anchor.click();
URL.revokeObjectURL(url);
};
return (
<>
<Head>
<title>{getPageTitle('DarkFactory Tools Studio')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={icon.mdiRobotExcitedOutline} title="DarkFactory Tools Studio" main>
<BaseButton href="/tool_runs/tool_runs-list" label="Ver Tool runs" color="info" />
</SectionTitleLineWithButton>
<div className="mb-6 overflow-hidden rounded-[2rem] border border-fuchsia-500/20 bg-[#080A18] text-white shadow-2xl shadow-fuchsia-500/10">
<div className="grid gap-0 lg:grid-cols-[0.95fr_1.05fr]">
<div className="relative p-6 sm:p-8">
<div className="absolute left-10 top-8 h-40 w-40 rounded-full bg-fuchsia-500/20 blur-3xl" />
<div className="absolute bottom-8 right-10 h-40 w-40 rounded-full bg-cyan-500/20 blur-3xl" />
<div className="relative">
<p className="text-sm font-bold uppercase tracking-[0.25em] text-cyan-300">Generación con IA</p>
<h1 className="mt-3 text-4xl font-black tracking-tight sm:text-5xl">
Escribe un tema. Recibe un paquete listo para publicar.
</h1>
<p className="mt-4 text-slate-300">
Elige una herramienta, define plataforma y estilo, genera contenido y guarda el historial para descargar paquetes después.
</p>
<div className="mt-8 grid gap-3 sm:grid-cols-2">
{tools.map((tool) => (
<button
key={tool.id}
type="button"
onClick={() => setActiveToolId(tool.id)}
className={`rounded-2xl border p-4 text-left transition hover:-translate-y-0.5 ${activeToolId === tool.id ? 'border-cyan-300 bg-cyan-300 text-[#07101c]' : 'border-white/10 bg-white/[0.05] text-white hover:bg-white/[0.08]'}`}
>
<span className="text-2xl">{tool.icon}</span>
<span className="mt-2 block font-black">{tool.name}</span>
<span className={`mt-1 block text-xs ${activeToolId === tool.id ? 'text-slate-800' : 'text-slate-400'}`}>{tool.description}</span>
</button>
))}
</div>
</div>
</div>
<div className="border-t border-white/10 bg-white/[0.04] p-6 sm:p-8 lg:border-l lg:border-t-0">
<CardBox className="border-white/10 bg-[#0D1022] text-white" cardBoxClassName="p-0">
<div className="p-6">
<div className="mb-5 flex items-start gap-3">
<span className="text-3xl">{activeTool.icon}</span>
<div>
<h2 className="text-2xl font-black">{activeTool.name}</h2>
<p className="text-sm text-slate-400">{activeTool.description}</p>
</div>
</div>
<label className="mb-2 block text-sm font-bold text-slate-200" htmlFor="topic">
Tema o nicho
</label>
<textarea
id="topic"
value={topic}
onChange={(event) => setTopic(event.target.value)}
className="min-h-28 w-full rounded-2xl border border-white/10 bg-[#070814] p-4 text-white outline-none ring-cyan-300/40 transition placeholder:text-slate-500 focus:ring-4"
placeholder="Ej.: historias de terror con final inesperado para Shorts"
/>
{validationError && <p className="mt-2 text-sm font-semibold text-rose-300">{validationError}</p>}
<div className="mt-5 grid gap-4 sm:grid-cols-3">
<div>
<label className="mb-2 block text-sm font-bold text-slate-200" htmlFor="platform">Plataforma</label>
<select id="platform" value={platform} onChange={(event) => setPlatform(event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#070814] p-3 text-white outline-none focus:ring-4 focus:ring-cyan-300/40">
<option value="multi">Multi</option>
<option value="youtube">YouTube</option>
<option value="tiktok">TikTok</option>
<option value="reels">Reels</option>
</select>
</div>
<div>
<label className="mb-2 block text-sm font-bold text-slate-200" htmlFor="style">Estilo</label>
<select id="style" value={style} onChange={(event) => setStyle(event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#070814] p-3 text-white outline-none focus:ring-4 focus:ring-cyan-300/40">
{styles.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
</select>
</div>
<div>
<label className="mb-2 block text-sm font-bold text-slate-200" htmlFor="language">Idioma</label>
<select id="language" value={language} onChange={(event) => setLanguage(event.target.value)} className="w-full rounded-2xl border border-white/10 bg-[#070814] p-3 text-white outline-none focus:ring-4 focus:ring-cyan-300/40">
<option value="es">Español</option>
<option value="pt">Português</option>
<option value="en">English</option>
</select>
</div>
</div>
<button
type="button"
onClick={generate}
disabled={isAskingResponse}
className="mt-6 w-full rounded-2xl bg-gradient-to-r from-fuchsia-500 to-cyan-400 px-5 py-4 font-black text-white shadow-xl shadow-fuchsia-500/20 transition hover:-translate-y-0.5 disabled:cursor-not-allowed disabled:opacity-60"
>
{isAskingResponse ? 'Generando con IA...' : 'Gerar Agora'}
</button>
{errorMessage && <p className="mt-3 rounded-2xl border border-rose-400/30 bg-rose-400/10 p-3 text-sm font-semibold text-rose-200">{errorMessage}</p>}
{saveNotice && <p className="mt-3 rounded-2xl border border-cyan-300/20 bg-cyan-300/10 p-3 text-sm font-semibold text-cyan-100">{saveNotice}</p>}
</div>
</CardBox>
</div>
</div>
</div>
<div className="grid gap-6 lg:grid-cols-[0.8fr_1.2fr]">
<CardBox className="bg-white dark:bg-dark-900">
<div className="mb-4 flex items-center justify-between">
<div>
<h2 className="text-xl font-black">Historial de creaciones</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">Últimos paquetes generados en este navegador.</p>
</div>
<span className="rounded-full bg-fuchsia-500/10 px-3 py-1 text-sm font-bold text-fuchsia-500">{runs.length}</span>
</div>
{!runs.length && (
<div className="rounded-2xl border border-dashed border-gray-300 p-6 text-center dark:border-dark-700">
<p className="font-bold">Aún no hay resultados.</p>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">Genera tu primer paquete para ver el detalle y descargarlo.</p>
</div>
)}
<div className="space-y-3">
{runs.map((run) => (
<button
key={run.id}
type="button"
onClick={() => setSelectedRunId(run.id)}
className={`w-full rounded-2xl border p-4 text-left transition hover:-translate-y-0.5 ${selectedRun?.id === run.id ? 'border-cyan-400 bg-cyan-50 dark:bg-cyan-400/10' : 'border-gray-200 dark:border-dark-700'}`}
>
<div className="flex items-start justify-between gap-3">
<div>
<p className="font-black">{run.toolName}</p>
<p className="mt-1 line-clamp-2 text-sm text-gray-500 dark:text-gray-400">{run.topic}</p>
</div>
<span className={`rounded-full px-2 py-1 text-xs font-bold ${run.savedToApi ? 'bg-emerald-500/10 text-emerald-500' : 'bg-amber-500/10 text-amber-500'}`}>
{run.savedToApi ? 'DB' : 'Local'}
</span>
</div>
</button>
))}
</div>
</CardBox>
<CardBox className="bg-white dark:bg-dark-900">
{selectedRun ? (
<div>
<div className="mb-4 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<p className="text-sm font-bold uppercase tracking-[0.2em] text-cyan-500">Resultado</p>
<h2 className="text-2xl font-black">{selectedRun.toolName}</h2>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
{new Date(selectedRun.createdAt).toLocaleString()} · {selectedRun.platform} · {selectedRun.language}
</p>
</div>
<BaseButton onClick={downloadSelected} label="Download .txt" color="info" />
</div>
<pre className="max-h-[620px] overflow-auto whitespace-pre-wrap rounded-2xl border border-gray-200 bg-gray-50 p-5 text-sm leading-7 text-gray-800 dark:border-dark-700 dark:bg-[#070814] dark:text-slate-100">
{selectedRun.result}
</pre>
</div>
) : (
<div className="grid min-h-72 place-items-center rounded-2xl border border-dashed border-gray-300 text-center dark:border-dark-700">
<div>
<p className="text-4xl">📦</p>
<p className="mt-3 font-black">Tu paquete aparecerá aquí</p>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">Genera contenido para ver el detalle, copiarlo o descargarlo.</p>
</div>
</div>
)}
</CardBox>
</div>
</SectionMain>
</>
);
};
ToolsStudio.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};
export default ToolsStudio;