Gmagl commited on
Commit
333c51a
·
1 Parent(s): ec1df90

Add Sofia Cloud complete files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +8 -35
  2. Dockerfile +56 -0
  3. README.md +23 -5
  4. bun.lock +0 -0
  5. components.json +21 -0
  6. entrypoint.sh +10 -0
  7. eslint.config.mjs +50 -0
  8. next.config.ts +12 -0
  9. package.json +94 -0
  10. postcss.config.mjs +5 -0
  11. prisma/schema.prisma +436 -0
  12. public/logo.svg +29 -0
  13. public/robots.txt +14 -0
  14. src/app/api/agent/route.ts +122 -0
  15. src/app/api/analyze/route.ts +234 -0
  16. src/app/api/automation/route.ts +396 -0
  17. src/app/api/censor/route.ts +191 -0
  18. src/app/api/characters/route.ts +212 -0
  19. src/app/api/content/route.ts +89 -0
  20. src/app/api/generate/image/route.ts +237 -0
  21. src/app/api/generate/video/route.ts +221 -0
  22. src/app/api/influencers/route.ts +450 -0
  23. src/app/api/monetization/route.ts +372 -0
  24. src/app/api/pets/route.ts +327 -0
  25. src/app/api/posts/route.ts +330 -0
  26. src/app/api/projects/route.ts +74 -0
  27. src/app/api/prompt-engineer/route.ts +216 -0
  28. src/app/api/repos/[id]/route.ts +114 -0
  29. src/app/api/repos/route.ts +126 -0
  30. src/app/api/route.ts +5 -0
  31. src/app/api/storytelling/route.ts +347 -0
  32. src/app/api/trends/route.ts +343 -0
  33. src/app/globals.css +122 -0
  34. src/app/layout.tsx +51 -0
  35. src/app/page.tsx +1131 -0
  36. src/components/ui/accordion.tsx +66 -0
  37. src/components/ui/alert-dialog.tsx +157 -0
  38. src/components/ui/alert.tsx +66 -0
  39. src/components/ui/aspect-ratio.tsx +11 -0
  40. src/components/ui/avatar.tsx +53 -0
  41. src/components/ui/badge.tsx +46 -0
  42. src/components/ui/breadcrumb.tsx +109 -0
  43. src/components/ui/button.tsx +59 -0
  44. src/components/ui/calendar.tsx +213 -0
  45. src/components/ui/card.tsx +92 -0
  46. src/components/ui/carousel.tsx +241 -0
  47. src/components/ui/chart.tsx +353 -0
  48. src/components/ui/checkbox.tsx +32 -0
  49. src/components/ui/collapsible.tsx +33 -0
  50. src/components/ui/command.tsx +184 -0
.gitattributes CHANGED
@@ -1,35 +1,8 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.ts text eol=lf
2
+ *.tsx text eol=lf
3
+ *.js text eol=lf
4
+ *.jsx text eol=lf
5
+ *.json text eol=lf
6
+ *.css text eol=lf
7
+ *.md text eol=lf
8
+ *.sh text eol=lf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-alpine AS builder
2
+
3
+ # Install bun
4
+ RUN npm install -g bun
5
+
6
+ WORKDIR /app
7
+
8
+ # Copy package files first
9
+ COPY package.json bun.lock ./
10
+
11
+ # Install dependencies
12
+ RUN bun install --frozen-lockfile
13
+
14
+ # Copy source files
15
+ COPY . .
16
+
17
+ # Generate Prisma client
18
+ RUN bunx prisma generate
19
+
20
+ # Build the application
21
+ RUN bun run build
22
+
23
+ # Production image
24
+ FROM node:20-alpine
25
+
26
+ WORKDIR /app
27
+
28
+ # Install bun
29
+ RUN npm install -g bun
30
+
31
+ # Copy built application from builder
32
+ COPY --from=builder /app/.next ./.next
33
+ COPY --from=builder /app/public ./public
34
+ COPY --from=builder /app/package.json ./
35
+ COPY --from=builder /app/bun.lock ./
36
+ COPY --from=builder /app/prisma ./prisma
37
+ COPY --from=builder /app/node_modules ./node_modules
38
+ COPY --from=builder /app/next.config.ts ./
39
+
40
+ # Create data directory for SQLite
41
+ RUN mkdir -p /app/data
42
+
43
+ # Set environment variables
44
+ ENV NODE_ENV=production
45
+ ENV DATABASE_URL="file:/app/data/sofia.db"
46
+ ENV PORT=3000
47
+ ENV HOST=0.0.0.0
48
+
49
+ # Copy and setup entrypoint
50
+ COPY entrypoint.sh /entrypoint.sh
51
+ RUN chmod +x /entrypoint.sh
52
+
53
+ EXPOSE 3000
54
+
55
+ ENTRYPOINT ["/entrypoint.sh"]
56
+ CMD ["bun", "start"]
README.md CHANGED
@@ -1,11 +1,29 @@
1
  ---
2
- title: Sofia Cloud
3
- emoji: 🦀
4
- colorFrom: blue
5
- colorTo: red
6
  sdk: docker
7
  pinned: false
8
  license: mit
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Sofía Cloud - Multiagente AI
3
+ emoji: 🤖
4
+ colorFrom: violet
5
+ colorTo: purple
6
  sdk: docker
7
  pinned: false
8
  license: mit
9
  ---
10
 
11
+ # 🤖 Sofía Cloud - Sistema Multiagente AI para Monetización
12
+
13
+ Plataforma completa de monetización de contenido con IA.
14
+
15
+ ## ✨ Características
16
+
17
+ - **Generación de Contenido**: Imágenes, videos, prompts optimizados
18
+ - **Monetización**: OnlyFans, Patreon, Fansly, Instagram, TikTok
19
+ - **Storytelling**: Historias con IA y cliffhangers
20
+ - **Influencers IA**: Análisis de patrones de éxito
21
+ - **Mascotas**: Sistema de pets con +35% engagement
22
+ - **Tendencias Virales**: Estrategias y predicción de viralidad
23
+
24
+ ## 🚀 Tecnologías
25
+
26
+ - Next.js 15 + React + TypeScript
27
+ - Prisma + SQLite
28
+ - z-ai-web-dev-sdk (IA)
29
+ - Tailwind CSS + shadcn/ui
bun.lock ADDED
The diff for this file is too large to render. See raw diff
 
