Spaces:
Build error
Build error
Gmagl commited on
Commit ·
1eae852
1
Parent(s): 31af3f9
Fix build errors, update UI dependencies, and configure Docker for npm deployment
Browse files- .gitignore +37 -0
- Dockerfile +20 -25
- entrypoint.sh +1 -1
- next.config.ts +6 -11
- package-lock.json +0 -0
- package.json +59 -44
- prisma/schema.prisma +156 -72
- src/app/api/automation/route.ts +2 -2
- src/app/api/characters/route.ts +41 -172
- src/app/api/content/route.ts +1 -1
- src/app/api/generate/image/route.ts +50 -197
- src/app/api/generate/video/route.ts +49 -100
- src/app/api/influencers/route.ts +2 -2
- src/app/api/pets/route.ts +2 -2
- src/app/api/prompt-engineer/route.ts +13 -42
- src/app/api/repos/[id]/route.ts +1 -1
- src/app/api/storytelling/route.ts +3 -3
- src/app/api/trends/route.ts +3 -3
- src/app/layout.tsx +8 -8
- src/app/page.tsx +10 -11
- src/components/ui/chart.tsx +15 -15
- src/components/ui/resizable.tsx +5 -3
- src/lib/ai.ts +54 -0
- tsconfig.json +1 -1
.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
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
-
#
|
| 6 |
-
RUN apt-get update && apt-get install -y
|
| 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 |
-
#
|
| 11 |
-
COPY package.json ./
|
| 12 |
|
| 13 |
-
#
|
| 14 |
-
RUN
|
| 15 |
|
| 16 |
-
#
|
| 17 |
COPY . .
|
| 18 |
|
| 19 |
-
#
|
| 20 |
-
RUN
|
| 21 |
|
| 22 |
-
#
|
| 23 |
-
|
| 24 |
-
ENV NODE_ENV=production
|
| 25 |
-
ENV PORT=7860
|
| 26 |
-
ENV HOST=0.0.0.0
|
| 27 |
|
| 28 |
-
#
|
| 29 |
-
RUN
|
| 30 |
|
| 31 |
-
#
|
| 32 |
-
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
EXPOSE
|
| 35 |
|
| 36 |
-
|
| 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 &&
|
| 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
|
|
|
|
|
|
|
| 2 |
|
| 3 |
const nextConfig: NextConfig = {
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 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
|
| 7 |
"build": "next build",
|
| 8 |
-
"start": "next start -p
|
| 9 |
"lint": "eslint ."
|
| 10 |
},
|
| 11 |
"dependencies": {
|
| 12 |
-
"@hookform/resolvers": "^
|
| 13 |
-
"@prisma/client": "^6.
|
| 14 |
-
"@radix-ui/react-accordion": "^1.2.
|
| 15 |
-
"@radix-ui/react-alert-dialog": "^1.1.
|
| 16 |
-
"@radix-ui/react-
|
| 17 |
-
"@radix-ui/react-
|
| 18 |
-
"@radix-ui/react-
|
| 19 |
-
"@radix-ui/react-
|
| 20 |
-
"@radix-ui/react-
|
| 21 |
-
"@radix-ui/react-
|
| 22 |
-
"@radix-ui/react-
|
| 23 |
-
"@radix-ui/react-
|
| 24 |
-
"@radix-ui/react-
|
| 25 |
-
"@radix-ui/react-
|
| 26 |
-
"@radix-ui/react-
|
| 27 |
-
"@radix-ui/react-
|
| 28 |
-
"@radix-ui/react-
|
| 29 |
-
"@radix-ui/react-
|
| 30 |
-
"@radix-ui/react-
|
| 31 |
-
"@radix-ui/react-
|
| 32 |
-
"@radix-ui/react-
|
| 33 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
"clsx": "^2.1.1",
|
| 35 |
-
"
|
| 36 |
-
"
|
| 37 |
-
"
|
| 38 |
-
"
|
| 39 |
-
"
|
| 40 |
-
"
|
| 41 |
-
"
|
| 42 |
-
"
|
| 43 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
"tw-animate-css": "^1.3.5",
|
|
|
|
| 45 |
"z-ai-web-dev-sdk": "^0.0.16",
|
| 46 |
-
"zod": "^
|
| 47 |
},
|
| 48 |
"devDependencies": {
|
| 49 |
-
"@tailwindcss/postcss": "^4
|
| 50 |
-
"@types/node": "
|
| 51 |
-
"@types/react": "19
|
| 52 |
-
"@types/react-dom": "19
|
| 53 |
-
"
|
| 54 |
-
"
|
| 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 = "
|
| 10 |
url = env("DATABASE_URL")
|
| 11 |
}
|
| 12 |
|
|
@@ -15,11 +15,21 @@ datasource db {
|
|
| 15 |
// ============================================
|
| 16 |
|
| 17 |
model User {
|
| 18 |
-
id
|
| 19 |
-
email
|
| 20 |
-
name
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
| 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?
|
| 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?
|
| 172 |
-
status String @default("active")
|
| 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
|
| 186 |
amount Float
|
| 187 |
currency String @default("USD")
|
| 188 |
postId String?
|
| 189 |
subscriberId String?
|
| 190 |
-
status String @default("pending")
|
| 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?
|
| 205 |
-
type String
|
| 206 |
-
status String @default("draft")
|
| 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?
|
| 214 |
-
postUrl String?
|
| 215 |
-
engagementStats String?
|
| 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?
|
| 232 |
-
targetAudience String?
|
| 233 |
-
tone String?
|
| 234 |
-
structure String?
|
| 235 |
-
characterIds String?
|
| 236 |
totalEpisodes Int @default(1)
|
| 237 |
currentEpisode Int @default(1)
|
| 238 |
-
status String @default("draft")
|
| 239 |
-
monetizationStrategy String?
|
| 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
|
| 255 |
-
hook String?
|
| 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?
|
| 271 |
-
completionRate Float?
|
| 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
|
| 289 |
-
trigger String
|
| 290 |
-
triggerConfig String?
|
| 291 |
-
actions String
|
| 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
|
| 305 |
input String?
|
| 306 |
output String?
|
| 307 |
error String?
|
| 308 |
-
duration Int?
|
| 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
|
| 320 |
name String
|
| 321 |
description String?
|
| 322 |
volume Int?
|
| 323 |
growth Float?
|
| 324 |
startDate DateTime?
|
| 325 |
endDate DateTime?
|
| 326 |
-
relatedTags String?
|
| 327 |
-
contentIdeas String?
|
| 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
|
| 353 |
platform String
|
| 354 |
-
structure String
|
| 355 |
-
hooks String?
|
| 356 |
-
ctas String?
|
| 357 |
-
hashtags String?
|
| 358 |
-
bestTimes String?
|
| 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
|
| 372 |
breed String?
|
| 373 |
description String?
|
| 374 |
referenceImage String?
|
| 375 |
-
traits String?
|
| 376 |
-
personality String?
|
| 377 |
color String?
|
| 378 |
-
accessories String?
|
| 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?
|
| 400 |
-
contentTypes String?
|
| 401 |
-
postingSchedule String?
|
| 402 |
-
visualStyle String?
|
| 403 |
-
monetizationType String?
|
| 404 |
-
signatureElements String?
|
| 405 |
petCompanion Boolean @default(false)
|
| 406 |
petType String?
|
| 407 |
-
analysis String?
|
| 408 |
-
lessons String?
|
| 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
|
| 424 |
-
hook String?
|
| 425 |
-
structure String?
|
| 426 |
-
elements String?
|
| 427 |
estimatedReach Int?
|
| 428 |
difficulty String @default("medium")
|
| 429 |
-
timeframe String?
|
| 430 |
-
examples String?
|
| 431 |
-
tags String?
|
| 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,
|
| 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 |
-
|
| 12 |
-
|
| 13 |
-
|
| 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 |
-
|
| 65 |
-
|
| 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 =
|
| 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
|
| 115 |
-
console.error("Error generando referencia:", genError);
|
| 116 |
-
}
|
| 117 |
}
|
| 118 |
|
| 119 |
-
|
| 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
|
| 134 |
-
referenceImage,
|
| 135 |
-
traits:
|
| 136 |
-
|
| 137 |
-
JSON.stringify({ name, description }),
|
| 138 |
-
},
|
| 139 |
});
|
| 140 |
|
| 141 |
-
return NextResponse.json({
|
| 142 |
-
|
| 143 |
-
|
| 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 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
);
|
| 168 |
-
}
|
| 169 |
|
| 170 |
-
const character = await db.character.update({
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 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 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 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,
|
| 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 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 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 |
-
|
| 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 =
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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 |
-
|
| 139 |
-
|
| 140 |
-
|
| 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 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
data: {
|
| 185 |
-
status: "failed",
|
| 186 |
-
metadata: JSON.stringify({ error: String(genError) })
|
| 187 |
-
}
|
| 188 |
-
});
|
| 189 |
-
}
|
| 190 |
|
| 191 |
-
return NextResponse.json(
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
);
|
| 195 |
}
|
| 196 |
} catch (error) {
|
| 197 |
-
|
| 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 |
-
|
| 216 |
-
|
| 217 |
-
|
| 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
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 37 |
|
| 38 |
-
const
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 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 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
});
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
| 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 |
-
|
| 115 |
-
|
| 116 |
-
);
|
| 117 |
}
|
| 118 |
} catch (error) {
|
| 119 |
-
|
| 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,
|
| 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,
|
| 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,
|
| 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 |
-
|
| 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 |
-
|
| 48 |
-
|
| 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
|
| 75 |
-
|
| 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,
|
| 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,
|
| 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,
|
| 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: "
|
| 18 |
-
description: "
|
| 19 |
-
keywords: ["
|
| 20 |
-
authors: [{ name: "
|
| 21 |
icons: {
|
| 22 |
icon: "/logo.svg",
|
| 23 |
},
|
| 24 |
openGraph: {
|
| 25 |
-
title: "
|
| 26 |
-
description: "
|
| 27 |
type: "website",
|
| 28 |
},
|
| 29 |
twitter: {
|
| 30 |
card: "summary_large_image",
|
| 31 |
-
title: "
|
| 32 |
-
description: "
|
| 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<
|
| 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<
|
| 104 |
-
const [characterConcept, setCharacterConcept] = useState<
|
| 105 |
|
| 106 |
// Trends
|
| 107 |
-
const [trends, setTrends] = useState<
|
| 108 |
-
const [viralStrategies, setViralStrategies] = useState<
|
| 109 |
-
const [contentIdeas, setContentIdeas] = useState<
|
| 110 |
-
const [trendAnalysis, setTrendAnalysis] = useState<
|
| 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">
|
| 386 |
-
<p className="text-xs text-slate-400">
|
| 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 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
}
|
| 97 |
`
|
| 98 |
)
|
|
@@ -118,7 +118,7 @@ function ChartTooltipContent({
|
|
| 118 |
color,
|
| 119 |
nameKey,
|
| 120 |
labelKey,
|
| 121 |
-
}:
|
| 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 |
-
|
| 261 |
-
|
| 262 |
-
|
| 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 |
-
|
| 320 |
-
|
| 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
|
|
|
|
| 6 |
|
| 7 |
import { cn } from "@/lib/utils"
|
| 8 |
|
| 9 |
function ResizablePanelGroup({
|
| 10 |
className,
|
| 11 |
...props
|
| 12 |
-
}: React.ComponentProps<
|
| 13 |
return (
|
| 14 |
<ResizablePrimitive.PanelGroup
|
| 15 |
-
|
|
|
|
| 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": "
|
| 20 |
"incremental": true,
|
| 21 |
"plugins": [
|
| 22 |
{
|
|
|
|
| 16 |
"moduleResolution": "bundler",
|
| 17 |
"resolveJsonModule": true,
|
| 18 |
"isolatedModules": true,
|
| 19 |
+
"jsx": "preserve",
|
| 20 |
"incremental": true,
|
| 21 |
"plugins": [
|
| 22 |
{
|