Gmagl commited on
Commit
1eae852
·
1 Parent(s): 31af3f9

Fix build errors, update UI dependencies, and configure Docker for npm deployment

Browse files
.gitignore ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # dependencies
2
+ /node_modules
3
+ /.pnp
4
+ .pnp.js
5
+
6
+ # testing
7
+ /coverage
8
+
9
+ # next.js
10
+ /.next/
11
+ /out/
12
+
13
+ # production
14
+ /build
15
+
16
+ # misc
17
+ .DS_Store
18
+ *.pem
19
+
20
+ # debug
21
+ npm-debug.log*
22
+ yarn-debug.log*
23
+ yarn-error.log*
24
+
25
+ # local env files
26
+ .env*.local
27
+ .env
28
+
29
+ # vercel
30
+ .vercel
31
+
32
+ # typescript
33
+ *.tsbuildinfo
34
+ next-env.d.ts
35
+
36
+ # logs
37
+ build.log
Dockerfile CHANGED
@@ -1,38 +1,33 @@
1
- FROM oven/bun:latest
2
 
3
  WORKDIR /app
4
 
5
- # 1. Instalamos Node.js para que Next.js no dé errores de "npm not found"
6
- RUN apt-get update && apt-get install -y curl gnupg && \
7
- curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
8
- apt-get install -y nodejs && rm -rf /var/lib/apt/lists/*
9
 
10
- # 2. Copiamos solo los archivos de dependencias (para aprovechar la caché)
11
- COPY package.json ./
12
 
13
- # 3. Instalamos las dependencias
14
- RUN bun install
15
 
16
- # 4. AHORA copiamos el resto del código del proyecto
17
  COPY . .
18
 
19
- # 5. Creamos la carpeta de datos ANTES de Prisma
20
- RUN mkdir -p /app/data && chmod -R 777 /app/data
21
 
22
- # 6. Configuramos la variable de entorno de la base de datos
23
- ENV DATABASE_URL="file:/app/data/sofia.db"
24
- ENV NODE_ENV=production
25
- ENV PORT=7860
26
- ENV HOST=0.0.0.0
27
 
28
- # 7. Generamos el cliente de Prisma (necesita los archivos copiados en el paso 4)
29
- RUN bunx prisma generate
30
 
31
- # 8. Construimos la aplicación
32
- RUN bun run build
 
 
33
 
34
- EXPOSE 7860
35
 
36
- # 9. COMANDO FINAL: Empuja los cambios a la DB y arranca la app
37
- # Lo hacemos aquí para que se ejecute siempre que el contenedor inicie
38
- CMD ["sh", "-c", "bunx prisma db push && bun run start"]
 
1
+ FROM node:20-slim
2
 
3
  WORKDIR /app
4
 
5
+ # Install dependencies for native modules
6
+ RUN apt-get update && apt-get install -y openssl python3 make g++ && rm -rf /var/lib/apt/lists/*
 
 
7
 
8
+ # Copy package files
9
+ COPY package.json package-lock.json ./
10
 
11
+ # Install dependencies
12
+ RUN npm ci
13
 
14
+ # Copy source files
15
  COPY . .
16
 
17
+ # Generate Prisma client
18
+ RUN npx prisma generate
19
 
20
+ # Build the application
21
+ RUN npm run build
 
 
 
22
 
23
+ # Create data directory
24
+ RUN mkdir -p /app/data
25
 
26
+ # Environment variables
27
+ ENV NODE_ENV=production
28
+ ENV PORT=3000
29
+ ENV HOST=0.0.0.0
30
 
31
+ EXPOSE 3000
32
 
33
+ CMD ["npm", "run", "start"]
 
 
entrypoint.sh CHANGED
@@ -3,7 +3,7 @@
3
  # Initialize database if it doesn't exist
4
  if [ ! -f /app/data/sofia.db ]; then
5
  echo "Initializing database..."
6
- cd /app && bunx prisma db push --skip-generate
7
  fi
8
 
9
  # Start the application
 
3
  # Initialize database if it doesn't exist
4
  if [ ! -f /app/data/sofia.db ]; then
5
  echo "Initializing database..."
6
+ cd /app && npx prisma db push --skip-generate
7
  fi
8
 
9
  # Start the application
next.config.ts CHANGED
@@ -1,16 +1,11 @@
1
- import type { NextConfig } from "next";
 
 
2
 
3
  const nextConfig: NextConfig = {
4
- // 1. Ignoramos los errores estrictos de TypeScript para que no detengan el Build
5
- typescript: {
6
- ignoreBuildErrors: true,
7
- },
8
- // 2. Ignoramos los avisos de código (ESLint) para que tampoco molesten
9
- eslint: {
10
- ignoreDuringBuilds: true,
11
- },
12
- // 3. Forzamos la salida independiente (recomendado para Docker/Hugging Face)
13
- output: "standalone",
14
  };
15
 
16
  export default nextConfig;
 
1
+ import type { NextConfig } from 'next';
2
+
3
+ import path from 'path';
4
 
5
  const nextConfig: NextConfig = {
6
+ output: 'standalone',
7
+ reactStrictMode: true,
8
+ outputFileTracingRoot: process.cwd(),
 
 
 
 
 
 
 
9
  };
10
 
11
  export default nextConfig;
package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -3,57 +3,72 @@
3
  "version": "1.0.0",
4
  "private": true,
5
  "scripts": {
6
- "dev": "next dev -p 7860 -H 0.0.0.0",
7
  "build": "next build",
8
- "start": "next start -p 7860 -H 0.0.0.0",
9
  "lint": "eslint ."
10
  },
11
  "dependencies": {
12
- "@hookform/resolvers": "^3.9.0",
13
- "@prisma/client": "^6.1.0",
14
- "@radix-ui/react-accordion": "^1.2.0",
15
- "@radix-ui/react-alert-dialog": "^1.1.0",
16
- "@radix-ui/react-avatar": "^1.1.0",
17
- "@radix-ui/react-checkbox": "^1.1.0",
18
- "@radix-ui/react-dialog": "^1.1.0",
19
- "@radix-ui/react-dropdown-menu": "^2.1.0",
20
- "@radix-ui/react-label": "^2.1.0",
21
- "@radix-ui/react-popover": "^1.1.0",
22
- "@radix-ui/react-progress": "^1.1.0",
23
- "@radix-ui/react-scroll-area": "^1.1.0",
24
- "@radix-ui/react-select": "^2.1.0",
25
- "@radix-ui/react-separator": "^1.1.0",
26
- "@radix-ui/react-slider": "^1.2.0",
27
- "@radix-ui/react-slot": "^1.1.0",
28
- "@radix-ui/react-switch": "^1.1.0",
29
- "@radix-ui/react-tabs": "^1.1.0",
30
- "@radix-ui/react-toast": "^1.2.0",
31
- "@radix-ui/react-toggle": "^1.1.0",
32
- "@radix-ui/react-tooltip": "^1.1.0",
33
- "class-variance-authority": "^0.7.0",
 
 
 
 
 
 
 
 
34
  "clsx": "^2.1.1",
35
- "framer-motion": "^11.0.0",
36
- "lucide-react": "^0.446.0",
37
- "next": "15.1.0",
38
- "prisma": "^6.1.0",
39
- "react": "19.0.0",
40
- "react-dom": "19.0.0",
41
- "react-hook-form": "^7.53.0",
42
- "sonner": "^1.5.0",
43
- "tailwind-merge": "^2.5.2",
 
 
 
 
 
 
 
 
 
44
  "tw-animate-css": "^1.3.5",
 
45
  "z-ai-web-dev-sdk": "^0.0.16",
46
- "zod": "^3.23.8"
47
  },
48
  "devDependencies": {
49
- "@tailwindcss/postcss": "^4.0.0",
50
- "@types/node": "^20.0.0",
51
- "@types/react": "19.0.0",
52
- "@types/react-dom": "19.0.0",
53
- "eslint": "^9.0.0",
54
- "eslint-config-next": "15.1.0",
55
- "postcss": "^8.4.0",
56
- "tailwindcss": "^4.0.0",
57
- "typescript": "^5.0.0"
58
  }
59
- }
 
3
  "version": "1.0.0",
4
  "private": true,
5
  "scripts": {
6
+ "dev": "next dev -p 3000 -H 0.0.0.0",
7
  "build": "next build",
8
+ "start": "next start -p 3000 -H 0.0.0.0",
9
  "lint": "eslint ."
10
  },
11
  "dependencies": {
12
+ "@hookform/resolvers": "^5.1.1",
13
+ "@prisma/client": "^6.11.1",
14
+ "@radix-ui/react-accordion": "^1.2.12",
15
+ "@radix-ui/react-alert-dialog": "^1.1.15",
16
+ "@radix-ui/react-aspect-ratio": "^1.1.8",
17
+ "@radix-ui/react-avatar": "^1.1.11",
18
+ "@radix-ui/react-checkbox": "^1.3.3",
19
+ "@radix-ui/react-collapsible": "^1.1.12",
20
+ "@radix-ui/react-context-menu": "^2.2.16",
21
+ "@radix-ui/react-dialog": "^1.1.15",
22
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
23
+ "@radix-ui/react-hover-card": "^1.1.15",
24
+ "@radix-ui/react-label": "^2.1.8",
25
+ "@radix-ui/react-menubar": "^1.1.16",
26
+ "@radix-ui/react-navigation-menu": "^1.2.14",
27
+ "@radix-ui/react-popover": "^1.1.15",
28
+ "@radix-ui/react-progress": "^1.1.8",
29
+ "@radix-ui/react-radio-group": "^1.3.8",
30
+ "@radix-ui/react-scroll-area": "^1.2.10",
31
+ "@radix-ui/react-select": "^2.2.6",
32
+ "@radix-ui/react-separator": "^1.1.8",
33
+ "@radix-ui/react-slider": "^1.3.6",
34
+ "@radix-ui/react-slot": "^1.2.4",
35
+ "@radix-ui/react-switch": "^1.2.6",
36
+ "@radix-ui/react-tabs": "^1.1.13",
37
+ "@radix-ui/react-toast": "^1.2.15",
38
+ "@radix-ui/react-toggle": "^1.1.10",
39
+ "@radix-ui/react-toggle-group": "^1.1.11",
40
+ "@radix-ui/react-tooltip": "^1.2.8",
41
+ "class-variance-authority": "^0.7.1",
42
  "clsx": "^2.1.1",
43
+ "cmdk": "^1.1.1",
44
+ "date-fns": "^4.1.0",
45
+ "embla-carousel-react": "^8.6.0",
46
+ "framer-motion": "^12.23.2",
47
+ "input-otp": "^1.4.2",
48
+ "lucide-react": "^0.525.0",
49
+ "next": "^15.3.0",
50
+ "next-themes": "^0.4.6",
51
+ "prisma": "^6.11.1",
52
+ "react": "^19.0.0",
53
+ "react-day-picker": "^9.14.0",
54
+ "react-dom": "^19.0.0",
55
+ "react-hook-form": "^7.60.0",
56
+ "react-resizable-panels": "^4.6.5",
57
+ "recharts": "^3.7.0",
58
+ "sonner": "^2.0.6",
59
+ "tailwind-merge": "^3.3.1",
60
+ "tailwindcss-animate": "^1.0.7",
61
  "tw-animate-css": "^1.3.5",
62
+ "vaul": "^1.1.2",
63
  "z-ai-web-dev-sdk": "^0.0.16",
64
+ "zod": "^4.0.2"
65
  },
66
  "devDependencies": {
67
+ "@tailwindcss/postcss": "^4",
68
+ "@types/node": "25.3.2",
69
+ "@types/react": "^19",
70
+ "@types/react-dom": "^19",
71
+ "tailwindcss": "^4",
72
+ "typescript": "^5"
 
 
 
73
  }
74
+ }
prisma/schema.prisma CHANGED
@@ -6,7 +6,7 @@ generator client {
6
  }
7
 
8
  datasource db {
9
- provider = "sqlite"
10
  url = env("DATABASE_URL")
11
  }
12
 
@@ -15,11 +15,21 @@ datasource db {
15
  // ============================================
16
 
17
  model User {
18
- id String @id @default(cuid())
19
- email String @unique
20
- name String?
21
- createdAt DateTime @default(now())
22
- updatedAt DateTime @updatedAt
 
 
 
 
 
 
 
 
 
 
23
  }
24
 
25
  model Project {
@@ -69,6 +79,75 @@ model AgentTask {
69
  completedAt DateTime?
70
  }
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  // ============================================
73
  // MODELOS DE CONTENIDO MULTIMEDIA
74
  // ============================================
@@ -84,6 +163,7 @@ model Content {
84
  thumbnail String?
85
  platform String @default("general")
86
  status String @default("pending")
 
87
  metadata String?
88
  projectId String?
89
  project Project? @relation(fields: [projectId], references: [id])
@@ -93,20 +173,24 @@ model Content {
93
  pet Pet? @relation(fields: [petId], references: [id])
94
  censorFlags CensorFlag[]
95
  posts Post[]
 
96
  createdAt DateTime @default(now())
97
  updatedAt DateTime @updatedAt
98
  }
99
 
100
- model Character {
101
- id String @id @default(cuid())
102
- name String
103
- description String?
104
- referenceImage String?
105
- traits String?
106
- contents Content[]
107
- pets Pet[]
108
- createdAt DateTime @default(now())
109
- updatedAt DateTime @updatedAt
 
 
 
110
  }
111
 
112
  // ============================================
@@ -145,7 +229,7 @@ model MonetizationPlatform {
145
  name String // OnlyFans, Patreon, Fansly, etc.
146
  type String // "subscription", "tips", "ppv", "mixed"
147
  url String?
148
- apiKey String? // Encriptado
149
  accountId String?
150
  accountName String?
151
  legalTerms String? // JSON con términos legales
@@ -168,8 +252,8 @@ model Subscriber {
168
  platform MonetizationPlatform @relation(fields: [platformId], references: [id])
169
  externalId String?
170
  username String?
171
- tier String? // Nivel de suscripción
172
- status String @default("active") // active, expired, cancelled
173
  joinedAt DateTime?
174
  expiresAt DateTime?
175
  totalSpent Float @default(0)
@@ -182,12 +266,12 @@ model Earning {
182
  id String @id @default(cuid())
183
  platformId String
184
  platform MonetizationPlatform @relation(fields: [platformId], references: [id])
185
- type String // subscription, tip, ppv, referral
186
  amount Float
187
  currency String @default("USD")
188
  postId String?
189
  subscriberId String?
190
- status String @default("pending") // pending, processed, paid
191
  processedAt DateTime?
192
  metadata String?
193
  createdAt DateTime @default(now())
@@ -201,18 +285,18 @@ model Post {
201
  id String @id @default(cuid())
202
  title String?
203
  caption String?
204
- hashtags String? // JSON array
205
- type String // reel, photo, carousel, story, post
206
- status String @default("draft") // draft, scheduled, published, failed
207
  contentId String?
208
  content Content? @relation(fields: [contentId], references: [id])
209
  platformId String?
210
  platform MonetizationPlatform? @relation(fields: [platformId], references: [id])
211
  scheduledAt DateTime?
212
  publishedAt DateTime?
213
- externalPostId String? // ID en la plataforma externa
214
- postUrl String? // URL del post publicado
215
- engagementStats String? // JSON con likes, views, shares, etc.
216
  storyId String?
217
  story Story? @relation(fields: [storyId], references: [id])
218
  metadata String?
@@ -228,15 +312,15 @@ model Story {
228
  id String @id @default(cuid())
229
  title String
230
  description String?
231
- genre String? // romance, drama, comedy, thriller, etc.
232
- targetAudience String? // Demografía objetivo
233
- tone String? // romantic, funny, dramatic, mysterious
234
- structure String? // JSON con estructura narrativa
235
- characterIds String? // JSON array de IDs de personajes
236
  totalEpisodes Int @default(1)
237
  currentEpisode Int @default(1)
238
- status String @default("draft") // draft, active, completed, paused
239
- monetizationStrategy String? // JSON con estrategia de monetización
240
  posts Post[]
241
  episodes StoryEpisode[]
242
  analytics StoryAnalytics?
@@ -251,8 +335,8 @@ model StoryEpisode {
251
  episodeNum Int
252
  title String
253
  synopsis String?
254
- content String // Guión o descripción detallada
255
- hook String? // Gancho para la siguiente episodio
256
  cliffhanger String?
257
  status String @default("draft")
258
  scheduledAt DateTime?
@@ -267,8 +351,8 @@ model StoryAnalytics {
267
  story Story @relation(fields: [storyId], references: [id])
268
  totalViews Int @default(0)
269
  totalEngagement Float @default(0)
270
- avgWatchTime Float? // En segundos
271
- completionRate Float? // Porcentaje
272
  revenue Float @default(0)
273
  subscriberGain Int @default(0)
274
  bestPerformingEpisode Int?
@@ -285,10 +369,10 @@ model Automation {
285
  id String @id @default(cuid())
286
  name String
287
  description String?
288
- type String // content_generation, posting, engagement, monetization
289
- trigger String // schedule, event, manual, webhook
290
- triggerConfig String? // JSON con configuración del trigger
291
- actions String // JSON array de acciones a ejecutar
292
  isActive Boolean @default(true)
293
  lastRunAt DateTime?
294
  nextRunAt DateTime?
@@ -301,11 +385,11 @@ model Automation {
301
  model AutomationLog {
302
  id String @id @default(cuid())
303
  automationId String?
304
- status String // success, failed, partial
305
  input String?
306
  output String?
307
  error String?
308
- duration Int? // Milisegundos
309
  createdAt DateTime @default(now())
310
  }
311
 
@@ -316,15 +400,15 @@ model AutomationLog {
316
  model Trend {
317
  id String @id @default(cuid())
318
  platform String
319
- type String // hashtag, sound, challenge, topic
320
  name String
321
  description String?
322
  volume Int?
323
  growth Float?
324
  startDate DateTime?
325
  endDate DateTime?
326
- relatedTags String? // JSON array
327
- contentIdeas String? // JSON con ideas de contenido
328
  isActive Boolean @default(true)
329
  createdAt DateTime @default(now())
330
  updatedAt DateTime @updatedAt
@@ -349,13 +433,13 @@ model PromptTemplate {
349
  model ContentTemplate {
350
  id String @id @default(cuid())
351
  name String
352
- type String // reel, carousel, story, post
353
  platform String
354
- structure String // JSON con estructura del contenido
355
- hooks String? // JSON array de ganchos
356
- ctas String? // JSON array de call-to-actions
357
- hashtags String? // JSON array de hashtags
358
- bestTimes String? // JSON con mejores horarios
359
  isActive Boolean @default(true)
360
  createdAt DateTime @default(now())
361
  updatedAt DateTime @updatedAt
@@ -368,14 +452,14 @@ model ContentTemplate {
368
  model Pet {
369
  id String @id @default(cuid())
370
  name String
371
- type String // dog, cat, bird, etc.
372
  breed String?
373
  description String?
374
  referenceImage String?
375
- traits String? // JSON con características
376
- personality String? // playful, calm, energetic
377
  color String?
378
- accessories String? // JSON array de accesorios
379
  characterId String?
380
  character Character? @relation(fields: [characterId], references: [id], onDelete: Cascade)
381
  contents Content[]
@@ -396,16 +480,16 @@ model AIInfluencer {
396
  followers Int?
397
  engagement Float?
398
  niche String?
399
- style String? // JSON con estilo de contenido
400
- contentTypes String? // JSON array de tipos de contenido
401
- postingSchedule String? // JSON con horarios
402
- visualStyle String? // JSON con estilo visual
403
- monetizationType String? // JSON con tipo de monetización
404
- signatureElements String? // JSON con elementos distintivos
405
  petCompanion Boolean @default(false)
406
  petType String?
407
- analysis String? // JSON con análisis detallado
408
- lessons String? // JSON con lecciones aprendidas
409
  isActive Boolean @default(true)
410
  createdAt DateTime @default(now())
411
  updatedAt DateTime @updatedAt
@@ -420,15 +504,15 @@ model ViralStrategy {
420
  name String
421
  description String?
422
  platform String
423
- contentType String // reel, post, story, carousel
424
- hook String? // Gancho inicial
425
- structure String? // JSON con estructura viral
426
- elements String? // JSON con elementos clave
427
  estimatedReach Int?
428
  difficulty String @default("medium")
429
- timeframe String? // Tiempo para viralizar
430
- examples String? // JSON con ejemplos
431
- tags String? // JSON array de tags
432
  successRate Float?
433
  isActive Boolean @default(true)
434
  createdAt DateTime @default(now())
 
6
  }
7
 
8
  datasource db {
9
+ provider = "postgresql"
10
  url = env("DATABASE_URL")
11
  }
12
 
 
15
  // ============================================
16
 
17
  model User {
18
+ id String @id @default(cuid())
19
+ email String? @unique
20
+ name String?
21
+ externalId String? // ID externo (Telegram chatId, etc.)
22
+ source String? // "telegram", "web", "onlyfans", etc.
23
+ tier String @default("free") // free, basic, premium, pro
24
+ stripeCustomerId String?
25
+ totalSpent Float @default(0)
26
+ createdAt DateTime @default(now())
27
+ updatedAt DateTime @updatedAt
28
+
29
+ chatSessions ChatSession[]
30
+ assetDeliveries AssetDelivery[]
31
+
32
+ @@unique([externalId, source])
33
  }
34
 
35
  model Project {
 
79
  completedAt DateTime?
80
  }
81
 
82
+ // ============================================
83
+ // MODELOS DE INFLUENCER (CHARACTER)
84
+ // ============================================
85
+
86
+ model Character {
87
+ id String @id @default(cuid())
88
+ name String
89
+ slug String @unique
90
+ displayName String?
91
+ shortBio String?
92
+ description String?
93
+ referenceImage String? // Imagen canónica para consistencia facial
94
+ bodyReference String? // Referencia corporal
95
+ traits String? // JSON con rasgos de personalidad
96
+ orientation String? // Orientación/estilo (mujer, hombre gay, etc.)
97
+ styleDescription String? // Descripción visual para generación de imágenes
98
+ systemPrompt String? // Prompt base para el chat
99
+ telegramBotToken String? // Token del bot de Telegram específico
100
+ // Storytelling
101
+ backstory String? // Historia de fondo
102
+ personality String? // Personalidad detallada
103
+ interests String? // JSON array de intereses
104
+ catchphrases String? // JSON array de frases típicas
105
+ // Configuración de chat por tier
106
+ chatFreeLimit Int @default(10) // Mensajes gratis por día
107
+ chatBasicTone String? // Tono para tier basic
108
+ chatPremiumTone String? // Tono para tier premium (íntimo)
109
+ chatProTone String? // Tono para tier pro (novia IA)
110
+ isActive Boolean @default(true)
111
+ contents Content[]
112
+ pets Pet[]
113
+ chatSessions ChatSession[]
114
+ assetDeliveries AssetDelivery[]
115
+ createdAt DateTime @default(now())
116
+ updatedAt DateTime @updatedAt
117
+ }
118
+
119
+ // ============================================
120
+ // MODELOS DE CHAT / NOVIA IA
121
+ // ============================================
122
+
123
+ model ChatSession {
124
+ id String @id @default(cuid())
125
+ userId String
126
+ user User @relation(fields: [userId], references: [id])
127
+ characterId String
128
+ character Character @relation(fields: [characterId], references: [id])
129
+ channel String @default("telegram") // telegram, web, etc.
130
+ heatScore Int @default(0) // 0-100, sube con interacción
131
+ msgCountToday Int @default(0)
132
+ lastMessageAt DateTime?
133
+ isActive Boolean @default(true)
134
+ messages ChatMessage[]
135
+ createdAt DateTime @default(now())
136
+ updatedAt DateTime @updatedAt
137
+
138
+ @@unique([userId, characterId, channel])
139
+ }
140
+
141
+ model ChatMessage {
142
+ id String @id @default(cuid())
143
+ sessionId String
144
+ session ChatSession @relation(fields: [sessionId], references: [id])
145
+ role String // "user", "assistant", "system"
146
+ content String
147
+ metadata String? // JSON (ej: si se envió un asset)
148
+ createdAt DateTime @default(now())
149
+ }
150
+
151
  // ============================================
152
  // MODELOS DE CONTENIDO MULTIMEDIA
153
  // ============================================
 
163
  thumbnail String?
164
  platform String @default("general")
165
  status String @default("pending")
166
+ nsfwLevel Int @default(0) // 0=SFW, 1=suggestive, 2=NSFW, 3=explicit
167
  metadata String?
168
  projectId String?
169
  project Project? @relation(fields: [projectId], references: [id])
 
173
  pet Pet? @relation(fields: [petId], references: [id])
174
  censorFlags CensorFlag[]
175
  posts Post[]
176
+ assetDeliveries AssetDelivery[]
177
  createdAt DateTime @default(now())
178
  updatedAt DateTime @updatedAt
179
  }
180
 
181
+ // Tracking de qué assets se han enviado a qué usuario
182
+ model AssetDelivery {
183
+ id String @id @default(cuid())
184
+ contentId String
185
+ content Content @relation(fields: [contentId], references: [id])
186
+ userId String
187
+ user User @relation(fields: [userId], references: [id])
188
+ characterId String
189
+ character Character @relation(fields: [characterId], references: [id])
190
+ channel String @default("telegram")
191
+ deliveredAt DateTime @default(now())
192
+
193
+ @@unique([contentId, userId]) // No repetir el mismo asset al mismo usuario
194
  }
195
 
196
  // ============================================
 
229
  name String // OnlyFans, Patreon, Fansly, etc.
230
  type String // "subscription", "tips", "ppv", "mixed"
231
  url String?
232
+ apiKey String?
233
  accountId String?
234
  accountName String?
235
  legalTerms String? // JSON con términos legales
 
252
  platform MonetizationPlatform @relation(fields: [platformId], references: [id])
253
  externalId String?
254
  username String?
255
+ tier String?
256
+ status String @default("active")
257
  joinedAt DateTime?
258
  expiresAt DateTime?
259
  totalSpent Float @default(0)
 
266
  id String @id @default(cuid())
267
  platformId String
268
  platform MonetizationPlatform @relation(fields: [platformId], references: [id])
269
+ type String
270
  amount Float
271
  currency String @default("USD")
272
  postId String?
273
  subscriberId String?
274
+ status String @default("pending")
275
  processedAt DateTime?
276
  metadata String?
277
  createdAt DateTime @default(now())
 
285
  id String @id @default(cuid())
286
  title String?
287
  caption String?
288
+ hashtags String?
289
+ type String
290
+ status String @default("draft")
291
  contentId String?
292
  content Content? @relation(fields: [contentId], references: [id])
293
  platformId String?
294
  platform MonetizationPlatform? @relation(fields: [platformId], references: [id])
295
  scheduledAt DateTime?
296
  publishedAt DateTime?
297
+ externalPostId String?
298
+ postUrl String?
299
+ engagementStats String?
300
  storyId String?
301
  story Story? @relation(fields: [storyId], references: [id])
302
  metadata String?
 
312
  id String @id @default(cuid())
313
  title String
314
  description String?
315
+ genre String?
316
+ targetAudience String?
317
+ tone String?
318
+ structure String?
319
+ characterIds String?
320
  totalEpisodes Int @default(1)
321
  currentEpisode Int @default(1)
322
+ status String @default("draft")
323
+ monetizationStrategy String?
324
  posts Post[]
325
  episodes StoryEpisode[]
326
  analytics StoryAnalytics?
 
335
  episodeNum Int
336
  title String
337
  synopsis String?
338
+ content String
339
+ hook String?
340
  cliffhanger String?
341
  status String @default("draft")
342
  scheduledAt DateTime?
 
351
  story Story @relation(fields: [storyId], references: [id])
352
  totalViews Int @default(0)
353
  totalEngagement Float @default(0)
354
+ avgWatchTime Float?
355
+ completionRate Float?
356
  revenue Float @default(0)
357
  subscriberGain Int @default(0)
358
  bestPerformingEpisode Int?
 
369
  id String @id @default(cuid())
370
  name String
371
  description String?
372
+ type String
373
+ trigger String
374
+ triggerConfig String?
375
+ actions String
376
  isActive Boolean @default(true)
377
  lastRunAt DateTime?
378
  nextRunAt DateTime?
 
385
  model AutomationLog {
386
  id String @id @default(cuid())
387
  automationId String?
388
+ status String
389
  input String?
390
  output String?
391
  error String?
392
+ duration Int?
393
  createdAt DateTime @default(now())
394
  }
395
 
 
400
  model Trend {
401
  id String @id @default(cuid())
402
  platform String
403
+ type String
404
  name String
405
  description String?
406
  volume Int?
407
  growth Float?
408
  startDate DateTime?
409
  endDate DateTime?
410
+ relatedTags String?
411
+ contentIdeas String?
412
  isActive Boolean @default(true)
413
  createdAt DateTime @default(now())
414
  updatedAt DateTime @updatedAt
 
433
  model ContentTemplate {
434
  id String @id @default(cuid())
435
  name String
436
+ type String
437
  platform String
438
+ structure String
439
+ hooks String?
440
+ ctas String?
441
+ hashtags String?
442
+ bestTimes String?
443
  isActive Boolean @default(true)
444
  createdAt DateTime @default(now())
445
  updatedAt DateTime @updatedAt
 
452
  model Pet {
453
  id String @id @default(cuid())
454
  name String
455
+ type String
456
  breed String?
457
  description String?
458
  referenceImage String?
459
+ traits String?
460
+ personality String?
461
  color String?
462
+ accessories String?
463
  characterId String?
464
  character Character? @relation(fields: [characterId], references: [id], onDelete: Cascade)
465
  contents Content[]
 
480
  followers Int?
481
  engagement Float?
482
  niche String?
483
+ style String?
484
+ contentTypes String?
485
+ postingSchedule String?
486
+ visualStyle String?
487
+ monetizationType String?
488
+ signatureElements String?
489
  petCompanion Boolean @default(false)
490
  petType String?
491
+ analysis String?
492
+ lessons String?
493
  isActive Boolean @default(true)
494
  createdAt DateTime @default(now())
495
  updatedAt DateTime @updatedAt
 
504
  name String
505
  description String?
506
  platform String
507
+ contentType String
508
+ hook String?
509
+ structure String?
510
+ elements String?
511
  estimatedReach Int?
512
  difficulty String @default("medium")
513
+ timeframe String?
514
+ examples String?
515
+ tags String?
516
  successRate Float?
517
  isActive Boolean @default(true)
518
  createdAt DateTime @default(now())
src/app/api/automation/route.ts CHANGED
@@ -12,7 +12,7 @@ export async function GET(request: NextRequest) {
12
  try {
13
  const { searchParams } = new URL(request.url);
14
  const type = searchParams.get("type");
15
- const where: Record<string, unknown> = {};
16
  if (type) where.type = type;
17
 
18
  const automations = await db.automation.findMany({ where, orderBy: { createdAt: "desc" } });
@@ -60,7 +60,7 @@ export async function PUT(request: NextRequest) {
60
  try {
61
  const zai = await ZAI.create();
62
  await zai.chat.completions.create({ messages: [{ role: "user", content: "test" }] });
63
- } catch {}
64
 
65
  await db.automation.update({ where: { id }, data: { lastRunAt: new Date(), runCount: { increment: 1 } } });
66
  await db.automationLog.create({ data: { automationId: id, status: "success", duration: 100 } });
 
12
  try {
13
  const { searchParams } = new URL(request.url);
14
  const type = searchParams.get("type");
15
+ const where: Record<string, any> = {};
16
  if (type) where.type = type;
17
 
18
  const automations = await db.automation.findMany({ where, orderBy: { createdAt: "desc" } });
 
60
  try {
61
  const zai = await ZAI.create();
62
  await zai.chat.completions.create({ messages: [{ role: "user", content: "test" }] });
63
+ } catch { }
64
 
65
  await db.automation.update({ where: { id }, data: { lastRunAt: new Date(), runCount: { increment: 1 } } });
66
  await db.automationLog.create({ data: { automationId: id, status: "success", duration: 100 } });
src/app/api/characters/route.ts CHANGED
@@ -2,221 +2,90 @@ import { NextRequest, NextResponse } from "next/server";
2
  import { db } from "@/lib/db";
3
  import ZAI from "z-ai-web-dev-sdk";
4
 
5
- // ================================
6
- // GET - Listar personajes
7
- // ================================
8
- export async function GET() {
9
  try {
10
- const characters = await db.character.findMany({
11
- include: {
12
- contents: {
13
- take: 5,
14
- orderBy: { createdAt: "desc" },
15
- },
16
- },
17
- orderBy: { createdAt: "desc" },
18
- });
19
-
20
- return NextResponse.json({
21
- success: true,
22
- characters,
23
- total: characters.length,
24
- });
25
- } catch (error) {
26
- console.error("Error fetching characters:", error);
27
- return NextResponse.json(
28
- { success: false, error: "Error al obtener personajes" },
29
- { status: 500 }
30
- );
31
  }
32
  }
33
 
34
- // ================================
35
- // POST - Crear personaje
36
- // ================================
37
  export async function POST(request: NextRequest) {
38
  try {
39
  const body = await request.json();
40
- const {
41
- name,
42
- description,
43
- generateReference = false,
44
- traits,
45
- } = body;
46
 
47
  if (!name) {
48
- return NextResponse.json(
49
- { success: false, error: "El nombre del personaje es requerido" },
50
- { status: 400 }
51
- );
52
  }
53
 
54
  let referenceImage: string | null = null;
55
  let characterTraits: string | null = null;
56
 
57
- // =================================
58
- // Generar imagen de referencia
59
- // =================================
60
  if (generateReference) {
61
  try {
62
  const zai = await ZAI.create();
63
-
64
- const characterPrompt = `Character reference sheet: ${name}. ${
65
- description || "Original character"
66
- }. Full body view, multiple angles (front, side, back), consistent character design, neutral pose, white background, professional character sheet.`;
67
-
68
- const response: any = await zai.images.generations.create({
69
- prompt: characterPrompt,
70
- size: "1344x768",
71
  });
72
-
73
- const imageBase64 =
74
- response?.data?.[0]?.base64 || null;
75
-
76
  if (imageBase64) {
77
- referenceImage = `data:image/png;base64,${imageBase64}`;
78
- }
79
-
80
- // =================================
81
- // Extraer traits con IA
82
- // =================================
83
- if (!traits) {
84
- const traitsResponse: any =
85
- await zai.chat.completions.create({
86
- messages: [
87
- {
88
- role: "system",
89
- content:
90
- 'Eres experto en diseño de personajes. Responde SOLO JSON: { "face": "", "body": "", "hair": "", "clothing": "", "colors": "", "distinctive": "" }',
91
- },
92
- {
93
- role: "user",
94
- content: `Personaje: ${name}. Descripción: ${
95
- description || "Original character"
96
- }`,
97
- },
98
- ],
99
- });
100
-
101
- const traitsText =
102
- traitsResponse?.choices?.[0]?.message?.content || "";
103
-
104
- const jsonMatch = traitsText.match(/\{[\s\S]*\}/);
105
-
106
- if (jsonMatch) {
107
- characterTraits = jsonMatch[0];
108
- } else {
109
- characterTraits = JSON.stringify({
110
- description: description || "Original character",
111
- });
112
  }
113
  }
114
- } catch (genError) {
115
- console.error("Error generando referencia:", genError);
116
- }
117
  }
118
 
119
- // Traits desde request
120
- if (traits) {
121
- characterTraits =
122
- typeof traits === "string"
123
- ? traits
124
- : JSON.stringify(traits);
125
- }
126
 
127
- // =================================
128
- // Crear personaje en BD
129
- // =================================
130
  const character = await db.character.create({
131
  data: {
132
  name,
133
- description: description || null,
134
- referenceImage,
135
- traits:
136
- characterTraits ||
137
- JSON.stringify({ name, description }),
138
- },
139
  });
140
 
141
- return NextResponse.json({
142
- success: true,
143
- character,
144
- message: `Personaje "${name}" creado exitosamente`,
145
- });
146
- } catch (error) {
147
- console.error("Error creating character:", error);
148
- return NextResponse.json(
149
- { success: false, error: "Error al crear personaje" },
150
- { status: 500 }
151
- );
152
  }
153
  }
154
 
155
- // ================================
156
- // PUT - Actualizar personaje
157
- // ================================
158
  export async function PUT(request: NextRequest) {
159
  try {
160
  const body = await request.json();
161
  const { id, name, description, traits } = body;
 
162
 
163
- if (!id) {
164
- return NextResponse.json(
165
- { success: false, error: "ID del personaje requerido" },
166
- { status: 400 }
167
- );
168
- }
169
 
170
- const character = await db.character.update({
171
- where: { id },
172
- data: {
173
- name: name || undefined,
174
- description: description || undefined,
175
- traits: traits || undefined,
176
- },
177
- });
178
-
179
- return NextResponse.json({
180
- success: true,
181
- character,
182
- });
183
- } catch (error) {
184
- console.error("Error updating character:", error);
185
- return NextResponse.json(
186
- { success: false, error: "Error al actualizar personaje" },
187
- { status: 500 }
188
- );
189
  }
190
  }
191
 
192
- // ================================
193
- // DELETE - Eliminar personaje
194
- // ================================
195
  export async function DELETE(request: NextRequest) {
196
  try {
197
  const { searchParams } = new URL(request.url);
198
  const id = searchParams.get("id");
199
-
200
- if (!id) {
201
- return NextResponse.json(
202
- { success: false, error: "ID requerido" },
203
- { status: 400 }
204
- );
205
- }
206
-
207
- await db.character.delete({
208
- where: { id },
209
- });
210
-
211
- return NextResponse.json({
212
- success: true,
213
- message: "Personaje eliminado",
214
- });
215
- } catch (error) {
216
- console.error("Error deleting character:", error);
217
- return NextResponse.json(
218
- { success: false, error: "Error al eliminar personaje" },
219
- { status: 500 }
220
- );
221
  }
222
- }
 
2
  import { db } from "@/lib/db";
3
  import ZAI from "z-ai-web-dev-sdk";
4
 
5
+ export async function GET(request: NextRequest) {
 
 
 
6
  try {
7
+ const characters = await db.character.findMany({ orderBy: { createdAt: "desc" } });
8
+ return NextResponse.json({ success: true, characters, total: characters.length });
9
+ } catch {
10
+ return NextResponse.json({ success: false, error: "Error" }, { status: 500 });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  }
12
  }
13
 
 
 
 
14
  export async function POST(request: NextRequest) {
15
  try {
16
  const body = await request.json();
17
+ const { name, description, generateReference, traits } = body;
 
 
 
 
 
18
 
19
  if (!name) {
20
+ return NextResponse.json({ success: false, error: "Nombre requerido" }, { status: 400 });
 
 
 
21
  }
22
 
23
  let referenceImage: string | null = null;
24
  let characterTraits: string | null = null;
25
 
 
 
 
26
  if (generateReference) {
27
  try {
28
  const zai = await ZAI.create();
29
+ const response = await zai.images.generations.create({
30
+ prompt: "Character reference: " + name + ", " + (description || "original character"),
31
+ size: "1024x1024"
 
 
 
 
 
32
  });
33
+ const imageBase64 = response.data[0]?.base64;
 
 
 
34
  if (imageBase64) {
35
+ referenceImage = "generated_" + Date.now();
36
+ if (!traits) {
37
+ characterTraits = JSON.stringify({ name, description });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  }
39
  }
40
+ } catch { }
 
 
41
  }
42
 
43
+ const finalTraits = traits || characterTraits || JSON.stringify({ name });
 
 
 
 
 
 
44
 
 
 
 
45
  const character = await db.character.create({
46
  data: {
47
  name,
48
+ description: description ? String(description) : null,
49
+ referenceImage: referenceImage ? String(referenceImage) : null,
50
+ traits: String(finalTraits)
51
+ } as any
 
 
52
  });
53
 
54
+ return NextResponse.json({ success: true, character });
55
+ } catch {
56
+ return NextResponse.json({ success: false, error: "Error" }, { status: 500 });
 
 
 
 
 
 
 
 
57
  }
58
  }
59
 
 
 
 
60
  export async function PUT(request: NextRequest) {
61
  try {
62
  const body = await request.json();
63
  const { id, name, description, traits } = body;
64
+ if (!id) return NextResponse.json({ success: false, error: "ID requerido" }, { status: 400 });
65
 
66
+ const data: { name?: string; description?: string | null; traits?: string } = {};
67
+ if (name) data.name = name;
68
+ if (description !== undefined) data.description = description || null;
69
+ if (traits) data.traits = traits;
 
 
70
 
71
+ const character = await db.character.update({ where: { id }, data });
72
+ return NextResponse.json({ success: true, character });
73
+ } catch {
74
+ return NextResponse.json({ success: false, error: "Error" }, { status: 500 });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  }
76
  }
77
 
 
 
 
78
  export async function DELETE(request: NextRequest) {
79
  try {
80
  const { searchParams } = new URL(request.url);
81
  const id = searchParams.get("id");
82
+ if (!id) return NextResponse.json({ success: false, error: "ID requerido" }, { status: 400 });
83
+
84
+ await db.pet.deleteMany({ where: { characterId: id } });
85
+ await db.content.updateMany({ where: { characterId: id }, data: { characterId: null } });
86
+ await db.character.delete({ where: { id } });
87
+ return NextResponse.json({ success: true });
88
+ } catch {
89
+ return NextResponse.json({ success: false, error: "Error" }, { status: 500 });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  }
91
+ }
src/app/api/content/route.ts CHANGED
@@ -8,7 +8,7 @@ export async function GET(request: NextRequest) {
8
  const platform = searchParams.get("platform");
9
  const limit = parseInt(searchParams.get("limit") || "50");
10
 
11
- const where: Record<string, unknown> = {};
12
  if (type) where.type = type;
13
  if (platform) where.platform = platform;
14
 
 
8
  const platform = searchParams.get("platform");
9
  const limit = parseInt(searchParams.get("limit") || "50");
10
 
11
+ const where: Record<string, any> = {};
12
  if (type) where.type = type;
13
  if (platform) where.platform = platform;
14
 
src/app/api/generate/image/route.ts CHANGED
@@ -1,233 +1,86 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
  import ZAI from "z-ai-web-dev-sdk";
3
  import { db } from "@/lib/db";
4
- import type { Content } from "@prisma/client";
5
 
6
- // Reglas de censura por plataforma
7
- const CENSOR_RULES: Record<string, string[]> = {
8
- youtube: [
9
- "no nudity",
10
- "no sexual content",
11
- "no graphic violence",
12
- "family friendly"
13
- ],
14
- tiktok: [
15
- "no nudity",
16
- "no sexual suggestiveness",
17
- "no graphic violence",
18
- "no self-harm content",
19
- "age appropriate"
20
- ],
21
- instagram: [
22
- "no nudity",
23
- "no graphic violence",
24
- "artistic content acceptable with moderation",
25
- "no self-harm imagery"
26
- ],
27
- twitter: [
28
- "content warning if sensitive",
29
- "no illegal content"
30
- ],
31
- general: [
32
- "safe for work",
33
- "no explicit content"
34
- ]
35
  };
36
 
37
- // Aplicar filtros de censura al prompt
38
- function applyCensorFilter(prompt: string, platform: string): string {
39
- const rules = CENSOR_RULES[platform] || CENSOR_RULES.general;
40
- const censorAdditions = rules.map(rule => `(${rule})`).join(", ");
41
- return `${prompt}, ${censorAdditions}`;
42
- }
43
-
44
  export async function POST(request: NextRequest) {
45
  try {
46
  const body = await request.json();
 
47
 
48
- const {
49
- prompt,
50
- optimizedPrompt,
51
- platform = "general",
52
- character,
53
- style = "realistic",
54
- size = "1024x1024",
55
- saveToDb = true,
56
- projectId
57
- } = body;
58
-
59
- const basePrompt: string | undefined = optimizedPrompt || prompt;
60
-
61
- if (!basePrompt) {
62
- return NextResponse.json(
63
- { success: false, error: "Se requiere un prompt" },
64
- { status: 400 }
65
- );
66
  }
67
 
68
- let finalPrompt = applyCensorFilter(basePrompt, platform);
69
-
70
- // Estilos
71
- if (style && style !== "realistic") {
72
- const styleAdditions: Record<string, string> = {
73
- anime: "anime style, manga aesthetic, vibrant colors, cel shading",
74
- realistic: "photorealistic, highly detailed, 8K, professional photography",
75
- artistic: "digital art, artistic style, creative interpretation, masterpiece",
76
- "oil-painting": "oil painting style, classical art, brush strokes, museum quality",
77
- sketch: "pencil sketch, hand drawn, artistic lines, detailed shading",
78
- "3d": "3D render, octane render, cinema 4D, volumetric lighting"
79
- };
80
-
81
- if (styleAdditions[style]) {
82
- finalPrompt = `${finalPrompt}, ${styleAdditions[style]}`;
 
 
 
 
 
83
  }
84
- }
85
-
86
- // Personaje
87
- if (character) {
88
- finalPrompt = `${finalPrompt}, character: ${character}`;
89
- }
90
-
91
- console.log(
92
- "🖼️ Generando imagen:",
93
- finalPrompt.substring(0, 100) + "..."
94
- );
95
-
96
- // ✅ Crear registro en BD
97
- let contentRecord: Content | null = null;
98
-
99
- if (saveToDb) {
100
- contentRecord = await db.content.create({
101
- data: {
102
- type: "image",
103
- title: basePrompt.substring(0, 50),
104
- description: basePrompt,
105
- prompt: basePrompt,
106
- optimizedPrompt: finalPrompt,
107
- platform,
108
- status: "processing",
109
- projectId: projectId || null
110
- }
111
- });
112
- }
113
 
114
  try {
115
  const zai = await ZAI.create();
116
-
117
  const response = await zai.images.generations.create({
118
  prompt: finalPrompt,
119
- size: size as
120
- | "1024x1024"
121
- | "768x1344"
122
- | "864x1152"
123
- | "1344x768"
124
- | "1152x864"
125
- | "1440x720"
126
- | "720x1440"
127
  });
128
 
129
- const imageBase64 = (response as any)?.data?.[0]?.base64;
130
-
131
- if (!imageBase64) {
132
- throw new Error("No se recibió imagen del generador");
133
- }
134
-
135
- const filename = `image_${Date.now()}.png`;
136
- const imageUrl = `/download/images/${filename}`;
137
 
138
- // ✅ actualizar BD
139
- if (contentRecord) {
140
- await db.content.update({
141
- where: { id: contentRecord.id },
142
- data: {
143
- status: "completed",
144
- filePath: imageUrl,
145
- thumbnail: imageUrl,
146
- metadata: JSON.stringify({ base64: imageBase64 })
147
- }
148
- });
149
- }
150
-
151
- // ✅ tarea agente (no romper si falla)
152
- try {
153
- await db.agentTask.create({
154
- data: {
155
- type: "generate_image",
156
- status: "completed",
157
- input: basePrompt,
158
- output: `Imagen generada: ${filename}`,
159
- completedAt: new Date()
160
- }
161
- });
162
- } catch (e) {
163
- console.warn("agentTask warning:", e);
164
- }
165
-
166
- return NextResponse.json({
167
- success: true,
168
- image: {
169
- id: contentRecord?.id ?? null,
170
- filename,
171
- url: imageUrl,
172
- base64: imageBase64,
173
- prompt: finalPrompt,
174
- platform,
175
- size
176
- }
177
  });
178
- } catch (genError) {
179
- console.error("Error generando imagen:", genError);
180
 
181
- if (contentRecord) {
182
- await db.content.update({
183
- where: { id: contentRecord.id },
184
- data: {
185
- status: "failed",
186
- metadata: JSON.stringify({ error: String(genError) })
187
- }
188
- });
189
- }
190
 
191
- return NextResponse.json(
192
- { success: false, error: "Error al generar imagen: " + String(genError) },
193
- { status: 500 }
194
- );
195
  }
196
  } catch (error) {
197
- console.error("Error en generate image:", error);
198
- return NextResponse.json(
199
- { success: false, error: "Error interno del servidor" },
200
- { status: 500 }
201
- );
202
  }
203
  }
204
 
205
- // ✅ LISTAR IMÁGENES
206
  export async function GET(request: NextRequest) {
207
  try {
208
  const { searchParams } = new URL(request.url);
209
  const platform = searchParams.get("platform");
210
  const limit = parseInt(searchParams.get("limit") || "20");
211
-
212
- const where: Record<string, unknown> = { type: "image" };
213
  if (platform) where.platform = platform;
214
-
215
- const images = await db.content.findMany({
216
- where,
217
- orderBy: { createdAt: "desc" },
218
- take: limit
219
- });
220
-
221
- return NextResponse.json({
222
- success: true,
223
- images,
224
- total: images.length
225
- });
226
- } catch (error) {
227
- console.error("Error listing images:", error);
228
- return NextResponse.json(
229
- { success: false, error: "Error al listar imágenes" },
230
- { status: 500 }
231
- );
232
  }
233
- }
 
1
  import { NextRequest, NextResponse } from "next/server";
2
  import ZAI from "z-ai-web-dev-sdk";
3
  import { db } from "@/lib/db";
 
4
 
5
+ const CENSOR_RULES: Record<string, string> = {
6
+ youtube: "family friendly, no nudity, no violence",
7
+ tiktok: "age appropriate, no suggestive content",
8
+ instagram: "community guidelines, no graphic content",
9
+ twitter: "content warning if sensitive",
10
+ general: "safe for work"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  };
12
 
 
 
 
 
 
 
 
13
  export async function POST(request: NextRequest) {
14
  try {
15
  const body = await request.json();
16
+ const { prompt, optimizedPrompt, platform = "general", style = "realistic", size = "1024x1024" } = body;
17
 
18
+ if (!prompt && !optimizedPrompt) {
19
+ return NextResponse.json({ success: false, error: "Se requiere un prompt" }, { status: 400 });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  }
21
 
22
+ let finalPrompt = optimizedPrompt || prompt;
23
+ const censorRule = CENSOR_RULES[platform] || CENSOR_RULES.general;
24
+ finalPrompt = finalPrompt + ", " + censorRule;
25
+
26
+ const styleMap: Record<string, string> = {
27
+ anime: "anime style, manga aesthetic",
28
+ realistic: "photorealistic, highly detailed",
29
+ artistic: "digital art, creative"
30
+ };
31
+ if (styleMap[style]) finalPrompt = finalPrompt + ", " + styleMap[style];
32
+
33
+ const contentRecord = await db.content.create({
34
+ data: {
35
+ type: "image",
36
+ title: prompt.substring(0, 50),
37
+ description: prompt,
38
+ prompt: prompt,
39
+ optimizedPrompt: finalPrompt,
40
+ platform: platform,
41
+ status: "processing"
42
  }
43
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
  try {
46
  const zai = await ZAI.create();
 
47
  const response = await zai.images.generations.create({
48
  prompt: finalPrompt,
49
+ size: size as "1024x1024"
 
 
 
 
 
 
 
50
  });
51
 
52
+ const imageBase64 = response.data[0]?.base64;
53
+ if (!imageBase64) throw new Error("No se recibio imagen");
 
 
 
 
 
 
54
 
55
+ await db.content.update({
56
+ where: { id: contentRecord.id },
57
+ data: { status: "completed", filePath: "generated" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  });
 
 
59
 
60
+ await db.agentTask.create({
61
+ data: { type: "generate_image", status: "completed", input: prompt, output: "Imagen generada", completedAt: new Date() }
62
+ });
 
 
 
 
 
 
63
 
64
+ return NextResponse.json({ success: true, image: { id: contentRecord.id, base64: imageBase64, prompt: finalPrompt, platform, size } });
65
+ } catch (genError) {
66
+ await db.content.update({ where: { id: contentRecord.id }, data: { status: "failed" } });
67
+ return NextResponse.json({ success: false, error: String(genError) }, { status: 500 });
68
  }
69
  } catch (error) {
70
+ return NextResponse.json({ success: false, error: "Error interno" }, { status: 500 });
 
 
 
 
71
  }
72
  }
73
 
 
74
  export async function GET(request: NextRequest) {
75
  try {
76
  const { searchParams } = new URL(request.url);
77
  const platform = searchParams.get("platform");
78
  const limit = parseInt(searchParams.get("limit") || "20");
79
+ const where: Record<string, any> = { type: "image" };
 
80
  if (platform) where.platform = platform;
81
+ const images = await db.content.findMany({ where, orderBy: { createdAt: "desc" }, take: limit });
82
+ return NextResponse.json({ success: true, images, total: images.length });
83
+ } catch {
84
+ return NextResponse.json({ success: false, error: "Error" }, { status: 500 });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  }
86
+ }
src/app/api/generate/video/route.ts CHANGED
@@ -1,125 +1,74 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
  import ZAI from "z-ai-web-dev-sdk";
3
  import { db } from "@/lib/db";
4
- import fs from "fs/promises";
5
- import path from "path";
6
 
7
- const DOWNLOAD_DIR = path.join(process.cwd(), "download", "videos");
8
-
9
- async function ensureDir() {
10
- try {
11
- await fs.mkdir(DOWNLOAD_DIR, { recursive: true });
12
- } catch {}
13
- }
14
 
15
  export async function POST(request: NextRequest) {
16
  try {
17
  const body = await request.json();
18
- const {
19
- prompt,
20
- optimizedPrompt,
21
- platform = "general",
22
- style = "cinematic",
23
- duration = "short",
24
- aspectRatio = "16:9",
25
- saveToDb = true,
26
- projectId,
27
- } = body;
28
 
29
  if (!prompt && !optimizedPrompt) {
30
- return NextResponse.json(
31
- { success: false, error: "Se requiere un prompt" },
32
- { status: 400 }
33
- );
34
  }
35
 
36
- await ensureDir();
 
 
37
 
38
- const finalPrompt = optimizedPrompt || prompt;
39
-
40
- // ✅ DB record
41
- let contentRecord: any = null;
42
- if (saveToDb) {
43
- contentRecord = await db.content.create({
44
- data: {
45
- type: "video",
46
- title: (prompt || "").substring(0, 50),
47
- description: prompt,
48
- prompt: prompt,
49
- optimizedPrompt: finalPrompt,
50
- platform,
51
- status: "processing",
52
- projectId: projectId || null,
53
- metadata: JSON.stringify({ style, duration, aspectRatio }),
54
- },
55
- });
56
- }
57
 
58
  try {
59
  const zai = await ZAI.create();
 
 
 
60
 
61
- // ✅ SDK correcto (sin la S)
62
- const response: any = await zai.video.generations.create({
63
- prompt: finalPrompt,
64
  });
65
 
66
- // ✅ Extraer URL de forma tolerante
67
- const videoUrl: string | null =
68
- response?.video_url ||
69
- response?.url ||
70
- response?.data?.[0]?.url ||
71
- null;
72
-
73
- if (!videoUrl) {
74
- throw new Error("No se recibió video del generador");
75
- }
76
-
77
- // ✅ actualizar DB
78
- if (contentRecord) {
79
- await db.content.update({
80
- where: { id: contentRecord.id },
81
- data: {
82
- status: "completed",
83
- filePath: videoUrl,
84
- },
85
- });
86
- }
87
-
88
- return NextResponse.json({
89
- success: true,
90
- video: {
91
- id: contentRecord?.id,
92
- url: videoUrl,
93
- prompt: finalPrompt,
94
- platform,
95
- style,
96
- duration,
97
- aspectRatio,
98
- },
99
  });
100
- } catch (genError) {
101
- console.error("Error generando video:", genError);
102
-
103
- if (contentRecord) {
104
- await db.content.update({
105
- where: { id: contentRecord.id },
106
- data: {
107
- status: "failed",
108
- metadata: JSON.stringify({ error: String(genError) }),
109
- },
110
- });
111
- }
112
 
113
- return NextResponse.json(
114
- { success: false, error: "Error al generar video" },
115
- { status: 500 }
116
- );
117
  }
118
  } catch (error) {
119
- console.error("Error en generate video:", error);
120
- return NextResponse.json(
121
- { success: false, error: "Error interno" },
122
- { status: 500 }
123
- );
124
  }
125
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { NextRequest, NextResponse } from "next/server";
2
  import ZAI from "z-ai-web-dev-sdk";
3
  import { db } from "@/lib/db";
 
 
4
 
5
+ const VIDEO_CENSOR_RULES: Record<string, string> = {
6
+ youtube: "family friendly, no violence, no nudity",
7
+ tiktok: "age appropriate, no dangerous content",
8
+ instagram: "community guidelines compliant",
9
+ general: "safe for work"
10
+ };
 
11
 
12
  export async function POST(request: NextRequest) {
13
  try {
14
  const body = await request.json();
15
+ const { prompt, optimizedPrompt, platform = "general", style = "cinematic" } = body;
 
 
 
 
 
 
 
 
 
16
 
17
  if (!prompt && !optimizedPrompt) {
18
+ return NextResponse.json({ success: false, error: "Se requiere un prompt" }, { status: 400 });
 
 
 
19
  }
20
 
21
+ let finalPrompt = optimizedPrompt || prompt;
22
+ const censorRule = VIDEO_CENSOR_RULES[platform] || VIDEO_CENSOR_RULES.general;
23
+ finalPrompt = finalPrompt + ", " + censorRule + ", short clip";
24
 
25
+ const contentRecord = await db.content.create({
26
+ data: {
27
+ type: "video",
28
+ title: prompt.substring(0, 50),
29
+ description: prompt,
30
+ prompt: prompt,
31
+ optimizedPrompt: finalPrompt,
32
+ platform: platform,
33
+ status: "processing"
34
+ }
35
+ });
 
 
 
 
 
 
 
 
36
 
37
  try {
38
  const zai = await ZAI.create();
39
+ const response = await (zai as any).videos.generations.create({ prompt: finalPrompt });
40
+ const videoData = response.data?.[0];
41
+ if (!videoData) throw new Error("No se recibio video");
42
 
43
+ await db.content.update({
44
+ where: { id: contentRecord.id },
45
+ data: { status: "completed", filePath: "generated" }
46
  });
47
 
48
+ await db.agentTask.create({
49
+ data: { type: "generate_video", status: "completed", input: prompt, output: "Video generado", completedAt: new Date() }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  });
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
+ return NextResponse.json({ success: true, video: { id: contentRecord.id, prompt: finalPrompt, platform, style } });
53
+ } catch (genError) {
54
+ await db.content.update({ where: { id: contentRecord.id }, data: { status: "failed" } });
55
+ return NextResponse.json({ success: false, error: String(genError) }, { status: 500 });
56
  }
57
  } catch (error) {
58
+ return NextResponse.json({ success: false, error: "Error interno" }, { status: 500 });
 
 
 
 
59
  }
60
  }
61
+
62
+ export async function GET(request: NextRequest) {
63
+ try {
64
+ const { searchParams } = new URL(request.url);
65
+ const platform = searchParams.get("platform");
66
+ const limit = parseInt(searchParams.get("limit") || "20");
67
+ const where: Record<string, any> = { type: "video" };
68
+ if (platform) where.platform = platform;
69
+ const videos = await db.content.findMany({ where, orderBy: { createdAt: "desc" }, take: limit });
70
+ return NextResponse.json({ success: true, videos, total: videos.length });
71
+ } catch {
72
+ return NextResponse.json({ success: false, error: "Error" }, { status: 500 });
73
+ }
74
+ }
src/app/api/influencers/route.ts CHANGED
@@ -39,11 +39,11 @@ export async function POST(request: NextRequest) {
39
  ]
40
  });
41
 
42
- let analysis: Record<string, unknown[]> = {};
43
  try {
44
  const match = completion.choices[0]?.message?.content?.match(/\{[\s\S]*\}/);
45
  if (match) analysis = JSON.parse(match[0]);
46
- } catch {}
47
 
48
  return NextResponse.json({ success: true, influencers: FAMOUS_INFLUENCERS.slice(0, 4), analysis });
49
  } catch {
 
39
  ]
40
  });
41
 
42
+ let analysis: Record<string, any> = {};
43
  try {
44
  const match = completion.choices[0]?.message?.content?.match(/\{[\s\S]*\}/);
45
  if (match) analysis = JSON.parse(match[0]);
46
+ } catch { }
47
 
48
  return NextResponse.json({ success: true, influencers: FAMOUS_INFLUENCERS.slice(0, 4), analysis });
49
  } catch {
src/app/api/pets/route.ts CHANGED
@@ -14,7 +14,7 @@ export async function GET(request: NextRequest) {
14
  try {
15
  const { searchParams } = new URL(request.url);
16
  const characterId = searchParams.get("characterId");
17
- const where: Record<string, unknown> = { isActive: true };
18
  if (characterId) where.characterId = characterId;
19
 
20
  const pets = await db.pet.findMany({ where, orderBy: { createdAt: "desc" } });
@@ -53,7 +53,7 @@ export async function PUT(request: NextRequest) {
53
  const { id, personality, isActive } = body;
54
  if (!id) return NextResponse.json({ success: false, error: "ID requerido" }, { status: 400 });
55
 
56
- const data: Record<string, unknown> = {};
57
  if (personality) data.personality = personality;
58
  if (isActive !== undefined) data.isActive = isActive;
59
 
 
14
  try {
15
  const { searchParams } = new URL(request.url);
16
  const characterId = searchParams.get("characterId");
17
+ const where: Record<string, any> = { isActive: true };
18
  if (characterId) where.characterId = characterId;
19
 
20
  const pets = await db.pet.findMany({ where, orderBy: { createdAt: "desc" } });
 
53
  const { id, personality, isActive } = body;
54
  if (!id) return NextResponse.json({ success: false, error: "ID requerido" }, { status: 400 });
55
 
56
+ const data: Record<string, any> = {};
57
  if (personality) data.personality = personality;
58
  if (isActive !== undefined) data.isActive = isActive;
59
 
src/app/api/prompt-engineer/route.ts CHANGED
@@ -7,75 +7,46 @@ export async function POST(request: NextRequest) {
7
  const { prompt, type, platform } = body;
8
 
9
  if (!prompt) {
10
- return NextResponse.json(
11
- { success: false, error: "Prompt requerido" },
12
- { status: 400 }
13
- );
14
  }
15
 
16
  const zai = await ZAI.create();
17
 
18
- // Guías por plataforma
19
  const platformGuides: Record<string, string> = {
20
  instagram: "Formato cuadrado o vertical, colores vibrantes, lifestyle",
21
  tiktok: "Video corto, dinamico, trending sounds",
22
  youtube: "Thumbnail llamativo, titulo optimizado, larga duracion",
23
  onlyfans: "Contenido exclusivo, conexion personal",
24
- general: "Contenido universal, apto para todas las plataformas",
25
  };
26
 
27
- // Guías por tipo
28
  const typeGuides: Record<string, string> = {
29
  image: "Prompt para generacion de imagen detallado",
30
  video: "Prompt para video con movimiento y escenas",
31
  reel: "Prompt para reel corto y dinamico",
32
- carousel: "Prompt para serie de imagenes coherentes",
33
  };
34
 
35
- const guide =
36
- platformGuides[platform || "general"] ||
37
- platformGuides.general;
38
 
39
- const typeGuide =
40
- typeGuides[type || "image"] ||
41
- typeGuides.image;
42
-
43
- // ✅ PROMPT BIEN FORMADO
44
  const completion = await zai.chat.completions.create({
45
  messages: [
46
- {
47
- role: "system",
48
- content:
49
- "Eres un ingeniero de prompts experto. Optimiza el prompt del usuario para generar mejor contenido. Responde SOLO con el prompt optimizado, sin explicaciones.",
50
- },
51
- {
52
- role: "user",
53
- content: `Optimiza este prompt para ${platform || "general"}:
54
-
55
- Guia: ${guide}
56
- Tipo: ${typeGuide}
57
-
58
- Prompt original:
59
- ${prompt}`,
60
- },
61
- ],
62
  });
63
 
64
- const optimizedPrompt =
65
- completion.choices?.[0]?.message?.content || prompt;
66
 
67
  return NextResponse.json({
68
  success: true,
69
  originalPrompt: prompt,
70
  optimizedPrompt,
71
  type: type || "image",
72
- platform: platform || "general",
73
  });
74
- } catch (error) {
75
- console.error("Prompt engineer error:", error);
76
- return NextResponse.json(
77
- { success: false, error: "Error" },
78
- { status: 500 }
79
- );
80
  }
81
- }
 
7
  const { prompt, type, platform } = body;
8
 
9
  if (!prompt) {
10
+ return NextResponse.json({ success: false, error: "Prompt requerido" }, { status: 400 });
 
 
 
11
  }
12
 
13
  const zai = await ZAI.create();
14
 
 
15
  const platformGuides: Record<string, string> = {
16
  instagram: "Formato cuadrado o vertical, colores vibrantes, lifestyle",
17
  tiktok: "Video corto, dinamico, trending sounds",
18
  youtube: "Thumbnail llamativo, titulo optimizado, larga duracion",
19
  onlyfans: "Contenido exclusivo, conexion personal",
20
+ general: "Contenido universal, apto para todas las plataformas"
21
  };
22
 
 
23
  const typeGuides: Record<string, string> = {
24
  image: "Prompt para generacion de imagen detallado",
25
  video: "Prompt para video con movimiento y escenas",
26
  reel: "Prompt para reel corto y dinamico",
27
+ carousel: "Prompt para serie de imagenes coherentes"
28
  };
29
 
30
+ const guide = platformGuides[platform || "general"] || platformGuides.general;
31
+ const typeGuide = typeGuides[type || "image"] || typeGuides.image;
 
32
 
 
 
 
 
 
33
  const completion = await zai.chat.completions.create({
34
  messages: [
35
+ { role: "system", content: "Eres un ingeniero de prompts experto. Optimiza el prompt del usuario para generar mejor contenido. Responde SOLO con el prompt optimizado, sin explicaciones." },
36
+ { role: "user", content: `Optimiza este prompt para ${platform || "general"}:\nGuia: ${guide}\nTipo: ${typeGuide}\nPrompt original: ${prompt}` }
37
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  });
39
 
40
+ const optimizedPrompt = completion.choices[0]?.message?.content || prompt;
 
41
 
42
  return NextResponse.json({
43
  success: true,
44
  originalPrompt: prompt,
45
  optimizedPrompt,
46
  type: type || "image",
47
+ platform: platform || "general"
48
  });
49
+ } catch {
50
+ return NextResponse.json({ success: false, error: "Error" }, { status: 500 });
 
 
 
 
51
  }
52
+ }
src/app/api/repos/[id]/route.ts CHANGED
@@ -31,7 +31,7 @@ export async function GET(
31
  // Leer estructura de archivos del repo
32
  const repoPath = path.join(REPOS_DIR, repo.name);
33
  let files: string[] = [];
34
- let readme = null;
35
 
36
  try {
37
  const entries = await fs.readdir(repoPath, { withFileTypes: true });
 
31
  // Leer estructura de archivos del repo
32
  const repoPath = path.join(REPOS_DIR, repo.name);
33
  let files: string[] = [];
34
+ let readme: string | null = null;
35
 
36
  try {
37
  const entries = await fs.readdir(repoPath, { withFileTypes: true });
src/app/api/storytelling/route.ts CHANGED
@@ -14,7 +14,7 @@ export async function GET(request: NextRequest) {
14
  try {
15
  const { searchParams } = new URL(request.url);
16
  const status = searchParams.get("status");
17
- const where: Record<string, unknown> = {};
18
  if (status) where.status = status;
19
 
20
  const stories = await db.story.findMany({
@@ -51,7 +51,7 @@ export async function POST(request: NextRequest) {
51
  try {
52
  const match = completion.choices[0]?.message?.content?.match(/\{[\s\S]*\}/);
53
  if (match) storyData = JSON.parse(match[0]);
54
- } catch {}
55
 
56
  const story = await db.story.create({
57
  data: {
@@ -100,7 +100,7 @@ export async function PUT(request: NextRequest) {
100
  const { id, status, currentEpisode } = body;
101
  if (!id) return NextResponse.json({ success: false, error: "ID requerido" }, { status: 400 });
102
 
103
- const data: Record<string, unknown> = {};
104
  if (status) data.status = status;
105
  if (currentEpisode) data.currentEpisode = currentEpisode;
106
 
 
14
  try {
15
  const { searchParams } = new URL(request.url);
16
  const status = searchParams.get("status");
17
+ const where: Record<string, any> = {};
18
  if (status) where.status = status;
19
 
20
  const stories = await db.story.findMany({
 
51
  try {
52
  const match = completion.choices[0]?.message?.content?.match(/\{[\s\S]*\}/);
53
  if (match) storyData = JSON.parse(match[0]);
54
+ } catch { }
55
 
56
  const story = await db.story.create({
57
  data: {
 
100
  const { id, status, currentEpisode } = body;
101
  if (!id) return NextResponse.json({ success: false, error: "ID requerido" }, { status: 400 });
102
 
103
+ const data: Record<string, any> = {};
104
  if (status) data.status = status;
105
  if (currentEpisode) data.currentEpisode = currentEpisode;
106
 
src/app/api/trends/route.ts CHANGED
@@ -46,15 +46,15 @@ export async function POST(request: NextRequest) {
46
  ]
47
  });
48
 
49
- let analysis: Record<string, unknown> = {};
50
  try {
51
  const match = completion.choices[0]?.message?.content?.match(/\{[\s\S]*\}/);
52
  if (match) analysis = JSON.parse(match[0]);
53
- } catch {}
54
 
55
  await db.trend.create({
56
  data: { platform: platform || "all", type: "analysis", name: "Analisis " + (niche || "general"), contentIdeas: JSON.stringify(analysis) }
57
- }).catch(() => {});
58
 
59
  return NextResponse.json({ success: true, analysis });
60
  } catch {
 
46
  ]
47
  });
48
 
49
+ let analysis: Record<string, any> = {};
50
  try {
51
  const match = completion.choices[0]?.message?.content?.match(/\{[\s\S]*\}/);
52
  if (match) analysis = JSON.parse(match[0]);
53
+ } catch { }
54
 
55
  await db.trend.create({
56
  data: { platform: platform || "all", type: "analysis", name: "Analisis " + (niche || "general"), contentIdeas: JSON.stringify(analysis) }
57
+ }).catch(() => { });
58
 
59
  return NextResponse.json({ success: true, analysis });
60
  } catch {
src/app/layout.tsx CHANGED
@@ -14,22 +14,22 @@ const geistMono = Geist_Mono({
14
  });
15
 
16
  export const metadata: Metadata = {
17
- title: "Sofía Cloud - Multiagente AI",
18
- description: "Sistema multiagente para análisis de código, gestión de repositorios y desarrollo asistido por IA.",
19
- keywords: ["Sofía", "AI", "Next.js", "TypeScript", "Tailwind CSS", "shadcn/ui", "Agentes", "Código"],
20
- authors: [{ name: "Sofía Cloud Team" }],
21
  icons: {
22
  icon: "/logo.svg",
23
  },
24
  openGraph: {
25
- title: "Sofía Cloud - Multiagente AI",
26
- description: "Sistema multiagente para análisis de código y desarrollo con IA",
27
  type: "website",
28
  },
29
  twitter: {
30
  card: "summary_large_image",
31
- title: "Sofía Cloud - Multiagente AI",
32
- description: "Sistema multiagente para análisis de código y desarrollo con IA",
33
  },
34
  };
35
 
 
14
  });
15
 
16
  export const metadata: Metadata = {
17
+ title: "AI Influencer Studio",
18
+ description: "Plataforma SaaS para crear, gestionar y monetizar influencers de IA de forma escalable.",
19
+ keywords: ["AI", "Influencer", "SaaS", "Monetización", "OnlyFans", "Patreon", "Fanvue", "Automatización"],
20
+ authors: [{ name: "AI Influencer Studio" }],
21
  icons: {
22
  icon: "/logo.svg",
23
  },
24
  openGraph: {
25
+ title: "AI Influencer Studio",
26
+ description: "Plataforma para crear y monetizar influencers de IA",
27
  type: "website",
28
  },
29
  twitter: {
30
  card: "summary_large_image",
31
+ title: "AI Influencer Studio",
32
+ description: "Plataforma para crear y monetizar influencers de IA",
33
  },
34
  };
35
 
src/app/page.tsx CHANGED
@@ -49,7 +49,7 @@ export default function Dashboard() {
49
  const [userPrompt, setUserPrompt] = useState("");
50
  const [promptType, setPromptType] = useState("image");
51
  const [targetPlatform, setTargetPlatform] = useState("general");
52
- const [optimizedPrompt, setOptimizedPrompt] = useState<Record<string, unknown> | null>(null);
53
  const [promptLoading, setPromptLoading] = useState(false);
54
 
55
  // Image Generation
@@ -100,14 +100,14 @@ export default function Dashboard() {
100
  const [influencerPlatform, setInfluencerPlatform] = useState("");
101
  const [influencerWithPets, setInfluencerWithPets] = useState(false);
102
  const [influencerLoading, setInfluencerLoading] = useState(false);
103
- const [influencerAnalysis, setInfluencerAnalysis] = useState<Record<string, unknown> | null>(null);
104
- const [characterConcept, setCharacterConcept] = useState<Record<string, unknown> | null>(null);
105
 
106
  // Trends
107
- const [trends, setTrends] = useState<Record<string, unknown>[]>([]);
108
- const [viralStrategies, setViralStrategies] = useState<Record<string, unknown>[]>([]);
109
- const [contentIdeas, setContentIdeas] = useState<Record<string, unknown>[]>([]);
110
- const [trendAnalysis, setTrendAnalysis] = useState<Record<string, unknown> | null>(null);
111
  const [trendLoading, setTrendLoading] = useState(false);
112
 
113
  // Stats
@@ -382,8 +382,8 @@ export default function Dashboard() {
382
  <Bot className="h-5 w-5" />
383
  </div>
384
  <div>
385
- <h1 className="text-lg font-bold bg-gradient-to-r from-violet-400 to-purple-400 bg-clip-text text-transparent">Sofía Cloud</h1>
386
- <p className="text-xs text-slate-400">Monetización Pro</p>
387
  </div>
388
  </div>
389
 
@@ -402,8 +402,7 @@ export default function Dashboard() {
402
  { id: "content", icon: FolderGit2, label: "Contenido" },
403
  ].map((item) => (
404
  <button key={item.id} onClick={() => setActiveTab(item.id)}
405
- className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all text-sm ${
406
- activeTab === item.id ? "bg-violet-500/20 text-violet-400 border border-violet-500/30" : "text-slate-400 hover:bg-slate-800"}`}>
407
  <item.icon className="h-4 w-4" /><span>{item.label}</span>
408
  </button>
409
  ))}
 
49
  const [userPrompt, setUserPrompt] = useState("");
50
  const [promptType, setPromptType] = useState("image");
51
  const [targetPlatform, setTargetPlatform] = useState("general");
52
+ const [optimizedPrompt, setOptimizedPrompt] = useState<any | null>(null);
53
  const [promptLoading, setPromptLoading] = useState(false);
54
 
55
  // Image Generation
 
100
  const [influencerPlatform, setInfluencerPlatform] = useState("");
101
  const [influencerWithPets, setInfluencerWithPets] = useState(false);
102
  const [influencerLoading, setInfluencerLoading] = useState(false);
103
+ const [influencerAnalysis, setInfluencerAnalysis] = useState<any | null>(null);
104
+ const [characterConcept, setCharacterConcept] = useState<any | null>(null);
105
 
106
  // Trends
107
+ const [trends, setTrends] = useState<any[]>([]);
108
+ const [viralStrategies, setViralStrategies] = useState<any[]>([]);
109
+ const [contentIdeas, setContentIdeas] = useState<any[]>([]);
110
+ const [trendAnalysis, setTrendAnalysis] = useState<any | null>(null);
111
  const [trendLoading, setTrendLoading] = useState(false);
112
 
113
  // Stats
 
382
  <Bot className="h-5 w-5" />
383
  </div>
384
  <div>
385
+ <h1 className="text-lg font-bold bg-gradient-to-r from-violet-400 to-purple-400 bg-clip-text text-transparent">AI Influencer Studio</h1>
386
+ <p className="text-xs text-slate-400">Plataforma SaaS</p>
387
  </div>
388
  </div>
389
 
 
402
  { id: "content", icon: FolderGit2, label: "Contenido" },
403
  ].map((item) => (
404
  <button key={item.id} onClick={() => setActiveTab(item.id)}
405
+ className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all text-sm ${activeTab === item.id ? "bg-violet-500/20 text-violet-400 border border-violet-500/30" : "text-slate-400 hover:bg-slate-800"}`}>
 
406
  <item.icon className="h-4 w-4" /><span>{item.label}</span>
407
  </button>
408
  ))}
src/components/ui/chart.tsx CHANGED
@@ -86,13 +86,13 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
86
  ([theme, prefix]) => `
87
  ${prefix} [data-chart=${id}] {
88
  ${colorConfig
89
- .map(([key, itemConfig]) => {
90
- const color =
91
- itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
92
- itemConfig.color
93
- return color ? ` --color-${key}: ${color};` : null
94
- })
95
- .join("\n")}
96
  }
97
  `
98
  )
@@ -118,7 +118,7 @@ function ChartTooltipContent({
118
  color,
119
  nameKey,
120
  labelKey,
121
- }: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
122
  React.ComponentProps<"div"> & {
123
  hideLabel?: boolean
124
  hideIndicator?: boolean
@@ -256,11 +256,11 @@ function ChartLegendContent({
256
  payload,
257
  verticalAlign = "bottom",
258
  nameKey,
259
- }: React.ComponentProps<"div"> &
260
- Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
261
- hideIcon?: boolean
262
- nameKey?: string
263
- }) {
264
  const { config } = useChart()
265
 
266
  if (!payload?.length) {
@@ -316,8 +316,8 @@ function getPayloadConfigFromPayload(
316
 
317
  const payloadPayload =
318
  "payload" in payload &&
319
- typeof payload.payload === "object" &&
320
- payload.payload !== null
321
  ? payload.payload
322
  : undefined
323
 
 
86
  ([theme, prefix]) => `
87
  ${prefix} [data-chart=${id}] {
88
  ${colorConfig
89
+ .map(([key, itemConfig]) => {
90
+ const color =
91
+ itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
92
+ itemConfig.color
93
+ return color ? ` --color-${key}: ${color};` : null
94
+ })
95
+ .join("\n")}
96
  }
97
  `
98
  )
 
118
  color,
119
  nameKey,
120
  labelKey,
121
+ }: any &
122
  React.ComponentProps<"div"> & {
123
  hideLabel?: boolean
124
  hideIndicator?: boolean
 
256
  payload,
257
  verticalAlign = "bottom",
258
  nameKey,
259
+ }: React.ComponentProps<"div"> & any & {
260
+ hideIcon?: boolean
261
+ nameKey?: string
262
+ payload?: any[]
263
+ }) {
264
  const { config } = useChart()
265
 
266
  if (!payload?.length) {
 
316
 
317
  const payloadPayload =
318
  "payload" in payload &&
319
+ typeof payload.payload === "object" &&
320
+ payload.payload !== null
321
  ? payload.payload
322
  : undefined
323
 
src/components/ui/resizable.tsx CHANGED
@@ -2,17 +2,19 @@
2
 
3
  import * as React from "react"
4
  import { GripVerticalIcon } from "lucide-react"
5
- import * as ResizablePrimitive from "react-resizable-panels"
 
6
 
7
  import { cn } from "@/lib/utils"
8
 
9
  function ResizablePanelGroup({
10
  className,
11
  ...props
12
- }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
13
  return (
14
  <ResizablePrimitive.PanelGroup
15
- data-slot="resizable-panel-group"
 
16
  className={cn(
17
  "flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
18
  className
 
2
 
3
  import * as React from "react"
4
  import { GripVerticalIcon } from "lucide-react"
5
+ import * as ResizablePrimitiveRaw from "react-resizable-panels"
6
+ const ResizablePrimitive = ResizablePrimitiveRaw as any;
7
 
8
  import { cn } from "@/lib/utils"
9
 
10
  function ResizablePanelGroup({
11
  className,
12
  ...props
13
+ }: React.ComponentProps<any>) {
14
  return (
15
  <ResizablePrimitive.PanelGroup
16
+ autoSaveId="persistence"
17
+ direction="horizontal"
18
  className={cn(
19
  "flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
20
  className
src/lib/ai.ts ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Shared AI helper — uses DeepSeek API (OpenAI-compatible format)
2
+ // Very cheap: ~$0.14 per million tokens
3
+
4
+ const CHAT_API_URL = process.env.CHAT_API_URL || "https://api.deepseek.com/v1/chat/completions";
5
+ const CHAT_API_KEY = process.env.CHAT_API_KEY || "";
6
+ const CHAT_MODEL = process.env.CHAT_MODEL || "deepseek-chat";
7
+
8
+ interface ChatMessage {
9
+ role: "system" | "user" | "assistant";
10
+ content: string;
11
+ }
12
+
13
+ interface ChatCompletionChoice {
14
+ message: { content: string | null };
15
+ }
16
+
17
+ interface ChatCompletionResponse {
18
+ choices: ChatCompletionChoice[];
19
+ }
20
+
21
+ interface CreateOptions {
22
+ messages: ChatMessage[];
23
+ temperature?: number;
24
+ max_tokens?: number;
25
+ }
26
+
27
+ export const ai = {
28
+ chat: {
29
+ completions: {
30
+ create: async (options: CreateOptions): Promise<ChatCompletionResponse> => {
31
+ const res = await fetch(CHAT_API_URL, {
32
+ method: "POST",
33
+ headers: {
34
+ "Authorization": `Bearer ${CHAT_API_KEY}`,
35
+ "Content-Type": "application/json",
36
+ },
37
+ body: JSON.stringify({
38
+ model: CHAT_MODEL,
39
+ messages: options.messages,
40
+ temperature: options.temperature ?? 0.7,
41
+ max_tokens: options.max_tokens ?? 1000,
42
+ }),
43
+ });
44
+
45
+ if (!res.ok) {
46
+ const errorText = await res.text();
47
+ throw new Error(`AI API error: ${res.status} ${errorText}`);
48
+ }
49
+
50
+ return res.json();
51
+ },
52
+ },
53
+ },
54
+ };
tsconfig.json CHANGED
@@ -16,7 +16,7 @@
16
  "moduleResolution": "bundler",
17
  "resolveJsonModule": true,
18
  "isolatedModules": true,
19
- "jsx": "react-jsx",
20
  "incremental": true,
21
  "plugins": [
22
  {
 
16
  "moduleResolution": "bundler",
17
  "resolveJsonModule": true,
18
  "isolatedModules": true,
19
+ "jsx": "preserve",
20
  "incremental": true,
21
  "plugins": [
22
  {