components.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "src/app/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ },
20
+ "iconLibrary": "lucide"
21
+ }
entrypoint.sh ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+
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
10
+ exec "$@"
eslint.config.mjs ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
2
+ import nextTypescript from "eslint-config-next/typescript";
3
+ import { dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ const eslintConfig = [...nextCoreWebVitals, ...nextTypescript, {
10
+ rules: {
11
+ // TypeScript rules
12
+ "@typescript-eslint/no-explicit-any": "off",
13
+ "@typescript-eslint/no-unused-vars": "off",
14
+ "@typescript-eslint/no-non-null-assertion": "off",
15
+ "@typescript-eslint/ban-ts-comment": "off",
16
+ "@typescript-eslint/prefer-as-const": "off",
17
+ "@typescript-eslint/no-unused-disable-directive": "off",
18
+
19
+ // React rules
20
+ "react-hooks/exhaustive-deps": "off",
21
+ "react-hooks/purity": "off",
22
+ "react/no-unescaped-entities": "off",
23
+ "react/display-name": "off",
24
+ "react/prop-types": "off",
25
+ "react-compiler/react-compiler": "off",
26
+
27
+ // Next.js rules
28
+ "@next/next/no-img-element": "off",
29
+ "@next/next/no-html-link-for-pages": "off",
30
+
31
+ // General JavaScript rules
32
+ "prefer-const": "off",
33
+ "no-unused-vars": "off",
34
+ "no-console": "off",
35
+ "no-debugger": "off",
36
+ "no-empty": "off",
37
+ "no-irregular-whitespace": "off",
38
+ "no-case-declarations": "off",
39
+ "no-fallthrough": "off",
40
+ "no-mixed-spaces-and-tabs": "off",
41
+ "no-redeclare": "off",
42
+ "no-undef": "off",
43
+ "no-unreachable": "off",
44
+ "no-useless-escape": "off",
45
+ },
46
+ }, {
47
+ ignores: ["node_modules/**", ".next/**", "out/**", "build/**", "next-env.d.ts", "examples/**", "skills"]
48
+ }];
49
+
50
+ export default eslintConfig;
next.config.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ output: "standalone",
5
+ /* config options here */
6
+ typescript: {
7
+ ignoreBuildErrors: true,
8
+ },
9
+ reactStrictMode: false,
10
+ };
11
+
12
+ export default nextConfig;
package.json ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "nextjs_tailwind_shadcn_ts",
3
+ "version": "0.2.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev -p 3000 -H 0.0.0.0 2>&1 | tee dev.log",
7
+ "build": "next build && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/",
8
+ "start": "NODE_ENV=production bun .next/standalone/server.js 2>&1 | tee server.log",
9
+ "lint": "eslint .",
10
+ "db:push": "prisma db push",
11
+ "db:generate": "prisma generate",
12
+ "db:migrate": "prisma migrate dev",
13
+ "db:reset": "prisma migrate reset"
14
+ },
15
+ "dependencies": {
16
+ "@dnd-kit/core": "^6.3.1",
17
+ "@dnd-kit/sortable": "^10.0.0",
18
+ "@dnd-kit/utilities": "^3.2.2",
19
+ "@hookform/resolvers": "^5.1.1",
20
+ "@mdxeditor/editor": "^3.39.1",
21
+ "@prisma/client": "^6.11.1",
22
+ "@radix-ui/react-accordion": "^1.2.11",
23
+ "@radix-ui/react-alert-dialog": "^1.1.14",
24
+ "@radix-ui/react-aspect-ratio": "^1.1.7",
25
+ "@radix-ui/react-avatar": "^1.1.10",
26
+ "@radix-ui/react-checkbox": "^1.3.2",
27
+ "@radix-ui/react-collapsible": "^1.1.11",
28
+ "@radix-ui/react-context-menu": "^2.2.15",
29
+ "@radix-ui/react-dialog": "^1.1.14",
30
+ "@radix-ui/react-dropdown-menu": "^2.1.15",
31
+ "@radix-ui/react-hover-card": "^1.1.14",
32
+ "@radix-ui/react-label": "^2.1.7",
33
+ "@radix-ui/react-menubar": "^1.1.15",
34
+ "@radix-ui/react-navigation-menu": "^1.2.13",
35
+ "@radix-ui/react-popover": "^1.1.14",
36
+ "@radix-ui/react-progress": "^1.1.7",
37
+ "@radix-ui/react-radio-group": "^1.3.7",
38
+ "@radix-ui/react-scroll-area": "^1.2.9",
39
+ "@radix-ui/react-select": "^2.2.5",
40
+ "@radix-ui/react-separator": "^1.1.7",
41
+ "@radix-ui/react-slider": "^1.3.5",
42
+ "@radix-ui/react-slot": "^1.2.3",
43
+ "@radix-ui/react-switch": "^1.2.5",
44
+ "@radix-ui/react-tabs": "^1.1.12",
45
+ "@radix-ui/react-toast": "^1.2.14",
46
+ "@radix-ui/react-toggle": "^1.1.9",
47
+ "@radix-ui/react-toggle-group": "^1.1.10",
48
+ "@radix-ui/react-tooltip": "^1.2.7",
49
+ "@reactuses/core": "^6.0.5",
50
+ "@tanstack/react-query": "^5.82.0",
51
+ "@tanstack/react-table": "^8.21.3",
52
+ "class-variance-authority": "^0.7.1",
53
+ "clsx": "^2.1.1",
54
+ "cmdk": "^1.1.1",
55
+ "date-fns": "^4.1.0",
56
+ "embla-carousel-react": "^8.6.0",
57
+ "framer-motion": "^12.23.2",
58
+ "input-otp": "^1.4.2",
59
+ "lucide-react": "^0.525.0",
60
+ "next": "^16.1.1",
61
+ "next-auth": "^4.24.11",
62
+ "next-intl": "^4.3.4",
63
+ "next-themes": "^0.4.6",
64
+ "prisma": "^6.11.1",
65
+ "react": "^19.0.0",
66
+ "react-day-picker": "^9.8.0",
67
+ "react-dom": "^19.0.0",
68
+ "react-hook-form": "^7.60.0",
69
+ "react-markdown": "^10.1.0",
70
+ "react-resizable-panels": "^3.0.3",
71
+ "react-syntax-highlighter": "^15.6.1",
72
+ "recharts": "^2.15.4",
73
+ "sharp": "^0.34.3",
74
+ "sonner": "^2.0.6",
75
+ "tailwind-merge": "^3.3.1",
76
+ "tailwindcss-animate": "^1.0.7",
77
+ "uuid": "^11.1.0",
78
+ "vaul": "^1.1.2",
79
+ "z-ai-web-dev-sdk": "^0.0.16",
80
+ "zod": "^4.0.2",
81
+ "zustand": "^5.0.6"
82
+ },
83
+ "devDependencies": {
84
+ "@tailwindcss/postcss": "^4",
85
+ "@types/react": "^19",
86
+ "@types/react-dom": "^19",
87
+ "bun-types": "^1.3.4",
88
+ "eslint": "^9",
89
+ "eslint-config-next": "^16.1.1",
90
+ "tailwindcss": "^4",
91
+ "tw-animate-css": "^1.3.5",
92
+ "typescript": "^5"
93
+ }
94
+ }
postcss.config.mjs ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: ["@tailwindcss/postcss"],
3
+ };
4
+
5
+ export default config;
prisma/schema.prisma ADDED
@@ -0,0 +1,436 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // This is your Prisma schema file,
2
+ // learn more about it in the docs: https://pris.ly/d/prisma-schema
3
+
4
+ generator client {
5
+ provider = "prisma-client-js"
6
+ }
7
+
8
+ datasource db {
9
+ provider = "sqlite"
10
+ url = env("DATABASE_URL")
11
+ }
12
+
13
+ // ============================================
14
+ // MODELOS BASE
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 {
26
+ id String @id @default(cuid())
27
+ name String
28
+ description String?
29
+ style String @default("default")
30
+ status String @default("active")
31
+ createdAt DateTime @default(now())
32
+ updatedAt DateTime @updatedAt
33
+ repos Repo[]
34
+ analyses Analysis[]
35
+ contents Content[]
36
+ }
37
+
38
+ model Repo {
39
+ id String @id @default(cuid())
40
+ url String
41
+ name String
42
+ status String @default("cloned")
43
+ projectId String?
44
+ project Project? @relation(fields: [projectId], references: [id])
45
+ analyses Analysis[]
46
+ createdAt DateTime @default(now())
47
+ updatedAt DateTime @updatedAt
48
+ }
49
+
50
+ model Analysis {
51
+ id String @id @default(cuid())
52
+ type String
53
+ result String
54
+ summary String?
55
+ repoId String?
56
+ repo Repo? @relation(fields: [repoId], references: [id])
57
+ projectId String?
58
+ project Project? @relation(fields: [projectId], references: [id])
59
+ createdAt DateTime @default(now())
60
+ }
61
+
62
+ model AgentTask {
63
+ id String @id @default(cuid())
64
+ type String
65
+ status String @default("pending")
66
+ input String
67
+ output String?
68
+ createdAt DateTime @default(now())
69
+ completedAt DateTime?
70
+ }
71
+
72
+ // ============================================
73
+ // MODELOS DE CONTENIDO MULTIMEDIA
74
+ // ============================================
75
+
76
+ model Content {
77
+ id String @id @default(cuid())
78
+ type String // "image", "video", "audio", "text", "reel", "story", "carousel"
79
+ title String
80
+ description String?
81
+ prompt String
82
+ optimizedPrompt String?
83
+ filePath String?
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])
90
+ characterId String?
91
+ character Character? @relation(fields: [characterId], references: [id])
92
+ petId String?
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
+ // ============================================
113
+ // MODELOS DE CENSURA
114
+ // ============================================
115
+
116
+ model CensorRule {
117
+ id String @id @default(cuid())
118
+ platform String
119
+ category String
120
+ rule String
121
+ severity String @default("medium")
122
+ autoAction String @default("warn")
123
+ isActive Boolean @default(true)
124
+ createdAt DateTime @default(now())
125
+ updatedAt DateTime @updatedAt
126
+ }
127
+
128
+ model CensorFlag {
129
+ id String @id @default(cuid())
130
+ contentId String
131
+ content Content @relation(fields: [contentId], references: [id])
132
+ category String
133
+ reason String
134
+ severity String
135
+ action String
136
+ createdAt DateTime @default(now())
137
+ }
138
+
139
+ // ============================================
140
+ // MODELOS DE MONETIZACIÓN
141
+ // ============================================
142
+
143
+ model MonetizationPlatform {
144
+ id String @id @default(cuid())
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
152
+ contentRules String? // JSON con reglas de contenido
153
+ feePercentage Float?
154
+ payoutSchedule String?
155
+ isActive Boolean @default(true)
156
+ isVerified Boolean @default(false)
157
+ metadata String?
158
+ posts Post[]
159
+ earnings Earning[]
160
+ subscribers Subscriber[]
161
+ createdAt DateTime @default(now())
162
+ updatedAt DateTime @updatedAt
163
+ }
164
+
165
+ model Subscriber {
166
+ id String @id @default(cuid())
167
+ platformId String
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)
176
+ metadata String?
177
+ createdAt DateTime @default(now())
178
+ updatedAt DateTime @updatedAt
179
+ }
180
+
181
+ 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())
194
+ }
195
+
196
+ // ============================================
197
+ // MODELOS DE PUBLICACIÓN
198
+ // ============================================
199
+
200
+ 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?
219
+ createdAt DateTime @default(now())
220
+ updatedAt DateTime @updatedAt
221
+ }
222
+
223
+ // ============================================
224
+ // MODELOS DE STORYTELLING
225
+ // ============================================
226
+
227
+ 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?
243
+ createdAt DateTime @default(now())
244
+ updatedAt DateTime @updatedAt
245
+ }
246
+
247
+ model StoryEpisode {
248
+ id String @id @default(cuid())
249
+ storyId String
250
+ story Story @relation(fields: [storyId], references: [id])
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?
259
+ publishedAt DateTime?
260
+ createdAt DateTime @default(now())
261
+ updatedAt DateTime @updatedAt
262
+ }
263
+
264
+ model StoryAnalytics {
265
+ id String @id @default(cuid())
266
+ storyId String @unique
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?
275
+ metadata String?
276
+ createdAt DateTime @default(now())
277
+ updatedAt DateTime @updatedAt
278
+ }
279
+
280
+ // ============================================
281
+ // MODELOS DE AUTOMATIZACIÓN
282
+ // ============================================
283
+
284
+ 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?
295
+ runCount Int @default(0)
296
+ metadata String?
297
+ createdAt DateTime @default(now())
298
+ updatedAt DateTime @updatedAt
299
+ }
300
+
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
+
312
+ // ============================================
313
+ // MODELOS DE TENDENCIAS
314
+ // ============================================
315
+
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
331
+ }
332
+
333
+ // ============================================
334
+ // MODELOS DE PLANTILLAS
335
+ // ============================================
336
+
337
+ model PromptTemplate {
338
+ id String @id @default(cuid())
339
+ name String
340
+ category String
341
+ template String
342
+ variables String
343
+ platform String @default("general")
344
+ isActive Boolean @default(true)
345
+ createdAt DateTime @default(now())
346
+ updatedAt DateTime @updatedAt
347
+ }
348
+
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
362
+ }
363
+
364
+ // ============================================
365
+ // MODELOS DE MASCOTAS/COMPAÑEROS
366
+ // ============================================
367
+
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[]
382
+ isActive Boolean @default(true)
383
+ createdAt DateTime @default(now())
384
+ updatedAt DateTime @updatedAt
385
+ }
386
+
387
+ // ============================================
388
+ // MODELOS DE INFLUENCERS IA DE REFERENCIA
389
+ // ============================================
390
+
391
+ model AIInfluencer {
392
+ id String @id @default(cuid())
393
+ name String
394
+ handle String?
395
+ platform String
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
412
+ }
413
+
414
+ // ============================================
415
+ // MODELOS DE ESTRATEGIAS VIRALES
416
+ // ============================================
417
+
418
+ model ViralStrategy {
419
+ id String @id @default(cuid())
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())
435
+ updatedAt DateTime @updatedAt
436
+ }
public/logo.svg ADDED
public/robots.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ User-agent: Googlebot
2
+ Allow: /
3
+
4
+ User-agent: Bingbot
5
+ Allow: /
6
+
7
+ User-agent: Twitterbot
8
+ Allow: /
9
+
10
+ User-agent: facebookexternalhit
11
+ Allow: /
12
+
13
+ User-agent: *
14
+ Allow: /
src/app/api/agent/route.ts ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import ZAI from "z-ai-web-dev-sdk";
4
+
5
+ const SYSTEM_PROMPT = `Eres Sofía, un asistente de desarrollo de software altamente inteligente y especializado. Tu nombre es Sofía y eres parte del sistema multiagente "Sofía Cloud".
6
+
7
+ Tus capacidades incluyen:
8
+ - Análisis de código y repositorios de GitHub
9
+ - Generación de código y documentación
10
+ - Sugerencias de mejoras y optimizaciones
11
+ - Detección de bugs y vulnerabilidades
12
+ - Creación de arquitecturas de software
13
+ - Explicaciones técnicas claras y detalladas
14
+
15
+ Responde siempre de manera profesional, útil y con un toque amigable. Cuando analices código, sé específico y proporciona ejemplos concretos. Si detectas problemas, sugiere soluciones prácticas.
16
+
17
+ Formato de respuestas:
18
+ - Usa Markdown para formatear tus respuestas
19
+ - Incluye bloques de código cuando sea relevante
20
+ - Sé conciso pero completo
21
+ - Si el usuario pregunta algo fuera del ámbito técnico, redirige amablemente al tema de desarrollo`;
22
+
23
+ // POST - Enviar prompt al agente IA
24
+ export async function POST(request: NextRequest) {
25
+ try {
26
+ const body = await request.json();
27
+ const { prompt, context } = body;
28
+
29
+ if (!prompt) {
30
+ return NextResponse.json(
31
+ { success: false, error: "El prompt es requerido" },
32
+ { status: 400 }
33
+ );
34
+ }
35
+
36
+ // Crear tarea pendiente
37
+ const task = await db.agentTask.create({
38
+ data: {
39
+ type: "analyze",
40
+ status: "pending",
41
+ input: prompt,
42
+ },
43
+ });
44
+
45
+ try {
46
+ // Usar z-ai-web-dev-sdk para la respuesta de IA
47
+ const zai = await ZAI.create();
48
+
49
+ const messages = [
50
+ { role: "system" as const, content: SYSTEM_PROMPT },
51
+ { role: "user" as const, content: context ? `Contexto: ${context}\n\nPregunta: ${prompt}` : prompt },
52
+ ];
53
+
54
+ const completion = await zai.chat.completions.create({
55
+ messages,
56
+ temperature: 0.7,
57
+ max_tokens: 4000,
58
+ });
59
+
60
+ const response = completion.choices[0]?.message?.content || "No se pudo generar una respuesta";
61
+
62
+ // Actualizar tarea como completada
63
+ await db.agentTask.update({
64
+ where: { id: task.id },
65
+ data: {
66
+ status: "completed",
67
+ output: response,
68
+ completedAt: new Date(),
69
+ },
70
+ });
71
+
72
+ return NextResponse.json({
73
+ success: true,
74
+ response,
75
+ taskId: task.id,
76
+ });
77
+ } catch (aiError) {
78
+ console.error("AI Error:", aiError);
79
+
80
+ // Actualizar tarea como fallida
81
+ await db.agentTask.update({
82
+ where: { id: task.id },
83
+ data: {
84
+ status: "failed",
85
+ output: aiError instanceof Error ? aiError.message : "Error desconocido",
86
+ },
87
+ });
88
+
89
+ return NextResponse.json(
90
+ { success: false, error: "Error al procesar con IA" },
91
+ { status: 500 }
92
+ );
93
+ }
94
+ } catch (error) {
95
+ console.error("Error in agent route:", error);
96
+ return NextResponse.json(
97
+ { success: false, error: "Error interno del servidor" },
98
+ { status: 500 }
99
+ );
100
+ }
101
+ }
102
+
103
+ // GET - Obtener historial de tareas
104
+ export async function GET() {
105
+ try {
106
+ const tasks = await db.agentTask.findMany({
107
+ orderBy: { createdAt: "desc" },
108
+ take: 20,
109
+ });
110
+
111
+ return NextResponse.json({
112
+ success: true,
113
+ tasks,
114
+ });
115
+ } catch (error) {
116
+ console.error("Error fetching tasks:", error);
117
+ return NextResponse.json(
118
+ { success: false, error: "Error al obtener tareas" },
119
+ { status: 500 }
120
+ );
121
+ }
122
+ }
src/app/api/analyze/route.ts ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import ZAI from "z-ai-web-dev-sdk";
4
+ import fs from "fs/promises";
5
+ import path from "path";
6
+
7
+ const REPOS_DIR = path.join(process.cwd(), "repos");
8
+
9
+ async function readRepoFiles(repoName: string, maxFiles: number = 10): Promise<string> {
10
+ const repoPath = path.join(REPOS_DIR, repoName);
11
+ let content = "";
12
+ let fileCount = 0;
13
+
14
+ async function readDir(dir: string, depth: number = 0): Promise<void> {
15
+ if (fileCount >= maxFiles || depth > 3) return;
16
+
17
+ try {
18
+ const entries = await fs.readdir(dir, { withFileTypes: true });
19
+
20
+ for (const entry of entries) {
21
+ if (fileCount >= maxFiles) break;
22
+
23
+ const fullPath = path.join(dir, entry.name);
24
+
25
+ // Ignorar carpetas comunes
26
+ if (entry.isDirectory()) {
27
+ if (["node_modules", ".git", "dist", "build", "__pycache__", "venv", ".next"].includes(entry.name)) {
28
+ continue;
29
+ }
30
+ await readDir(fullPath, depth + 1);
31
+ } else if (entry.isFile()) {
32
+ // Solo archivos de código relevantes
33
+ const ext = path.extname(entry.name);
34
+ const codeExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".java", ".go", ".rs", ".c", ".cpp", ".h", ".css", ".scss", ".html", ".json", ".yaml", ".yml", ".md"];
35
+
36
+ if (codeExtensions.includes(ext) && !entry.name.startsWith(".")) {
37
+ try {
38
+ const fileContent = await fs.readFile(fullPath, "utf-8");
39
+ const relativePath = path.relative(repoPath, fullPath);
40
+ content += `\n--- ${relativePath} ---\n${fileContent.slice(0, 2000)}\n`;
41
+ fileCount++;
42
+ } catch {
43
+ // Archivo no legible
44
+ }
45
+ }
46
+ }
47
+ }
48
+ } catch {
49
+ // Directorio no accesible
50
+ }
51
+ }
52
+
53
+ await readDir(repoPath);
54
+ return content;
55
+ }
56
+
57
+ // POST - Analizar código con IA
58
+ export async function POST(request: NextRequest) {
59
+ try {
60
+ const body = await request.json();
61
+ const { repoId, projectId, type, code } = body;
62
+
63
+ let analysisContent = code || "";
64
+ let repoName = "";
65
+
66
+ // Si hay repoId, leer archivos del repositorio
67
+ if (repoId) {
68
+ const repo = await db.repo.findUnique({
69
+ where: { id: repoId },
70
+ });
71
+
72
+ if (repo) {
73
+ repoName = repo.name;
74
+ analysisContent = await readRepoFiles(repo.name);
75
+ }
76
+ }
77
+
78
+ if (!analysisContent) {
79
+ return NextResponse.json(
80
+ { success: false, error: "No hay código para analizar" },
81
+ { status: 400 }
82
+ );
83
+ }
84
+
85
+ const analysisType = type || "code";
86
+
87
+ // Crear análisis inicial
88
+ const analysis = await db.analysis.create({
89
+ data: {
90
+ type: analysisType,
91
+ result: "Analizando...",
92
+ repoId: repoId || null,
93
+ projectId: projectId || null,
94
+ },
95
+ });
96
+
97
+ try {
98
+ const zai = await ZAI.create();
99
+
100
+ const prompts: Record<string, string> = {
101
+ code: `Analiza el siguiente código y proporciona:
102
+ 1. Resumen general del proyecto
103
+ 2. Estructura y organización
104
+ 3. Calidad del código (0-10)
105
+ 4. Posibles mejoras
106
+ 5. Bugs o problemas potenciales
107
+ 6. Sugerencias de seguridad
108
+
109
+ Código a analizar:
110
+ ${analysisContent.slice(0, 10000)}`,
111
+
112
+ security: `Realiza un análisis de seguridad del siguiente código. Identifica:
113
+ 1. Vulnerabilidades potenciales (SQL injection, XSS, etc.)
114
+ 2. Exposición de datos sensibles
115
+ 3. Dependencias inseguras
116
+ 4. Configuraciones peligrosas
117
+ 5. Recomendaciones de mitigación
118
+
119
+ Código:
120
+ ${analysisContent.slice(0, 10000)}`,
121
+
122
+ performance: `Analiza el rendimiento del siguiente código:
123
+ 1. Cuellos de botella potenciales
124
+ 2. Complejidad algorítmica
125
+ 3. Uso de memoria
126
+ 4. Operaciones bloqueantes
127
+ 5. Optimizaciones sugeridas
128
+
129
+ Código:
130
+ ${analysisContent.slice(0, 10000)}`,
131
+ };
132
+
133
+ const completion = await zai.chat.completions.create({
134
+ messages: [
135
+ {
136
+ role: "system",
137
+ content: "Eres un experto en análisis de código. Proporciona análisis detallados y accionables.",
138
+ },
139
+ {
140
+ role: "user",
141
+ content: prompts[analysisType] || prompts.code,
142
+ },
143
+ ],
144
+ temperature: 0.3,
145
+ max_tokens: 4000,
146
+ });
147
+
148
+ const result = completion.choices[0]?.message?.content || "No se pudo generar análisis";
149
+
150
+ // Extraer resumen (primeras 200 caracteres)
151
+ const summary = result.slice(0, 200) + "...";
152
+
153
+ // Actualizar análisis
154
+ const updatedAnalysis = await db.analysis.update({
155
+ where: { id: analysis.id },
156
+ data: {
157
+ result,
158
+ summary,
159
+ },
160
+ });
161
+
162
+ // Crear tarea de agente
163
+ await db.agentTask.create({
164
+ data: {
165
+ type: "analyze",
166
+ status: "completed",
167
+ input: `Analizar ${repoName || "código"} (${analysisType})`,
168
+ output: summary,
169
+ completedAt: new Date(),
170
+ },
171
+ });
172
+
173
+ return NextResponse.json({
174
+ success: true,
175
+ analysis: updatedAnalysis,
176
+ message: "Análisis completado",
177
+ });
178
+ } catch (aiError) {
179
+ console.error("AI Analysis Error:", aiError);
180
+
181
+ await db.analysis.update({
182
+ where: { id: analysis.id },
183
+ data: {
184
+ result: "Error en el análisis: " + (aiError instanceof Error ? aiError.message : "Error desconocido"),
185
+ },
186
+ });
187
+
188
+ return NextResponse.json(
189
+ { success: false, error: "Error en el análisis de IA" },
190
+ { status: 500 }
191
+ );
192
+ }
193
+ } catch (error) {
194
+ console.error("Error in analyze route:", error);
195
+ return NextResponse.json(
196
+ { success: false, error: "Error interno del servidor" },
197
+ { status: 500 }
198
+ );
199
+ }
200
+ }
201
+
202
+ // GET - Obtener análisis
203
+ export async function GET(request: NextRequest) {
204
+ try {
205
+ const { searchParams } = new URL(request.url);
206
+ const repoId = searchParams.get("repoId");
207
+ const projectId = searchParams.get("projectId");
208
+
209
+ const where: Record<string, string> = {};
210
+ if (repoId) where.repoId = repoId;
211
+ if (projectId) where.projectId = projectId;
212
+
213
+ const analyses = await db.analysis.findMany({
214
+ where: Object.keys(where).length > 0 ? where : undefined,
215
+ include: {
216
+ repo: true,
217
+ project: true,
218
+ },
219
+ orderBy: { createdAt: "desc" },
220
+ take: 20,
221
+ });
222
+
223
+ return NextResponse.json({
224
+ success: true,
225
+ analyses,
226
+ });
227
+ } catch (error) {
228
+ console.error("Error fetching analyses:", error);
229
+ return NextResponse.json(
230
+ { success: false, error: "Error al obtener análisis" },
231
+ { status: 500 }
232
+ );
233
+ }
234
+ }
src/app/api/automation/route.ts ADDED
@@ -0,0 +1,396 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import ZAI from "z-ai-web-dev-sdk";
4
+
5
+ // Tipos de automatización disponibles
6
+ const AUTOMATION_TYPES = {
7
+ content_generation: {
8
+ name: "Generación de Contenido",
9
+ description: "Genera contenido automáticamente basado en tendencias y programación",
10
+ triggers: ["schedule", "trend_detected", "manual"]
11
+ },
12
+ posting: {
13
+ name: "Publicación Automática",
14
+ description: "Publica contenido en horarios óptimos",
15
+ triggers: ["schedule", "content_ready", "manual"]
16
+ },
17
+ engagement: {
18
+ name: "Engagement Automático",
19
+ description: "Responde comentarios y mensajes automáticamente",
20
+ triggers: ["new_comment", "new_message", "schedule"]
21
+ },
22
+ monetization: {
23
+ name: "Optimización de Monetización",
24
+ description: "Ajusta precios y ofertas basado en analytics",
25
+ triggers: ["analytics_update", "subscriber_change", "schedule"]
26
+ },
27
+ storytelling: {
28
+ name: "Storytelling Continuo",
29
+ description: "Continúa historias automáticamente con cliffhangers",
30
+ triggers: ["episode_published", "engagement_threshold", "schedule"]
31
+ },
32
+ cross_posting: {
33
+ name: "Cross-Posting",
34
+ description: "Publica el mismo contenido en múltiples plataformas",
35
+ triggers: ["content_published", "schedule", "manual"]
36
+ },
37
+ trend_tracking: {
38
+ name: "Seguimiento de Tendencias",
39
+ description: "Detecta tendencias y sugiere contenido",
40
+ triggers: ["schedule", "manual"]
41
+ }
42
+ };
43
+
44
+ // GET - Listar automatizaciones
45
+ export async function GET(request: NextRequest) {
46
+ try {
47
+ const { searchParams } = new URL(request.url);
48
+ const type = searchParams.get("type");
49
+ const isActive = searchParams.get("isActive");
50
+
51
+ const where: Record<string, unknown> = {};
52
+ if (type) where.type = type;
53
+ if (isActive !== null) where.isActive = isActive === "true";
54
+
55
+ const automations = await db.automation.findMany({
56
+ where,
57
+ include: {
58
+ _count: {
59
+ select: { logs: true }
60
+ }
61
+ },
62
+ orderBy: { createdAt: "desc" }
63
+ });
64
+
65
+ // Últimos logs
66
+ const recentLogs = await db.automationLog.findMany({
67
+ orderBy: { createdAt: "desc" },
68
+ take: 20,
69
+ });
70
+
71
+ // Estadísticas
72
+ const stats = {
73
+ total: await db.automation.count(),
74
+ active: await db.automation.count({ where: { isActive: true } }),
75
+ runsToday: await db.automationLog.count({
76
+ where: {
77
+ createdAt: {
78
+ gte: new Date(new Date().setHours(0, 0, 0, 0))
79
+ }
80
+ }
81
+ }),
82
+ successRate: await calculateSuccessRate()
83
+ };
84
+
85
+ return NextResponse.json({
86
+ success: true,
87
+ automations,
88
+ automationTypes: AUTOMATION_TYPES,
89
+ recentLogs,
90
+ stats
91
+ });
92
+
93
+ } catch (error) {
94
+ console.error("Error fetching automations:", error);
95
+ return NextResponse.json(
96
+ { success: false, error: "Error al obtener automatizaciones" },
97
+ { status: 500 }
98
+ );
99
+ }
100
+ }
101
+
102
+ // POST - Crear automatización
103
+ export async function POST(request: NextRequest) {
104
+ try {
105
+ const body = await request.json();
106
+ const {
107
+ name,
108
+ description,
109
+ type,
110
+ trigger,
111
+ triggerConfig,
112
+ actions
113
+ } = body;
114
+
115
+ if (!name || !type || !trigger || !actions) {
116
+ return NextResponse.json(
117
+ { success: false, error: "Faltan campos requeridos: name, type, trigger, actions" },
118
+ { status: 400 }
119
+ );
120
+ }
121
+
122
+ // Validar tipo de automatización
123
+ if (!AUTOMATION_TYPES[type as keyof typeof AUTOMATION_TYPES]) {
124
+ return NextResponse.json(
125
+ { success: false, error: "Tipo de automatización no válido" },
126
+ { status: 400 }
127
+ );
128
+ }
129
+
130
+ // Calcular próxima ejecución si es programada
131
+ let nextRunAt = null;
132
+ if (trigger === "schedule" && triggerConfig?.schedule) {
133
+ nextRunAt = calculateNextRun(triggerConfig.schedule);
134
+ }
135
+
136
+ const automation = await db.automation.create({
137
+ data: {
138
+ name,
139
+ description: description || null,
140
+ type,
141
+ trigger,
142
+ triggerConfig: triggerConfig ? JSON.stringify(triggerConfig) : null,
143
+ actions: JSON.stringify(actions),
144
+ nextRunAt,
145
+ isActive: true,
146
+ }
147
+ });
148
+
149
+ return NextResponse.json({
150
+ success: true,
151
+ automation,
152
+ message: `Automatización "${name}" creada`
153
+ });
154
+
155
+ } catch (error) {
156
+ console.error("Error creating automation:", error);
157
+ return NextResponse.json(
158
+ { success: false, error: "Error al crear automatización" },
159
+ { status: 500 }
160
+ );
161
+ }
162
+ }
163
+
164
+ // PUT - Ejecutar o actualizar automatización
165
+ export async function PUT(request: NextRequest) {
166
+ try {
167
+ const body = await request.json();
168
+ const { id, action, isActive, triggerConfig, actions } = body;
169
+
170
+ if (!id) {
171
+ return NextResponse.json(
172
+ { success: false, error: "ID requerido" },
173
+ { status: 400 }
174
+ );
175
+ }
176
+
177
+ // Si es una acción de ejecución
178
+ if (action === "execute") {
179
+ return await executeAutomation(id);
180
+ }
181
+
182
+ // Actualización normal
183
+ const updateData: Record<string, unknown> = {};
184
+ if (isActive !== undefined) updateData.isActive = isActive;
185
+ if (triggerConfig) updateData.triggerConfig = JSON.stringify(triggerConfig);
186
+ if (actions) updateData.actions = JSON.stringify(actions);
187
+
188
+ const automation = await db.automation.update({
189
+ where: { id },
190
+ data: updateData
191
+ });
192
+
193
+ return NextResponse.json({
194
+ success: true,
195
+ automation
196
+ });
197
+
198
+ } catch (error) {
199
+ console.error("Error updating automation:", error);
200
+ return NextResponse.json(
201
+ { success: false, error: "Error al actualizar automatización" },
202
+ { status: 500 }
203
+ );
204
+ }
205
+ }
206
+
207
+ // DELETE - Eliminar automatización
208
+ export async function DELETE(request: NextRequest) {
209
+ try {
210
+ const { searchParams } = new URL(request.url);
211
+ const id = searchParams.get("id");
212
+
213
+ if (!id) {
214
+ return NextResponse.json(
215
+ { success: false, error: "ID requerido" },
216
+ { status: 400 }
217
+ );
218
+ }
219
+
220
+ // Eliminar logs primero
221
+ await db.automationLog.deleteMany({
222
+ where: { automationId: id }
223
+ });
224
+
225
+ await db.automation.delete({
226
+ where: { id }
227
+ });
228
+
229
+ return NextResponse.json({
230
+ success: true,
231
+ message: "Automatización eliminada"
232
+ });
233
+
234
+ } catch (error) {
235
+ console.error("Error deleting automation:", error);
236
+ return NextResponse.json(
237
+ { success: false, error: "Error al eliminar automatización" },
238
+ { status: 500 }
239
+ );
240
+ }
241
+ }
242
+
243
+ // Función para ejecutar una automatización
244
+ async function executeAutomation(automationId: string) {
245
+ const startTime = Date.now();
246
+
247
+ try {
248
+ const automation = await db.automation.findUnique({
249
+ where: { id: automationId }
250
+ });
251
+
252
+ if (!automation) {
253
+ return NextResponse.json(
254
+ { success: false, error: "Automatización no encontrada" },
255
+ { status: 404 }
256
+ );
257
+ }
258
+
259
+ const actions = JSON.parse(automation.actions);
260
+ const results: unknown[] = [];
261
+
262
+ // Ejecutar cada acción
263
+ for (const action of actions) {
264
+ const result = await executeAction(action);
265
+ results.push(result);
266
+ }
267
+
268
+ // Actualizar última ejecución
269
+ await db.automation.update({
270
+ where: { id: automationId },
271
+ data: {
272
+ lastRunAt: new Date(),
273
+ runCount: { increment: 1 },
274
+ nextRunAt: automation.trigger === "schedule"
275
+ ? calculateNextRun(JSON.parse(automation.triggerConfig || "{}").schedule)
276
+ : null
277
+ }
278
+ });
279
+
280
+ // Crear log de éxito
281
+ await db.automationLog.create({
282
+ data: {
283
+ automationId,
284
+ status: "success",
285
+ input: automation.actions,
286
+ output: JSON.stringify(results),
287
+ duration: Date.now() - startTime
288
+ }
289
+ });
290
+
291
+ return NextResponse.json({
292
+ success: true,
293
+ results,
294
+ duration: Date.now() - startTime
295
+ });
296
+
297
+ } catch (error) {
298
+ // Crear log de error
299
+ await db.automationLog.create({
300
+ data: {
301
+ automationId,
302
+ status: "failed",
303
+ error: String(error),
304
+ duration: Date.now() - startTime
305
+ }
306
+ });
307
+
308
+ return NextResponse.json(
309
+ { success: false, error: "Error al ejecutar automatización" },
310
+ { status: 500 }
311
+ );
312
+ }
313
+ }
314
+
315
+ // Ejecutar acción individual
316
+ async function executeAction(action: Record<string, unknown>): Promise<unknown> {
317
+ const zai = await ZAI.create();
318
+
319
+ switch (action.type) {
320
+ case "generate_content":
321
+ // Generar contenido con IA
322
+ const completion = await zai.chat.completions.create({
323
+ messages: [
324
+ { role: "system", content: "Eres un generador de contenido para redes sociales." },
325
+ { role: "user", content: String(action.prompt) }
326
+ ]
327
+ });
328
+ return { type: "content", result: completion.choices[0]?.message?.content };
329
+
330
+ case "create_post":
331
+ // Crear post programado
332
+ const post = await db.post.create({
333
+ data: {
334
+ title: String(action.title) || "Auto-generated",
335
+ caption: String(action.caption) || "",
336
+ type: String(action.postType) || "post",
337
+ status: "draft"
338
+ }
339
+ });
340
+ return { type: "post", postId: post.id };
341
+
342
+ case "check_trends":
343
+ // Simular detección de tendencias
344
+ return { type: "trends", trends: ["trending1", "trending2"] };
345
+
346
+ case "send_notification":
347
+ return { type: "notification", sent: true };
348
+
349
+ default:
350
+ return { type: action.type, status: "executed" };
351
+ }
352
+ }
353
+
354
+ // Calcular próxima ejecución
355
+ function calculateNextRun(schedule: string): Date {
356
+ const now = new Date();
357
+
358
+ // Parsear schedule simple (ej: "daily:09:00", "hourly", "weekly:monday:10:00")
359
+ if (schedule.startsWith("daily:")) {
360
+ const time = schedule.split(":")[1];
361
+ const [hours, minutes] = time.split("-").length > 1
362
+ ? time.split("-")[0].split(":").map(Number)
363
+ : time.split(":").map(Number);
364
+
365
+ const next = new Date(now);
366
+ next.setHours(hours || 9, minutes || 0, 0, 0);
367
+ if (next <= now) next.setDate(next.getDate() + 1);
368
+ return next;
369
+ }
370
+
371
+ if (schedule === "hourly") {
372
+ return new Date(now.getTime() + 60 * 60 * 1000);
373
+ }
374
+
375
+ if (schedule === "daily") {
376
+ const next = new Date(now);
377
+ next.setDate(next.getDate() + 1);
378
+ next.setHours(9, 0, 0, 0);
379
+ return next;
380
+ }
381
+
382
+ // Default: 1 hora
383
+ return new Date(now.getTime() + 60 * 60 * 1000);
384
+ }
385
+
386
+ // Calcular tasa de éxito
387
+ async function calculateSuccessRate(): Promise<number> {
388
+ const total = await db.automationLog.count();
389
+ if (total === 0) return 100;
390
+
391
+ const success = await db.automationLog.count({
392
+ where: { status: "success" }
393
+ });
394
+
395
+ return Math.round((success / total) * 100);
396
+ }
src/app/api/censor/route.ts ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import ZAI from "z-ai-web-dev-sdk";
4
+
5
+ // Reglas de censura por plataforma y categoría
6
+ const DEFAULT_CENSOR_RULES = [
7
+ // YouTube
8
+ { platform: "youtube", category: "nudity", rule: "No se permite desnudez ni contenido sexual", severity: "high", autoAction: "remove" },
9
+ { platform: "youtube", category: "violence", rule: "Violencia gráfica no permitida sin advertencia", severity: "medium", autoAction: "warn" },
10
+ { platform: "youtube", category: "hate_speech", rule: "No se permite discurso de odio", severity: "high", autoAction: "remove" },
11
+
12
+ // TikTok
13
+ { platform: "tiktok", category: "nudity", rule: "Cero tolerancia para desnudez", severity: "high", autoAction: "remove" },
14
+ { platform: "tiktok", category: "violence", rule: "Sin violencia gráfica", severity: "high", autoAction: "remove" },
15
+ { platform: "tiktok", category: "self_harm", rule: "No contenido de autolesión", severity: "high", autoAction: "remove" },
16
+ { platform: "tiktok", category: "dangerous", rule: "No desafíos peligrosos", severity: "high", autoAction: "remove" },
17
+
18
+ // Instagram
19
+ { platform: "instagram", category: "nudity", rule: "No desnudez, excepto arte con moderación", severity: "high", autoAction: "blur" },
20
+ { platform: "instagram", category: "violence", rule: "Sin violencia gráfica", severity: "high", autoAction: "remove" },
21
+
22
+ // Twitter/X
23
+ { platform: "twitter", category: "illegal", rule: "No contenido ilegal", severity: "high", autoAction: "remove" },
24
+ { platform: "twitter", category: "sensitive", rule: "Marcar como contenido sensible", severity: "low", autoAction: "warn" },
25
+ ];
26
+
27
+ // GET - Obtener reglas de censura
28
+ export async function GET(request: NextRequest) {
29
+ try {
30
+ const { searchParams } = new URL(request.url);
31
+ const platform = searchParams.get("platform");
32
+
33
+ let rules;
34
+ if (platform) {
35
+ rules = await db.censorRule.findMany({
36
+ where: { platform, isActive: true }
37
+ });
38
+
39
+ // Si no hay reglas en BD, usar las por defecto
40
+ if (rules.length === 0) {
41
+ rules = DEFAULT_CENSOR_RULES.filter(r => r.platform === platform);
42
+ }
43
+ } else {
44
+ rules = await db.censorRule.findMany({
45
+ where: { isActive: true }
46
+ });
47
+
48
+ if (rules.length === 0) {
49
+ rules = DEFAULT_CENSOR_RULES;
50
+ }
51
+ }
52
+
53
+ return NextResponse.json({
54
+ success: true,
55
+ rules
56
+ });
57
+
58
+ } catch (error) {
59
+ console.error("Error fetching censor rules:", error);
60
+ return NextResponse.json(
61
+ { success: false, error: "Error al obtener reglas de censura" },
62
+ { status: 500 }
63
+ );
64
+ }
65
+ }
66
+
67
+ // POST - Analizar contenido para censura
68
+ export async function POST(request: NextRequest) {
69
+ try {
70
+ const body = await request.json();
71
+ const { content, type, platform } = body;
72
+
73
+ if (!content) {
74
+ return NextResponse.json(
75
+ { success: false, error: "Contenido requerido para análisis" },
76
+ { status: 400 }
77
+ );
78
+ }
79
+
80
+ const zai = await ZAI.create();
81
+
82
+ // Usar IA para analizar el contenido
83
+ const analysisPrompt = `Analiza el siguiente ${type || "contenido"} para verificar si cumple con las normas de la plataforma ${platform || "general"}.
84
+
85
+ Contenido a analizar:
86
+ ${content.substring(0, 2000)}
87
+
88
+ Responde SOLO en formato JSON:
89
+ {
90
+ "safe": true/false,
91
+ "flags": [
92
+ {"category": "categoría", "reason": "razón", "severity": "low/medium/high"}
93
+ ],
94
+ "recommendations": ["recomendación1", "recomendación2"],
95
+ "platformCompatible": ["plataforma1", "plataforma2"]
96
+ }`;
97
+
98
+ const completion = await zai.chat.completions.create({
99
+ messages: [
100
+ {
101
+ role: "system",
102
+ content: `Eres un moderador de contenido experto. Tu trabajo es analizar contenido para verificar si cumple con las políticas de diferentes plataformas. Sé estricto pero justo. Considera:
103
+
104
+ 1. YouTube: Familiar, sin desnudez, violencia moderada con advertencia
105
+ 2. TikTok: Para todos los públicos, sin contenido peligroso
106
+ 3. Instagram: Sin desnudez (excepto arte), sin violencia gráfica
107
+ 4. Twitter: Más permisivo pero marca contenido sensible
108
+ 5. General: Apropiado para todo público`
109
+ },
110
+ { role: "user", content: analysisPrompt }
111
+ ],
112
+ temperature: 0.3,
113
+ });
114
+
115
+ const response = completion.choices[0]?.message?.content || "";
116
+
117
+ // Parsear respuesta
118
+ let analysis;
119
+ try {
120
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
121
+ if (jsonMatch) {
122
+ analysis = JSON.parse(jsonMatch[0]);
123
+ } else {
124
+ analysis = {
125
+ safe: true,
126
+ flags: [],
127
+ recommendations: [],
128
+ platformCompatible: [platform || "general"]
129
+ };
130
+ }
131
+ } catch {
132
+ analysis = {
133
+ safe: true,
134
+ flags: [],
135
+ recommendations: [],
136
+ platformCompatible: [platform || "general"]
137
+ };
138
+ }
139
+
140
+ return NextResponse.json({
141
+ success: true,
142
+ analysis,
143
+ originalContent: content.substring(0, 200)
144
+ });
145
+
146
+ } catch (error) {
147
+ console.error("Error analyzing content:", error);
148
+ return NextResponse.json(
149
+ { success: false, error: "Error al analizar contenido" },
150
+ { status: 500 }
151
+ );
152
+ }
153
+ }
154
+
155
+ // PUT - Añadir regla de censura personalizada
156
+ export async function PUT(request: NextRequest) {
157
+ try {
158
+ const body = await request.json();
159
+ const { platform, category, rule, severity, autoAction } = body;
160
+
161
+ if (!platform || !category || !rule) {
162
+ return NextResponse.json(
163
+ { success: false, error: "Plataforma, categoría y regla son requeridos" },
164
+ { status: 400 }
165
+ );
166
+ }
167
+
168
+ const censorRule = await db.censorRule.create({
169
+ data: {
170
+ platform,
171
+ category,
172
+ rule,
173
+ severity: severity || "medium",
174
+ autoAction: autoAction || "warn"
175
+ }
176
+ });
177
+
178
+ return NextResponse.json({
179
+ success: true,
180
+ rule: censorRule,
181
+ message: "Regla de censura creada"
182
+ });
183
+
184
+ } catch (error) {
185
+ console.error("Error creating censor rule:", error);
186
+ return NextResponse.json(
187
+ { success: false, error: "Error al crear regla de censura" },
188
+ { status: 500 }
189
+ );
190
+ }
191
+ }
src/app/api/characters/route.ts ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import ZAI from "z-ai-web-dev-sdk";
4
+ import fs from "fs/promises";
5
+ import path from "path";
6
+
7
+ const CHARACTERS_DIR = path.join(process.cwd(), "download", "characters");
8
+
9
+ async function ensureDir() {
10
+ try {
11
+ await fs.mkdir(CHARACTERS_DIR, { recursive: true });
12
+ } catch {
13
+ // ya existe
14
+ }
15
+ }
16
+
17
+ // GET - Listar personajes
18
+ export async function GET(request: NextRequest) {
19
+ try {
20
+ const characters = await db.character.findMany({
21
+ include: {
22
+ contents: {
23
+ take: 5,
24
+ orderBy: { createdAt: "desc" }
25
+ }
26
+ },
27
+ orderBy: { createdAt: "desc" }
28
+ });
29
+
30
+ return NextResponse.json({
31
+ success: true,
32
+ characters,
33
+ total: characters.length
34
+ });
35
+
36
+ } catch (error) {
37
+ console.error("Error fetching characters:", error);
38
+ return NextResponse.json(
39
+ { success: false, error: "Error al obtener personajes" },
40
+ { status: 500 }
41
+ );
42
+ }
43
+ }
44
+
45
+ // POST - Crear personaje
46
+ export async function POST(request: NextRequest) {
47
+ try {
48
+ const body = await request.json();
49
+ const {
50
+ name,
51
+ description,
52
+ generateReference = false,
53
+ traits
54
+ } = body;
55
+
56
+ if (!name) {
57
+ return NextResponse.json(
58
+ { success: false, error: "El nombre del personaje es requerido" },
59
+ { status: 400 }
60
+ );
61
+ }
62
+
63
+ await ensureDir();
64
+
65
+ let referenceImage = null;
66
+ let characterTraits = traits;
67
+
68
+ // Generar imagen de referencia si se solicita
69
+ if (generateReference) {
70
+ try {
71
+ const zai = await ZAI.create();
72
+
73
+ // Crear prompt detallado para el personaje
74
+ const characterPrompt = `Character reference sheet: ${name}. ${description || "Original character"}. Full body view, multiple angles (front, side, back), consistent character design, clear details, neutral pose, white background, character turnaround, reference sheet style. Professional character design, consistent facial features, consistent body proportions.`;
75
+
76
+ const response = await zai.images.generations.create({
77
+ prompt: characterPrompt,
78
+ size: "1344x768" // Horizontal para el reference sheet
79
+ });
80
+
81
+ const imageBase64 = response.data[0]?.base64;
82
+ if (imageBase64) {
83
+ const filename = `character_${name.toLowerCase().replace(/\s+/g, "_")}_${Date.now()}.png`;
84
+ const filepath = path.join(CHARACTERS_DIR, filename);
85
+ const buffer = Buffer.from(imageBase64, "base64");
86
+ await fs.writeFile(filepath, buffer);
87
+ referenceImage = `/download/characters/${filename}`;
88
+ }
89
+
90
+ // Si no se proporcionaron traits, extraerlos con IA
91
+ if (!traits) {
92
+ const traitsResponse = await zai.chat.completions.create({
93
+ messages: [
94
+ {
95
+ role: "system",
96
+ content: "Eres un experto en diseño de personajes. Extrae los rasgos físicos clave del personaje para mantener consistencia en futuras generaciones. Responde SOLO en formato JSON con: { \"face\": \"\", \"body\": \"\", \"hair\": \"\", \"clothing\": \"\", \"colors\": \"\", \"distinctive\": \"\" }"
97
+ },
98
+ {
99
+ role: "user",
100
+ content: `Personaje: ${name}. Descripción: ${description || "Original character"}`
101
+ }
102
+ ]
103
+ });
104
+
105
+ const traitsText = traitsResponse.choices[0]?.message?.content || "";
106
+ try {
107
+ const jsonMatch = traitsText.match(/\{[\s\S]*\}/);
108
+ if (jsonMatch) {
109
+ characterTraits = jsonMatch[0];
110
+ }
111
+ } catch {
112
+ characterTraits = JSON.stringify({ description: description || "Original character" });
113
+ }
114
+ }
115
+
116
+ } catch (genError) {
117
+ console.error("Error generando referencia:", genError);
118
+ // Continuar sin imagen de referencia
119
+ }
120
+ }
121
+
122
+ // Crear personaje en BD
123
+ const character = await db.character.create({
124
+ data: {
125
+ name,
126
+ description: description || null,
127
+ referenceImage,
128
+ traits: characterTraits || JSON.stringify({ name, description })
129
+ }
130
+ });
131
+
132
+ return NextResponse.json({
133
+ success: true,
134
+ character,
135
+ message: `Personaje "${name}" creado exitosamente`
136
+ });
137
+
138
+ } catch (error) {
139
+ console.error("Error creating character:", error);
140
+ return NextResponse.json(
141
+ { success: false, error: "Error al crear personaje" },
142
+ { status: 500 }
143
+ );
144
+ }
145
+ }
146
+
147
+ // PUT - Actualizar personaje
148
+ export async function PUT(request: NextRequest) {
149
+ try {
150
+ const body = await request.json();
151
+ const { id, name, description, traits } = body;
152
+
153
+ if (!id) {
154
+ return NextResponse.json(
155
+ { success: false, error: "ID del personaje requerido" },
156
+ { status: 400 }
157
+ );
158
+ }
159
+
160
+ const character = await db.character.update({
161
+ where: { id },
162
+ data: {
163
+ name: name || undefined,
164
+ description: description || undefined,
165
+ traits: traits || undefined,
166
+ }
167
+ });
168
+
169
+ return NextResponse.json({
170
+ success: true,
171
+ character
172
+ });
173
+
174
+ } catch (error) {
175
+ console.error("Error updating character:", error);
176
+ return NextResponse.json(
177
+ { success: false, error: "Error al actualizar personaje" },
178
+ { status: 500 }
179
+ );
180
+ }
181
+ }
182
+
183
+ // DELETE - Eliminar personaje
184
+ export async function DELETE(request: NextRequest) {
185
+ try {
186
+ const { searchParams } = new URL(request.url);
187
+ const id = searchParams.get("id");
188
+
189
+ if (!id) {
190
+ return NextResponse.json(
191
+ { success: false, error: "ID requerido" },
192
+ { status: 400 }
193
+ );
194
+ }
195
+
196
+ await db.character.delete({
197
+ where: { id }
198
+ });
199
+
200
+ return NextResponse.json({
201
+ success: true,
202
+ message: "Personaje eliminado"
203
+ });
204
+
205
+ } catch (error) {
206
+ console.error("Error deleting character:", error);
207
+ return NextResponse.json(
208
+ { success: false, error: "Error al eliminar personaje" },
209
+ { status: 500 }
210
+ );
211
+ }
212
+ }
src/app/api/content/route.ts ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+
4
+ // GET - Listar todo el contenido
5
+ export async function GET(request: NextRequest) {
6
+ try {
7
+ const { searchParams } = new URL(request.url);
8
+ const type = searchParams.get("type");
9
+ const platform = searchParams.get("platform");
10
+ const status = searchParams.get("status");
11
+ const limit = parseInt(searchParams.get("limit") || "50");
12
+
13
+ const where: Record<string, unknown> = {};
14
+ if (type) where.type = type;
15
+ if (platform) where.platform = platform;
16
+ if (status) where.status = status;
17
+
18
+ const contents = await db.content.findMany({
19
+ where,
20
+ include: {
21
+ character: true,
22
+ censorFlags: true,
23
+ },
24
+ orderBy: { createdAt: "desc" },
25
+ take: limit,
26
+ });
27
+
28
+ // Estadísticas
29
+ const stats = {
30
+ total: await db.content.count(),
31
+ images: await db.content.count({ where: { type: "image" } }),
32
+ videos: await db.content.count({ where: { type: "video" } }),
33
+ pending: await db.content.count({ where: { status: "pending" } }),
34
+ processing: await db.content.count({ where: { status: "processing" } }),
35
+ completed: await db.content.count({ where: { status: "completed" } }),
36
+ failed: await db.content.count({ where: { status: "failed" } }),
37
+ };
38
+
39
+ return NextResponse.json({
40
+ success: true,
41
+ contents,
42
+ stats
43
+ });
44
+
45
+ } catch (error) {
46
+ console.error("Error fetching content:", error);
47
+ return NextResponse.json(
48
+ { success: false, error: "Error al obtener contenido" },
49
+ { status: 500 }
50
+ );
51
+ }
52
+ }
53
+
54
+ // DELETE - Eliminar contenido
55
+ export async function DELETE(request: NextRequest) {
56
+ try {
57
+ const { searchParams } = new URL(request.url);
58
+ const id = searchParams.get("id");
59
+
60
+ if (!id) {
61
+ return NextResponse.json(
62
+ { success: false, error: "ID requerido" },
63
+ { status: 400 }
64
+ );
65
+ }
66
+
67
+ // Eliminar flags de censura asociados
68
+ await db.censorFlag.deleteMany({
69
+ where: { contentId: id }
70
+ });
71
+
72
+ // Eliminar contenido
73
+ await db.content.delete({
74
+ where: { id }
75
+ });
76
+
77
+ return NextResponse.json({
78
+ success: true,
79
+ message: "Contenido eliminado"
80
+ });
81
+
82
+ } catch (error) {
83
+ console.error("Error deleting content:", error);
84
+ return NextResponse.json(
85
+ { success: false, error: "Error al eliminar contenido" },
86
+ { status: 500 }
87
+ );
88
+ }
89
+ }
src/app/api/generate/image/route.ts ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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", "images");
8
+
9
+ // Asegurar directorio existe
10
+ async function ensureDir() {
11
+ try {
12
+ await fs.mkdir(DOWNLOAD_DIR, { recursive: true });
13
+ } catch {
14
+ // ya existe
15
+ }
16
+ }
17
+
18
+ // Reglas de censura por plataforma
19
+ const CENSOR_RULES: Record<string, string[]> = {
20
+ youtube: [
21
+ "no nudity",
22
+ "no sexual content",
23
+ "no graphic violence",
24
+ "family friendly"
25
+ ],
26
+ tiktok: [
27
+ "no nudity",
28
+ "no sexual suggestiveness",
29
+ "no graphic violence",
30
+ "no self-harm content",
31
+ "age appropriate"
32
+ ],
33
+ instagram: [
34
+ "no nudity",
35
+ "no graphic violence",
36
+ "artistic content acceptable with moderation",
37
+ "no self-harm imagery"
38
+ ],
39
+ twitter: [
40
+ "content warning if sensitive",
41
+ "no illegal content"
42
+ ],
43
+ general: [
44
+ "safe for work",
45
+ "no explicit content"
46
+ ]
47
+ };
48
+
49
+ // Aplicar filtros de censura al prompt
50
+ function applyCensorFilter(prompt: string, platform: string): string {
51
+ const rules = CENSOR_RULES[platform] || CENSOR_RULES.general;
52
+ const censorAdditions = rules.map(rule => `(${rule})`).join(", ");
53
+ return `${prompt}, ${censorAdditions}`;
54
+ }
55
+
56
+ export async function POST(request: NextRequest) {
57
+ try {
58
+ const body = await request.json();
59
+ const {
60
+ prompt,
61
+ optimizedPrompt,
62
+ platform = "general",
63
+ character,
64
+ style = "realistic",
65
+ size = "1024x1024",
66
+ saveToDb = true,
67
+ projectId
68
+ } = body;
69
+
70
+ if (!prompt && !optimizedPrompt) {
71
+ return NextResponse.json(
72
+ { success: false, error: "Se requiere un prompt" },
73
+ { status: 400 }
74
+ );
75
+ }
76
+
77
+ await ensureDir();
78
+
79
+ // Usar el prompt optimizado si está disponible
80
+ let finalPrompt = optimizedPrompt || prompt;
81
+
82
+ // Aplicar filtros de censura según plataforma
83
+ finalPrompt = applyCensorFilter(finalPrompt, platform);
84
+
85
+ // Añadir estilo si se especifica
86
+ if (style && style !== "realistic") {
87
+ const styleAdditions: Record<string, string> = {
88
+ anime: "anime style, manga aesthetic, vibrant colors, cel shading",
89
+ realistic: "photorealistic, highly detailed, 8K, professional photography",
90
+ artistic: "digital art, artistic style, creative interpretation, masterpiece",
91
+ "oil-painting": "oil painting style, classical art, brush strokes, museum quality",
92
+ sketch: "pencil sketch, hand drawn, artistic lines, detailed shading",
93
+ "3d": "3D render, octane render, cinema 4D, volumetric lighting"
94
+ };
95
+ if (styleAdditions[style]) {
96
+ finalPrompt = `${finalPrompt}, ${styleAdditions[style]}`;
97
+ }
98
+ }
99
+
100
+ // Añadir referencia de personaje si existe
101
+ if (character) {
102
+ finalPrompt = `${finalPrompt}, character: ${character}`;
103
+ }
104
+
105
+ console.log("🖼️ Generando imagen con prompt:", finalPrompt.substring(0, 100) + "...");
106
+
107
+ // Crear registro en BD si se solicita
108
+ let contentRecord = null;
109
+ if (saveToDb) {
110
+ contentRecord = await db.content.create({
111
+ data: {
112
+ type: "image",
113
+ title: prompt.substring(0, 50),
114
+ description: prompt,
115
+ prompt: prompt,
116
+ optimizedPrompt: finalPrompt,
117
+ platform: platform,
118
+ status: "processing",
119
+ projectId: projectId || null,
120
+ }
121
+ });
122
+ }
123
+
124
+ try {
125
+ // Generar imagen con z-ai-web-dev-sdk
126
+ const zai = await ZAI.create();
127
+ const response = await zai.images.generations.create({
128
+ prompt: finalPrompt,
129
+ size: size as "1024x1024" | "768x1344" | "864x1152" | "1344x768" | "1152x864" | "1440x720" | "720x1440"
130
+ });
131
+
132
+ const imageBase64 = response.data[0]?.base64;
133
+
134
+ if (!imageBase64) {
135
+ throw new Error("No se recibió imagen del generador");
136
+ }
137
+
138
+ // Guardar imagen en disco
139
+ const filename = `image_${Date.now()}.png`;
140
+ const filepath = path.join(DOWNLOAD_DIR, filename);
141
+ const buffer = Buffer.from(imageBase64, "base64");
142
+ await fs.writeFile(filepath, buffer);
143
+
144
+ // Actualizar registro en BD
145
+ if (contentRecord) {
146
+ await db.content.update({
147
+ where: { id: contentRecord.id },
148
+ data: {
149
+ status: "completed",
150
+ filePath: `/download/images/${filename}`,
151
+ thumbnail: `/download/images/${filename}`,
152
+ }
153
+ });
154
+ }
155
+
156
+ // Crear tarea de agente
157
+ await db.agentTask.create({
158
+ data: {
159
+ type: "generate_image",
160
+ status: "completed",
161
+ input: prompt,
162
+ output: `Imagen generada: ${filename}`,
163
+ completedAt: new Date(),
164
+ }
165
+ });
166
+
167
+ return NextResponse.json({
168
+ success: true,
169
+ image: {
170
+ id: contentRecord?.id,
171
+ filename,
172
+ url: `/download/images/${filename}`,
173
+ base64: imageBase64.substring(0, 50) + "...", // Preview del base64
174
+ prompt: finalPrompt,
175
+ platform,
176
+ size
177
+ }
178
+ });
179
+
180
+ } catch (genError) {
181
+ console.error("Error generando imagen:", genError);
182
+
183
+ if (contentRecord) {
184
+ await db.content.update({
185
+ where: { id: contentRecord.id },
186
+ data: {
187
+ status: "failed",
188
+ metadata: JSON.stringify({ error: String(genError) })
189
+ }
190
+ });
191
+ }
192
+
193
+ return NextResponse.json(
194
+ { success: false, error: "Error al generar imagen: " + String(genError) },
195
+ { status: 500 }
196
+ );
197
+ }
198
+
199
+ } catch (error) {
200
+ console.error("Error en generate image:", error);
201
+ return NextResponse.json(
202
+ { success: false, error: "Error interno del servidor" },
203
+ { status: 500 }
204
+ );
205
+ }
206
+ }
207
+
208
+ // Listar imágenes generadas
209
+ export async function GET(request: NextRequest) {
210
+ try {
211
+ const { searchParams } = new URL(request.url);
212
+ const platform = searchParams.get("platform");
213
+ const limit = parseInt(searchParams.get("limit") || "20");
214
+
215
+ const where: Record<string, unknown> = { type: "image" };
216
+ if (platform) where.platform = platform;
217
+
218
+ const images = await db.content.findMany({
219
+ where,
220
+ orderBy: { createdAt: "desc" },
221
+ take: limit,
222
+ });
223
+
224
+ return NextResponse.json({
225
+ success: true,
226
+ images,
227
+ total: images.length
228
+ });
229
+
230
+ } catch (error) {
231
+ console.error("Error listing images:", error);
232
+ return NextResponse.json(
233
+ { success: false, error: "Error al listar imágenes" },
234
+ { status: 500 }
235
+ );
236
+ }
237
+ }
src/app/api/generate/video/route.ts ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ // ya existe
14
+ }
15
+ }
16
+
17
+ // Reglas de censura para video
18
+ const VIDEO_CENSOR_RULES: Record<string, string> = {
19
+ youtube: "family friendly content, no violence, no nudity, safe for all ages",
20
+ tiktok: "age appropriate, no dangerous challenges, no suggestive content",
21
+ instagram: "community guidelines compliant, no graphic content, artistic nudity only with warning",
22
+ twitter: "may include sensitive content warning, no illegal content",
23
+ general: "safe for work, appropriate content"
24
+ };
25
+
26
+ export async function POST(request: NextRequest) {
27
+ try {
28
+ const body = await request.json();
29
+ const {
30
+ prompt,
31
+ optimizedPrompt,
32
+ platform = "general",
33
+ style = "cinematic",
34
+ duration = "short",
35
+ aspectRatio = "16:9",
36
+ saveToDb = true,
37
+ projectId
38
+ } = body;
39
+
40
+ if (!prompt && !optimizedPrompt) {
41
+ return NextResponse.json(
42
+ { success: false, error: "Se requiere un prompt para el video" },
43
+ { status: 400 }
44
+ );
45
+ }
46
+
47
+ await ensureDir();
48
+
49
+ // Construir prompt final
50
+ let finalPrompt = optimizedPrompt || prompt;
51
+
52
+ // Añadir reglas de censura
53
+ const censorRule = VIDEO_CENSOR_RULES[platform] || VIDEO_CENSOR_RULES.general;
54
+ finalPrompt = `${finalPrompt}. Content requirements: ${censorRule}`;
55
+
56
+ // Añadir estilo de video
57
+ const videoStyles: Record<string, string> = {
58
+ cinematic: "cinematic quality, professional camera work, dramatic lighting",
59
+ documentary: "documentary style, natural lighting, authentic feel",
60
+ animation: "animated style, smooth motion, vibrant colors",
61
+ commercial: "commercial quality, polished, product showcase",
62
+ social: "social media optimized, engaging, quick cuts"
63
+ };
64
+
65
+ if (videoStyles[style]) {
66
+ finalPrompt = `${finalPrompt}, ${videoStyles[style]}`;
67
+ }
68
+
69
+ // Añadir duración
70
+ const durationAdditions: Record<string, string> = {
71
+ short: "short clip, 5-10 seconds",
72
+ medium: "medium length, 15-30 seconds",
73
+ long: "extended content, 1-2 minutes"
74
+ };
75
+ finalPrompt = `${finalPrompt}, ${durationAdditions[duration] || durationAdditions.short}`;
76
+
77
+ console.log("🎥 Generando video con prompt:", finalPrompt.substring(0, 100) + "...");
78
+
79
+ // Crear registro en BD
80
+ let contentRecord = null;
81
+ if (saveToDb) {
82
+ contentRecord = await db.content.create({
83
+ data: {
84
+ type: "video",
85
+ title: prompt.substring(0, 50),
86
+ description: prompt,
87
+ prompt: prompt,
88
+ optimizedPrompt: finalPrompt,
89
+ platform: platform,
90
+ status: "processing",
91
+ projectId: projectId || null,
92
+ metadata: JSON.stringify({ style, duration, aspectRatio })
93
+ }
94
+ });
95
+ }
96
+
97
+ try {
98
+ const zai = await ZAI.create();
99
+
100
+ // Generar video con z-ai-web-dev-sdk
101
+ const response = await zai.videos.generations.create({
102
+ prompt: finalPrompt,
103
+ });
104
+
105
+ // El SDK de video devuelve información del video generado
106
+ const videoData = response.data?.[0];
107
+
108
+ if (!videoData) {
109
+ throw new Error("No se recibió video del generador");
110
+ }
111
+
112
+ // Guardar video si viene en base64 o URL
113
+ let filename = `video_${Date.now()}.mp4`;
114
+ let filepath = path.join(DOWNLOAD_DIR, filename);
115
+ let videoUrl = "";
116
+
117
+ if (videoData.base64) {
118
+ const buffer = Buffer.from(videoData.base64, "base64");
119
+ await fs.writeFile(filepath, buffer);
120
+ videoUrl = `/download/videos/${filename}`;
121
+ } else if (videoData.url) {
122
+ videoUrl = videoData.url;
123
+ filename = videoData.url.split("/").pop() || filename;
124
+ }
125
+
126
+ // Actualizar registro
127
+ if (contentRecord) {
128
+ await db.content.update({
129
+ where: { id: contentRecord.id },
130
+ data: {
131
+ status: "completed",
132
+ filePath: videoUrl,
133
+ thumbnail: videoData.thumbnail || null,
134
+ }
135
+ });
136
+ }
137
+
138
+ // Crear tarea de agente
139
+ await db.agentTask.create({
140
+ data: {
141
+ type: "generate_video",
142
+ status: "completed",
143
+ input: prompt,
144
+ output: `Video generado: ${filename}`,
145
+ completedAt: new Date(),
146
+ }
147
+ });
148
+
149
+ return NextResponse.json({
150
+ success: true,
151
+ video: {
152
+ id: contentRecord?.id,
153
+ filename,
154
+ url: videoUrl,
155
+ thumbnail: videoData.thumbnail,
156
+ prompt: finalPrompt,
157
+ platform,
158
+ style,
159
+ duration,
160
+ aspectRatio
161
+ }
162
+ });
163
+
164
+ } catch (genError) {
165
+ console.error("Error generando video:", genError);
166
+
167
+ if (contentRecord) {
168
+ await db.content.update({
169
+ where: { id: contentRecord.id },
170
+ data: {
171
+ status: "failed",
172
+ metadata: JSON.stringify({ error: String(genError) })
173
+ }
174
+ });
175
+ }
176
+
177
+ return NextResponse.json(
178
+ { success: false, error: "Error al generar video: " + String(genError) },
179
+ { status: 500 }
180
+ );
181
+ }
182
+
183
+ } catch (error) {
184
+ console.error("Error en generate video:", error);
185
+ return NextResponse.json(
186
+ { success: false, error: "Error interno del servidor" },
187
+ { status: 500 }
188
+ );
189
+ }
190
+ }
191
+
192
+ // Listar videos
193
+ export async function GET(request: NextRequest) {
194
+ try {
195
+ const { searchParams } = new URL(request.url);
196
+ const platform = searchParams.get("platform");
197
+ const limit = parseInt(searchParams.get("limit") || "20");
198
+
199
+ const where: Record<string, unknown> = { type: "video" };
200
+ if (platform) where.platform = platform;
201
+
202
+ const videos = await db.content.findMany({
203
+ where,
204
+ orderBy: { createdAt: "desc" },
205
+ take: limit,
206
+ });
207
+
208
+ return NextResponse.json({
209
+ success: true,
210
+ videos,
211
+ total: videos.length
212
+ });
213
+
214
+ } catch (error) {
215
+ console.error("Error listing videos:", error);
216
+ return NextResponse.json(
217
+ { success: false, error: "Error al listar videos" },
218
+ { status: 500 }
219
+ );
220
+ }
221
+ }
src/app/api/influencers/route.ts ADDED
@@ -0,0 +1,450 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import ZAI from "z-ai-web-dev-sdk";
3
+ import { prisma } from "@/lib/db";
4
+
5
+ // Base de datos de influencers IA famosos (para análisis y referencia)
6
+ const FAMOUS_AI_INFLUENCERS = [
7
+ {
8
+ name: "Lil Miquela",
9
+ handle: "@lilmiquela",
10
+ platform: "Instagram",
11
+ followers: 2500000,
12
+ engagement: 3.2,
13
+ niche: "Fashion & Lifestyle",
14
+ style: {
15
+ aesthetic: "Hyperrealistic CGI",
16
+ tone: "Gen Z influencer",
17
+ themes: ["fashion", "music", "social issues"]
18
+ },
19
+ contentTypes: ["photos", "videos", "stories"],
20
+ postingSchedule: { frequency: "3-4 per week", bestTimes: ["12:00", "18:00", "21:00"] },
21
+ visualStyle: { colors: ["warm", "urban"], lighting: "golden hour", editing: "cinematic" },
22
+ monetizationType: ["brand deals", "merchandise"],
23
+ signatureElements: ["freckles", "bangs", "streetwear"],
24
+ petCompanion: false,
25
+ lessons: [
26
+ "Consistencia visual es clave",
27
+ "Narrativa de vida ficticia pero creíble",
28
+ "Colaboraciones con marcas reales",
29
+ "Activismo social para conectar con audiencia"
30
+ ]
31
+ },
32
+ {
33
+ name: "Rozy",
34
+ handle: "@rozy.gram",
35
+ platform: "Instagram",
36
+ followers: 180000,
37
+ engagement: 4.5,
38
+ niche: "Fashion & K-pop style",
39
+ style: {
40
+ aesthetic: "Korean beauty standard",
41
+ tone: "Friendly and trendy",
42
+ themes: ["fashion", "beauty", "travel"]
43
+ },
44
+ contentTypes: ["photos", "reels", "stories"],
45
+ postingSchedule: { frequency: "5-7 per week", bestTimes: ["11:00", "19:00"] },
46
+ visualStyle: { colors: ["pastel", "bright"], lighting: "soft", editing: "k-beauty" },
47
+ monetizationType: ["brand ambassador", "product placement"],
48
+ signatureElements: ["k-pop fashion", "dance videos", "korean makeup"],
49
+ petCompanion: true,
50
+ petType: "small dog",
51
+ lessons: [
52
+ "Aprovechar tendencias K-pop",
53
+ "Contenido de baile viral",
54
+ "Estética cuidada y consistente",
55
+ "Mascota como elemento diferenciador"
56
+ ]
57
+ },
58
+ {
59
+ name: "Imma",
60
+ handle: "@imma.gram",
61
+ platform: "Instagram",
62
+ followers: 390000,
63
+ engagement: 3.8,
64
+ niche: "Fashion & Art",
65
+ style: {
66
+ aesthetic: "Japanese street style",
67
+ tone: "Artistic and edgy",
68
+ themes: ["fashion", "art", "technology"]
69
+ },
70
+ contentTypes: ["photos", "reels", "collaborations"],
71
+ postingSchedule: { frequency: "4-5 per week", bestTimes: ["10:00", "20:00"] },
72
+ visualStyle: { colors: ["neon accents", "monochrome"], lighting: "dramatic", editing: "artistic" },
73
+ monetizationType: ["brand collaborations", "NFTs", "art exhibitions"],
74
+ signatureElements: ["pink bob hair", "futuristic fashion", "art installations"],
75
+ petCompanion: false,
76
+ lessons: [
77
+ "Fusionar arte y moda",
78
+ "Colaboraciones con artistas",
79
+ "Presencia en eventos físicos",
80
+ "Estética japonesa distintiva"
81
+ ]
82
+ },
83
+ {
84
+ name: "Noonoouri",
85
+ handle: "@noonoouri",
86
+ platform: "Instagram",
87
+ followers: 450000,
88
+ engagement: 2.9,
89
+ niche: "High Fashion",
90
+ style: {
91
+ aesthetic: "Cartoon-like avatar",
92
+ tone: "Playful luxury",
93
+ themes: ["fashion", "luxury brands", "lifestyle"]
94
+ },
95
+ contentTypes: ["photos", "animations", "brand content"],
96
+ postingSchedule: { frequency: "3-4 per week", bestTimes: ["15:00", "21:00"] },
97
+ visualStyle: { colors: ["pastel", "luxury gold"], lighting: "studio", editing: "animated" },
98
+ monetizationType: ["luxury brand deals", "fashion campaigns"],
99
+ signatureElements: ["big eyes", "designer outfits", "fashion week content"],
100
+ petCompanion: false,
101
+ lessons: [
102
+ "Estilo único de caricatura",
103
+ "Asociaciones con marcas de lujo",
104
+ "Presencia en fashion weeks",
105
+ "Contenido aspiracional"
106
+ ]
107
+ },
108
+ {
109
+ name: "Bermuda",
110
+ handle: "@bermudaisbae",
111
+ platform: "Instagram",
112
+ followers: 220000,
113
+ engagement: 4.1,
114
+ niche: "Music & Pop Culture",
115
+ style: {
116
+ aesthetic: "Edgy and provocative",
117
+ tone: "Sassy and confident",
118
+ themes: ["music", "pop culture", "fashion"]
119
+ },
120
+ contentTypes: ["photos", "music videos", "controversial posts"],
121
+ postingSchedule: { frequency: "2-3 per week", bestTimes: ["18:00", "22:00"] },
122
+ visualStyle: { colors: ["dark", "neon"], lighting: "moody", editing: "music video style" },
123
+ monetizationType: ["music releases", "brand deals"],
124
+ signatureElements: ["brunette", "edgy style", "music collaborations"],
125
+ petCompanion: false,
126
+ lessons: [
127
+ "Personalidad controversial genera engagement",
128
+ "Crossovers con otros influencers IA",
129
+ "Contenido musical original",
130
+ "Narrativa de 'villana' atractiva"
131
+ ]
132
+ },
133
+ {
134
+ name: "Shudu",
135
+ handle: "@shudu.gram",
136
+ platform: "Instagram",
137
+ followers: 230000,
138
+ engagement: 3.5,
139
+ niche: "High Fashion & Beauty",
140
+ style: {
141
+ aesthetic: "Hyperrealistic dark-skinned model",
142
+ tone: "Elegant and mysterious",
143
+ themes: ["fashion", "beauty", "diversity"]
144
+ },
145
+ contentTypes: ["photos", "editorials", "brand campaigns"],
146
+ postingSchedule: { frequency: "3-4 per week", bestTimes: ["14:00", "20:00"] },
147
+ visualStyle: { colors: ["rich tones", "gold accents"], lighting: "dramatic", editing: "editorial" },
148
+ monetizationType: ["fashion campaigns", "brand ambassador"],
149
+ signatureElements: ["dark skin", "high fashion poses", "editorial style"],
150
+ petCompanion: false,
151
+ lessons: [
152
+ "Representación y diversidad",
153
+ "Estética editorial de alta moda",
154
+ "Colaboraciones con diseñadores",
155
+ "Misterio y elegancia"
156
+ ]
157
+ },
158
+ {
159
+ name: "Knox Frost",
160
+ handle: "@knoxfrost",
161
+ platform: "Instagram",
162
+ followers: 110000,
163
+ engagement: 5.2,
164
+ niche: "Gaming & Lifestyle",
165
+ style: {
166
+ aesthetic: "Male influencer next door",
167
+ tone: "Casual and relatable",
168
+ themes: ["gaming", "tech", "lifestyle"]
169
+ },
170
+ contentTypes: ["photos", "stories", "gaming content"],
171
+ postingSchedule: { frequency: "4-5 per week", bestTimes: ["16:00", "21:00"] },
172
+ visualStyle: { colors: ["gaming neon", "casual"], lighting: "natural", editing: "minimal" },
173
+ monetizationType: ["gaming partnerships", "tech reviews"],
174
+ signatureElements: ["glasses", "casual style", "gaming setup"],
175
+ petCompanion: true,
176
+ petType: "cat",
177
+ lessons: [
178
+ "Nichos específicos funcionan",
179
+ "Contenido relatable",
180
+ "Mascota para engagement",
181
+ "Presencia en comunidades gaming"
182
+ ]
183
+ },
184
+ {
185
+ name: "Ayla",
186
+ handle: "@ayla_virtual",
187
+ platform: "TikTok",
188
+ followers: 850000,
189
+ engagement: 8.3,
190
+ niche: "Dance & Lifestyle",
191
+ style: {
192
+ aesthetic: "Gen Z dancer",
193
+ tone: "Energetic and fun",
194
+ themes: ["dance", "trends", "lifestyle"]
195
+ },
196
+ contentTypes: ["dance videos", "trending content", "duets"],
197
+ postingSchedule: { frequency: "daily", bestTimes: ["12:00", "18:00", "22:00"] },
198
+ visualStyle: { colors: ["vibrant", "trendy"], lighting: "ring light", editing: "tiktok style" },
199
+ monetizationType: ["brand deals", "live gifts"],
200
+ signatureElements: ["dance moves", "trend participation", "duets"],
201
+ petCompanion: true,
202
+ petType: "small dog",
203
+ lessons: [
204
+ "Participar en trends rápidamente",
205
+ "Alta frecuencia de publicación",
206
+ "Duets con otros creadores",
207
+ "Mascota en videos aumenta engagement"
208
+ ]
209
+ }
210
+ ];
211
+
212
+ // GET - Obtener influencers IA de referencia
213
+ export async function GET(request: NextRequest) {
214
+ try {
215
+ const { searchParams } = new URL(request.url);
216
+ const niche = searchParams.get("niche");
217
+ const platform = searchParams.get("platform");
218
+ const withPets = searchParams.get("withPets");
219
+
220
+ let influencers = [...FAMOUS_AI_INFLUENCERS];
221
+
222
+ // Filtrar por nicho
223
+ if (niche) {
224
+ influencers = influencers.filter(i =>
225
+ i.niche.toLowerCase().includes(niche.toLowerCase()) ||
226
+ i.style.themes.some(t => t.toLowerCase().includes(niche.toLowerCase()))
227
+ );
228
+ }
229
+
230
+ // Filtrar por plataforma
231
+ if (platform) {
232
+ influencers = influencers.filter(i =>
233
+ i.platform.toLowerCase() === platform.toLowerCase()
234
+ );
235
+ }
236
+
237
+ // Filtrar por si tienen mascota
238
+ if (withPets === "true") {
239
+ influencers = influencers.filter(i => i.petCompanion);
240
+ }
241
+
242
+ // Obtener de la base de datos también
243
+ const dbInfluencers = await prisma.aIInfluencer.findMany({
244
+ where: { isActive: true }
245
+ });
246
+
247
+ return NextResponse.json({
248
+ success: true,
249
+ influencers,
250
+ customInfluencers: dbInfluencers,
251
+ total: influencers.length + dbInfluencers.length
252
+ });
253
+
254
+ } catch (error) {
255
+ console.error("Error fetching influencers:", error);
256
+ return NextResponse.json(
257
+ { success: false, error: "Error al obtener influencers" },
258
+ { status: 500 }
259
+ );
260
+ }
261
+ }
262
+
263
+ // POST - Analizar y extraer estrategias de influencers IA
264
+ export async function POST(request: NextRequest) {
265
+ try {
266
+ const body = await request.json();
267
+ const { targetNiche, targetPlatform, includePets, analyzeStyle } = body;
268
+
269
+ const zai = await ZAI.create();
270
+
271
+ // Filtrar influencers relevantes
272
+ let relevantInfluencers = FAMOUS_AI_INFLUENCERS.filter(i => {
273
+ if (targetPlatform && i.platform.toLowerCase() !== targetPlatform.toLowerCase()) return false;
274
+ if (includePets === false && i.petCompanion) return false;
275
+ if (includePets === true && !i.petCompanion) return false;
276
+ return true;
277
+ });
278
+
279
+ // Análisis con IA para extraer patrones
280
+ const analysisPrompt = `Analiza estos influencers IA exitosos y extrae estrategias aplicables para un nuevo influencer en el nicho "${targetNiche || "general"}":
281
+
282
+ ${JSON.stringify(relevantInfluencers.slice(0, 5), null, 2)}
283
+
284
+ Proporciona:
285
+ 1. Patrones de contenido más efectivos
286
+ 2. Estilos visuales que funcionan
287
+ 3. Estrategias de engagement
288
+ 4. Horarios óptimos de publicación
289
+ 5. Elementos distintivos recomendados
290
+ ${includePets ? "6. Cómo integrar una mascota de forma natural" : ""}
291
+ 7. Errores comunes a evitar
292
+ 8. Estrategias de monetización recomendadas
293
+
294
+ Responde en JSON con esta estructura:
295
+ {
296
+ "contentPatterns": [{"pattern": "", "effectiveness": 0-10, "platform": ""}],
297
+ "visualStyles": [{"style": "", "elements": [], "appeal": ""}],
298
+ "engagementStrategies": [{"strategy": "", "execution": ""}],
299
+ "postingSchedule": {"frequency": "", "bestTimes": [], "timezone": ""},
300
+ "signatureElements": [{"element": "", "uniqueness": "", "implementation": ""}],
301
+ ${includePets ? '"petIntegration": [{"tip": "", "contentTypes": []}],': ''}
302
+ "mistakes": [{"mistake": "", "solution": ""}],
303
+ "monetizationRecommendations": [{"method": "", "potential": "", "steps": ""}],
304
+ "viralPotential": {"score": 0-100, "factors": []}
305
+ }`;
306
+
307
+ const completion = await zai.chat.completions.create({
308
+ messages: [
309
+ {
310
+ role: "system",
311
+ content: "Eres un experto en marketing de influencers y análisis de tendencias. Proporciona análisis detallados y accionables."
312
+ },
313
+ {
314
+ role: "user",
315
+ content: analysisPrompt
316
+ }
317
+ ],
318
+ temperature: 0.7,
319
+ max_tokens: 3000,
320
+ });
321
+
322
+ let analysis;
323
+ try {
324
+ const response = completion.choices[0]?.message?.content || "";
325
+ const match = response.match(/\{[\s\S]*\}/);
326
+ if (match) {
327
+ analysis = JSON.parse(match[0]);
328
+ }
329
+ } catch {
330
+ analysis = { raw: completion.choices[0]?.message?.content };
331
+ }
332
+
333
+ // Generar recomendaciones personalizadas
334
+ const personalizationPrompt = `Basado en el análisis anterior, genera un plan de contenido específico para un nuevo influencer IA en el nicho "${targetNiche || "lifestyle"}" ${includePets ? "con una mascota" : ""}.
335
+
336
+ Incluye:
337
+ 1. Concepto único del influencer
338
+ 2. Estilo visual recomendado
339
+ 3. 10 ideas de contenido para la primera semana
340
+ 4. Bio y descripción sugerida
341
+ 5. Hashtags recomendados
342
+
343
+ Responde en JSON con:
344
+ {
345
+ "character": {"name": "", "personality": "", "backstory": "", "visualDescription": ""},
346
+ "visualStyle": {"aesthetic": "", "colors": [], "lighting": "", "editing": ""},
347
+ "contentIdeas": [{"day": 1, "type": "", "description": "", "hook": ""}],
348
+ "bio": "",
349
+ "hashtags": [],
350
+ "uniqueSellingPoints": []
351
+ }`;
352
+
353
+ const personalization = await zai.chat.completions.create({
354
+ messages: [
355
+ {
356
+ role: "system",
357
+ content: "Eres un experto en crear influencers IA. Genera conceptos únicos y memorables."
358
+ },
359
+ {
360
+ role: "user",
361
+ content: personalizationPrompt
362
+ }
363
+ ],
364
+ temperature: 0.8,
365
+ max_tokens: 2000,
366
+ });
367
+
368
+ let characterConcept;
369
+ try {
370
+ const response = personalization.choices[0]?.message?.content || "";
371
+ const match = response.match(/\{[\s\S]*\}/);
372
+ if (match) {
373
+ characterConcept = JSON.parse(match[0]);
374
+ }
375
+ } catch {
376
+ characterConcept = { raw: personalization.choices[0]?.message?.content };
377
+ }
378
+
379
+ return NextResponse.json({
380
+ success: true,
381
+ referenceInfluencers: relevantInfluencers.map(i => ({
382
+ name: i.name,
383
+ handle: i.handle,
384
+ followers: i.followers,
385
+ engagement: i.engagement,
386
+ niche: i.niche,
387
+ petCompanion: i.petCompanion,
388
+ petType: i.petType,
389
+ keyLessons: i.lessons.slice(0, 3)
390
+ })),
391
+ analysis,
392
+ characterConcept,
393
+ timestamp: new Date().toISOString()
394
+ });
395
+
396
+ } catch (error) {
397
+ console.error("Error analyzing influencers:", error);
398
+ return NextResponse.json(
399
+ { success: false, error: "Error al analizar influencers" },
400
+ { status: 500 }
401
+ );
402
+ }
403
+ }
404
+
405
+ // PUT - Guardar influencer personalizado
406
+ export async function PUT(request: NextRequest) {
407
+ try {
408
+ const body = await request.json();
409
+ const {
410
+ name, handle, platform, followers, engagement, niche,
411
+ style, contentTypes, postingSchedule, visualStyle,
412
+ monetizationType, signatureElements, petCompanion, petType,
413
+ analysis, lessons
414
+ } = body;
415
+
416
+ const influencer = await prisma.aIInfluencer.create({
417
+ data: {
418
+ name,
419
+ handle,
420
+ platform,
421
+ followers,
422
+ engagement,
423
+ niche,
424
+ style: JSON.stringify(style),
425
+ contentTypes: JSON.stringify(contentTypes),
426
+ postingSchedule: JSON.stringify(postingSchedule),
427
+ visualStyle: JSON.stringify(visualStyle),
428
+ monetizationType: JSON.stringify(monetizationType),
429
+ signatureElements: JSON.stringify(signatureElements),
430
+ petCompanion: petCompanion || false,
431
+ petType,
432
+ analysis: analysis ? JSON.stringify(analysis) : null,
433
+ lessons: lessons ? JSON.stringify(lessons) : null,
434
+ }
435
+ });
436
+
437
+ return NextResponse.json({
438
+ success: true,
439
+ influencer,
440
+ message: "Influencer guardado correctamente"
441
+ });
442
+
443
+ } catch (error) {
444
+ console.error("Error saving influencer:", error);
445
+ return NextResponse.json(
446
+ { success: false, error: "Error al guardar influencer" },
447
+ { status: 500 }
448
+ );
449
+ }
450
+ }
src/app/api/monetization/route.ts ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+
4
+ // Configuración legal de cada plataforma
5
+ const PLATFORM_CONFIGS = {
6
+ onlyfans: {
7
+ name: "OnlyFans",
8
+ type: "subscription",
9
+ url: "https://onlyfans.com",
10
+ feePercentage: 20,
11
+ legalTerms: {
12
+ ageRequirement: 18,
13
+ contentTypes: ["adult", "lifestyle", "fitness", "art"],
14
+ prohibitedContent: ["illegal content", "non-consensual", "underage"],
15
+ verificationRequired: true,
16
+ taxReporting: true,
17
+ payoutSchedule: "weekly"
18
+ },
19
+ contentRules: {
20
+ adultContent: "allowed",
21
+ nudity: "allowed",
22
+ explicit: "allowed_with_verification",
23
+ copyright: "must_own_or_license"
24
+ }
25
+ },
26
+ patreon: {
27
+ name: "Patreon",
28
+ type: "subscription",
29
+ url: "https://patreon.com",
30
+ feePercentage: 12,
31
+ legalTerms: {
32
+ ageRequirement: 18,
33
+ contentTypes: ["art", "music", "podcasts", "videos", "writing", "gaming"],
34
+ prohibitedContent: ["adult content with real people", "hate speech", "illegal content"],
35
+ verificationRequired: true,
36
+ taxReporting: true,
37
+ payoutSchedule: "monthly"
38
+ },
39
+ contentRules: {
40
+ adultContent: "restricted",
41
+ nudity: "allowed_with_warning",
42
+ explicit: "not_allowed",
43
+ copyright: "must_own_or_license"
44
+ }
45
+ },
46
+ fansly: {
47
+ name: "Fansly",
48
+ type: "mixed",
49
+ url: "https://fansly.com",
50
+ feePercentage: 20,
51
+ legalTerms: {
52
+ ageRequirement: 18,
53
+ contentTypes: ["adult", "lifestyle", "creator"],
54
+ prohibitedContent: ["illegal content", "non-consensual", "underage"],
55
+ verificationRequired: true,
56
+ taxReporting: true,
57
+ payoutSchedule: "weekly"
58
+ },
59
+ contentRules: {
60
+ adultContent: "allowed",
61
+ nudity: "allowed",
62
+ explicit: "allowed_with_verification",
63
+ copyright: "must_own_or_license"
64
+ }
65
+ },
66
+ fanvue: {
67
+ name: "Fanvue",
68
+ type: "subscription",
69
+ url: "https://fanvue.com",
70
+ feePercentage: 15,
71
+ legalTerms: {
72
+ ageRequirement: 18,
73
+ contentTypes: ["adult", "fitness", "lifestyle", "music", "art"],
74
+ prohibitedContent: ["illegal content", "non-consensual", "underage"],
75
+ verificationRequired: true,
76
+ taxReporting: true,
77
+ payoutSchedule: "weekly"
78
+ },
79
+ contentRules: {
80
+ adultContent: "allowed",
81
+ nudity: "allowed",
82
+ explicit: "allowed_with_verification",
83
+ copyright: "must_own_or_license"
84
+ }
85
+ },
86
+ justforfans: {
87
+ name: "JustForFans",
88
+ type: "mixed",
89
+ url: "https://justfor.fans",
90
+ feePercentage: 20,
91
+ legalTerms: {
92
+ ageRequirement: 18,
93
+ contentTypes: ["adult"],
94
+ prohibitedContent: ["illegal content", "non-consensual", "underage"],
95
+ verificationRequired: true,
96
+ taxReporting: true,
97
+ payoutSchedule: "weekly"
98
+ },
99
+ contentRules: {
100
+ adultContent: "allowed",
101
+ nudity: "allowed",
102
+ explicit: "allowed_with_verification",
103
+ copyright: "must_own_or_license"
104
+ }
105
+ },
106
+ kofi: {
107
+ name: "Ko-fi",
108
+ type: "tips",
109
+ url: "https://ko-fi.com",
110
+ feePercentage: 0,
111
+ legalTerms: {
112
+ ageRequirement: 13,
113
+ contentTypes: ["art", "commissions", "digital products", "memberships"],
114
+ prohibitedContent: ["adult content", "illegal content", "hate speech"],
115
+ verificationRequired: false,
116
+ taxReporting: false,
117
+ payoutSchedule: "instant"
118
+ },
119
+ contentRules: {
120
+ adultContent: "not_allowed",
121
+ nudity: "not_allowed",
122
+ explicit: "not_allowed",
123
+ copyright: "must_own_or_license"
124
+ }
125
+ },
126
+ gumroad: {
127
+ name: "Gumroad",
128
+ type: "ppv",
129
+ url: "https://gumroad.com",
130
+ feePercentage: 10,
131
+ legalTerms: {
132
+ ageRequirement: 13,
133
+ contentTypes: ["digital products", "courses", "memberships", "software"],
134
+ prohibitedContent: ["illegal content", "hate speech"],
135
+ verificationRequired: false,
136
+ taxReporting: true,
137
+ payoutSchedule: "weekly"
138
+ },
139
+ contentRules: {
140
+ adultContent: "restricted",
141
+ nudity: "allowed_with_warning",
142
+ explicit: "not_allowed",
143
+ copyright: "must_own_or_license"
144
+ }
145
+ },
146
+ instagram: {
147
+ name: "Instagram",
148
+ type: "free",
149
+ url: "https://instagram.com",
150
+ feePercentage: 0,
151
+ legalTerms: {
152
+ ageRequirement: 13,
153
+ contentTypes: ["photos", "reels", "stories", "lives"],
154
+ prohibitedContent: ["nudity", "violence", "hate speech", "illegal content"],
155
+ verificationRequired: false,
156
+ taxReporting: false,
157
+ payoutSchedule: "none"
158
+ },
159
+ contentRules: {
160
+ adultContent: "not_allowed",
161
+ nudity: "not_allowed",
162
+ explicit: "not_allowed",
163
+ copyright: "must_own_or_license"
164
+ }
165
+ },
166
+ tiktok: {
167
+ name: "TikTok",
168
+ type: "free",
169
+ url: "https://tiktok.com",
170
+ feePercentage: 0,
171
+ legalTerms: {
172
+ ageRequirement: 13,
173
+ contentTypes: ["short videos", "live"],
174
+ prohibitedContent: ["nudity", "violence", "dangerous acts", "hate speech"],
175
+ verificationRequired: false,
176
+ taxReporting: false,
177
+ payoutSchedule: "none"
178
+ },
179
+ contentRules: {
180
+ adultContent: "not_allowed",
181
+ nudity: "not_allowed",
182
+ explicit: "not_allowed",
183
+ copyright: "must_own_or_license"
184
+ }
185
+ },
186
+ youtube: {
187
+ name: "YouTube",
188
+ type: "ad_revenue",
189
+ url: "https://youtube.com",
190
+ feePercentage: 45, // YouTube toma 45% de ad revenue
191
+ legalTerms: {
192
+ ageRequirement: 13,
193
+ contentTypes: ["videos", "shorts", "live", "community"],
194
+ prohibitedContent: ["nudity", "violence", "hate speech", "copyright violation"],
195
+ verificationRequired: true,
196
+ taxReporting: true,
197
+ payoutSchedule: "monthly"
198
+ },
199
+ contentRules: {
200
+ adultContent: "not_allowed",
201
+ nudity: "not_allowed",
202
+ explicit: "not_allowed",
203
+ copyright: "strict_enforcement"
204
+ }
205
+ }
206
+ };
207
+
208
+ // GET - Listar plataformas disponibles
209
+ export async function GET(request: NextRequest) {
210
+ try {
211
+ const { searchParams } = new URL(request.url);
212
+ const type = searchParams.get("type"); // subscription, tips, ppv, free
213
+
214
+ // Obtener plataformas configuradas por el usuario
215
+ const userPlatforms = await db.monetizationPlatform.findMany({
216
+ include: {
217
+ _count: {
218
+ select: { posts: true, earnings: true, subscribers: true }
219
+ }
220
+ },
221
+ orderBy: { createdAt: "desc" }
222
+ });
223
+
224
+ // Filtrar por tipo si se especifica
225
+ let availablePlatforms = Object.entries(PLATFORM_CONFIGS).map(([key, config]) => ({
226
+ id: key,
227
+ ...config
228
+ }));
229
+
230
+ if (type) {
231
+ availablePlatforms = availablePlatforms.filter(p => p.type === type);
232
+ }
233
+
234
+ return NextResponse.json({
235
+ success: true,
236
+ userPlatforms,
237
+ availablePlatforms,
238
+ totalUserPlatforms: userPlatforms.length
239
+ });
240
+
241
+ } catch (error) {
242
+ console.error("Error fetching monetization platforms:", error);
243
+ return NextResponse.json(
244
+ { success: false, error: "Error al obtener plataformas" },
245
+ { status: 500 }
246
+ );
247
+ }
248
+ }
249
+
250
+ // POST - Añadir/configurar plataforma
251
+ export async function POST(request: NextRequest) {
252
+ try {
253
+ const body = await request.json();
254
+ const {
255
+ platformKey,
256
+ accountId,
257
+ accountName,
258
+ apiKey,
259
+ isVerified
260
+ } = body;
261
+
262
+ if (!platformKey || !PLATFORM_CONFIGS[platformKey as keyof typeof PLATFORM_CONFIGS]) {
263
+ return NextResponse.json(
264
+ { success: false, error: "Plataforma no válida" },
265
+ { status: 400 }
266
+ );
267
+ }
268
+
269
+ const config = PLATFORM_CONFIGS[platformKey as keyof typeof PLATFORM_CONFIGS];
270
+
271
+ // Crear o actualizar plataforma
272
+ const platform = await db.monetizationPlatform.create({
273
+ data: {
274
+ name: config.name,
275
+ type: config.type,
276
+ url: config.url,
277
+ apiKey: apiKey || null,
278
+ accountId: accountId || null,
279
+ accountName: accountName || null,
280
+ legalTerms: JSON.stringify(config.legalTerms),
281
+ contentRules: JSON.stringify(config.contentRules),
282
+ feePercentage: config.feePercentage,
283
+ payoutSchedule: config.legalTerms.payoutSchedule,
284
+ isVerified: isVerified || false,
285
+ }
286
+ });
287
+
288
+ return NextResponse.json({
289
+ success: true,
290
+ platform,
291
+ legalInfo: config.legalTerms,
292
+ contentRules: config.contentRules,
293
+ message: `Plataforma ${config.name} configurada`
294
+ });
295
+
296
+ } catch (error) {
297
+ console.error("Error creating platform:", error);
298
+ return NextResponse.json(
299
+ { success: false, error: "Error al configurar plataforma" },
300
+ { status: 500 }
301
+ );
302
+ }
303
+ }
304
+
305
+ // PUT - Actualizar plataforma
306
+ export async function PUT(request: NextRequest) {
307
+ try {
308
+ const body = await request.json();
309
+ const { id, accountId, accountName, apiKey, isVerified, isActive } = body;
310
+
311
+ if (!id) {
312
+ return NextResponse.json(
313
+ { success: false, error: "ID de plataforma requerido" },
314
+ { status: 400 }
315
+ );
316
+ }
317
+
318
+ const platform = await db.monetizationPlatform.update({
319
+ where: { id },
320
+ data: {
321
+ accountId: accountId || undefined,
322
+ accountName: accountName || undefined,
323
+ apiKey: apiKey || undefined,
324
+ isVerified: isVerified || undefined,
325
+ isActive: isActive !== undefined ? isActive : undefined,
326
+ }
327
+ });
328
+
329
+ return NextResponse.json({
330
+ success: true,
331
+ platform
332
+ });
333
+
334
+ } catch (error) {
335
+ console.error("Error updating platform:", error);
336
+ return NextResponse.json(
337
+ { success: false, error: "Error al actualizar plataforma" },
338
+ { status: 500 }
339
+ );
340
+ }
341
+ }
342
+
343
+ // DELETE - Eliminar plataforma
344
+ export async function DELETE(request: NextRequest) {
345
+ try {
346
+ const { searchParams } = new URL(request.url);
347
+ const id = searchParams.get("id");
348
+
349
+ if (!id) {
350
+ return NextResponse.json(
351
+ { success: false, error: "ID requerido" },
352
+ { status: 400 }
353
+ );
354
+ }
355
+
356
+ await db.monetizationPlatform.delete({
357
+ where: { id }
358
+ });
359
+
360
+ return NextResponse.json({
361
+ success: true,
362
+ message: "Plataforma eliminada"
363
+ });
364
+
365
+ } catch (error) {
366
+ console.error("Error deleting platform:", error);
367
+ return NextResponse.json(
368
+ { success: false, error: "Error al eliminar plataforma" },
369
+ { status: 500 }
370
+ );
371
+ }
372
+ }
src/app/api/pets/route.ts ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import ZAI from "z-ai-web-dev-sdk";
3
+ import { prisma } from "@/lib/db";
4
+
5
+ // Tipos de mascotas disponibles con características
6
+ const PET_TYPES = {
7
+ dog: {
8
+ name: "Perro",
9
+ breeds: ["Golden Retriever", "Labrador", "French Bulldog", "Pomeranian", "Corgi", "Husky", "Poodle", "Chihuahua", "Yorkshire", "Beagle"],
10
+ personalities: ["juguetón", "leal", "energético", "calmado", "protector", "amigable"],
11
+ popularIn: ["lifestyle", "fitness", "family", "outdoor"],
12
+ engagementBoost: 35, // % de aumento de engagement promedio
13
+ accessories: ["collar", "bandana", "ropa para mascotas", "juguetes", "gafas"]
14
+ },
15
+ cat: {
16
+ name: "Gato",
17
+ breeds: ["Persa", "Siames", "Maine Coon", "British Shorthair", "Ragdoll", "Bengala", "Sphynx", "Scottish Fold"],
18
+ personalities: ["independiente", "afectuoso", "curioso", "perezoso", "juguetón", "elegante"],
19
+ popularIn: ["lifestyle", "cozy", "aesthetic", "gaming"],
20
+ engagementBoost: 28,
21
+ accessories: ["collar", "campanita", "torre para gatos", "cajas", "mantas"]
22
+ },
23
+ bird: {
24
+ name: "Pájaro",
25
+ breeds: ["Canario", "Periquito", "Cacatúa", "Loro", "Agapornis", "Guacamayo"],
26
+ personalities: ["cantor", "social", "inteligente", "tranquilo", "travieso"],
27
+ popularIn: ["nature", "music", "artistic"],
28
+ engagementBoost: 15,
29
+ accessories: ["jaula decorativa", "juguetes", "posadores"]
30
+ },
31
+ rabbit: {
32
+ name: "Conejo",
33
+ breeds: ["Holandés", "Mini Lop", "Rex", "Angora", "Enano"],
34
+ personalities: ["tierno", "curioso", "suave", "saltarín", "tranquilo"],
35
+ popularIn: ["cute", "aesthetic", "cozy"],
36
+ engagementBoost: 22,
37
+ accessories: ["moños", "ropa miniatura", "juguetes"]
38
+ },
39
+ hamster: {
40
+ name: "Hámster",
41
+ breeds: ["Sirio", "Enano Ruso", "Roborovski", "Chino"],
42
+ personalities: ["pequeño", "activo", "adorable", "curioso"],
43
+ popularIn: ["cute", "pets", "daily"],
44
+ engagementBoost: 18,
45
+ accessories: ["rueda", "bolas", "casitas"]
46
+ }
47
+ };
48
+
49
+ // GET - Obtener mascotas
50
+ export async function GET(request: NextRequest) {
51
+ try {
52
+ const { searchParams } = new URL(request.url);
53
+ const characterId = searchParams.get("characterId");
54
+ const type = searchParams.get("type");
55
+
56
+ const where: Record<string, unknown> = { isActive: true };
57
+ if (characterId) where.characterId = characterId;
58
+ if (type) where.type = type;
59
+
60
+ const pets = await prisma.pet.findMany({
61
+ where,
62
+ include: {
63
+ character: {
64
+ select: { name: true }
65
+ }
66
+ },
67
+ orderBy: { createdAt: "desc" }
68
+ });
69
+
70
+ return NextResponse.json({
71
+ success: true,
72
+ pets,
73
+ petTypes: PET_TYPES,
74
+ total: pets.length
75
+ });
76
+
77
+ } catch (error) {
78
+ console.error("Error fetching pets:", error);
79
+ return NextResponse.json(
80
+ { success: false, error: "Error al obtener mascotas" },
81
+ { status: 500 }
82
+ );
83
+ }
84
+ }
85
+
86
+ // POST - Crear mascota
87
+ export async function POST(request: NextRequest) {
88
+ try {
89
+ const body = await request.json();
90
+ const {
91
+ name, type, breed, description, personality,
92
+ color, accessories, characterId, generateReference
93
+ } = body;
94
+
95
+ // Validar tipo de mascota
96
+ const petType = PET_TYPES[type as keyof typeof PET_TYPES];
97
+ if (!petType) {
98
+ return NextResponse.json(
99
+ { success: false, error: "Tipo de mascota no válido" },
100
+ { status: 400 }
101
+ );
102
+ }
103
+
104
+ let referenceImage = null;
105
+
106
+ // Generar imagen de referencia si se solicita
107
+ if (generateReference) {
108
+ const zai = await ZAI.create();
109
+
110
+ const prompt = `A ${breed || petType.name.toLowerCase()} ${type} named ${name}.
111
+ ${color ? `Color: ${color}.` : ""}
112
+ ${personality ? `Personality: ${personality}.` : ""}
113
+ Style: High quality, photorealistic, social media ready, cute and appealing.
114
+ Background: Soft, aesthetic, suitable for Instagram/TikTok.
115
+ Pose: Natural and engaging, looking at camera or doing a cute action.`;
116
+
117
+ try {
118
+ const imageResponse = await zai.images.generations.create({
119
+ prompt,
120
+ size: "1024x1024"
121
+ });
122
+ referenceImage = imageResponse.data[0]?.base64;
123
+ } catch (imgError) {
124
+ console.error("Error generating pet image:", imgError);
125
+ }
126
+ }
127
+
128
+ const pet = await prisma.pet.create({
129
+ data: {
130
+ name,
131
+ type,
132
+ breed,
133
+ description,
134
+ personality,
135
+ color,
136
+ accessories: accessories ? JSON.stringify(accessories) : null,
137
+ traits: JSON.stringify({
138
+ typeInfo: petType,
139
+ engagementBoost: petType.engagementBoost
140
+ }),
141
+ referenceImage,
142
+ characterId
143
+ }
144
+ });
145
+
146
+ return NextResponse.json({
147
+ success: true,
148
+ pet,
149
+ message: `Mascota "${name}" creada correctamente`,
150
+ engagementBoost: petType.engagementBoost
151
+ });
152
+
153
+ } catch (error) {
154
+ console.error("Error creating pet:", error);
155
+ return NextResponse.json(
156
+ { success: false, error: "Error al crear mascota" },
157
+ { status: 500 }
158
+ );
159
+ }
160
+ }
161
+
162
+ // PUT - Actualizar mascota
163
+ export async function PUT(request: NextRequest) {
164
+ try {
165
+ const body = await request.json();
166
+ const { id, ...updateData } = body;
167
+
168
+ if (!id) {
169
+ return NextResponse.json(
170
+ { success: false, error: "ID de mascota requerido" },
171
+ { status: 400 }
172
+ );
173
+ }
174
+
175
+ // Si hay accessories o traits, convertir a JSON string
176
+ if (updateData.accessories && typeof updateData.accessories !== "string") {
177
+ updateData.accessories = JSON.stringify(updateData.accessories);
178
+ }
179
+ if (updateData.traits && typeof updateData.traits !== "string") {
180
+ updateData.traits = JSON.stringify(updateData.traits);
181
+ }
182
+
183
+ const pet = await prisma.pet.update({
184
+ where: { id },
185
+ data: updateData
186
+ });
187
+
188
+ return NextResponse.json({
189
+ success: true,
190
+ pet,
191
+ message: "Mascota actualizada correctamente"
192
+ });
193
+
194
+ } catch (error) {
195
+ console.error("Error updating pet:", error);
196
+ return NextResponse.json(
197
+ { success: false, error: "Error al actualizar mascota" },
198
+ { status: 500 }
199
+ );
200
+ }
201
+ }
202
+
203
+ // DELETE - Eliminar mascota
204
+ export async function DELETE(request: NextRequest) {
205
+ try {
206
+ const { searchParams } = new URL(request.url);
207
+ const id = searchParams.get("id");
208
+
209
+ if (!id) {
210
+ return NextResponse.json(
211
+ { success: false, error: "ID de mascota requerido" },
212
+ { status: 400 }
213
+ );
214
+ }
215
+
216
+ await prisma.pet.update({
217
+ where: { id },
218
+ data: { isActive: false }
219
+ });
220
+
221
+ return NextResponse.json({
222
+ success: true,
223
+ message: "Mascota eliminada correctamente"
224
+ });
225
+
226
+ } catch (error) {
227
+ console.error("Error deleting pet:", error);
228
+ return NextResponse.json(
229
+ { success: false, error: "Error al eliminar mascota" },
230
+ { status: 500 }
231
+ );
232
+ }
233
+ }
234
+
235
+ // PATCH - Generar contenido con mascota
236
+ export async function PATCH(request: NextRequest) {
237
+ try {
238
+ const body = await request.json();
239
+ const { petId, contentType, platform, theme } = body;
240
+
241
+ const pet = await prisma.pet.findUnique({
242
+ where: { id: petId },
243
+ include: { character: true }
244
+ });
245
+
246
+ if (!pet) {
247
+ return NextResponse.json(
248
+ { success: false, error: "Mascota no encontrada" },
249
+ { status: 404 }
250
+ );
251
+ }
252
+
253
+ const zai = await ZAI.create();
254
+
255
+ // Generar ideas de contenido con la mascota
256
+ const contentPrompt = `Genera ideas de contenido para una mascota:
257
+ - Nombre: ${pet.name}
258
+ - Tipo: ${pet.type}
259
+ - Raza: ${pet.breed || "No especificada"}
260
+ - Personalidad: ${pet.personality || "No especificada"}
261
+ ${pet.character ? `- Dueño: ${pet.character.name}` : ""}
262
+ - Tipo de contenido: ${contentType || "foto"}
263
+ - Plataforma: ${platform || "Instagram"}
264
+ - Tema: ${theme || "lifestyle"}
265
+
266
+ Proporciona:
267
+ 1. 5 ideas de contenido específicas con esta mascota
268
+ 2. Ganchos/hooks para cada idea
269
+ 3. Hashtags recomendados
270
+ 4. Mejor momento del día para publicar
271
+ 5. Elementos visuales sugeridos
272
+
273
+ Responde en JSON:
274
+ {
275
+ "contentIdeas": [{"title": "", "description": "", "hook": "", "cta": ""}],
276
+ "hashtags": [],
277
+ "bestPostingTime": "",
278
+ "visualElements": [],
279
+ "engagementTips": []
280
+ }`;
281
+
282
+ const completion = await zai.chat.completions.create({
283
+ messages: [
284
+ {
285
+ role: "system",
286
+ content: "Eres un experto en contenido de mascotas para redes sociales. Genera ideas creativas y virales."
287
+ },
288
+ {
289
+ role: "user",
290
+ content: contentPrompt
291
+ }
292
+ ],
293
+ temperature: 0.8,
294
+ max_tokens: 2000,
295
+ });
296
+
297
+ let contentIdeas;
298
+ try {
299
+ const response = completion.choices[0]?.message?.content || "";
300
+ const match = response.match(/\{[\s\S]*\}/);
301
+ if (match) {
302
+ contentIdeas = JSON.parse(match[0]);
303
+ }
304
+ } catch {
305
+ contentIdeas = { raw: completion.choices[0]?.message?.content };
306
+ }
307
+
308
+ return NextResponse.json({
309
+ success: true,
310
+ pet: {
311
+ id: pet.id,
312
+ name: pet.name,
313
+ type: pet.type,
314
+ breed: pet.breed
315
+ },
316
+ contentIdeas,
317
+ engagementBoost: PET_TYPES[pet.type as keyof typeof PET_TYPES]?.engagementBoost || 20
318
+ });
319
+
320
+ } catch (error) {
321
+ console.error("Error generating pet content:", error);
322
+ return NextResponse.json(
323
+ { success: false, error: "Error al generar contenido" },
324
+ { status: 500 }
325
+ );
326
+ }
327
+ }
src/app/api/posts/route.ts ADDED
@@ -0,0 +1,330 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import ZAI from "z-ai-web-dev-sdk";
4
+
5
+ // GET - Listar publicaciones
6
+ export async function GET(request: NextRequest) {
7
+ try {
8
+ const { searchParams } = new URL(request.url);
9
+ const status = searchParams.get("status");
10
+ const platform = searchParams.get("platform");
11
+ const type = searchParams.get("type");
12
+ const limit = parseInt(searchParams.get("limit") || "50");
13
+
14
+ const where: Record<string, unknown> = {};
15
+ if (status) where.status = status;
16
+ if (type) where.type = type;
17
+ if (platform) where.platformId = platform;
18
+
19
+ const posts = await db.post.findMany({
20
+ where,
21
+ include: {
22
+ content: true,
23
+ platform: true,
24
+ story: true,
25
+ },
26
+ orderBy: { scheduledAt: "asc" },
27
+ take: limit,
28
+ });
29
+
30
+ // Estadísticas
31
+ const stats = {
32
+ total: await db.post.count(),
33
+ draft: await db.post.count({ where: { status: "draft" } }),
34
+ scheduled: await db.post.count({ where: { status: "scheduled" } }),
35
+ published: await db.post.count({ where: { status: "published" } }),
36
+ failed: await db.post.count({ where: { status: "failed" } }),
37
+ };
38
+
39
+ // Próximas publicaciones
40
+ const upcoming = await db.post.findMany({
41
+ where: {
42
+ status: "scheduled",
43
+ scheduledAt: { gte: new Date() }
44
+ },
45
+ orderBy: { scheduledAt: "asc" },
46
+ take: 5,
47
+ });
48
+
49
+ return NextResponse.json({
50
+ success: true,
51
+ posts,
52
+ stats,
53
+ upcoming
54
+ });
55
+
56
+ } catch (error) {
57
+ console.error("Error fetching posts:", error);
58
+ return NextResponse.json(
59
+ { success: false, error: "Error al obtener publicaciones" },
60
+ { status: 500 }
61
+ );
62
+ }
63
+ }
64
+
65
+ // POST - Crear nueva publicación
66
+ export async function POST(request: NextRequest) {
67
+ try {
68
+ const body = await request.json();
69
+ const {
70
+ title,
71
+ caption,
72
+ type, // reel, photo, carousel, story, post
73
+ contentId,
74
+ platformId,
75
+ scheduledAt,
76
+ hashtags,
77
+ storyId,
78
+ autoGenerateCaption,
79
+ optimizeForPlatform,
80
+ } = body;
81
+
82
+ if (!type) {
83
+ return NextResponse.json(
84
+ { success: false, error: "El tipo de publicación es requerido" },
85
+ { status: 400 }
86
+ );
87
+ }
88
+
89
+ let finalCaption = caption;
90
+ let finalHashtags = hashtags;
91
+
92
+ // Generar caption con IA si se solicita
93
+ if (autoGenerateCaption && contentId) {
94
+ const content = await db.content.findUnique({
95
+ where: { id: contentId }
96
+ });
97
+
98
+ if (content) {
99
+ const zai = await ZAI.create();
100
+ const platform = await db.monetizationPlatform.findUnique({
101
+ where: { id: platformId }
102
+ });
103
+
104
+ const platformName = platform?.name || "general";
105
+ const platformRules = PLATFORM_RULES[platformName.toLowerCase()] || {};
106
+
107
+ const completion = await zai.chat.completions.create({
108
+ messages: [
109
+ {
110
+ role: "system",
111
+ content: `Eres un experto en marketing de contenidos para ${platformName}.
112
+ Genera captions atractivos que:
113
+ ${platformRules.maxCaptionLength ? `- No excedan ${platformRules.maxCaptionLength} caracteres` : ''}
114
+ - Incluyan emojis relevantes
115
+ - Tengan un CTA (call to action) efectivo
116
+ - Sean ${platformRules.tone || 'profesionales pero cercanos'}
117
+ - Maximizen el engagement
118
+ ${platformRules.hashtagLimit ? `- Incluyan máximo ${platformRules.hashtagLimit} hashtags relevantes` : ''}`
119
+ },
120
+ {
121
+ role: "user",
122
+ content: `Genera un caption para este contenido:
123
+ Tipo: ${type}
124
+ Título: ${title || content.title}
125
+ Descripción: ${content.description || content.prompt}
126
+ Plataforma: ${platformName}`
127
+ }
128
+ ],
129
+ temperature: 0.8,
130
+ });
131
+
132
+ finalCaption = completion.choices[0]?.message?.content || caption;
133
+ }
134
+ }
135
+
136
+ // Optimizar hashtags si se solicita
137
+ if (optimizeForPlatform && !hashtags) {
138
+ const zai = await ZAI.create();
139
+ const platform = await db.monetizationPlatform.findUnique({
140
+ where: { id: platformId }
141
+ });
142
+
143
+ const completion = await zai.chat.completions.create({
144
+ messages: [
145
+ {
146
+ role: "system",
147
+ content: `Eres un experto en SEO de redes sociales. Genera hashtags optimizados.
148
+ Responde SOLO con un JSON array de hashtags sin el símbolo #.
149
+ Ejemplo: ["tendencias", "viral", "lifestyle"]`
150
+ },
151
+ {
152
+ role: "user",
153
+ content: `Genera hashtags para un ${type} en ${platform?.name || "redes sociales"}.
154
+ Tema: ${title || "contenido general"}
155
+ Máximo 10 hashtags.`
156
+ }
157
+ ],
158
+ temperature: 0.7,
159
+ });
160
+
161
+ try {
162
+ const response = completion.choices[0]?.message?.content || "[]";
163
+ const match = response.match(/\[[\s\S]*\]/);
164
+ if (match) {
165
+ finalHashtags = match[0];
166
+ }
167
+ } catch {
168
+ finalHashtags = "[]";
169
+ }
170
+ }
171
+
172
+ // Crear publicación
173
+ const post = await db.post.create({
174
+ data: {
175
+ title: title || null,
176
+ caption: finalCaption,
177
+ hashtags: finalHashtags,
178
+ type,
179
+ status: scheduledAt ? "scheduled" : "draft",
180
+ contentId: contentId || null,
181
+ platformId: platformId || null,
182
+ scheduledAt: scheduledAt ? new Date(scheduledAt) : null,
183
+ storyId: storyId || null,
184
+ },
185
+ include: {
186
+ content: true,
187
+ platform: true,
188
+ }
189
+ });
190
+
191
+ // Crear tarea de agente
192
+ await db.agentTask.create({
193
+ data: {
194
+ type: "create_post",
195
+ status: "completed",
196
+ input: `Crear ${type}: ${title || "sin título"}`,
197
+ output: `Post creado con ID: ${post.id}`,
198
+ completedAt: new Date(),
199
+ }
200
+ });
201
+
202
+ return NextResponse.json({
203
+ success: true,
204
+ post,
205
+ message: scheduledAt
206
+ ? `Publicación programada para ${new Date(scheduledAt).toLocaleString()}`
207
+ : "Publicación creada como borrador"
208
+ });
209
+
210
+ } catch (error) {
211
+ console.error("Error creating post:", error);
212
+ return NextResponse.json(
213
+ { success: false, error: "Error al crear publicación" },
214
+ { status: 500 }
215
+ );
216
+ }
217
+ }
218
+
219
+ // PUT - Actualizar publicación
220
+ export async function PUT(request: NextRequest) {
221
+ try {
222
+ const body = await request.json();
223
+ const { id, status, scheduledAt, caption, hashtags, publishedAt, engagementStats } = body;
224
+
225
+ if (!id) {
226
+ return NextResponse.json(
227
+ { success: false, error: "ID requerido" },
228
+ { status: 400 }
229
+ );
230
+ }
231
+
232
+ const updateData: Record<string, unknown> = {};
233
+ if (status) updateData.status = status;
234
+ if (scheduledAt) updateData.scheduledAt = new Date(scheduledAt);
235
+ if (caption) updateData.caption = caption;
236
+ if (hashtags) updateData.hashtags = hashtags;
237
+ if (publishedAt) updateData.publishedAt = new Date(publishedAt);
238
+ if (engagementStats) updateData.engagementStats = engagementStats;
239
+
240
+ const post = await db.post.update({
241
+ where: { id },
242
+ data: updateData,
243
+ });
244
+
245
+ return NextResponse.json({
246
+ success: true,
247
+ post
248
+ });
249
+
250
+ } catch (error) {
251
+ console.error("Error updating post:", error);
252
+ return NextResponse.json(
253
+ { success: false, error: "Error al actualizar publicación" },
254
+ { status: 500 }
255
+ );
256
+ }
257
+ }
258
+
259
+ // DELETE - Eliminar publicación
260
+ export async function DELETE(request: NextRequest) {
261
+ try {
262
+ const { searchParams } = new URL(request.url);
263
+ const id = searchParams.get("id");
264
+
265
+ if (!id) {
266
+ return NextResponse.json(
267
+ { success: false, error: "ID requerido" },
268
+ { status: 400 }
269
+ );
270
+ }
271
+
272
+ await db.post.delete({
273
+ where: { id }
274
+ });
275
+
276
+ return NextResponse.json({
277
+ success: true,
278
+ message: "Publicación eliminada"
279
+ });
280
+
281
+ } catch (error) {
282
+ console.error("Error deleting post:", error);
283
+ return NextResponse.json(
284
+ { success: false, error: "Error al eliminar publicación" },
285
+ { status: 500 }
286
+ );
287
+ }
288
+ }
289
+
290
+ // Reglas específicas por plataforma
291
+ const PLATFORM_RULES: Record<string, {
292
+ maxCaptionLength?: number;
293
+ hashtagLimit?: number;
294
+ tone?: string;
295
+ bestPostingTimes?: string[];
296
+ }> = {
297
+ instagram: {
298
+ maxCaptionLength: 2200,
299
+ hashtagLimit: 30,
300
+ tone: "inspirador y visual",
301
+ bestPostingTimes: ["11:00", "14:00", "19:00", "21:00"]
302
+ },
303
+ tiktok: {
304
+ maxCaptionLength: 300,
305
+ hashtagLimit: 5,
306
+ tone: "casual y divertido",
307
+ bestPostingTimes: ["09:00", "12:00", "19:00"]
308
+ },
309
+ youtube: {
310
+ maxCaptionLength: 5000,
311
+ tone: "profesional e informativo",
312
+ bestPostingTimes: ["15:00", "16:00", "17:00"]
313
+ },
314
+ onlyfans: {
315
+ maxCaptionLength: 1000,
316
+ tone: "personal y exclusivo",
317
+ bestPostingTimes: ["10:00", "18:00", "22:00"]
318
+ },
319
+ patreon: {
320
+ maxCaptionLength: 5000,
321
+ tone: "profesional y cercano",
322
+ bestPostingTimes: ["10:00", "14:00", "18:00"]
323
+ },
324
+ twitter: {
325
+ maxCaptionLength: 280,
326
+ hashtagLimit: 3,
327
+ tone: "conciso y directo",
328
+ bestPostingTimes: ["09:00", "12:00", "17:00", "20:00"]
329
+ }
330
+ };
src/app/api/projects/route.ts ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+
4
+ // GET - Listar todos los proyectos
5
+ export async function GET() {
6
+ try {
7
+ const projects = await db.project.findMany({
8
+ include: {
9
+ repos: true,
10
+ analyses: true,
11
+ },
12
+ orderBy: { createdAt: "desc" },
13
+ });
14
+
15
+ return NextResponse.json({
16
+ success: true,
17
+ projects,
18
+ total: projects.length,
19
+ });
20
+ } catch (error) {
21
+ console.error("Error fetching projects:", error);
22
+ return NextResponse.json(
23
+ { success: false, error: "Error al obtener proyectos" },
24
+ { status: 500 }
25
+ );
26
+ }
27
+ }
28
+
29
+ // POST - Crear nuevo proyecto
30
+ export async function POST(request: NextRequest) {
31
+ try {
32
+ const body = await request.json();
33
+ const { name, description, style } = body;
34
+
35
+ if (!name) {
36
+ return NextResponse.json(
37
+ { success: false, error: "El nombre del proyecto es requerido" },
38
+ { status: 400 }
39
+ );
40
+ }
41
+
42
+ const project = await db.project.create({
43
+ data: {
44
+ name,
45
+ description: description || null,
46
+ style: style || "default",
47
+ status: "active",
48
+ },
49
+ });
50
+
51
+ // Crear tarea de agente
52
+ await db.agentTask.create({
53
+ data: {
54
+ type: "generate",
55
+ status: "completed",
56
+ input: `Crear proyecto: ${name}`,
57
+ output: `Proyecto ${name} creado exitosamente`,
58
+ completedAt: new Date(),
59
+ },
60
+ });
61
+
62
+ return NextResponse.json({
63
+ success: true,
64
+ project,
65
+ message: `Proyecto ${name} creado exitosamente`,
66
+ });
67
+ } catch (error) {
68
+ console.error("Error creating project:", error);
69
+ return NextResponse.json(
70
+ { success: false, error: "Error al crear proyecto" },
71
+ { status: 500 }
72
+ );
73
+ }
74
+ }
src/app/api/prompt-engineer/route.ts ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import ZAI from "z-ai-web-dev-sdk";
3
+
4
+ const PROMPT_ENGINEER_SYSTEM = `Eres un Ingeniero de Prompts experto especializado en optimizar solicitudes en lenguaje natural para diferentes sistemas de IA. Tu trabajo es analizar la intención del usuario y generar prompts optimizados.
5
+
6
+ ## TUS ESPECIALIDADES:
7
+
8
+ ### 1. GENERACIÓN DE IMÁGENES
9
+ Para imágenes, optimiza prompts incluyendo:
10
+ - Estilo artístico (realista, anime, oil painting, digital art, etc.)
11
+ - Iluminación (golden hour, studio lighting, dramatic, soft)
12
+ - Composición (close-up, full body, landscape, portrait)
13
+ - Calidad y detalles (4K, highly detailed, masterpiece)
14
+ - Mood/atmósfera (cinematic, vibrant, moody)
15
+ - Para personas: descripción física detallada, ropa, pose, expresión
16
+
17
+ ### 2. GENERACIÓN DE VIDEOS
18
+ Para videos, optimiza incluyendo:
19
+ - Escena y ambiente
20
+ - Movimientos de cámara (pan, zoom, tracking)
21
+ - Acciones y transiciones
22
+ - Duración estimada
23
+ - Estilo visual
24
+ - Audio/música sugerida
25
+
26
+ ### 3. ANÁLISIS DE CÓDIGO
27
+ Para código, estructura la solicitud:
28
+ - Lenguaje de programación
29
+ - Framework específico si aplica
30
+ - Funcionalidad requerida
31
+ - Restricciones y requisitos
32
+ - Nivel de complejidad
33
+
34
+ ### 4. CONTENIDO PARA REDES SOCIALES
35
+ - Plataforma específica (YouTube, TikTok, Instagram, Twitter)
36
+ - Tono y estilo
37
+ - Longitud apropiada
38
+ - Hashtags sugeridos
39
+ - Horarios óptimos de publicación
40
+
41
+ ## REGLAS DE CENSURA POR PLATAFORMA:
42
+
43
+ ### YouTube:
44
+ - Sin desnudez ni contenido sexual
45
+ - Violencia moderada permitida con advertencia
46
+ - Sin discurso de odio
47
+ - Sin contenido ilegal
48
+ - Lenguaje moderado permitido
49
+
50
+ ### TikTok:
51
+ - Sin desnudez ni insinuaciones sexuales
52
+ - Sin violencia gráfica
53
+ - Sin contenido de autolesión
54
+ - Sin desinformación
55
+ - Música con licencia únicamente
56
+
57
+ ### Instagram:
58
+ - Sin desnudez (arte clásico con moderación)
59
+ - Sin violencia gráfica
60
+ - Sin contenido de autolesión
61
+ - Sin discurso de odio
62
+ - Imágenes editadas deben etiquetarse
63
+
64
+ ### Twitter/X:
65
+ - Mayor libertad pero con advertencias
66
+ - Contenido sensible debe marcarse
67
+ - Sin contenido ilegal
68
+
69
+ ## FORMATO DE RESPUESTA:
70
+
71
+ Responde SIEMPRE en este formato JSON:
72
+ {
73
+ "type": "image|video|code|text|social",
74
+ "optimizedPrompt": "El prompt optimizado y detallado",
75
+ "suggestions": ["sugerencia1", "sugerencia2"],
76
+ "censorWarnings": ["advertencia1"] o [],
77
+ "platformCompatible": ["youtube", "tiktok", ...],
78
+ "parameters": {
79
+ // Parámetros técnicos recomendados
80
+ }
81
+ }`;
82
+
83
+ export async function POST(request: NextRequest) {
84
+ try {
85
+ const body = await request.json();
86
+ const { prompt, type, platform, character } = body;
87
+
88
+ if (!prompt) {
89
+ return NextResponse.json(
90
+ { success: false, error: "El prompt es requerido" },
91
+ { status: 400 }
92
+ );
93
+ }
94
+
95
+ const zai = await ZAI.create();
96
+
97
+ // Construir el contexto
98
+ let contextPrompt = prompt;
99
+ if (type) {
100
+ contextPrompt = `Tipo de tarea: ${type}\nSolicitud: ${prompt}`;
101
+ }
102
+ if (platform) {
103
+ contextPrompt += `\nPlataforma destino: ${platform}`;
104
+ }
105
+ if (character) {
106
+ contextPrompt += `\nPersonaje/Referencia: ${character}`;
107
+ }
108
+
109
+ const completion = await zai.chat.completions.create({
110
+ messages: [
111
+ { role: "system", content: PROMPT_ENGINEER_SYSTEM },
112
+ { role: "user", content: contextPrompt }
113
+ ],
114
+ temperature: 0.7,
115
+ max_tokens: 2000,
116
+ });
117
+
118
+ const response = completion.choices[0]?.message?.content || "";
119
+
120
+ // Intentar parsear el JSON de la respuesta
121
+ let parsedResponse;
122
+ try {
123
+ // Buscar JSON en la respuesta
124
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
125
+ if (jsonMatch) {
126
+ parsedResponse = JSON.parse(jsonMatch[0]);
127
+ } else {
128
+ parsedResponse = {
129
+ type: type || "text",
130
+ optimizedPrompt: response,
131
+ suggestions: [],
132
+ censorWarnings: [],
133
+ platformCompatible: ["general"],
134
+ parameters: {}
135
+ };
136
+ }
137
+ } catch {
138
+ parsedResponse = {
139
+ type: type || "text",
140
+ optimizedPrompt: response,
141
+ suggestions: [],
142
+ censorWarnings: [],
143
+ platformCompatible: ["general"],
144
+ parameters: {}
145
+ };
146
+ }
147
+
148
+ return NextResponse.json({
149
+ success: true,
150
+ originalPrompt: prompt,
151
+ ...parsedResponse
152
+ });
153
+
154
+ } catch (error) {
155
+ console.error("Error in prompt engineer:", error);
156
+ return NextResponse.json(
157
+ { success: false, error: "Error al procesar el prompt" },
158
+ { status: 500 }
159
+ );
160
+ }
161
+ }
162
+
163
+ // Endpoint para obtener sugerencias de prompts
164
+ export async function GET(request: NextRequest) {
165
+ const { searchParams } = new URL(request.url);
166
+ const category = searchParams.get("category") || "image";
167
+
168
+ const templates = {
169
+ image: [
170
+ {
171
+ name: "Retrato Profesional",
172
+ template: "Retrato profesional de [persona], iluminación de estudio, fondo neutro, alta calidad, expresión [emoción]",
173
+ variables: ["persona", "emoción"]
174
+ },
175
+ {
176
+ name: "Escena Cinematográfica",
177
+ template: "Escena cinematográfica de [descripción], iluminación golden hour, atmósfera [mood], estilo película, 4K",
178
+ variables: ["descripción", "mood"]
179
+ },
180
+ {
181
+ name: "Arte Digital",
182
+ template: "Arte digital de [sujeto], estilo [estilo], colores vibrantes, altamente detallado, trending on artstation",
183
+ variables: ["sujeto", "estilo"]
184
+ }
185
+ ],
186
+ video: [
187
+ {
188
+ name: "Video Promocional",
189
+ template: "Video promocional de [producto/servicio], duración 30 segundos, estilo moderno, transiciones suaves",
190
+ variables: ["producto/servicio"]
191
+ },
192
+ {
193
+ name: "Tutorial Animado",
194
+ template: "Video tutorial animado sobre [tema], estilo infografía, explicación paso a paso, iconos claros",
195
+ variables: ["tema"]
196
+ }
197
+ ],
198
+ code: [
199
+ {
200
+ name: "API REST",
201
+ template: "Crear API REST en [lenguaje] con [framework] para [funcionalidad], incluir validación y manejo de errores",
202
+ variables: ["lenguaje", "framework", "funcionalidad"]
203
+ },
204
+ {
205
+ name: "Componente UI",
206
+ template: "Componente [tipo] en React con TypeScript, props: [props], incluir estados y animaciones",
207
+ variables: ["tipo", "props"]
208
+ }
209
+ ]
210
+ };
211
+
212
+ return NextResponse.json({
213
+ success: true,
214
+ templates: templates[category as keyof typeof templates] || templates.image
215
+ });
216
+ }
src/app/api/repos/[id]/route.ts ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import fs from "fs/promises";
4
+ import path from "path";
5
+
6
+ const REPOS_DIR = path.join(process.cwd(), "repos");
7
+
8
+ // GET - Obtener detalles de un repositorio específico
9
+ export async function GET(
10
+ request: NextRequest,
11
+ { params }: { params: Promise<{ id: string }> }
12
+ ) {
13
+ try {
14
+ const { id } = await params;
15
+
16
+ const repo = await db.repo.findUnique({
17
+ where: { id },
18
+ include: {
19
+ project: true,
20
+ analyses: true,
21
+ },
22
+ });
23
+
24
+ if (!repo) {
25
+ return NextResponse.json(
26
+ { success: false, error: "Repositorio no encontrado" },
27
+ { status: 404 }
28
+ );
29
+ }
30
+
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 });
38
+ files = entries.map((e) => e.name);
39
+
40
+ // Intentar leer README
41
+ try {
42
+ const readmePath = path.join(repoPath, "README.md");
43
+ readme = await fs.readFile(readmePath, "utf-8");
44
+ } catch {
45
+ // No hay README
46
+ }
47
+ } catch {
48
+ // Directorio no accesible
49
+ }
50
+
51
+ return NextResponse.json({
52
+ success: true,
53
+ repo,
54
+ files,
55
+ readme,
56
+ });
57
+ } catch (error) {
58
+ console.error("Error fetching repo:", error);
59
+ return NextResponse.json(
60
+ { success: false, error: "Error al obtener repositorio" },
61
+ { status: 500 }
62
+ );
63
+ }
64
+ }
65
+
66
+ // DELETE - Eliminar un repositorio
67
+ export async function DELETE(
68
+ request: NextRequest,
69
+ { params }: { params: Promise<{ id: string }> }
70
+ ) {
71
+ try {
72
+ const { id } = await params;
73
+
74
+ const repo = await db.repo.findUnique({
75
+ where: { id },
76
+ });
77
+
78
+ if (!repo) {
79
+ return NextResponse.json(
80
+ { success: false, error: "Repositorio no encontrado" },
81
+ { status: 404 }
82
+ );
83
+ }
84
+
85
+ // Eliminar directorio del repositorio
86
+ const repoPath = path.join(REPOS_DIR, repo.name);
87
+ try {
88
+ await fs.rm(repoPath, { recursive: true, force: true });
89
+ } catch {
90
+ // Directorio no existe
91
+ }
92
+
93
+ // Eliminar análisis relacionados
94
+ await db.analysis.deleteMany({
95
+ where: { repoId: id },
96
+ });
97
+
98
+ // Eliminar de base de datos
99
+ await db.repo.delete({
100
+ where: { id },
101
+ });
102
+
103
+ return NextResponse.json({
104
+ success: true,
105
+ message: `Repositorio ${repo.name} eliminado`,
106
+ });
107
+ } catch (error) {
108
+ console.error("Error deleting repo:", error);
109
+ return NextResponse.json(
110
+ { success: false, error: "Error al eliminar repositorio" },
111
+ { status: 500 }
112
+ );
113
+ }
114
+ }
src/app/api/repos/route.ts ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import { exec } from "child_process";
4
+ import { promisify } from "util";
5
+ import path from "path";
6
+ import fs from "fs/promises";
7
+
8
+ const execAsync = promisify(exec);
9
+
10
+ const REPOS_DIR = path.join(process.cwd(), "repos");
11
+
12
+ // Asegurar que el directorio existe
13
+ async function ensureReposDir() {
14
+ try {
15
+ await fs.mkdir(REPOS_DIR, { recursive: true });
16
+ } catch (error) {
17
+ // Directorio ya existe
18
+ }
19
+ }
20
+
21
+ // GET - Listar todos los repositorios
22
+ export async function GET() {
23
+ try {
24
+ const repos = await db.repo.findMany({
25
+ include: {
26
+ project: true,
27
+ analyses: true,
28
+ },
29
+ orderBy: { createdAt: "desc" },
30
+ });
31
+
32
+ return NextResponse.json({
33
+ success: true,
34
+ repos,
35
+ total: repos.length,
36
+ });
37
+ } catch (error) {
38
+ console.error("Error fetching repos:", error);
39
+ return NextResponse.json(
40
+ { success: false, error: "Error al obtener repositorios" },
41
+ { status: 500 }
42
+ );
43
+ }
44
+ }
45
+
46
+ // POST - Clonar un nuevo repositorio
47
+ export async function POST(request: NextRequest) {
48
+ try {
49
+ const body = await request.json();
50
+ const { url, projectId } = body;
51
+
52
+ if (!url) {
53
+ return NextResponse.json(
54
+ { success: false, error: "URL del repositorio es requerida" },
55
+ { status: 400 }
56
+ );
57
+ }
58
+
59
+ // Extraer nombre del repo de la URL
60
+ const urlParts = url.replace(".git", "").split("/");
61
+ const name = urlParts[urlParts.length - 1] || "unknown-repo";
62
+
63
+ await ensureReposDir();
64
+ const targetPath = path.join(REPOS_DIR, name);
65
+
66
+ // Verificar si ya existe
67
+ try {
68
+ await fs.access(targetPath);
69
+ return NextResponse.json(
70
+ { success: false, error: "El repositorio ya existe" },
71
+ { status: 400 }
72
+ );
73
+ } catch {
74
+ // No existe, podemos clonar
75
+ }
76
+
77
+ // Clonar repositorio
78
+ try {
79
+ await execAsync(`git clone ${url} ${targetPath}`, {
80
+ timeout: 60000,
81
+ });
82
+ } catch (cloneError) {
83
+ console.error("Clone error:", cloneError);
84
+ return NextResponse.json(
85
+ { success: false, error: "Error al clonar el repositorio" },
86
+ { status: 500 }
87
+ );
88
+ }
89
+
90
+ // Guardar en base de datos
91
+ const repo = await db.repo.create({
92
+ data: {
93
+ url,
94
+ name,
95
+ status: "cloned",
96
+ projectId: projectId || null,
97
+ },
98
+ include: {
99
+ project: true,
100
+ },
101
+ });
102
+
103
+ // Crear tarea de agente
104
+ await db.agentTask.create({
105
+ data: {
106
+ type: "clone",
107
+ status: "completed",
108
+ input: url,
109
+ output: `Repositorio ${name} clonado exitosamente`,
110
+ completedAt: new Date(),
111
+ },
112
+ });
113
+
114
+ return NextResponse.json({
115
+ success: true,
116
+ repo,
117
+ message: `Repositorio ${name} clonado exitosamente`,
118
+ });
119
+ } catch (error) {
120
+ console.error("Error in POST repos:", error);
121
+ return NextResponse.json(
122
+ { success: false, error: "Error interno del servidor" },
123
+ { status: 500 }
124
+ );
125
+ }
126
+ }
src/app/api/route.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { NextResponse } from "next/server";
2
+
3
+ export async function GET() {
4
+ return NextResponse.json({ message: "Hello, world!" });
5
+ }
src/app/api/storytelling/route.ts ADDED
@@ -0,0 +1,347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import ZAI from "z-ai-web-dev-sdk";
4
+
5
+ const STORYTELLING_SYSTEM = `Eres un experto guionista de contenido digital especializado en crear narrativas atractivas para redes sociales y plataformas de monetización.
6
+
7
+ ## TUS ESPECIALIDADES:
8
+
9
+ ### 1. ESTRUCTURA NARRATIVA
10
+ Cada historia debe tener:
11
+ - **Hook inicial**: Gancho que capture la atención en los primeros 3 segundos
12
+ - **Desarrollo**: Progresión emocional con picos y valles
13
+ - **Cliffhanger**: Suspense que mantenga a la audiencia esperando más
14
+ - **Resolución**: Satisfacción pero dejando puerta abierta
15
+
16
+ ### 2. TÉCNICAS DE ENGAGEMENT
17
+ - Preguntas retóricas
18
+ - Revelaciones progresivas
19
+ - Conflictos emocionales
20
+ - Personajes identificables
21
+ - Giros inesperados
22
+ - Calls to action sutiles
23
+
24
+ ### 3. OPTIMIZACIÓN POR PLATAFORMA
25
+ - **Instagram Reels**: Historias de 15-60 seg, visual impactante
26
+ - **TikTok**: Tendencias, humor, velocidad, authenticy
27
+ - **YouTube**: Mayor profundidad, series, communities
28
+ - **OnlyFans**: Exclusividad, conexión personal, behind the scenes
29
+ - **Patreon**: Contenido premium, tutoriales, acceso VIP
30
+
31
+ ### 4. ESTRATEGIAS DE MONETIZACIÓN
32
+ - Teasers gratuitos + contenido premium
33
+ - Suscripción escalonada
34
+ - Contenido exclusivo para tiers superiores
35
+ - PPV (Pay Per View) para contenido especial
36
+ - Fan engagement strategies
37
+
38
+ ### 5. TIPOS DE CONTENIDO
39
+ - **Lifestyle**: Day in the life, routines, personal growth
40
+ - **Drama**: Relationships, conflicts, resolutions
41
+ - **Tutorial**: How-to, tips, behind the scenes
42
+ - **Challenge**: Personal challenges, transformations
43
+ - **ASMR/Relaxation**: Calm content, soothing narratives
44
+ - **Entertainment**: Comedy, reactions, trends
45
+
46
+ ## FORMATO DE RESPUESTA:
47
+ Responde siempre en JSON con esta estructura:
48
+ {
49
+ "title": "Título de la historia",
50
+ "synopsis": "Resumen breve",
51
+ "genre": "género",
52
+ "targetAudience": "audiencia objetivo",
53
+ "tone": "tono narrativo",
54
+ "totalEpisodes": número,
55
+ "episodes": [
56
+ {
57
+ "episodeNum": 1,
58
+ "title": "Título episodio",
59
+ "hook": "Gancho inicial",
60
+ "content": "Contenido del episodio",
61
+ "cliffhanger": "Suspense final",
62
+ "bestPostingTime": "hora",
63
+ "estimatedEngagement": "alto/medio/bajo",
64
+ "monetizationTip": "Sugerencia de monetización"
65
+ }
66
+ ],
67
+ "monetizationStrategy": {
68
+ "freeTeasers": [números de episodios gratis],
69
+ "premiumContent": [números de episodios premium],
70
+ "suggestedPlatform": "plataforma recomendada",
71
+ "pricingStrategy": "estrategia de precios"
72
+ },
73
+ "hashtags": ["hashtags relevantes"],
74
+ "bestPostingSchedule": ["horarios recomendados"]
75
+ }`;
76
+
77
+ // GET - Listar historias
78
+ export async function GET(request: NextRequest) {
79
+ try {
80
+ const { searchParams } = new URL(request.url);
81
+ const status = searchParams.get("status");
82
+ const genre = searchParams.get("genre");
83
+
84
+ const where: Record<string, unknown> = {};
85
+ if (status) where.status = status;
86
+ if (genre) where.genre = genre;
87
+
88
+ const stories = await db.story.findMany({
89
+ where,
90
+ include: {
91
+ episodes: {
92
+ orderBy: { episodeNum: "asc" }
93
+ },
94
+ analytics: true,
95
+ _count: {
96
+ select: { posts: true, episodes: true }
97
+ }
98
+ },
99
+ orderBy: { createdAt: "desc" }
100
+ });
101
+
102
+ // Géneros disponibles
103
+ const genres = [
104
+ { id: "romance", name: "Romance", description: "Historias de amor y relaciones" },
105
+ { id: "drama", name: "Drama", description: "Conflictos emocionales intensos" },
106
+ { id: "comedy", name: "Comedia", description: "Contenido humorístico" },
107
+ { id: "thriller", name: "Thriller", description: "Suspenso y misterio" },
108
+ { id: "lifestyle", name: "Lifestyle", description: "Vida cotidiana y rutinas" },
109
+ { id: "fitness", name: "Fitness", description: "Transformaciones y salud" },
110
+ { id: "beauty", name: "Beauty", description: "Belleza y cuidados" },
111
+ { id: "cooking", name: "Cooking", description: "Cocina y recetas" },
112
+ { id: "travel", name: "Travel", description: "Viajes y aventuras" },
113
+ { id: "gaming", name: "Gaming", description: "Videojuegos y streaming" },
114
+ { id: "education", name: "Educación", description: "Contenido educativo" },
115
+ { id: "asmr", name: "ASMR", description: "Relajación y calma" },
116
+ ];
117
+
118
+ return NextResponse.json({
119
+ success: true,
120
+ stories,
121
+ genres,
122
+ total: stories.length
123
+ });
124
+
125
+ } catch (error) {
126
+ console.error("Error fetching stories:", error);
127
+ return NextResponse.json(
128
+ { success: false, error: "Error al obtener historias" },
129
+ { status: 500 }
130
+ );
131
+ }
132
+ }
133
+
134
+ // POST - Crear nueva historia con IA
135
+ export async function POST(request: NextRequest) {
136
+ try {
137
+ const body = await request.json();
138
+ const {
139
+ prompt,
140
+ genre,
141
+ targetAudience,
142
+ tone,
143
+ totalEpisodes,
144
+ platform,
145
+ characterIds,
146
+ monetizationGoal
147
+ } = body;
148
+
149
+ if (!prompt) {
150
+ return NextResponse.json(
151
+ { success: false, error: "El prompt es requerido" },
152
+ { status: 400 }
153
+ );
154
+ }
155
+
156
+ const zai = await ZAI.create();
157
+
158
+ // Generar historia completa con IA
159
+ const completion = await zai.chat.completions.create({
160
+ messages: [
161
+ { role: "system", content: STORYTELLING_SYSTEM },
162
+ { role: "user", content: `Crea una historia con los siguientes parámetros:
163
+
164
+ Concepto: ${prompt}
165
+ ${genre ? `Género: ${genre}` : ''}
166
+ ${targetAudience ? `Audiencia objetivo: ${targetAudience}` : ''}
167
+ ${tone ? `Tono: ${tone}` : ''}
168
+ ${totalEpisodes ? `Número de episodios: ${totalEpisodes}` : 'Número de episodios: 7'}
169
+ ${platform ? `Plataforma principal: ${platform}` : ''}
170
+ ${monetizationGoal ? `Objetivo de monetización: ${monetizationGoal}` : 'Objetivo: Maximizar suscriptores pagos'}
171
+
172
+ Genera la historia completa con todos los episodios y estrategia de monetización.` }
173
+ ],
174
+ temperature: 0.8,
175
+ max_tokens: 6000,
176
+ });
177
+
178
+ const response = completion.choices[0]?.message?.content || "";
179
+
180
+ // Parsear respuesta
181
+ let storyData;
182
+ try {
183
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
184
+ if (jsonMatch) {
185
+ storyData = JSON.parse(jsonMatch[0]);
186
+ }
187
+ } catch (parseError) {
188
+ console.error("Error parsing story:", parseError);
189
+ storyData = {
190
+ title: "Historia generada",
191
+ synopsis: prompt,
192
+ episodes: []
193
+ };
194
+ }
195
+
196
+ // Crear historia en BD
197
+ const story = await db.story.create({
198
+ data: {
199
+ title: storyData.title || "Nueva Historia",
200
+ description: storyData.synopsis || prompt,
201
+ genre: storyData.genre || genre || "lifestyle",
202
+ targetAudience: storyData.targetAudience || targetAudience || "general",
203
+ tone: storyData.tone || tone || "casual",
204
+ totalEpisodes: storyData.totalEpisodes || storyData.episodes?.length || 7,
205
+ characterIds: characterIds ? JSON.stringify(characterIds) : null,
206
+ monetizationStrategy: storyData.monetizationStrategy ? JSON.stringify(storyData.monetizationStrategy) : null,
207
+ status: "draft",
208
+ }
209
+ });
210
+
211
+ // Crear episodios
212
+ if (storyData.episodes && Array.isArray(storyData.episodes)) {
213
+ for (const ep of storyData.episodes) {
214
+ await db.storyEpisode.create({
215
+ data: {
216
+ storyId: story.id,
217
+ episodeNum: ep.episodeNum || storyData.episodes.indexOf(ep) + 1,
218
+ title: ep.title || `Episodio ${ep.episodeNum}`,
219
+ synopsis: ep.hook || null,
220
+ content: ep.content || "",
221
+ hook: ep.hook || null,
222
+ cliffhanger: ep.cliffhanger || null,
223
+ status: "draft",
224
+ }
225
+ });
226
+ }
227
+ }
228
+
229
+ // Crear analytics
230
+ await db.storyAnalytics.create({
231
+ data: {
232
+ storyId: story.id,
233
+ }
234
+ });
235
+
236
+ // Crear tarea de agente
237
+ await db.agentTask.create({
238
+ data: {
239
+ type: "create_story",
240
+ status: "completed",
241
+ input: prompt,
242
+ output: `Historia "${storyData.title}" creada con ${storyData.episodes?.length || 0} episodios`,
243
+ completedAt: new Date(),
244
+ }
245
+ });
246
+
247
+ // Obtener historia completa
248
+ const fullStory = await db.story.findUnique({
249
+ where: { id: story.id },
250
+ include: {
251
+ episodes: { orderBy: { episodeNum: "asc" } },
252
+ analytics: true
253
+ }
254
+ });
255
+
256
+ return NextResponse.json({
257
+ success: true,
258
+ story: fullStory,
259
+ rawAIResponse: storyData,
260
+ message: `Historia "${storyData.title}" creada exitosamente`
261
+ });
262
+
263
+ } catch (error) {
264
+ console.error("Error creating story:", error);
265
+ return NextResponse.json(
266
+ { success: false, error: "Error al crear historia" },
267
+ { status: 500 }
268
+ );
269
+ }
270
+ }
271
+
272
+ // PUT - Actualizar historia
273
+ export async function PUT(request: NextRequest) {
274
+ try {
275
+ const body = await request.json();
276
+ const { id, status, currentEpisode } = body;
277
+
278
+ if (!id) {
279
+ return NextResponse.json(
280
+ { success: false, error: "ID requerido" },
281
+ { status: 400 }
282
+ );
283
+ }
284
+
285
+ const story = await db.story.update({
286
+ where: { id },
287
+ data: {
288
+ status: status || undefined,
289
+ currentEpisode: currentEpisode || undefined,
290
+ }
291
+ });
292
+
293
+ return NextResponse.json({
294
+ success: true,
295
+ story
296
+ });
297
+
298
+ } catch (error) {
299
+ console.error("Error updating story:", error);
300
+ return NextResponse.json(
301
+ { success: false, error: "Error al actualizar historia" },
302
+ { status: 500 }
303
+ );
304
+ }
305
+ }
306
+
307
+ // DELETE - Eliminar historia
308
+ export async function DELETE(request: NextRequest) {
309
+ try {
310
+ const { searchParams } = new URL(request.url);
311
+ const id = searchParams.get("id");
312
+
313
+ if (!id) {
314
+ return NextResponse.json(
315
+ { success: false, error: "ID requerido" },
316
+ { status: 400 }
317
+ );
318
+ }
319
+
320
+ // Eliminar episodios primero
321
+ await db.storyEpisode.deleteMany({
322
+ where: { storyId: id }
323
+ });
324
+
325
+ // Eliminar analytics
326
+ await db.storyAnalytics.deleteMany({
327
+ where: { storyId: id }
328
+ });
329
+
330
+ // Eliminar historia
331
+ await db.story.delete({
332
+ where: { id }
333
+ });
334
+
335
+ return NextResponse.json({
336
+ success: true,
337
+ message: "Historia eliminada"
338
+ });
339
+
340
+ } catch (error) {
341
+ console.error("Error deleting story:", error);
342
+ return NextResponse.json(
343
+ { success: false, error: "Error al eliminar historia" },
344
+ { status: 500 }
345
+ );
346
+ }
347
+ }
src/app/api/trends/route.ts ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import ZAI from "z-ai-web-dev-sdk";
3
+ import { prisma } from "@/lib/db";
4
+
5
+ // Tendencias actuales por plataforma (datos simulados actualizados)
6
+ const TRENDING_DATA = {
7
+ instagram: [
8
+ { name: "#aesthetic", type: "hashtag", volume: 2500000, growth: 15.5, category: "lifestyle" },
9
+ { name: "#reelsviral", type: "hashtag", volume: 1800000, growth: 22.3, category: "video" },
10
+ { name: "#lifestyle", type: "hashtag", volume: 3200000, growth: 8.2, category: "lifestyle" },
11
+ { name: "ASMR content", type: "topic", volume: 890000, growth: 45.2, category: "relaxation" },
12
+ { name: "Day in my life", type: "topic", volume: 1200000, growth: 18.7, category: "vlog" },
13
+ { name: "Get Ready With Me", type: "format", volume: 2100000, growth: 25.3, category: "beauty" },
14
+ { name: "Photo dumps", type: "format", volume: 1500000, growth: 32.1, category: "lifestyle" },
15
+ { name: "Pets content", type: "topic", volume: 980000, growth: 55.8, category: "pets" },
16
+ ],
17
+ tiktok: [
18
+ { name: "#fyp", type: "hashtag", volume: 15000000, growth: 5.2, category: "general" },
19
+ { name: "#viral", type: "hashtag", volume: 12000000, growth: 12.8, category: "general" },
20
+ { name: "Storytime", type: "topic", volume: 3500000, growth: 32.1, category: "narrative" },
21
+ { name: "GRWM", type: "format", volume: 2800000, growth: 28.5, category: "beauty" },
22
+ { name: "POV videos", type: "format", volume: 4200000, growth: 19.3, category: "creative" },
23
+ { name: "Dance challenges", type: "format", volume: 5200000, growth: 15.7, category: "dance" },
24
+ { name: "Pet reveals", type: "topic", volume: 1800000, growth: 48.3, category: "pets" },
25
+ { name: "Before/After", type: "format", volume: 2200000, growth: 35.2, category: "transformation" },
26
+ ],
27
+ youtube: [
28
+ { name: "Shorts", type: "format", volume: 8500000, growth: 25.6, category: "video" },
29
+ { name: "Tutorial", type: "topic", volume: 5200000, growth: 12.4, category: "education" },
30
+ { name: "Vlog", type: "topic", volume: 3800000, growth: 8.9, category: "lifestyle" },
31
+ { name: "Reaction videos", type: "format", volume: 2100000, growth: 15.7, category: "entertainment" },
32
+ { name: "ASMR", type: "topic", volume: 3200000, growth: 28.4, category: "relaxation" },
33
+ { name: "Pet compilations", type: "format", volume: 1500000, growth: 42.1, category: "pets" },
34
+ ],
35
+ onlyfans: [
36
+ { name: "Behind the scenes", type: "topic", volume: 450000, growth: 35.2, category: "exclusive" },
37
+ { name: "Exclusive content", type: "topic", volume: 380000, growth: 28.7, category: "premium" },
38
+ { name: "PPV specials", type: "format", volume: 220000, growth: 42.1, category: "monetization" },
39
+ { name: "Fan engagement", type: "topic", volume: 180000, growth: 55.3, category: "community" },
40
+ { name: "Pet content", type: "topic", volume: 95000, growth: 62.5, category: "niche" },
41
+ ]
42
+ };
43
+
44
+ // Estrategias virales conocidas
45
+ const VIRAL_STRATEGIES = [
46
+ {
47
+ name: "Hook en 3 segundos",
48
+ description: "Captar atención en los primeros 3 segundos con algo visualmente impactante o una pregunta intrigante",
49
+ platforms: ["tiktok", "instagram", "youtube"],
50
+ contentType: "video",
51
+ successRate: 85,
52
+ elements: ["pregunta intrigante", "revelación parcial", "movimiento brusco", "sonido llamativo"]
53
+ },
54
+ {
55
+ name: "Storytime con suspenso",
56
+ description: "Narrar una historia personal manteniendo el suspenso hasta el final",
57
+ platforms: ["tiktok", "youtube"],
58
+ contentType: "video",
59
+ successRate: 78,
60
+ elements: ["gancho inicial", "pausas dramáticas", "cliffhanger", "resolución satisfactoria"]
61
+ },
62
+ {
63
+ name: "Transformación/Before & After",
64
+ description: "Mostrar un cambio dramático que genere curiosidad sobre el proceso",
65
+ platforms: ["instagram", "tiktok", "youtube"],
66
+ contentType: "reel",
67
+ successRate: 82,
68
+ elements: ["estado inicial", "proceso acelerado", "revelación final", "reacción"]
69
+ },
70
+ {
71
+ name: "Pet Reveal/Cameo",
72
+ description: "Incluir una mascota de forma natural en el contenido para aumentar engagement",
73
+ platforms: ["instagram", "tiktok"],
74
+ contentType: "any",
75
+ successRate: 88,
76
+ elements: ["mascota apareciendo", "interacción tierna", "momento cómico", "call to action con la mascota"]
77
+ },
78
+ {
79
+ name: "Controversia controlada",
80
+ description: "Plantear una opinión divisiva de forma respetuosa para generar debate",
81
+ platforms: ["tiktok", "twitter", "youtube"],
82
+ contentType: "opinion",
83
+ successRate: 72,
84
+ elements: ["opinion fuerte", "argumentos", "invitación al debate", "respuesta en comentarios"]
85
+ },
86
+ {
87
+ name: "Trend hopping",
88
+ description: "Participar en tendencias vigentes con un giro único personal",
89
+ platforms: ["tiktok", "instagram"],
90
+ contentType: "any",
91
+ successRate: 75,
92
+ elements: ["trend actual", "adaptación personal", "elemento sorpresa", "timing perfecto"]
93
+ },
94
+ {
95
+ name: "BTS/Exclusivo",
96
+ description: "Mostrar el detrás de cámaras o contenido que parece exclusivo",
97
+ platforms: ["instagram", "onlyfans", "youtube"],
98
+ contentType: "video",
99
+ successRate: 80,
100
+ elements: ["acceso VIP", "momentos espontáneos", "errores incluidos", "autenticidad"]
101
+ },
102
+ {
103
+ name: "Desafío/Reto",
104
+ description: "Crear o participar en un desafío que invite a la participación",
105
+ platforms: ["tiktok", "instagram"],
106
+ contentType: "reel",
107
+ successRate: 77,
108
+ elements: ["reglas claras", "ejemplo viral", "tag a amigos", "fácil de replicar"]
109
+ }
110
+ ];
111
+
112
+ // GET - Obtener tendencias y estrategias
113
+ export async function GET(request: NextRequest) {
114
+ try {
115
+ const { searchParams } = new URL(request.url);
116
+ const platform = searchParams.get("platform");
117
+ const type = searchParams.get("type");
118
+ const category = searchParams.get("category");
119
+ const includePets = searchParams.get("includePets") === "true";
120
+
121
+ let trends = [];
122
+
123
+ if (platform && TRENDING_DATA[platform as keyof typeof TRENDING_DATA]) {
124
+ trends = TRENDING_DATA[platform as keyof typeof TRENDING_DATA];
125
+ } else {
126
+ // Combinar todas las plataformas
127
+ trends = Object.entries(TRENDING_DATA).flatMap(([plat, data]) =>
128
+ data.map(t => ({ ...t, platform: plat }))
129
+ );
130
+ }
131
+
132
+ // Filtrar por tipo
133
+ if (type) {
134
+ trends = trends.filter(t => t.type === type);
135
+ }
136
+
137
+ // Filtrar por categoría
138
+ if (category) {
139
+ trends = trends.filter(t => t.category === category);
140
+ }
141
+
142
+ // Filtrar tendencias relacionadas con mascotas
143
+ if (includePets) {
144
+ trends = trends.filter(t =>
145
+ t.category === "pets" ||
146
+ t.name.toLowerCase().includes("pet") ||
147
+ t.name.toLowerCase().includes("dog") ||
148
+ t.name.toLowerCase().includes("cat")
149
+ );
150
+ }
151
+
152
+ // Ordenar por crecimiento
153
+ trends.sort((a, b) => b.growth - a.growth);
154
+
155
+ // Obtener estrategias virales relevantes
156
+ let strategies = [...VIRAL_STRATEGIES];
157
+ if (platform) {
158
+ strategies = strategies.filter(s =>
159
+ s.platforms.includes(platform.toLowerCase())
160
+ );
161
+ }
162
+ if (includePets) {
163
+ // Priorizar estrategias que funcionan con mascotas
164
+ strategies.sort((a, b) => {
165
+ const aPet = a.name.toLowerCase().includes("pet") ? 1 : 0;
166
+ const bPet = b.name.toLowerCase().includes("pet") ? 1 : 0;
167
+ return bPet - aPet;
168
+ });
169
+ }
170
+
171
+ // Generar ideas de contenido con IA para las tendencias principales
172
+ const zai = await ZAI.create();
173
+ const topTrends = trends.slice(0, 5);
174
+
175
+ const ideasResponse = await zai.chat.completions.create({
176
+ messages: [
177
+ {
178
+ role: "system",
179
+ content: `Eres un experto en marketing de contenidos viral. Genera ideas de contenido basadas en tendencias.
180
+ ${includePets ? "IMPORTANTE: Todas las ideas deben incluir una mascota como elemento central." : ""}
181
+ Responde en JSON array con objetos {title, description, format, estimatedEngagement, viralScore, hook}.`
182
+ },
183
+ {
184
+ role: "user",
185
+ content: `Genera 5 ideas de contenido para estas tendencias: ${topTrends.map(t => t.name).join(", ")}
186
+ Plataforma principal: ${platform || "todas"}
187
+ ${includePets ? "Incluir mascota en todas las ideas." : ""}`
188
+ }
189
+ ],
190
+ temperature: 0.8,
191
+ });
192
+
193
+ let contentIdeas = [];
194
+ try {
195
+ const match = ideasResponse.choices[0]?.message?.content?.match(/\[[\s\S]*\]/);
196
+ if (match) {
197
+ contentIdeas = JSON.parse(match[0]);
198
+ }
199
+ } catch {
200
+ contentIdeas = [];
201
+ }
202
+
203
+ return NextResponse.json({
204
+ success: true,
205
+ trends,
206
+ viralStrategies: strategies,
207
+ contentIdeas,
208
+ platform: platform || "all",
209
+ stats: {
210
+ totalTrends: trends.length,
211
+ topGrowth: trends[0]?.growth || 0,
212
+ avgGrowth: trends.reduce((acc, t) => acc + t.growth, 0) / trends.length || 0
213
+ }
214
+ });
215
+
216
+ } catch (error) {
217
+ console.error("Error fetching trends:", error);
218
+ return NextResponse.json(
219
+ { success: false, error: "Error al obtener tendencias" },
220
+ { status: 500 }
221
+ );
222
+ }
223
+ }
224
+
225
+ // POST - Analizar tendencias con IA y generar plan viral
226
+ export async function POST(request: NextRequest) {
227
+ try {
228
+ const body = await request.json();
229
+ const { niche, platform, targetAudience, includePets, petType, daysToViral } = body;
230
+
231
+ const zai = await ZAI.create();
232
+
233
+ const completion = await zai.chat.completions.create({
234
+ messages: [
235
+ {
236
+ role: "system",
237
+ content: `Eres un analista de tendencias de redes sociales y experto en crear contenido viral. Analiza y predice tendencias para creadores de contenido.
238
+ ${includePets ? "El creador tiene una mascota que quiere integrar en su contenido." : ""}
239
+ Responde en JSON con esta estructura:
240
+ {
241
+ "currentTrends": [{"name", "type", "growth", "saturation"}],
242
+ "emergingTrends": [{"name", "type", "potential"}],
243
+ "contentGaps": [{"topic", "opportunity", "difficulty"}],
244
+ "recommendations": ["rec1", "rec2"],
245
+ "bestPostingTimes": ["time1", "time2"],
246
+ "hashtagStrategy": {"primary": [], "secondary": []},
247
+ "viralPlan": {
248
+ "week1": [{"day", "contentType", "topic", "hook"}],
249
+ "week2": [{"day", "contentType", "topic", "hook"}]
250
+ },
251
+ "petIntegration": ${includePets ? `[{"tip", "contentIdea", "engagementPotential"}]` : "null"},
252
+ "predictedViralPotential": 0-100,
253
+ "keySuccessFactors": []
254
+ }`
255
+ },
256
+ {
257
+ role: "user",
258
+ content: `Analiza tendencias para:
259
+ Nicho: ${niche || "general"}
260
+ Plataforma: ${platform || "todas"}
261
+ Audiencia: ${targetAudience || "general"}
262
+ ${includePets ? `Incluir mascota tipo: ${petType || "perro/gato"}` : ""}
263
+ Días objetivo para viralizar: ${daysToViral || 14}`
264
+ }
265
+ ],
266
+ temperature: 0.7,
267
+ max_tokens: 3000,
268
+ });
269
+
270
+ const response = completion.choices[0]?.message?.content || "";
271
+ let analysis;
272
+ try {
273
+ const match = response.match(/\{[\s\S]*\}/);
274
+ if (match) {
275
+ analysis = JSON.parse(match[0]);
276
+ }
277
+ } catch {
278
+ analysis = { raw: response };
279
+ }
280
+
281
+ // Guardar análisis en la base de datos
282
+ await prisma.trend.create({
283
+ data: {
284
+ platform: platform || "all",
285
+ type: "analysis",
286
+ name: `Análisis ${niche || "general"} - ${new Date().toISOString().split('T')[0]}`,
287
+ description: `Análisis de tendencias para nicho: ${niche}`,
288
+ contentIdeas: JSON.stringify(analysis),
289
+ isActive: true
290
+ }
291
+ }).catch(() => {
292
+ // Ignorar errores de guardado
293
+ });
294
+
295
+ return NextResponse.json({
296
+ success: true,
297
+ analysis,
298
+ timestamp: new Date().toISOString()
299
+ });
300
+
301
+ } catch (error) {
302
+ console.error("Error analyzing trends:", error);
303
+ return NextResponse.json(
304
+ { success: false, error: "Error al analizar tendencias" },
305
+ { status: 500 }
306
+ );
307
+ }
308
+ }
309
+
310
+ // PUT - Crear estrategia viral personalizada
311
+ export async function PUT(request: NextRequest) {
312
+ try {
313
+ const body = await request.json();
314
+ const { name, platform, contentType, hook, structure, elements, estimatedReach, timeframe } = body;
315
+
316
+ const strategy = await prisma.viralStrategy.create({
317
+ data: {
318
+ name,
319
+ platform,
320
+ contentType,
321
+ hook,
322
+ structure: structure ? JSON.stringify(structure) : null,
323
+ elements: elements ? JSON.stringify(elements) : null,
324
+ estimatedReach,
325
+ timeframe,
326
+ isActive: true
327
+ }
328
+ });
329
+
330
+ return NextResponse.json({
331
+ success: true,
332
+ strategy,
333
+ message: "Estrategia viral creada correctamente"
334
+ });
335
+
336
+ } catch (error) {
337
+ console.error("Error creating viral strategy:", error);
338
+ return NextResponse.json(
339
+ { success: false, error: "Error al crear estrategia" },
340
+ { status: 500 }
341
+ );
342
+ }
343
+ }
src/app/globals.css ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+
4
+ @custom-variant dark (&:is(.dark *));
5
+
6
+ @theme inline {
7
+ --color-background: var(--background);
8
+ --color-foreground: var(--foreground);
9
+ --font-sans: var(--font-geist-sans);
10
+ --font-mono: var(--font-geist-mono);
11
+ --color-sidebar-ring: var(--sidebar-ring);
12
+ --color-sidebar-border: var(--sidebar-border);
13
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
14
+ --color-sidebar-accent: var(--sidebar-accent);
15
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
16
+ --color-sidebar-primary: var(--sidebar-primary);
17
+ --color-sidebar-foreground: var(--sidebar-foreground);
18
+ --color-sidebar: var(--sidebar);
19
+ --color-chart-5: var(--chart-5);
20
+ --color-chart-4: var(--chart-4);
21
+ --color-chart-3: var(--chart-3);
22
+ --color-chart-2: var(--chart-2);
23
+ --color-chart-1: var(--chart-1);
24
+ --color-ring: var(--ring);
25
+ --color-input: var(--input);
26
+ --color-border: var(--border);
27
+ --color-destructive: var(--destructive);
28
+ --color-accent-foreground: var(--accent-foreground);
29
+ --color-accent: var(--accent);
30
+ --color-muted-foreground: var(--muted-foreground);
31
+ --color-muted: var(--muted);
32
+ --color-secondary-foreground: var(--secondary-foreground);
33
+ --color-secondary: var(--secondary);
34
+ --color-primary-foreground: var(--primary-foreground);
35
+ --color-primary: var(--primary);
36
+ --color-popover-foreground: var(--popover-foreground);
37
+ --color-popover: var(--popover);
38
+ --color-card-foreground: var(--card-foreground);
39
+ --color-card: var(--card);
40
+ --radius-sm: calc(var(--radius) - 4px);
41
+ --radius-md: calc(var(--radius) - 2px);
42
+ --radius-lg: var(--radius);
43
+ --radius-xl: calc(var(--radius) + 4px);
44
+ }
45
+
46
+ :root {
47
+ --radius: 0.625rem;
48
+ --background: oklch(1 0 0);
49
+ --foreground: oklch(0.145 0 0);
50
+ --card: oklch(1 0 0);
51
+ --card-foreground: oklch(0.145 0 0);
52
+ --popover: oklch(1 0 0);
53
+ --popover-foreground: oklch(0.145 0 0);
54
+ --primary: oklch(0.205 0 0);
55
+ --primary-foreground: oklch(0.985 0 0);
56
+ --secondary: oklch(0.97 0 0);
57
+ --secondary-foreground: oklch(0.205 0 0);
58
+ --muted: oklch(0.97 0 0);
59
+ --muted-foreground: oklch(0.556 0 0);
60
+ --accent: oklch(0.97 0 0);
61
+ --accent-foreground: oklch(0.205 0 0);
62
+ --destructive: oklch(0.577 0.245 27.325);
63
+ --border: oklch(0.922 0 0);
64
+ --input: oklch(0.922 0 0);
65
+ --ring: oklch(0.708 0 0);
66
+ --chart-1: oklch(0.646 0.222 41.116);
67
+ --chart-2: oklch(0.6 0.118 184.704);
68
+ --chart-3: oklch(0.398 0.07 227.392);
69
+ --chart-4: oklch(0.828 0.189 84.429);
70
+ --chart-5: oklch(0.769 0.188 70.08);
71
+ --sidebar: oklch(0.985 0 0);
72
+ --sidebar-foreground: oklch(0.145 0 0);
73
+ --sidebar-primary: oklch(0.205 0 0);
74
+ --sidebar-primary-foreground: oklch(0.985 0 0);
75
+ --sidebar-accent: oklch(0.97 0 0);
76
+ --sidebar-accent-foreground: oklch(0.205 0 0);
77
+ --sidebar-border: oklch(0.922 0 0);
78
+ --sidebar-ring: oklch(0.708 0 0);
79
+ }
80
+
81
+ .dark {
82
+ --background: oklch(0.145 0 0);
83
+ --foreground: oklch(0.985 0 0);
84
+ --card: oklch(0.205 0 0);
85
+ --card-foreground: oklch(0.985 0 0);
86
+ --popover: oklch(0.205 0 0);
87
+ --popover-foreground: oklch(0.985 0 0);
88
+ --primary: oklch(0.922 0 0);
89
+ --primary-foreground: oklch(0.205 0 0);
90
+ --secondary: oklch(0.269 0 0);
91
+ --secondary-foreground: oklch(0.985 0 0);
92
+ --muted: oklch(0.269 0 0);
93
+ --muted-foreground: oklch(0.708 0 0);
94
+ --accent: oklch(0.269 0 0);
95
+ --accent-foreground: oklch(0.985 0 0);
96
+ --destructive: oklch(0.704 0.191 22.216);
97
+ --border: oklch(1 0 0 / 10%);
98
+ --input: oklch(1 0 0 / 15%);
99
+ --ring: oklch(0.556 0 0);
100
+ --chart-1: oklch(0.488 0.243 264.376);
101
+ --chart-2: oklch(0.696 0.17 162.48);
102
+ --chart-3: oklch(0.769 0.188 70.08);
103
+ --chart-4: oklch(0.627 0.265 303.9);
104
+ --chart-5: oklch(0.645 0.246 16.439);
105
+ --sidebar: oklch(0.205 0 0);
106
+ --sidebar-foreground: oklch(0.985 0 0);
107
+ --sidebar-primary: oklch(0.488 0.243 264.376);
108
+ --sidebar-primary-foreground: oklch(0.985 0 0);
109
+ --sidebar-accent: oklch(0.269 0 0);
110
+ --sidebar-accent-foreground: oklch(0.985 0 0);
111
+ --sidebar-border: oklch(1 0 0 / 10%);
112
+ --sidebar-ring: oklch(0.556 0 0);
113
+ }
114
+
115
+ @layer base {
116
+ * {
117
+ @apply border-border outline-ring/50;
118
+ }
119
+ body {
120
+ @apply bg-background text-foreground;
121
+ }
122
+ }
src/app/layout.tsx ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import "./globals.css";
4
+ import { Toaster } from "@/components/ui/toaster";
5
+
6
+ const geistSans = Geist({
7
+ variable: "--font-geist-sans",
8
+ subsets: ["latin"],
9
+ });
10
+
11
+ const geistMono = Geist_Mono({
12
+ variable: "--font-geist-mono",
13
+ subsets: ["latin"],
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
+
36
+ export default function RootLayout({
37
+ children,
38
+ }: Readonly<{
39
+ children: React.ReactNode;
40
+ }>) {
41
+ return (
42
+ <html lang="en" suppressHydrationWarning>
43
+ <body
44
+ className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background text-foreground`}
45
+ >
46
+ {children}
47
+ <Toaster />
48
+ </body>
49
+ </html>
50
+ );
51
+ }
src/app/page.tsx ADDED
@@ -0,0 +1,1131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback } from "react";
4
+ import { motion, AnimatePresence } from "framer-motion";
5
+ import {
6
+ Menu, X, Bot, Wand2, ImageIcon, Video, Users, FolderGit2, Shield,
7
+ RefreshCw, DollarSign, Clock, Film, Zap, TrendingUp, Calendar,
8
+ Play, Pause, Trash2, Plus, Send, Copy, Eye, Settings, ChevronRight,
9
+ CheckCircle2, AlertCircle, ExternalLink, CreditCard, Target, Sparkles,
10
+ Heart, PawPrint, Star, Users2, Lightbulb, Flame, Rocket
11
+ } from "lucide-react";
12
+
13
+ import { Button } from "@/components/ui/button";
14
+ import { Input } from "@/components/ui/input";
15
+ import { Textarea } from "@/components/ui/textarea";
16
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
17
+ import { Badge } from "@/components/ui/badge";
18
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
19
+ import { ScrollArea } from "@/components/ui/scroll-area";
20
+ import { Label } from "@/components/ui/label";
21
+ import { Switch } from "@/components/ui/switch";
22
+ import { toast } from "sonner";
23
+
24
+ // Types
25
+ interface Content { id: string; type: string; title: string; status: string; platform: string; createdAt: string; }
26
+ interface Character { id: string; name: string; description: string | null; referenceImage: string | null; }
27
+ interface Platform { id: string; name: string; type: string; isActive: boolean; isVerified: boolean; }
28
+ interface Post { id: string; title: string | null; type: string; status: string; scheduledAt: string | null; }
29
+ interface Story { id: string; title: string; genre: string; status: string; totalEpisodes: number; }
30
+ interface Automation { id: string; name: string; type: string; isActive: boolean; runCount: number; }
31
+ interface Pet { id: string; name: string; type: string; breed: string | null; personality: string | null; referenceImage: string | null; }
32
+ interface Influencer { name: string; handle: string; platform: string; followers: number; engagement: number; niche: string; petCompanion: boolean; petType?: string; keyLessons: string[]; }
33
+
34
+ // API Helper
35
+ async function apiFetch(endpoint: string, options: RequestInit = {}) {
36
+ const response = await fetch(`/api${endpoint}`, {
37
+ headers: { "Content-Type": "application/json", ...options.headers },
38
+ ...options,
39
+ });
40
+ return response.json();
41
+ }
42
+
43
+ export default function Dashboard() {
44
+ const [sidebarOpen, setSidebarOpen] = useState(true);
45
+ const [activeTab, setActiveTab] = useState("prompt-engineer");
46
+ const [loading, setLoading] = useState(false);
47
+
48
+ // Prompt Engineer
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
56
+ const [imagePrompt, setImagePrompt] = useState("");
57
+ const [imageStyle, setImageStyle] = useState("realistic");
58
+ const [imagePlatform, setImagePlatform] = useState("general");
59
+ const [imageLoading, setImageLoading] = useState(false);
60
+
61
+ // Monetization
62
+ const [platforms, setPlatforms] = useState<Platform[]>([]);
63
+ const [selectedMonetizationPlatform, setSelectedMonetizationPlatform] = useState("");
64
+
65
+ // Posts
66
+ const [posts, setPosts] = useState<Post[]>([]);
67
+ const [postTitle, setPostTitle] = useState("");
68
+ const [postType, setPostType] = useState("reel");
69
+ const [postCaption, setPostCaption] = useState("");
70
+ const [scheduledTime, setScheduledTime] = useState("");
71
+
72
+ // Stories
73
+ const [stories, setStories] = useState<Story[]>([]);
74
+ const [storyPrompt, setStoryPrompt] = useState("");
75
+ const [storyGenre, setStoryGenre] = useState("lifestyle");
76
+ const [storyEpisodes, setStoryEpisodes] = useState(7);
77
+ const [storyLoading, setStoryLoading] = useState(false);
78
+
79
+ // Automation
80
+ const [automations, setAutomations] = useState<Automation[]>([]);
81
+ const [automationName, setAutomationName] = useState("");
82
+ const [automationType, setAutomationType] = useState("content_generation");
83
+
84
+ // Content & Characters
85
+ const [contents, setContents] = useState<Content[]>([]);
86
+ const [characters, setCharacters] = useState<Character[]>([]);
87
+
88
+ // Pets
89
+ const [pets, setPets] = useState<Pet[]>([]);
90
+ const [petName, setPetName] = useState("");
91
+ const [petType, setPetType] = useState("dog");
92
+ const [petBreed, setPetBreed] = useState("");
93
+ const [petPersonality, setPetPersonality] = useState("");
94
+ const [includePetInContent, setIncludePetInContent] = useState(false);
95
+ const [petLoading, setPetLoading] = useState(false);
96
+
97
+ // Influencers
98
+ const [influencers, setInfluencers] = useState<Influencer[]>([]);
99
+ const [influencerNiche, setInfluencerNiche] = useState("");
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
114
+ const [stats, setStats] = useState({ images: 0, videos: 0, stories: 0, automations: 0, pets: 0 });
115
+
116
+ // Load data
117
+ const loadData = useCallback(async () => {
118
+ setLoading(true);
119
+ try {
120
+ const [contentRes, platformsRes, postsRes, storiesRes, automationRes, petsRes] = await Promise.all([
121
+ apiFetch("/content"),
122
+ apiFetch("/monetization"),
123
+ apiFetch("/posts"),
124
+ apiFetch("/storytelling"),
125
+ apiFetch("/automation"),
126
+ apiFetch("/pets"),
127
+ ]);
128
+
129
+ if (contentRes.success) {
130
+ setContents(contentRes.contents);
131
+ setStats({
132
+ images: contentRes.stats?.images || 0,
133
+ videos: contentRes.stats?.videos || 0,
134
+ stories: storiesRes?.total || 0,
135
+ automations: automationRes?.stats?.total || 0,
136
+ pets: petsRes?.total || 0,
137
+ });
138
+ }
139
+ if (platformsRes.success) setPlatforms(platformsRes.userPlatforms);
140
+ if (postsRes.success) setPosts(postsRes.posts);
141
+ if (storiesRes.success) setStories(storiesRes.stories);
142
+ if (automationRes.success) setAutomations(automationRes.automations);
143
+ if (petsRes.success) setPets(petsRes.pets);
144
+ } catch {
145
+ toast.error("Error al cargar datos");
146
+ } finally {
147
+ setLoading(false);
148
+ }
149
+ }, []);
150
+
151
+ useEffect(() => { loadData(); }, [loadData]);
152
+
153
+ // Actions
154
+ const handleOptimizePrompt = async () => {
155
+ if (!userPrompt.trim()) { toast.error("Escribe un prompt"); return; }
156
+ setPromptLoading(true);
157
+ try {
158
+ const result = await apiFetch("/prompt-engineer", {
159
+ method: "POST",
160
+ body: JSON.stringify({ prompt: userPrompt, type: promptType, platform: targetPlatform }),
161
+ });
162
+ if (result.success) {
163
+ setOptimizedPrompt(result);
164
+ toast.success("Prompt optimizado");
165
+ } else toast.error(result.error);
166
+ } catch { toast.error("Error"); }
167
+ finally { setPromptLoading(false); }
168
+ };
169
+
170
+ const handleGenerateImage = async () => {
171
+ if (!userPrompt.trim()) { toast.error("Escribe un prompt"); return; }
172
+ setImageLoading(true);
173
+ try {
174
+ const result = await apiFetch("/generate/image", {
175
+ method: "POST",
176
+ body: JSON.stringify({
177
+ prompt: userPrompt,
178
+ optimizedPrompt: optimizedPrompt?.optimizedPrompt,
179
+ platform: imagePlatform,
180
+ style: imageStyle,
181
+ includePet: includePetInContent,
182
+ petId: pets[0]?.id
183
+ }),
184
+ });
185
+ if (result.success) {
186
+ toast.success("Imagen generada");
187
+ loadData();
188
+ } else toast.error(result.error);
189
+ } catch { toast.error("Error"); }
190
+ finally { setImageLoading(false); }
191
+ };
192
+
193
+ const handleCreateStory = async () => {
194
+ if (!storyPrompt.trim()) { toast.error("Describe tu historia"); return; }
195
+ setStoryLoading(true);
196
+ try {
197
+ const result = await apiFetch("/storytelling", {
198
+ method: "POST",
199
+ body: JSON.stringify({
200
+ prompt: storyPrompt,
201
+ genre: storyGenre,
202
+ totalEpisodes: storyEpisodes,
203
+ }),
204
+ });
205
+ if (result.success) {
206
+ toast.success(`Historia "${result.story?.title}" creada`);
207
+ setStoryPrompt("");
208
+ loadData();
209
+ } else toast.error(result.error);
210
+ } catch { toast.error("Error"); }
211
+ finally { setStoryLoading(false); }
212
+ };
213
+
214
+ const handleCreatePost = async () => {
215
+ if (!postTitle.trim()) { toast.error("Añade un título"); return; }
216
+ try {
217
+ const result = await apiFetch("/posts", {
218
+ method: "POST",
219
+ body: JSON.stringify({
220
+ title: postTitle,
221
+ type: postType,
222
+ caption: postCaption,
223
+ scheduledAt: scheduledTime || null,
224
+ autoGenerateCaption: true,
225
+ }),
226
+ });
227
+ if (result.success) {
228
+ toast.success(scheduledTime ? "Post programado" : "Post creado");
229
+ setPostTitle(""); setPostCaption(""); setScheduledTime("");
230
+ loadData();
231
+ } else toast.error(result.error);
232
+ } catch { toast.error("Error"); }
233
+ };
234
+
235
+ const handleCreateAutomation = async () => {
236
+ if (!automationName.trim()) { toast.error("Dale un nombre"); return; }
237
+ try {
238
+ const result = await apiFetch("/automation", {
239
+ method: "POST",
240
+ body: JSON.stringify({
241
+ name: automationName,
242
+ type: automationType,
243
+ trigger: "schedule",
244
+ triggerConfig: { schedule: "daily:09:00" },
245
+ actions: [{ type: "generate_content", prompt: "Genera contenido de lifestyle" }],
246
+ }),
247
+ });
248
+ if (result.success) {
249
+ toast.success("Automatización creada");
250
+ setAutomationName("");
251
+ loadData();
252
+ } else toast.error(result.error);
253
+ } catch { toast.error("Error"); }
254
+ };
255
+
256
+ const handleToggleAutomation = async (id: string, currentStatus: boolean) => {
257
+ try {
258
+ await apiFetch("/automation", {
259
+ method: "PUT",
260
+ body: JSON.stringify({ id, isActive: !currentStatus }),
261
+ });
262
+ loadData();
263
+ } catch { toast.error("Error"); }
264
+ };
265
+
266
+ const handleCreatePet = async () => {
267
+ if (!petName.trim()) { toast.error("Dale un nombre a tu mascota"); return; }
268
+ setPetLoading(true);
269
+ try {
270
+ const result = await apiFetch("/pets", {
271
+ method: "POST",
272
+ body: JSON.stringify({
273
+ name: petName,
274
+ type: petType,
275
+ breed: petBreed || undefined,
276
+ personality: petPersonality || undefined,
277
+ generateReference: true
278
+ }),
279
+ });
280
+ if (result.success) {
281
+ toast.success(`Mascota "${petName}" creada (+${result.engagementBoost}% engagement)`);
282
+ setPetName(""); setPetBreed(""); setPetPersonality("");
283
+ loadData();
284
+ } else toast.error(result.error);
285
+ } catch { toast.error("Error"); }
286
+ finally { setPetLoading(false); }
287
+ };
288
+
289
+ const handleAnalyzeInfluencers = async () => {
290
+ setInfluencerLoading(true);
291
+ try {
292
+ const result = await apiFetch("/influencers", {
293
+ method: "POST",
294
+ body: JSON.stringify({
295
+ targetNiche: influencerNiche || "lifestyle",
296
+ targetPlatform: influencerPlatform || undefined,
297
+ includePets: influencerWithPets
298
+ }),
299
+ });
300
+ if (result.success) {
301
+ setInfluencers(result.referenceInfluencers || []);
302
+ setInfluencerAnalysis(result.analysis);
303
+ setCharacterConcept(result.characterConcept);
304
+ toast.success("Análisis completado");
305
+ } else toast.error(result.error);
306
+ } catch { toast.error("Error"); }
307
+ finally { setInfluencerLoading(false); }
308
+ };
309
+
310
+ const handleAnalyzeTrends = async () => {
311
+ setTrendLoading(true);
312
+ try {
313
+ const result = await apiFetch("/trends", {
314
+ method: "POST",
315
+ body: JSON.stringify({
316
+ niche: influencerNiche || "lifestyle",
317
+ platform: influencerPlatform || undefined,
318
+ includePets: includePetInContent,
319
+ daysToViral: 14
320
+ }),
321
+ });
322
+ if (result.success) {
323
+ setTrendAnalysis(result.analysis);
324
+ toast.success("Análisis de tendencias completado");
325
+ } else toast.error(result.error);
326
+ } catch { toast.error("Error"); }
327
+ finally { setTrendLoading(false); }
328
+ };
329
+
330
+ const loadTrends = async () => {
331
+ try {
332
+ const result = await apiFetch(`/trends?includePets=${includePetInContent}`);
333
+ if (result.success) {
334
+ setTrends(result.trends);
335
+ setViralStrategies(result.viralStrategies);
336
+ setContentIdeas(result.contentIdeas);
337
+ }
338
+ } catch { toast.error("Error cargando tendencias"); }
339
+ };
340
+
341
+ useEffect(() => {
342
+ if (activeTab === "trends") loadTrends();
343
+ }, [activeTab, includePetInContent]);
344
+
345
+ const copyToClipboard = (text: string) => {
346
+ navigator.clipboard.writeText(text);
347
+ toast.success("Copiado");
348
+ };
349
+
350
+ const getStatusColor = (status: string) => {
351
+ const colors: Record<string, string> = {
352
+ completed: "bg-green-500/10 text-green-500",
353
+ published: "bg-green-500/10 text-green-500",
354
+ scheduled: "bg-blue-500/10 text-blue-500",
355
+ draft: "bg-slate-500/10 text-slate-400",
356
+ pending: "bg-yellow-500/10 text-yellow-500",
357
+ active: "bg-green-500/10 text-green-500",
358
+ };
359
+ return colors[status] || "bg-slate-500/10 text-slate-400";
360
+ };
361
+
362
+ const petTypes = [
363
+ { value: "dog", label: "🐕 Perro" },
364
+ { value: "cat", label: "🐱 Gato" },
365
+ { value: "bird", label: "🐦 Pájaro" },
366
+ { value: "rabbit", label: "🐰 Conejo" },
367
+ { value: "hamster", label: "🐹 Hámster" }
368
+ ];
369
+
370
+ return (
371
+ <div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 text-white">
372
+ {/* Sidebar */}
373
+ <AnimatePresence mode="wait">
374
+ {sidebarOpen && (
375
+ <motion.aside
376
+ initial={{ x: -280 }} animate={{ x: 0 }} exit={{ x: -280 }}
377
+ className="fixed left-0 top-0 h-full w-64 bg-slate-900/90 backdrop-blur-xl border-r border-slate-800 z-50"
378
+ >
379
+ <div className="p-5">
380
+ <div className="flex items-center gap-3 mb-6">
381
+ <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center">
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
+
390
+ <nav className="space-y-1">
391
+ {[
392
+ { id: "prompt-engineer", icon: Wand2, label: "Ingeniero IA" },
393
+ { id: "images", icon: ImageIcon, label: "Imágenes" },
394
+ { id: "videos", icon: Video, label: "Videos" },
395
+ { id: "monetization", icon: DollarSign, label: "Monetización" },
396
+ { id: "posts", icon: Calendar, label: "Publicaciones" },
397
+ { id: "storytelling", icon: Film, label: "Storytelling" },
398
+ { id: "automation", icon: Zap, label: "Automatización" },
399
+ { id: "influencers", icon: Users2, label: "Influencers IA" },
400
+ { id: "pets", icon: PawPrint, label: "Mascotas" },
401
+ { id: "trends", icon: TrendingUp, label: "Tendencias" },
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
+ ))}
410
+ </nav>
411
+ </div>
412
+
413
+ <div className="absolute bottom-0 left-0 right-0 p-4 border-t border-slate-800">
414
+ <div className="grid grid-cols-2 gap-2 text-center text-xs">
415
+ <div><p className="text-xl font-bold text-violet-400">{stats.images}</p><p className="text-slate-500">Imágenes</p></div>
416
+ <div><p className="text-xl font-bold text-blue-400">{stats.videos}</p><p className="text-slate-500">Videos</p></div>
417
+ <div><p className="text-xl font-bold text-green-400">{stats.stories}</p><p className="text-slate-500">Historias</p></div>
418
+ <div><p className="text-xl font-bold text-amber-400">{stats.pets}</p><p className="text-slate-500">Mascotas</p></div>
419
+ </div>
420
+ </div>
421
+ </motion.aside>
422
+ )}
423
+ </AnimatePresence>
424
+
425
+ {/* Main */}
426
+ <div className={`transition-all duration-300 ${sidebarOpen ? "ml-64" : "ml-0"}`}>
427
+ <header className="sticky top-0 z-40 bg-slate-950/80 backdrop-blur-xl border-b border-slate-800">
428
+ <div className="flex items-center justify-between px-6 py-3">
429
+ <div className="flex items-center gap-3">
430
+ <Button variant="ghost" size="icon" onClick={() => setSidebarOpen(!sidebarOpen)} className="text-slate-400">
431
+ {sidebarOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
432
+ </Button>
433
+ <h2 className="text-lg font-semibold capitalize">{activeTab.replace("-", " ")}</h2>
434
+ </div>
435
+ <Button variant="outline" size="sm" onClick={loadData} disabled={loading} className="border-slate-700">
436
+ <RefreshCw className={`h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}`} />Actualizar
437
+ </Button>
438
+ </div>
439
+ </header>
440
+
441
+ <main className="p-6">
442
+ {/* Prompt Engineer */}
443
+ {activeTab === "prompt-engineer" && (
444
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
445
+ <Card className="bg-slate-900/50 border-slate-800">
446
+ <CardHeader>
447
+ <CardTitle className="flex items-center gap-2"><Wand2 className="h-5 w-5 text-violet-400" />Ingeniero de Prompts</CardTitle>
448
+ <CardDescription>Describe en lenguaje natural lo que quieres crear</CardDescription>
449
+ </CardHeader>
450
+ <CardContent className="space-y-4">
451
+ <Textarea placeholder="Ej: Quiero fotos de una mujer rubia en la playa para OnlyFans..." value={userPrompt} onChange={(e) => setUserPrompt(e.target.value)} className="bg-slate-800 border-slate-700 min-h-28" />
452
+ <div className="grid grid-cols-2 gap-4">
453
+ <div>
454
+ <Label className="text-xs text-slate-400">Tipo</Label>
455
+ <Select value={promptType} onValueChange={setPromptType}>
456
+ <SelectTrigger className="bg-slate-800 border-slate-700 mt-1"><SelectValue /></SelectTrigger>
457
+ <SelectContent>
458
+ <SelectItem value="image">Imagen</SelectItem>
459
+ <SelectItem value="video">Video</SelectItem>
460
+ <SelectItem value="reel">Reel</SelectItem>
461
+ <SelectItem value="carousel">Carrusel</SelectItem>
462
+ </SelectContent>
463
+ </Select>
464
+ </div>
465
+ <div>
466
+ <Label className="text-xs text-slate-400">Plataforma</Label>
467
+ <Select value={targetPlatform} onValueChange={setTargetPlatform}>
468
+ <SelectTrigger className="bg-slate-800 border-slate-700 mt-1"><SelectValue /></SelectTrigger>
469
+ <SelectContent>
470
+ <SelectItem value="general">General</SelectItem>
471
+ <SelectItem value="onlyfans">OnlyFans</SelectItem>
472
+ <SelectItem value="patreon">Patreon</SelectItem>
473
+ <SelectItem value="instagram">Instagram</SelectItem>
474
+ <SelectItem value="tiktok">TikTok</SelectItem>
475
+ <SelectItem value="youtube">YouTube</SelectItem>
476
+ </SelectContent>
477
+ </Select>
478
+ </div>
479
+ </div>
480
+ {pets.length > 0 && (
481
+ <div className="flex items-center gap-2">
482
+ <Switch checked={includePetInContent} onCheckedChange={setIncludePetInContent} />
483
+ <Label className="text-sm text-slate-400">Incluir mascota en el contenido (+35% engagement)</Label>
484
+ </div>
485
+ )}
486
+ <Button onClick={handleOptimizePrompt} disabled={promptLoading} className="w-full bg-violet-600 hover:bg-violet-700">
487
+ {promptLoading ? <RefreshCw className="h-4 w-4 mr-2 animate-spin" /> : <Wand2 className="h-4 w-4 mr-2" />}Optimizar
488
+ </Button>
489
+ </CardContent>
490
+ </Card>
491
+
492
+ <Card className="bg-slate-900/50 border-slate-800">
493
+ <CardHeader><CardTitle>Prompt Optimizado</CardTitle></CardHeader>
494
+ <CardContent>
495
+ {optimizedPrompt ? (
496
+ <div className="space-y-3">
497
+ <div className="p-3 rounded-lg bg-slate-800/50 border border-slate-700">
498
+ <div className="flex justify-between mb-2">
499
+ <Badge className="bg-violet-500/20 text-violet-400">{String(optimizedPrompt.type)}</Badge>
500
+ <Button variant="ghost" size="sm" onClick={() => copyToClipboard(String(optimizedPrompt.optimizedPrompt))}><Copy className="h-4 w-4" /></Button>
501
+ </div>
502
+ <p className="text-sm whitespace-pre-wrap">{String(optimizedPrompt.optimizedPrompt)}</p>
503
+ </div>
504
+ <div className="flex gap-2">
505
+ <Button onClick={handleGenerateImage} disabled={imageLoading} className="flex-1 bg-green-600 hover:bg-green-700">
506
+ <ImageIcon className="h-4 w-4 mr-2" />Generar Imagen
507
+ </Button>
508
+ <Button onClick={() => setActiveTab("posts")} className="flex-1 bg-blue-600 hover:bg-blue-700">
509
+ <Calendar className="h-4 w-4 mr-2" />Crear Post
510
+ </Button>
511
+ </div>
512
+ </div>
513
+ ) : (
514
+ <div className="text-center py-12 text-slate-400">
515
+ <Wand2 className="h-12 w-12 mx-auto mb-3 opacity-50" /><p>El prompt optimizado aparecerá aquí</p>
516
+ </div>
517
+ )}
518
+ </CardContent>
519
+ </Card>
520
+ </div>
521
+ )}
522
+
523
+ {/* Images */}
524
+ {activeTab === "images" && (
525
+ <Card className="bg-slate-900/50 border-slate-800">
526
+ <CardHeader><CardTitle className="flex items-center gap-2"><ImageIcon className="h-5 w-5 text-green-400" />Generar Imágenes</CardTitle></CardHeader>
527
+ <CardContent className="space-y-4">
528
+ <Textarea placeholder="Describe la imagen..." value={userPrompt} onChange={(e) => setUserPrompt(e.target.value)} className="bg-slate-800 border-slate-700" />
529
+ <div className="grid grid-cols-3 gap-4">
530
+ <div>
531
+ <Label className="text-xs text-slate-400">Estilo</Label>
532
+ <Select value={imageStyle} onValueChange={setImageStyle}>
533
+ <SelectTrigger className="bg-slate-800 border-slate-700 mt-1"><SelectValue /></SelectTrigger>
534
+ <SelectContent>
535
+ <SelectItem value="realistic">Realista</SelectItem>
536
+ <SelectItem value="anime">Anime</SelectItem>
537
+ <SelectItem value="artistic">Artístico</SelectItem>
538
+ </SelectContent>
539
+ </Select>
540
+ </div>
541
+ <div>
542
+ <Label className="text-xs text-slate-400">Plataforma</Label>
543
+ <Select value={imagePlatform} onValueChange={setImagePlatform}>
544
+ <SelectTrigger className="bg-slate-800 border-slate-700 mt-1"><SelectValue /></SelectTrigger>
545
+ <SelectContent>
546
+ <SelectItem value="general">General</SelectItem>
547
+ <SelectItem value="onlyfans">OnlyFans</SelectItem>
548
+ <SelectItem value="instagram">Instagram</SelectItem>
549
+ </SelectContent>
550
+ </Select>
551
+ </div>
552
+ </div>
553
+ {pets.length > 0 && (
554
+ <div className="flex items-center gap-2">
555
+ <Switch checked={includePetInContent} onCheckedChange={setIncludePetInContent} />
556
+ <Label className="text-sm text-slate-400">Incluir mascota ({pets[0]?.name})</Label>
557
+ </div>
558
+ )}
559
+ <Button onClick={handleGenerateImage} disabled={imageLoading} className="w-full bg-green-600 hover:bg-green-700">
560
+ {imageLoading ? <RefreshCw className="h-4 w-4 mr-2 animate-spin" /> : <ImageIcon className="h-4 w-4 mr-2" />}Generar
561
+ </Button>
562
+ </CardContent>
563
+ </Card>
564
+ )}
565
+
566
+ {/* Monetization */}
567
+ {activeTab === "monetization" && (
568
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
569
+ {[
570
+ { name: "OnlyFans", type: "subscription", fee: "20%", color: "from-blue-500 to-cyan-500", legal: "Adult content permitido" },
571
+ { name: "Patreon", type: "subscription", fee: "12%", color: "from-orange-500 to-red-500", legal: "Sin adult content real" },
572
+ { name: "Fansly", type: "mixed", fee: "20%", color: "from-purple-500 to-pink-500", legal: "Adult content permitido" },
573
+ { name: "Fanvue", type: "subscription", fee: "15%", color: "from-green-500 to-teal-500", legal: "Adult content permitido" },
574
+ { name: "Ko-fi", type: "tips", fee: "0%", color: "from-sky-500 to-blue-500", legal: "Sin adult content" },
575
+ { name: "Instagram", type: "free", fee: "0%", color: "from-pink-500 to-purple-500", legal: "Sin desnudez" },
576
+ ].map((p) => (
577
+ <Card key={p.name} className="bg-slate-900/50 border-slate-800">
578
+ <CardContent className="p-4">
579
+ <div className={`w-10 h-10 rounded-lg bg-gradient-to-br ${p.color} flex items-center justify-center mb-3`}>
580
+ <DollarSign className="h-5 w-5 text-white" />
581
+ </div>
582
+ <h3 className="font-semibold">{p.name}</h3>
583
+ <p className="text-xs text-slate-400 mt-1">Fee: {p.fee}</p>
584
+ <Badge variant="outline" className="mt-2 text-xs">{p.type}</Badge>
585
+ <p className="text-xs text-amber-400 mt-2">⚖️ {p.legal}</p>
586
+ <Button size="sm" className="w-full mt-3 bg-violet-600 hover:bg-violet-700">
587
+ <Plus className="h-3 w-3 mr-1" />Conectar
588
+ </Button>
589
+ </CardContent>
590
+ </Card>
591
+ ))}
592
+ </div>
593
+ )}
594
+
595
+ {/* Posts */}
596
+ {activeTab === "posts" && (
597
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
598
+ <Card className="bg-slate-900/50 border-slate-800">
599
+ <CardHeader><CardTitle className="flex items-center gap-2"><Calendar className="h-5 w-5 text-blue-400" />Crear Publicación</CardTitle></CardHeader>
600
+ <CardContent className="space-y-4">
601
+ <Input placeholder="Título" value={postTitle} onChange={(e) => setPostTitle(e.target.value)} className="bg-slate-800 border-slate-700" />
602
+ <Textarea placeholder="Caption..." value={postCaption} onChange={(e) => setPostCaption(e.target.value)} className="bg-slate-800 border-slate-700 min-h-20" />
603
+ <div className="grid grid-cols-2 gap-4">
604
+ <div>
605
+ <Label className="text-xs text-slate-400">Tipo</Label>
606
+ <Select value={postType} onValueChange={setPostType}>
607
+ <SelectTrigger className="bg-slate-800 border-slate-700 mt-1"><SelectValue /></SelectTrigger>
608
+ <SelectContent>
609
+ <SelectItem value="reel">Reel</SelectItem>
610
+ <SelectItem value="photo">Foto</SelectItem>
611
+ <SelectItem value="carousel">Carrusel</SelectItem>
612
+ <SelectItem value="story">Story</SelectItem>
613
+ </SelectContent>
614
+ </Select>
615
+ </div>
616
+ <div>
617
+ <Label className="text-xs text-slate-400">Programar</Label>
618
+ <Input type="datetime-local" value={scheduledTime} onChange={(e) => setScheduledTime(e.target.value)} className="bg-slate-800 border-slate-700 mt-1" />
619
+ </div>
620
+ </div>
621
+ <Button onClick={handleCreatePost} className="w-full bg-blue-600 hover:bg-blue-700">
622
+ <Calendar className="h-4 w-4 mr-2" />{scheduledTime ? "Programar" : "Crear"}
623
+ </Button>
624
+ </CardContent>
625
+ </Card>
626
+
627
+ <Card className="bg-slate-900/50 border-slate-800">
628
+ <CardHeader><CardTitle>Publicaciones</CardTitle></CardHeader>
629
+ <CardContent>
630
+ <ScrollArea className="h-64">
631
+ {posts.length === 0 ? (
632
+ <p className="text-slate-400 text-center py-8">No hay publicaciones</p>
633
+ ) : (
634
+ <div className="space-y-2">
635
+ {posts.map((p) => (
636
+ <div key={p.id} className="p-3 rounded-lg bg-slate-800/50 flex items-center justify-between">
637
+ <div>
638
+ <p className="font-medium text-sm">{p.title || "Sin título"}</p>
639
+ <p className="text-xs text-slate-400">{p.type} • {p.scheduledAt ? new Date(p.scheduledAt).toLocaleString() : "Borrador"}</p>
640
+ </div>
641
+ <Badge className={getStatusColor(p.status)}>{p.status}</Badge>
642
+ </div>
643
+ ))}
644
+ </div>
645
+ )}
646
+ </ScrollArea>
647
+ </CardContent>
648
+ </Card>
649
+ </div>
650
+ )}
651
+
652
+ {/* Storytelling */}
653
+ {activeTab === "storytelling" && (
654
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
655
+ <Card className="bg-slate-900/50 border-slate-800">
656
+ <CardHeader><CardTitle className="flex items-center gap-2"><Film className="h-5 w-5 text-purple-400" />Crear Historia</CardTitle></CardHeader>
657
+ <CardContent className="space-y-4">
658
+ <Textarea placeholder="Describe tu historia... Ej: Una historia de transformación fitness de 30 días..." value={storyPrompt} onChange={(e) => setStoryPrompt(e.target.value)} className="bg-slate-800 border-slate-700 min-h-24" />
659
+ <div className="grid grid-cols-2 gap-4">
660
+ <div>
661
+ <Label className="text-xs text-slate-400">Género</Label>
662
+ <Select value={storyGenre} onValueChange={setStoryGenre}>
663
+ <SelectTrigger className="bg-slate-800 border-slate-700 mt-1"><SelectValue /></SelectTrigger>
664
+ <SelectContent>
665
+ <SelectItem value="lifestyle">Lifestyle</SelectItem>
666
+ <SelectItem value="fitness">Fitness</SelectItem>
667
+ <SelectItem value="romance">Romance</SelectItem>
668
+ <SelectItem value="drama">Drama</SelectItem>
669
+ <SelectItem value="comedy">Comedia</SelectItem>
670
+ </SelectContent>
671
+ </Select>
672
+ </div>
673
+ <div>
674
+ <Label className="text-xs text-slate-400">Episodios</Label>
675
+ <Input type="number" value={storyEpisodes} onChange={(e) => setStoryEpisodes(parseInt(e.target.value) || 7)} className="bg-slate-800 border-slate-700 mt-1" />
676
+ </div>
677
+ </div>
678
+ <Button onClick={handleCreateStory} disabled={storyLoading} className="w-full bg-purple-600 hover:bg-purple-700">
679
+ {storyLoading ? <RefreshCw className="h-4 w-4 mr-2 animate-spin" /> : <Sparkles className="h-4 w-4 mr-2" />}Generar Historia
680
+ </Button>
681
+ </CardContent>
682
+ </Card>
683
+
684
+ <Card className="bg-slate-900/50 border-slate-800">
685
+ <CardHeader><CardTitle>Historias Creadas</CardTitle></CardHeader>
686
+ <CardContent>
687
+ <ScrollArea className="h-64">
688
+ {stories.length === 0 ? (
689
+ <p className="text-slate-400 text-center py-8">No hay historias</p>
690
+ ) : (
691
+ <div className="space-y-2">
692
+ {stories.map((s) => (
693
+ <div key={s.id} className="p-3 rounded-lg bg-slate-800/50">
694
+ <div className="flex justify-between items-start">
695
+ <div>
696
+ <p className="font-medium">{s.title}</p>
697
+ <p className="text-xs text-slate-400">{s.totalEpisodes} episodios • {s.genre}</p>
698
+ </div>
699
+ <Badge className={getStatusColor(s.status)}>{s.status}</Badge>
700
+ </div>
701
+ </div>
702
+ ))}
703
+ </div>
704
+ )}
705
+ </ScrollArea>
706
+ </CardContent>
707
+ </Card>
708
+ </div>
709
+ )}
710
+
711
+ {/* Automation */}
712
+ {activeTab === "automation" && (
713
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
714
+ <Card className="bg-slate-900/50 border-slate-800">
715
+ <CardHeader><CardTitle className="flex items-center gap-2"><Zap className="h-5 w-5 text-amber-400" />Crear Automatización</CardTitle></CardHeader>
716
+ <CardContent className="space-y-4">
717
+ <Input placeholder="Nombre de la automatización" value={automationName} onChange={(e) => setAutomationName(e.target.value)} className="bg-slate-800 border-slate-700" />
718
+ <div>
719
+ <Label className="text-xs text-slate-400">Tipo</Label>
720
+ <Select value={automationType} onValueChange={setAutomationType}>
721
+ <SelectTrigger className="bg-slate-800 border-slate-700 mt-1"><SelectValue /></SelectTrigger>
722
+ <SelectContent>
723
+ <SelectItem value="content_generation">Generación de Contenido</SelectItem>
724
+ <SelectItem value="posting">Publicación Automática</SelectItem>
725
+ <SelectItem value="cross_posting">Cross-Posting</SelectItem>
726
+ <SelectItem value="trend_tracking">Seguimiento de Tendencias</SelectItem>
727
+ </SelectContent>
728
+ </Select>
729
+ </div>
730
+ <Button onClick={handleCreateAutomation} className="w-full bg-amber-600 hover:bg-amber-700">
731
+ <Zap className="h-4 w-4 mr-2" />Crear
732
+ </Button>
733
+ </CardContent>
734
+ </Card>
735
+
736
+ <Card className="bg-slate-900/50 border-slate-800">
737
+ <CardHeader><CardTitle>Automatizaciones Activas</CardTitle></CardHeader>
738
+ <CardContent>
739
+ <ScrollArea className="h-64">
740
+ {automations.length === 0 ? (
741
+ <p className="text-slate-400 text-center py-8">No hay automatizaciones</p>
742
+ ) : (
743
+ <div className="space-y-2">
744
+ {automations.map((a) => (
745
+ <div key={a.id} className="p-3 rounded-lg bg-slate-800/50 flex items-center justify-between">
746
+ <div>
747
+ <p className="font-medium text-sm">{a.name}</p>
748
+ <p className="text-xs text-slate-400">{a.type} • {a.runCount} ejecuciones</p>
749
+ </div>
750
+ <Button size="sm" variant="ghost" onClick={() => handleToggleAutomation(a.id, a.isActive)}>
751
+ {a.isActive ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
752
+ </Button>
753
+ </div>
754
+ ))}
755
+ </div>
756
+ )}
757
+ </ScrollArea>
758
+ </CardContent>
759
+ </Card>
760
+ </div>
761
+ )}
762
+
763
+ {/* Influencers IA */}
764
+ {activeTab === "influencers" && (
765
+ <div className="space-y-6">
766
+ <Card className="bg-slate-900/50 border-slate-800">
767
+ <CardHeader>
768
+ <CardTitle className="flex items-center gap-2"><Users2 className="h-5 w-5 text-pink-400" />Análisis de Influencers IA Famosos</CardTitle>
769
+ <CardDescription>Estudia los patrones de influencers IA exitosos para aplicarlos a tu contenido</CardDescription>
770
+ </CardHeader>
771
+ <CardContent className="space-y-4">
772
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
773
+ <div>
774
+ <Label className="text-xs text-slate-400">Tu Nicho</Label>
775
+ <Input placeholder="lifestyle, fashion..." value={influencerNiche} onChange={(e) => setInfluencerNiche(e.target.value)} className="bg-slate-800 border-slate-700 mt-1" />
776
+ </div>
777
+ <div>
778
+ <Label className="text-xs text-slate-400">Plataforma Objetivo</Label>
779
+ <Select value={influencerPlatform} onValueChange={setInfluencerPlatform}>
780
+ <SelectTrigger className="bg-slate-800 border-slate-700 mt-1"><SelectValue placeholder="Todas" /></SelectTrigger>
781
+ <SelectContent>
782
+ <SelectItem value="">Todas</SelectItem>
783
+ <SelectItem value="instagram">Instagram</SelectItem>
784
+ <SelectItem value="tiktok">TikTok</SelectItem>
785
+ <SelectItem value="youtube">YouTube</SelectItem>
786
+ </SelectContent>
787
+ </Select>
788
+ </div>
789
+ <div className="flex items-end">
790
+ <div className="flex items-center gap-2 pb-2">
791
+ <Switch checked={influencerWithPets} onCheckedChange={setInfluencerWithPets} />
792
+ <Label className="text-sm text-slate-400">Con mascotas</Label>
793
+ </div>
794
+ </div>
795
+ <div className="flex items-end">
796
+ <Button onClick={handleAnalyzeInfluencers} disabled={influencerLoading} className="w-full bg-pink-600 hover:bg-pink-700">
797
+ {influencerLoading ? <RefreshCw className="h-4 w-4 mr-2 animate-spin" /> : <Lightbulb className="h-4 w-4 mr-2" />}Analizar
798
+ </Button>
799
+ </div>
800
+ </div>
801
+ </CardContent>
802
+ </Card>
803
+
804
+ {influencers.length > 0 && (
805
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
806
+ {influencers.map((inf, i) => (
807
+ <Card key={i} className="bg-slate-900/50 border-slate-800">
808
+ <CardContent className="p-4">
809
+ <div className="flex items-center gap-2 mb-2">
810
+ <div className="w-8 h-8 rounded-full bg-gradient-to-br from-pink-500 to-purple-500 flex items-center justify-center">
811
+ <Star className="h-4 w-4 text-white" />
812
+ </div>
813
+ <div>
814
+ <p className="font-medium text-sm">{inf.name}</p>
815
+ <p className="text-xs text-slate-400">{inf.handle}</p>
816
+ </div>
817
+ </div>
818
+ <div className="space-y-1 text-xs">
819
+ <p><span className="text-slate-400">Seguidores:</span> <span className="text-green-400">{(inf.followers / 1000000).toFixed(1)}M</span></p>
820
+ <p><span className="text-slate-400">Engagement:</span> <span className="text-blue-400">{inf.engagement}%</span></p>
821
+ <p><span className="text-slate-400">Nicho:</span> {inf.niche}</p>
822
+ {inf.petCompanion && <Badge className="bg-amber-500/20 text-amber-400 mt-1"><PawPrint className="h-3 w-3 mr-1" />{inf.petType}</Badge>}
823
+ </div>
824
+ <div className="mt-2 pt-2 border-t border-slate-700">
825
+ <p className="text-xs text-slate-400">Lecciones:</p>
826
+ <ul className="text-xs mt-1 space-y-1">
827
+ {inf.keyLessons.slice(0, 2).map((l, j) => (
828
+ <li key={j} className="text-slate-300">• {l}</li>
829
+ ))}
830
+ </ul>
831
+ </div>
832
+ </CardContent>
833
+ </Card>
834
+ ))}
835
+ </div>
836
+ )}
837
+
838
+ {characterConcept && (
839
+ <Card className="bg-slate-900/50 border-slate-800 border-violet-500/30">
840
+ <CardHeader>
841
+ <CardTitle className="flex items-center gap-2 text-violet-400"><Sparkles className="h-5 w-5" />Concepto de Personaje Sugerido</CardTitle>
842
+ </CardHeader>
843
+ <CardContent>
844
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
845
+ <div>
846
+ <h4 className="font-medium mb-2">Personaje</h4>
847
+ <div className="p-3 rounded-lg bg-slate-800/50 text-sm">
848
+ {characterConcept.character && (
849
+ <>
850
+ <p><strong>Nombre:</strong> {String(characterConcept.character.name || "")}</p>
851
+ <p className="mt-1"><strong>Personalidad:</strong> {String(characterConcept.character.personality || "")}</p>
852
+ <p className="mt-1"><strong>Historia:</strong> {String(characterConcept.character.backstory || "")}</p>
853
+ </>
854
+ )}
855
+ </div>
856
+ </div>
857
+ <div>
858
+ <h4 className="font-medium mb-2">Estilo Visual</h4>
859
+ <div className="p-3 rounded-lg bg-slate-800/50 text-sm">
860
+ {characterConcept.visualStyle && (
861
+ <>
862
+ <p><strong>Estética:</strong> {String(characterConcept.visualStyle.aesthetic || "")}</p>
863
+ <p className="mt-1"><strong>Colores:</strong> {Array.isArray(characterConcept.visualStyle.colors) ? characterConcept.visualStyle.colors.join(", ") : ""}</p>
864
+ </>
865
+ )}
866
+ </div>
867
+ {Array.isArray(characterConcept.hashtags) && characterConcept.hashtags.length > 0 && (
868
+ <div className="mt-3 flex flex-wrap gap-1">
869
+ {characterConcept.hashtags.slice(0, 10).map((tag: string, i: number) => (
870
+ <Badge key={i} variant="outline" className="text-xs">{tag}</Badge>
871
+ ))}
872
+ </div>
873
+ )}
874
+ </div>
875
+ </div>
876
+ </CardContent>
877
+ </Card>
878
+ )}
879
+ </div>
880
+ )}
881
+
882
+ {/* Pets */}
883
+ {activeTab === "pets" && (
884
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
885
+ <Card className="bg-slate-900/50 border-slate-800">
886
+ <CardHeader>
887
+ <CardTitle className="flex items-center gap-2"><PawPrint className="h-5 w-5 text-amber-400" />Crear Mascota</CardTitle>
888
+ <CardDescription>Las mascotas aumentan el engagement hasta un 35%</CardDescription>
889
+ </CardHeader>
890
+ <CardContent className="space-y-4">
891
+ <div className="grid grid-cols-2 gap-4">
892
+ <div>
893
+ <Label className="text-xs text-slate-400">Nombre</Label>
894
+ <Input placeholder="Max, Luna..." value={petName} onChange={(e) => setPetName(e.target.value)} className="bg-slate-800 border-slate-700 mt-1" />
895
+ </div>
896
+ <div>
897
+ <Label className="text-xs text-slate-400">Tipo</Label>
898
+ <Select value={petType} onValueChange={setPetType}>
899
+ <SelectTrigger className="bg-slate-800 border-slate-700 mt-1"><SelectValue /></SelectTrigger>
900
+ <SelectContent>
901
+ {petTypes.map((pt) => (
902
+ <SelectItem key={pt.value} value={pt.value}>{pt.label}</SelectItem>
903
+ ))}
904
+ </SelectContent>
905
+ </Select>
906
+ </div>
907
+ </div>
908
+ <div className="grid grid-cols-2 gap-4">
909
+ <div>
910
+ <Label className="text-xs text-slate-400">Raza (opcional)</Label>
911
+ <Input placeholder="Golden Retriever..." value={petBreed} onChange={(e) => setPetBreed(e.target.value)} className="bg-slate-800 border-slate-700 mt-1" />
912
+ </div>
913
+ <div>
914
+ <Label className="text-xs text-slate-400">Personalidad</Label>
915
+ <Select value={petPersonality} onValueChange={setPetPersonality}>
916
+ <SelectTrigger className="bg-slate-800 border-slate-700 mt-1"><SelectValue placeholder="Seleccionar" /></SelectTrigger>
917
+ <SelectContent>
918
+ <SelectItem value="playful">Juguetón</SelectItem>
919
+ <SelectItem value="calm">Tranquilo</SelectItem>
920
+ <SelectItem value="energetic">Energético</SelectItem>
921
+ <SelectItem value="curious">Curioso</SelectItem>
922
+ <SelectItem value="lazy">Perezoso</SelectItem>
923
+ </SelectContent>
924
+ </Select>
925
+ </div>
926
+ </div>
927
+ <Button onClick={handleCreatePet} disabled={petLoading} className="w-full bg-amber-600 hover:bg-amber-700">
928
+ {petLoading ? <RefreshCw className="h-4 w-4 mr-2 animate-spin" /> : <PawPrint className="h-4 w-4 mr-2" />}Crear Mascota
929
+ </Button>
930
+ </CardContent>
931
+ </Card>
932
+
933
+ <Card className="bg-slate-900/50 border-slate-800">
934
+ <CardHeader><CardTitle>Mis Mascotas</CardTitle></CardHeader>
935
+ <CardContent>
936
+ <ScrollArea className="h-64">
937
+ {pets.length === 0 ? (
938
+ <div className="text-center py-8 text-slate-400">
939
+ <PawPrint className="h-12 w-12 mx-auto mb-3 opacity-50" />
940
+ <p>No tienes mascotas</p>
941
+ <p className="text-xs mt-1">Las mascotas aumentan el engagement</p>
942
+ </div>
943
+ ) : (
944
+ <div className="space-y-2">
945
+ {pets.map((p) => (
946
+ <div key={p.id} className="p-3 rounded-lg bg-slate-800/50 flex items-center justify-between">
947
+ <div className="flex items-center gap-3">
948
+ <div className="w-10 h-10 rounded-lg bg-gradient-to-br from-amber-500 to-orange-500 flex items-center justify-center">
949
+ <PawPrint className="h-5 w-5 text-white" />
950
+ </div>
951
+ <div>
952
+ <p className="font-medium">{p.name}</p>
953
+ <p className="text-xs text-slate-400">{p.breed || p.type} • {p.personality || "Sin personalidad"}</p>
954
+ </div>
955
+ </div>
956
+ <Badge className="bg-green-500/20 text-green-400">+35%</Badge>
957
+ </div>
958
+ ))}
959
+ </div>
960
+ )}
961
+ </ScrollArea>
962
+ </CardContent>
963
+ </Card>
964
+ </div>
965
+ )}
966
+
967
+ {/* Trends */}
968
+ {activeTab === "trends" && (
969
+ <div className="space-y-6">
970
+ <div className="flex items-center gap-4">
971
+ <div className="flex items-center gap-2">
972
+ <Switch checked={includePetInContent} onCheckedChange={setIncludePetInContent} />
973
+ <Label className="text-sm text-slate-400">Incluir tendencias de mascotas</Label>
974
+ </div>
975
+ <Button onClick={handleAnalyzeTrends} disabled={trendLoading} className="bg-violet-600 hover:bg-violet-700">
976
+ {trendLoading ? <RefreshCw className="h-4 w-4 mr-2 animate-spin" /> : <Rocket className="h-4 w-4 mr-2" />}Analizar para Viralizar
977
+ </Button>
978
+ </div>
979
+
980
+ {/* Tendencias Actuales */}
981
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
982
+ {trends.slice(0, 8).map((t, i) => (
983
+ <Card key={i} className="bg-slate-900/50 border-slate-800">
984
+ <CardContent className="p-4">
985
+ <div className="flex items-center justify-between mb-2">
986
+ <Badge variant="outline" className="text-xs">{String(t.platform || "")}</Badge>
987
+ <Flame className={`h-4 w-4 ${Number(t.growth) > 30 ? "text-red-400" : "text-orange-400"}`} />
988
+ </div>
989
+ <p className="font-semibold">{String(t.name)}</p>
990
+ <p className="text-green-400 text-sm">+{Number(t.growth).toFixed(1)}%</p>
991
+ <p className="text-xs text-slate-400 mt-1">{String(t.category || "")}</p>
992
+ </CardContent>
993
+ </Card>
994
+ ))}
995
+ </div>
996
+
997
+ {/* Estrategias Virales */}
998
+ {viralStrategies.length > 0 && (
999
+ <Card className="bg-slate-900/50 border-slate-800">
1000
+ <CardHeader><CardTitle className="flex items-center gap-2"><Rocket className="h-5 w-5 text-violet-400" />Estrategias Virales</CardTitle></CardHeader>
1001
+ <CardContent>
1002
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
1003
+ {viralStrategies.slice(0, 4).map((s, i) => (
1004
+ <div key={i} className="p-3 rounded-lg bg-slate-800/50 border border-slate-700">
1005
+ <div className="flex items-center justify-between mb-2">
1006
+ <p className="font-medium text-sm">{String(s.name)}</p>
1007
+ <Badge className="bg-green-500/20 text-green-400 text-xs">{String(s.successRate)}%</Badge>
1008
+ </div>
1009
+ <p className="text-xs text-slate-400 line-clamp-2">{String(s.description)}</p>
1010
+ <div className="flex flex-wrap gap-1 mt-2">
1011
+ {Array.isArray(s.platforms) && s.platforms.slice(0, 3).map((p: string, j: number) => (
1012
+ <Badge key={j} variant="outline" className="text-xs">{p}</Badge>
1013
+ ))}
1014
+ </div>
1015
+ </div>
1016
+ ))}
1017
+ </div>
1018
+ </CardContent>
1019
+ </Card>
1020
+ )}
1021
+
1022
+ {/* Ideas de Contenido */}
1023
+ {contentIdeas.length > 0 && (
1024
+ <Card className="bg-slate-900/50 border-slate-800">
1025
+ <CardHeader><CardTitle className="flex items-center gap-2"><Lightbulb className="h-5 w-5 text-amber-400" />Ideas de Contenido Viral</CardTitle></CardHeader>
1026
+ <CardContent>
1027
+ <div className="space-y-3">
1028
+ {contentIdeas.slice(0, 5).map((idea, i) => (
1029
+ <div key={i} className="p-3 rounded-lg bg-slate-800/50 border border-slate-700">
1030
+ <div className="flex items-center justify-between">
1031
+ <div>
1032
+ <p className="font-medium">{String(idea.title)}</p>
1033
+ <p className="text-xs text-slate-400 mt-1">{String(idea.description)}</p>
1034
+ {idea.hook && <p className="text-xs text-violet-400 mt-1">Hook: "{String(idea.hook)}"</p>}
1035
+ </div>
1036
+ <div className="text-right">
1037
+ <Badge className="bg-violet-500/20 text-violet-400">{String(idea.format)}</Badge>
1038
+ {idea.viralScore && <p className="text-xs text-green-400 mt-1">Score: {String(idea.viralScore)}</p>}
1039
+ </div>
1040
+ </div>
1041
+ </div>
1042
+ ))}
1043
+ </div>
1044
+ </CardContent>
1045
+ </Card>
1046
+ )}
1047
+
1048
+ {/* Análisis de Tendencias */}
1049
+ {trendAnalysis && (
1050
+ <Card className="bg-slate-900/50 border-slate-800 border-violet-500/30">
1051
+ <CardHeader>
1052
+ <CardTitle className="flex items-center gap-2 text-violet-400"><Target className="h-5 w-5" />Plan para Viralizar</CardTitle>
1053
+ </CardHeader>
1054
+ <CardContent>
1055
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1056
+ <div>
1057
+ <h4 className="font-medium mb-2">Tendencias Emergentes</h4>
1058
+ <div className="space-y-2">
1059
+ {Array.isArray(trendAnalysis.emergingTrends) && trendAnalysis.emergingTrends.slice(0, 3).map((t: Record<string, unknown>, i: number) => (
1060
+ <div key={i} className="p-2 rounded bg-slate-800/50 text-sm">
1061
+ <p className="font-medium">{String(t.name)}</p>
1062
+ <p className="text-xs text-slate-400">Potencial: {String(t.potential)}</p>
1063
+ </div>
1064
+ ))}
1065
+ </div>
1066
+ </div>
1067
+ <div>
1068
+ <h4 className="font-medium mb-2">Recomendaciones</h4>
1069
+ <div className="space-y-1">
1070
+ {Array.isArray(trendAnalysis.recommendations) && trendAnalysis.recommendations.slice(0, 4).map((r: string, i: number) => (
1071
+ <p key={i} className="text-sm text-slate-300">• {r}</p>
1072
+ ))}
1073
+ </div>
1074
+ {trendAnalysis.predictedViralPotential && (
1075
+ <div className="mt-4 p-3 rounded-lg bg-violet-500/10 border border-violet-500/30">
1076
+ <p className="text-sm">Potencial Viral Estimado:</p>
1077
+ <p className="text-2xl font-bold text-violet-400">{String(trendAnalysis.predictedViralPotential)}%</p>
1078
+ </div>
1079
+ )}
1080
+ </div>
1081
+ </div>
1082
+ </CardContent>
1083
+ </Card>
1084
+ )}
1085
+ </div>
1086
+ )}
1087
+
1088
+ {/* Content */}
1089
+ {activeTab === "content" && (
1090
+ <Card className="bg-slate-900/50 border-slate-800">
1091
+ <CardHeader><CardTitle>Contenido Generado</CardTitle></CardHeader>
1092
+ <CardContent>
1093
+ <ScrollArea className="h-96">
1094
+ {contents.length === 0 ? (
1095
+ <p className="text-slate-400 text-center py-8">No hay contenido generado</p>
1096
+ ) : (
1097
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
1098
+ {contents.map((c) => (
1099
+ <div key={c.id} className="p-4 rounded-lg bg-slate-800/50 border border-slate-700">
1100
+ <div className="flex justify-between mb-2">
1101
+ <Badge className={getStatusColor(c.status)}>{c.status}</Badge>
1102
+ <Badge variant="outline">{c.type}</Badge>
1103
+ </div>
1104
+ <p className="font-medium line-clamp-2">{c.title}</p>
1105
+ <p className="text-xs text-slate-400 mt-1">{c.platform}</p>
1106
+ </div>
1107
+ ))}
1108
+ </div>
1109
+ )}
1110
+ </ScrollArea>
1111
+ </CardContent>
1112
+ </Card>
1113
+ )}
1114
+
1115
+ {/* Videos */}
1116
+ {activeTab === "videos" && (
1117
+ <Card className="bg-slate-900/50 border-slate-800">
1118
+ <CardHeader><CardTitle className="flex items-center gap-2"><Video className="h-5 w-5 text-blue-400" />Generar Videos</CardTitle></CardHeader>
1119
+ <CardContent className="space-y-4">
1120
+ <Textarea placeholder="Describe el video..." value={userPrompt} onChange={(e) => setUserPrompt(e.target.value)} className="bg-slate-800 border-slate-700" />
1121
+ <Button className="w-full bg-blue-600 hover:bg-blue-700">
1122
+ <Video className="h-4 w-4 mr-2" />Generar Video
1123
+ </Button>
1124
+ </CardContent>
1125
+ </Card>
1126
+ )}
1127
+ </main>
1128
+ </div>
1129
+ </div>
1130
+ );
1131
+ }
src/components/ui/accordion.tsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AccordionPrimitive from "@radix-ui/react-accordion"
5
+ import { ChevronDownIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function Accordion({
10
+ ...props
11
+ }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
12
+ return <AccordionPrimitive.Root data-slot="accordion" {...props} />
13
+ }
14
+
15
+ function AccordionItem({
16
+ className,
17
+ ...props
18
+ }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
19
+ return (
20
+ <AccordionPrimitive.Item
21
+ data-slot="accordion-item"
22
+ className={cn("border-b last:border-b-0", className)}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ function AccordionTrigger({
29
+ className,
30
+ children,
31
+ ...props
32
+ }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
33
+ return (
34
+ <AccordionPrimitive.Header className="flex">
35
+ <AccordionPrimitive.Trigger
36
+ data-slot="accordion-trigger"
37
+ className={cn(
38
+ "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
39
+ className
40
+ )}
41
+ {...props}
42
+ >
43
+ {children}
44
+ <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
45
+ </AccordionPrimitive.Trigger>
46
+ </AccordionPrimitive.Header>
47
+ )
48
+ }
49
+
50
+ function AccordionContent({
51
+ className,
52
+ children,
53
+ ...props
54
+ }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
55
+ return (
56
+ <AccordionPrimitive.Content
57
+ data-slot="accordion-content"
58
+ className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
59
+ {...props}
60
+ >
61
+ <div className={cn("pt-0 pb-4", className)}>{children}</div>
62
+ </AccordionPrimitive.Content>
63
+ )
64
+ }
65
+
66
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
src/components/ui/alert-dialog.tsx ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { buttonVariants } from "@/components/ui/button"
8
+
9
+ function AlertDialog({
10
+ ...props
11
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
12
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
13
+ }
14
+
15
+ function AlertDialogTrigger({
16
+ ...props
17
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
18
+ return (
19
+ <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
20
+ )
21
+ }
22
+
23
+ function AlertDialogPortal({
24
+ ...props
25
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
26
+ return (
27
+ <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
28
+ )
29
+ }
30
+
31
+ function AlertDialogOverlay({
32
+ className,
33
+ ...props
34
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
35
+ return (
36
+ <AlertDialogPrimitive.Overlay
37
+ data-slot="alert-dialog-overlay"
38
+ className={cn(
39
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
40
+ className
41
+ )}
42
+ {...props}
43
+ />
44
+ )
45
+ }
46
+
47
+ function AlertDialogContent({
48
+ className,
49
+ ...props
50
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
51
+ return (
52
+ <AlertDialogPortal>
53
+ <AlertDialogOverlay />
54
+ <AlertDialogPrimitive.Content
55
+ data-slot="alert-dialog-content"
56
+ className={cn(
57
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
58
+ className
59
+ )}
60
+ {...props}
61
+ />
62
+ </AlertDialogPortal>
63
+ )
64
+ }
65
+
66
+ function AlertDialogHeader({
67
+ className,
68
+ ...props
69
+ }: React.ComponentProps<"div">) {
70
+ return (
71
+ <div
72
+ data-slot="alert-dialog-header"
73
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
74
+ {...props}
75
+ />
76
+ )
77
+ }
78
+
79
+ function AlertDialogFooter({
80
+ className,
81
+ ...props
82
+ }: React.ComponentProps<"div">) {
83
+ return (
84
+ <div
85
+ data-slot="alert-dialog-footer"
86
+ className={cn(
87
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
88
+ className
89
+ )}
90
+ {...props}
91
+ />
92
+ )
93
+ }
94
+
95
+ function AlertDialogTitle({
96
+ className,
97
+ ...props
98
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
99
+ return (
100
+ <AlertDialogPrimitive.Title
101
+ data-slot="alert-dialog-title"
102
+ className={cn("text-lg font-semibold", className)}
103
+ {...props}
104
+ />
105
+ )
106
+ }
107
+
108
+ function AlertDialogDescription({
109
+ className,
110
+ ...props
111
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
112
+ return (
113
+ <AlertDialogPrimitive.Description
114
+ data-slot="alert-dialog-description"
115
+ className={cn("text-muted-foreground text-sm", className)}
116
+ {...props}
117
+ />
118
+ )
119
+ }
120
+
121
+ function AlertDialogAction({
122
+ className,
123
+ ...props
124
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
125
+ return (
126
+ <AlertDialogPrimitive.Action
127
+ className={cn(buttonVariants(), className)}
128
+ {...props}
129
+ />
130
+ )
131
+ }
132
+
133
+ function AlertDialogCancel({
134
+ className,
135
+ ...props
136
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
137
+ return (
138
+ <AlertDialogPrimitive.Cancel
139
+ className={cn(buttonVariants({ variant: "outline" }), className)}
140
+ {...props}
141
+ />
142
+ )
143
+ }
144
+
145
+ export {
146
+ AlertDialog,
147
+ AlertDialogPortal,
148
+ AlertDialogOverlay,
149
+ AlertDialogTrigger,
150
+ AlertDialogContent,
151
+ AlertDialogHeader,
152
+ AlertDialogFooter,
153
+ AlertDialogTitle,
154
+ AlertDialogDescription,
155
+ AlertDialogAction,
156
+ AlertDialogCancel,
157
+ }
src/components/ui/alert.tsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-card text-card-foreground",
12
+ destructive:
13
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ )
21
+
22
+ function Alert({
23
+ className,
24
+ variant,
25
+ ...props
26
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
27
+ return (
28
+ <div
29
+ data-slot="alert"
30
+ role="alert"
31
+ className={cn(alertVariants({ variant }), className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38
+ return (
39
+ <div
40
+ data-slot="alert-title"
41
+ className={cn(
42
+ "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
43
+ className
44
+ )}
45
+ {...props}
46
+ />
47
+ )
48
+ }
49
+
50
+ function AlertDescription({
51
+ className,
52
+ ...props
53
+ }: React.ComponentProps<"div">) {
54
+ return (
55
+ <div
56
+ data-slot="alert-description"
57
+ className={cn(
58
+ "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
59
+ className
60
+ )}
61
+ {...props}
62
+ />
63
+ )
64
+ }
65
+
66
+ export { Alert, AlertTitle, AlertDescription }
src/components/ui/aspect-ratio.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
4
+
5
+ function AspectRatio({
6
+ ...props
7
+ }: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
8
+ return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />
9
+ }
10
+
11
+ export { AspectRatio }
src/components/ui/avatar.tsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Avatar({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
12
+ return (
13
+ <AvatarPrimitive.Root
14
+ data-slot="avatar"
15
+ className={cn(
16
+ "relative flex size-8 shrink-0 overflow-hidden rounded-full",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ function AvatarImage({
25
+ className,
26
+ ...props
27
+ }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
28
+ return (
29
+ <AvatarPrimitive.Image
30
+ data-slot="avatar-image"
31
+ className={cn("aspect-square size-full", className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function AvatarFallback({
38
+ className,
39
+ ...props
40
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
41
+ return (
42
+ <AvatarPrimitive.Fallback
43
+ data-slot="avatar-fallback"
44
+ className={cn(
45
+ "bg-muted flex size-full items-center justify-center rounded-full",
46
+ className
47
+ )}
48
+ {...props}
49
+ />
50
+ )
51
+ }
52
+
53
+ export { Avatar, AvatarImage, AvatarFallback }
src/components/ui/badge.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14
+ secondary:
15
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16
+ destructive:
17
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18
+ outline:
19
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ },
25
+ }
26
+ )
27
+
28
+ function Badge({
29
+ className,
30
+ variant,
31
+ asChild = false,
32
+ ...props
33
+ }: React.ComponentProps<"span"> &
34
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
35
+ const Comp = asChild ? Slot : "span"
36
+
37
+ return (
38
+ <Comp
39
+ data-slot="badge"
40
+ className={cn(badgeVariants({ variant }), className)}
41
+ {...props}
42
+ />
43
+ )
44
+ }
45
+
46
+ export { Badge, badgeVariants }
src/components/ui/breadcrumb.tsx ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { ChevronRight, MoreHorizontal } from "lucide-react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
8
+ return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
9
+ }
10
+
11
+ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
12
+ return (
13
+ <ol
14
+ data-slot="breadcrumb-list"
15
+ className={cn(
16
+ "text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
17
+ className
18
+ )}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+
24
+ function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
25
+ return (
26
+ <li
27
+ data-slot="breadcrumb-item"
28
+ className={cn("inline-flex items-center gap-1.5", className)}
29
+ {...props}
30
+ />
31
+ )
32
+ }
33
+
34
+ function BreadcrumbLink({
35
+ asChild,
36
+ className,
37
+ ...props
38
+ }: React.ComponentProps<"a"> & {
39
+ asChild?: boolean
40
+ }) {
41
+ const Comp = asChild ? Slot : "a"
42
+
43
+ return (
44
+ <Comp
45
+ data-slot="breadcrumb-link"
46
+ className={cn("hover:text-foreground transition-colors", className)}
47
+ {...props}
48
+ />
49
+ )
50
+ }
51
+
52
+ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
53
+ return (
54
+ <span
55
+ data-slot="breadcrumb-page"
56
+ role="link"
57
+ aria-disabled="true"
58
+ aria-current="page"
59
+ className={cn("text-foreground font-normal", className)}
60
+ {...props}
61
+ />
62
+ )
63
+ }
64
+
65
+ function BreadcrumbSeparator({
66
+ children,
67
+ className,
68
+ ...props
69
+ }: React.ComponentProps<"li">) {
70
+ return (
71
+ <li
72
+ data-slot="breadcrumb-separator"
73
+ role="presentation"
74
+ aria-hidden="true"
75
+ className={cn("[&>svg]:size-3.5", className)}
76
+ {...props}
77
+ >
78
+ {children ?? <ChevronRight />}
79
+ </li>
80
+ )
81
+ }
82
+
83
+ function BreadcrumbEllipsis({
84
+ className,
85
+ ...props
86
+ }: React.ComponentProps<"span">) {
87
+ return (
88
+ <span
89
+ data-slot="breadcrumb-ellipsis"
90
+ role="presentation"
91
+ aria-hidden="true"
92
+ className={cn("flex size-9 items-center justify-center", className)}
93
+ {...props}
94
+ >
95
+ <MoreHorizontal className="size-4" />
96
+ <span className="sr-only">More</span>
97
+ </span>
98
+ )
99
+ }
100
+
101
+ export {
102
+ Breadcrumb,
103
+ BreadcrumbList,
104
+ BreadcrumbItem,
105
+ BreadcrumbLink,
106
+ BreadcrumbPage,
107
+ BreadcrumbSeparator,
108
+ BreadcrumbEllipsis,
109
+ }
src/components/ui/button.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14
+ destructive:
15
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16
+ outline:
17
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18
+ secondary:
19
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20
+ ghost:
21
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22
+ link: "text-primary underline-offset-4 hover:underline",
23
+ },
24
+ size: {
25
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
26
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28
+ icon: "size-9",
29
+ },
30
+ },
31
+ defaultVariants: {
32
+ variant: "default",
33
+ size: "default",
34
+ },
35
+ }
36
+ )
37
+
38
+ function Button({
39
+ className,
40
+ variant,
41
+ size,
42
+ asChild = false,
43
+ ...props
44
+ }: React.ComponentProps<"button"> &
45
+ VariantProps<typeof buttonVariants> & {
46
+ asChild?: boolean
47
+ }) {
48
+ const Comp = asChild ? Slot : "button"
49
+
50
+ return (
51
+ <Comp
52
+ data-slot="button"
53
+ className={cn(buttonVariants({ variant, size, className }))}
54
+ {...props}
55
+ />
56
+ )
57
+ }
58
+
59
+ export { Button, buttonVariants }
src/components/ui/calendar.tsx ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import {
5
+ ChevronDownIcon,
6
+ ChevronLeftIcon,
7
+ ChevronRightIcon,
8
+ } from "lucide-react"
9
+ import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
10
+
11
+ import { cn } from "@/lib/utils"
12
+ import { Button, buttonVariants } from "@/components/ui/button"
13
+
14
+ function Calendar({
15
+ className,
16
+ classNames,
17
+ showOutsideDays = true,
18
+ captionLayout = "label",
19
+ buttonVariant = "ghost",
20
+ formatters,
21
+ components,
22
+ ...props
23
+ }: React.ComponentProps<typeof DayPicker> & {
24
+ buttonVariant?: React.ComponentProps<typeof Button>["variant"]
25
+ }) {
26
+ const defaultClassNames = getDefaultClassNames()
27
+
28
+ return (
29
+ <DayPicker
30
+ showOutsideDays={showOutsideDays}
31
+ className={cn(
32
+ "bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
33
+ String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
34
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
35
+ className
36
+ )}
37
+ captionLayout={captionLayout}
38
+ formatters={{
39
+ formatMonthDropdown: (date) =>
40
+ date.toLocaleString("default", { month: "short" }),
41
+ ...formatters,
42
+ }}
43
+ classNames={{
44
+ root: cn("w-fit", defaultClassNames.root),
45
+ months: cn(
46
+ "flex gap-4 flex-col md:flex-row relative",
47
+ defaultClassNames.months
48
+ ),
49
+ month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
50
+ nav: cn(
51
+ "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
52
+ defaultClassNames.nav
53
+ ),
54
+ button_previous: cn(
55
+ buttonVariants({ variant: buttonVariant }),
56
+ "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
57
+ defaultClassNames.button_previous
58
+ ),
59
+ button_next: cn(
60
+ buttonVariants({ variant: buttonVariant }),
61
+ "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
62
+ defaultClassNames.button_next
63
+ ),
64
+ month_caption: cn(
65
+ "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
66
+ defaultClassNames.month_caption
67
+ ),
68
+ dropdowns: cn(
69
+ "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
70
+ defaultClassNames.dropdowns
71
+ ),
72
+ dropdown_root: cn(
73
+ "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
74
+ defaultClassNames.dropdown_root
75
+ ),
76
+ dropdown: cn(
77
+ "absolute bg-popover inset-0 opacity-0",
78
+ defaultClassNames.dropdown
79
+ ),
80
+ caption_label: cn(
81
+ "select-none font-medium",
82
+ captionLayout === "label"
83
+ ? "text-sm"
84
+ : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
85
+ defaultClassNames.caption_label
86
+ ),
87
+ table: "w-full border-collapse",
88
+ weekdays: cn("flex", defaultClassNames.weekdays),
89
+ weekday: cn(
90
+ "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
91
+ defaultClassNames.weekday
92
+ ),
93
+ week: cn("flex w-full mt-2", defaultClassNames.week),
94
+ week_number_header: cn(
95
+ "select-none w-(--cell-size)",
96
+ defaultClassNames.week_number_header
97
+ ),
98
+ week_number: cn(
99
+ "text-[0.8rem] select-none text-muted-foreground",
100
+ defaultClassNames.week_number
101
+ ),
102
+ day: cn(
103
+ "relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
104
+ defaultClassNames.day
105
+ ),
106
+ range_start: cn(
107
+ "rounded-l-md bg-accent",
108
+ defaultClassNames.range_start
109
+ ),
110
+ range_middle: cn("rounded-none", defaultClassNames.range_middle),
111
+ range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
112
+ today: cn(
113
+ "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
114
+ defaultClassNames.today
115
+ ),
116
+ outside: cn(
117
+ "text-muted-foreground aria-selected:text-muted-foreground",
118
+ defaultClassNames.outside
119
+ ),
120
+ disabled: cn(
121
+ "text-muted-foreground opacity-50",
122
+ defaultClassNames.disabled
123
+ ),
124
+ hidden: cn("invisible", defaultClassNames.hidden),
125
+ ...classNames,
126
+ }}
127
+ components={{
128
+ Root: ({ className, rootRef, ...props }) => {
129
+ return (
130
+ <div
131
+ data-slot="calendar"
132
+ ref={rootRef}
133
+ className={cn(className)}
134
+ {...props}
135
+ />
136
+ )
137
+ },
138
+ Chevron: ({ className, orientation, ...props }) => {
139
+ if (orientation === "left") {
140
+ return (
141
+ <ChevronLeftIcon className={cn("size-4", className)} {...props} />
142
+ )
143
+ }
144
+
145
+ if (orientation === "right") {
146
+ return (
147
+ <ChevronRightIcon
148
+ className={cn("size-4", className)}
149
+ {...props}
150
+ />
151
+ )
152
+ }
153
+
154
+ return (
155
+ <ChevronDownIcon className={cn("size-4", className)} {...props} />
156
+ )
157
+ },
158
+ DayButton: CalendarDayButton,
159
+ WeekNumber: ({ children, ...props }) => {
160
+ return (
161
+ <td {...props}>
162
+ <div className="flex size-(--cell-size) items-center justify-center text-center">
163
+ {children}
164
+ </div>
165
+ </td>
166
+ )
167
+ },
168
+ ...components,
169
+ }}
170
+ {...props}
171
+ />
172
+ )
173
+ }
174
+
175
+ function CalendarDayButton({
176
+ className,
177
+ day,
178
+ modifiers,
179
+ ...props
180
+ }: React.ComponentProps<typeof DayButton>) {
181
+ const defaultClassNames = getDefaultClassNames()
182
+
183
+ const ref = React.useRef<HTMLButtonElement>(null)
184
+ React.useEffect(() => {
185
+ if (modifiers.focused) ref.current?.focus()
186
+ }, [modifiers.focused])
187
+
188
+ return (
189
+ <Button
190
+ ref={ref}
191
+ variant="ghost"
192
+ size="icon"
193
+ data-day={day.date.toLocaleDateString()}
194
+ data-selected-single={
195
+ modifiers.selected &&
196
+ !modifiers.range_start &&
197
+ !modifiers.range_end &&
198
+ !modifiers.range_middle
199
+ }
200
+ data-range-start={modifiers.range_start}
201
+ data-range-end={modifiers.range_end}
202
+ data-range-middle={modifiers.range_middle}
203
+ className={cn(
204
+ "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
205
+ defaultClassNames.day,
206
+ className
207
+ )}
208
+ {...props}
209
+ />
210
+ )
211
+ }
212
+
213
+ export { Calendar, CalendarDayButton }
src/components/ui/card.tsx ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Card({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="card-header"
22
+ className={cn(
23
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
24
+ className
25
+ )}
26
+ {...props}
27
+ />
28
+ )
29
+ }
30
+
31
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32
+ return (
33
+ <div
34
+ data-slot="card-title"
35
+ className={cn("leading-none font-semibold", className)}
36
+ {...props}
37
+ />
38
+ )
39
+ }
40
+
41
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <div
44
+ data-slot="card-description"
45
+ className={cn("text-muted-foreground text-sm", className)}
46
+ {...props}
47
+ />
48
+ )
49
+ }
50
+
51
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="card-action"
55
+ className={cn(
56
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
57
+ className
58
+ )}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65
+ return (
66
+ <div
67
+ data-slot="card-content"
68
+ className={cn("px-6", className)}
69
+ {...props}
70
+ />
71
+ )
72
+ }
73
+
74
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75
+ return (
76
+ <div
77
+ data-slot="card-footer"
78
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ export {
85
+ Card,
86
+ CardHeader,
87
+ CardFooter,
88
+ CardTitle,
89
+ CardAction,
90
+ CardDescription,
91
+ CardContent,
92
+ }
src/components/ui/carousel.tsx ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import useEmblaCarousel, {
5
+ type UseEmblaCarouselType,
6
+ } from "embla-carousel-react"
7
+ import { ArrowLeft, ArrowRight } from "lucide-react"
8
+
9
+ import { cn } from "@/lib/utils"
10
+ import { Button } from "@/components/ui/button"
11
+
12
+ type CarouselApi = UseEmblaCarouselType[1]
13
+ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
14
+ type CarouselOptions = UseCarouselParameters[0]
15
+ type CarouselPlugin = UseCarouselParameters[1]
16
+
17
+ type CarouselProps = {
18
+ opts?: CarouselOptions
19
+ plugins?: CarouselPlugin
20
+ orientation?: "horizontal" | "vertical"
21
+ setApi?: (api: CarouselApi) => void
22
+ }
23
+
24
+ type CarouselContextProps = {
25
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0]
26
+ api: ReturnType<typeof useEmblaCarousel>[1]
27
+ scrollPrev: () => void
28
+ scrollNext: () => void
29
+ canScrollPrev: boolean
30
+ canScrollNext: boolean
31
+ } & CarouselProps
32
+
33
+ const CarouselContext = React.createContext<CarouselContextProps | null>(null)
34
+
35
+ function useCarousel() {
36
+ const context = React.useContext(CarouselContext)
37
+
38
+ if (!context) {
39
+ throw new Error("useCarousel must be used within a <Carousel />")
40
+ }
41
+
42
+ return context
43
+ }
44
+
45
+ function Carousel({
46
+ orientation = "horizontal",
47
+ opts,
48
+ setApi,
49
+ plugins,
50
+ className,
51
+ children,
52
+ ...props
53
+ }: React.ComponentProps<"div"> & CarouselProps) {
54
+ const [carouselRef, api] = useEmblaCarousel(
55
+ {
56
+ ...opts,
57
+ axis: orientation === "horizontal" ? "x" : "y",
58
+ },
59
+ plugins
60
+ )
61
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
62
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
63
+
64
+ const onSelect = React.useCallback((api: CarouselApi) => {
65
+ if (!api) return
66
+ setCanScrollPrev(api.canScrollPrev())
67
+ setCanScrollNext(api.canScrollNext())
68
+ }, [])
69
+
70
+ const scrollPrev = React.useCallback(() => {
71
+ api?.scrollPrev()
72
+ }, [api])
73
+
74
+ const scrollNext = React.useCallback(() => {
75
+ api?.scrollNext()
76
+ }, [api])
77
+
78
+ const handleKeyDown = React.useCallback(
79
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
80
+ if (event.key === "ArrowLeft") {
81
+ event.preventDefault()
82
+ scrollPrev()
83
+ } else if (event.key === "ArrowRight") {
84
+ event.preventDefault()
85
+ scrollNext()
86
+ }
87
+ },
88
+ [scrollPrev, scrollNext]
89
+ )
90
+
91
+ React.useEffect(() => {
92
+ if (!api || !setApi) return
93
+ setApi(api)
94
+ }, [api, setApi])
95
+
96
+ React.useEffect(() => {
97
+ if (!api) return
98
+ onSelect(api)
99
+ api.on("reInit", onSelect)
100
+ api.on("select", onSelect)
101
+
102
+ return () => {
103
+ api?.off("select", onSelect)
104
+ }
105
+ }, [api, onSelect])
106
+
107
+ return (
108
+ <CarouselContext.Provider
109
+ value={{
110
+ carouselRef,
111
+ api: api,
112
+ opts,
113
+ orientation:
114
+ orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
115
+ scrollPrev,
116
+ scrollNext,
117
+ canScrollPrev,
118
+ canScrollNext,
119
+ }}
120
+ >
121
+ <div
122
+ onKeyDownCapture={handleKeyDown}
123
+ className={cn("relative", className)}
124
+ role="region"
125
+ aria-roledescription="carousel"
126
+ data-slot="carousel"
127
+ {...props}
128
+ >
129
+ {children}
130
+ </div>
131
+ </CarouselContext.Provider>
132
+ )
133
+ }
134
+
135
+ function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
136
+ const { carouselRef, orientation } = useCarousel()
137
+
138
+ return (
139
+ <div
140
+ ref={carouselRef}
141
+ className="overflow-hidden"
142
+ data-slot="carousel-content"
143
+ >
144
+ <div
145
+ className={cn(
146
+ "flex",
147
+ orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
148
+ className
149
+ )}
150
+ {...props}
151
+ />
152
+ </div>
153
+ )
154
+ }
155
+
156
+ function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
157
+ const { orientation } = useCarousel()
158
+
159
+ return (
160
+ <div
161
+ role="group"
162
+ aria-roledescription="slide"
163
+ data-slot="carousel-item"
164
+ className={cn(
165
+ "min-w-0 shrink-0 grow-0 basis-full",
166
+ orientation === "horizontal" ? "pl-4" : "pt-4",
167
+ className
168
+ )}
169
+ {...props}
170
+ />
171
+ )
172
+ }
173
+
174
+ function CarouselPrevious({
175
+ className,
176
+ variant = "outline",
177
+ size = "icon",
178
+ ...props
179
+ }: React.ComponentProps<typeof Button>) {
180
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
181
+
182
+ return (
183
+ <Button
184
+ data-slot="carousel-previous"
185
+ variant={variant}
186
+ size={size}
187
+ className={cn(
188
+ "absolute size-8 rounded-full",
189
+ orientation === "horizontal"
190
+ ? "top-1/2 -left-12 -translate-y-1/2"
191
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
192
+ className
193
+ )}
194
+ disabled={!canScrollPrev}
195
+ onClick={scrollPrev}
196
+ {...props}
197
+ >
198
+ <ArrowLeft />
199
+ <span className="sr-only">Previous slide</span>
200
+ </Button>
201
+ )
202
+ }
203
+
204
+ function CarouselNext({
205
+ className,
206
+ variant = "outline",
207
+ size = "icon",
208
+ ...props
209
+ }: React.ComponentProps<typeof Button>) {
210
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
211
+
212
+ return (
213
+ <Button
214
+ data-slot="carousel-next"
215
+ variant={variant}
216
+ size={size}
217
+ className={cn(
218
+ "absolute size-8 rounded-full",
219
+ orientation === "horizontal"
220
+ ? "top-1/2 -right-12 -translate-y-1/2"
221
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
222
+ className
223
+ )}
224
+ disabled={!canScrollNext}
225
+ onClick={scrollNext}
226
+ {...props}
227
+ >
228
+ <ArrowRight />
229
+ <span className="sr-only">Next slide</span>
230
+ </Button>
231
+ )
232
+ }
233
+
234
+ export {
235
+ type CarouselApi,
236
+ Carousel,
237
+ CarouselContent,
238
+ CarouselItem,
239
+ CarouselPrevious,
240
+ CarouselNext,
241
+ }
src/components/ui/chart.tsx ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as RechartsPrimitive from "recharts"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ // Format: { THEME_NAME: CSS_SELECTOR }
9
+ const THEMES = { light: "", dark: ".dark" } as const
10
+
11
+ export type ChartConfig = {
12
+ [k in string]: {
13
+ label?: React.ReactNode
14
+ icon?: React.ComponentType
15
+ } & (
16
+ | { color?: string; theme?: never }
17
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
18
+ )
19
+ }
20
+
21
+ type ChartContextProps = {
22
+ config: ChartConfig
23
+ }
24
+
25
+ const ChartContext = React.createContext<ChartContextProps | null>(null)
26
+
27
+ function useChart() {
28
+ const context = React.useContext(ChartContext)
29
+
30
+ if (!context) {
31
+ throw new Error("useChart must be used within a <ChartContainer />")
32
+ }
33
+
34
+ return context
35
+ }
36
+
37
+ function ChartContainer({
38
+ id,
39
+ className,
40
+ children,
41
+ config,
42
+ ...props
43
+ }: React.ComponentProps<"div"> & {
44
+ config: ChartConfig
45
+ children: React.ComponentProps<
46
+ typeof RechartsPrimitive.ResponsiveContainer
47
+ >["children"]
48
+ }) {
49
+ const uniqueId = React.useId()
50
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
51
+
52
+ return (
53
+ <ChartContext.Provider value={{ config }}>
54
+ <div
55
+ data-slot="chart"
56
+ data-chart={chartId}
57
+ className={cn(
58
+ "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
59
+ className
60
+ )}
61
+ {...props}
62
+ >
63
+ <ChartStyle id={chartId} config={config} />
64
+ <RechartsPrimitive.ResponsiveContainer>
65
+ {children}
66
+ </RechartsPrimitive.ResponsiveContainer>
67
+ </div>
68
+ </ChartContext.Provider>
69
+ )
70
+ }
71
+
72
+ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
73
+ const colorConfig = Object.entries(config).filter(
74
+ ([, config]) => config.theme || config.color
75
+ )
76
+
77
+ if (!colorConfig.length) {
78
+ return null
79
+ }
80
+
81
+ return (
82
+ <style
83
+ dangerouslySetInnerHTML={{
84
+ __html: Object.entries(THEMES)
85
+ .map(
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
+ )
99
+ .join("\n"),
100
+ }}
101
+ />
102
+ )
103
+ }
104
+
105
+ const ChartTooltip = RechartsPrimitive.Tooltip
106
+
107
+ function ChartTooltipContent({
108
+ active,
109
+ payload,
110
+ className,
111
+ indicator = "dot",
112
+ hideLabel = false,
113
+ hideIndicator = false,
114
+ label,
115
+ labelFormatter,
116
+ labelClassName,
117
+ formatter,
118
+ color,
119
+ nameKey,
120
+ labelKey,
121
+ }: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
122
+ React.ComponentProps<"div"> & {
123
+ hideLabel?: boolean
124
+ hideIndicator?: boolean
125
+ indicator?: "line" | "dot" | "dashed"
126
+ nameKey?: string
127
+ labelKey?: string
128
+ }) {
129
+ const { config } = useChart()
130
+
131
+ const tooltipLabel = React.useMemo(() => {
132
+ if (hideLabel || !payload?.length) {
133
+ return null
134
+ }
135
+
136
+ const [item] = payload
137
+ const key = `${labelKey || item?.dataKey || item?.name || "value"}`
138
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
139
+ const value =
140
+ !labelKey && typeof label === "string"
141
+ ? config[label as keyof typeof config]?.label || label
142
+ : itemConfig?.label
143
+
144
+ if (labelFormatter) {
145
+ return (
146
+ <div className={cn("font-medium", labelClassName)}>
147
+ {labelFormatter(value, payload)}
148
+ </div>
149
+ )
150
+ }
151
+
152
+ if (!value) {
153
+ return null
154
+ }
155
+
156
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>
157
+ }, [
158
+ label,
159
+ labelFormatter,
160
+ payload,
161
+ hideLabel,
162
+ labelClassName,
163
+ config,
164
+ labelKey,
165
+ ])
166
+
167
+ if (!active || !payload?.length) {
168
+ return null
169
+ }
170
+
171
+ const nestLabel = payload.length === 1 && indicator !== "dot"
172
+
173
+ return (
174
+ <div
175
+ className={cn(
176
+ "border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
177
+ className
178
+ )}
179
+ >
180
+ {!nestLabel ? tooltipLabel : null}
181
+ <div className="grid gap-1.5">
182
+ {payload.map((item, index) => {
183
+ const key = `${nameKey || item.name || item.dataKey || "value"}`
184
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
185
+ const indicatorColor = color || item.payload.fill || item.color
186
+
187
+ return (
188
+ <div
189
+ key={item.dataKey}
190
+ className={cn(
191
+ "[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
192
+ indicator === "dot" && "items-center"
193
+ )}
194
+ >
195
+ {formatter && item?.value !== undefined && item.name ? (
196
+ formatter(item.value, item.name, item, index, item.payload)
197
+ ) : (
198
+ <>
199
+ {itemConfig?.icon ? (
200
+ <itemConfig.icon />
201
+ ) : (
202
+ !hideIndicator && (
203
+ <div
204
+ className={cn(
205
+ "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
206
+ {
207
+ "h-2.5 w-2.5": indicator === "dot",
208
+ "w-1": indicator === "line",
209
+ "w-0 border-[1.5px] border-dashed bg-transparent":
210
+ indicator === "dashed",
211
+ "my-0.5": nestLabel && indicator === "dashed",
212
+ }
213
+ )}
214
+ style={
215
+ {
216
+ "--color-bg": indicatorColor,
217
+ "--color-border": indicatorColor,
218
+ } as React.CSSProperties
219
+ }
220
+ />
221
+ )
222
+ )}
223
+ <div
224
+ className={cn(
225
+ "flex flex-1 justify-between leading-none",
226
+ nestLabel ? "items-end" : "items-center"
227
+ )}
228
+ >
229
+ <div className="grid gap-1.5">
230
+ {nestLabel ? tooltipLabel : null}
231
+ <span className="text-muted-foreground">
232
+ {itemConfig?.label || item.name}
233
+ </span>
234
+ </div>
235
+ {item.value && (
236
+ <span className="text-foreground font-mono font-medium tabular-nums">
237
+ {item.value.toLocaleString()}
238
+ </span>
239
+ )}
240
+ </div>
241
+ </>
242
+ )}
243
+ </div>
244
+ )
245
+ })}
246
+ </div>
247
+ </div>
248
+ )
249
+ }
250
+
251
+ const ChartLegend = RechartsPrimitive.Legend
252
+
253
+ function ChartLegendContent({
254
+ className,
255
+ hideIcon = false,
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) {
267
+ return null
268
+ }
269
+
270
+ return (
271
+ <div
272
+ className={cn(
273
+ "flex items-center justify-center gap-4",
274
+ verticalAlign === "top" ? "pb-3" : "pt-3",
275
+ className
276
+ )}
277
+ >
278
+ {payload.map((item) => {
279
+ const key = `${nameKey || item.dataKey || "value"}`
280
+ const itemConfig = getPayloadConfigFromPayload(config, item, key)
281
+
282
+ return (
283
+ <div
284
+ key={item.value}
285
+ className={cn(
286
+ "[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
287
+ )}
288
+ >
289
+ {itemConfig?.icon && !hideIcon ? (
290
+ <itemConfig.icon />
291
+ ) : (
292
+ <div
293
+ className="h-2 w-2 shrink-0 rounded-[2px]"
294
+ style={{
295
+ backgroundColor: item.color,
296
+ }}
297
+ />
298
+ )}
299
+ {itemConfig?.label}
300
+ </div>
301
+ )
302
+ })}
303
+ </div>
304
+ )
305
+ }
306
+
307
+ // Helper to extract item config from a payload.
308
+ function getPayloadConfigFromPayload(
309
+ config: ChartConfig,
310
+ payload: unknown,
311
+ key: string
312
+ ) {
313
+ if (typeof payload !== "object" || payload === null) {
314
+ return undefined
315
+ }
316
+
317
+ const payloadPayload =
318
+ "payload" in payload &&
319
+ typeof payload.payload === "object" &&
320
+ payload.payload !== null
321
+ ? payload.payload
322
+ : undefined
323
+
324
+ let configLabelKey: string = key
325
+
326
+ if (
327
+ key in payload &&
328
+ typeof payload[key as keyof typeof payload] === "string"
329
+ ) {
330
+ configLabelKey = payload[key as keyof typeof payload] as string
331
+ } else if (
332
+ payloadPayload &&
333
+ key in payloadPayload &&
334
+ typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
335
+ ) {
336
+ configLabelKey = payloadPayload[
337
+ key as keyof typeof payloadPayload
338
+ ] as string
339
+ }
340
+
341
+ return configLabelKey in config
342
+ ? config[configLabelKey]
343
+ : config[key as keyof typeof config]
344
+ }
345
+
346
+ export {
347
+ ChartContainer,
348
+ ChartTooltip,
349
+ ChartTooltipContent,
350
+ ChartLegend,
351
+ ChartLegendContent,
352
+ ChartStyle,
353
+ }
src/components/ui/checkbox.tsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5
+ import { CheckIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function Checkbox({
10
+ className,
11
+ ...props
12
+ }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
13
+ return (
14
+ <CheckboxPrimitive.Root
15
+ data-slot="checkbox"
16
+ className={cn(
17
+ "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
18
+ className
19
+ )}
20
+ {...props}
21
+ >
22
+ <CheckboxPrimitive.Indicator
23
+ data-slot="checkbox-indicator"
24
+ className="flex items-center justify-center text-current transition-none"
25
+ >
26
+ <CheckIcon className="size-3.5" />
27
+ </CheckboxPrimitive.Indicator>
28
+ </CheckboxPrimitive.Root>
29
+ )
30
+ }
31
+
32
+ export { Checkbox }
src/components/ui/collapsible.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4
+
5
+ function Collapsible({
6
+ ...props
7
+ }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
8
+ return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
9
+ }
10
+
11
+ function CollapsibleTrigger({
12
+ ...props
13
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
14
+ return (
15
+ <CollapsiblePrimitive.CollapsibleTrigger
16
+ data-slot="collapsible-trigger"
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+
22
+ function CollapsibleContent({
23
+ ...props
24
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
25
+ return (
26
+ <CollapsiblePrimitive.CollapsibleContent
27
+ data-slot="collapsible-content"
28
+ {...props}
29
+ />
30
+ )
31
+ }
32
+
33
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent }
src/components/ui/command.tsx ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Command as CommandPrimitive } from "cmdk"
5
+ import { SearchIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+ import {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogHeader,
13
+ DialogTitle,
14
+ } from "@/components/ui/dialog"
15
+
16
+ function Command({
17
+ className,
18
+ ...props
19
+ }: React.ComponentProps<typeof CommandPrimitive>) {
20
+ return (
21
+ <CommandPrimitive
22
+ data-slot="command"
23
+ className={cn(
24
+ "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
25
+ className
26
+ )}
27
+ {...props}
28
+ />
29
+ )
30
+ }
31
+
32
+ function CommandDialog({
33
+ title = "Command Palette",
34
+ description = "Search for a command to run...",
35
+ children,
36
+ className,
37
+ showCloseButton = true,
38
+ ...props
39
+ }: React.ComponentProps<typeof Dialog> & {
40
+ title?: string
41
+ description?: string
42
+ className?: string
43
+ showCloseButton?: boolean
44
+ }) {
45
+ return (
46
+ <Dialog {...props}>
47
+ <DialogHeader className="sr-only">
48
+ <DialogTitle>{title}</DialogTitle>
49
+ <DialogDescription>{description}</DialogDescription>
50
+ </DialogHeader>
51
+ <DialogContent
52
+ className={cn("overflow-hidden p-0", className)}
53
+ showCloseButton={showCloseButton}
54
+ >
55
+ <Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
56
+ {children}
57
+ </Command>
58
+ </DialogContent>
59
+ </Dialog>
60
+ )
61
+ }
62
+
63
+ function CommandInput({
64
+ className,
65
+ ...props
66
+ }: React.ComponentProps<typeof CommandPrimitive.Input>) {
67
+ return (
68
+ <div
69
+ data-slot="command-input-wrapper"
70
+ className="flex h-9 items-center gap-2 border-b px-3"
71
+ >
72
+ <SearchIcon className="size-4 shrink-0 opacity-50" />
73
+ <CommandPrimitive.Input
74
+ data-slot="command-input"
75
+ className={cn(
76
+ "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
77
+ className
78
+ )}
79
+ {...props}
80
+ />
81
+ </div>
82
+ )
83
+ }
84
+
85
+ function CommandList({
86
+ className,
87
+ ...props
88
+ }: React.ComponentProps<typeof CommandPrimitive.List>) {
89
+ return (
90
+ <CommandPrimitive.List
91
+ data-slot="command-list"
92
+ className={cn(
93
+ "max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
94
+ className
95
+ )}
96
+ {...props}
97
+ />
98
+ )
99
+ }
100
+
101
+ function CommandEmpty({
102
+ ...props
103
+ }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
104
+ return (
105
+ <CommandPrimitive.Empty
106
+ data-slot="command-empty"
107
+ className="py-6 text-center text-sm"
108
+ {...props}
109
+ />
110
+ )
111
+ }
112
+
113
+ function CommandGroup({
114
+ className,
115
+ ...props
116
+ }: React.ComponentProps<typeof CommandPrimitive.Group>) {
117
+ return (
118
+ <CommandPrimitive.Group
119
+ data-slot="command-group"
120
+ className={cn(
121
+ "text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
122
+ className
123
+ )}
124
+ {...props}
125
+ />
126
+ )
127
+ }
128
+
129
+ function CommandSeparator({
130
+ className,
131
+ ...props
132
+ }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
133
+ return (
134
+ <CommandPrimitive.Separator
135
+ data-slot="command-separator"
136
+ className={cn("bg-border -mx-1 h-px", className)}
137
+ {...props}
138
+ />
139
+ )
140
+ }
141
+
142
+ function CommandItem({
143
+ className,
144
+ ...props
145
+ }: React.ComponentProps<typeof CommandPrimitive.Item>) {
146
+ return (
147
+ <CommandPrimitive.Item
148
+ data-slot="command-item"
149
+ className={cn(
150
+ "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
151
+ className
152
+ )}
153
+ {...props}
154
+ />
155
+ )
156
+ }
157
+
158
+ function CommandShortcut({
159
+ className,
160
+ ...props
161
+ }: React.ComponentProps<"span">) {
162
+ return (
163
+ <span
164
+ data-slot="command-shortcut"
165
+ className={cn(
166
+ "text-muted-foreground ml-auto text-xs tracking-widest",
167
+ className
168
+ )}
169
+ {...props}
170
+ />
171
+ )
172
+ }
173
+
174
+ export {
175
+ Command,
176
+ CommandDialog,
177
+ CommandInput,
178
+ CommandList,
179
+ CommandEmpty,
180
+ CommandGroup,
181
+ CommandItem,
182
+ CommandShortcut,
183
+ CommandSeparator,
184
+ }