@sundaysf/cli-v2 1.0.3 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/index.js +1 -1
- package/dist/bin/index.js.map +1 -1
- package/dist/templates/backend/.sundaysrc +7 -0
- package/dist/templates/backend/eslint.config.js.map +1 -0
- package/dist/templates/backend/gitignore +52 -0
- package/dist/templates/backend-db-sql/.sundaysrc +7 -0
- package/dist/templates/backend-db-sql/eslint.config.js.map +1 -0
- package/dist/templates/backend-db-sql/gitignore +65 -0
- package/dist/templates/backend-embedded-db-sql/.sundaysrc +7 -0
- package/dist/templates/backend-embedded-db-sql/eslint.config.js.map +1 -0
- package/dist/templates/backend-embedded-db-sql/gitignore +64 -0
- package/dist/templates/frontend-nextjs/.sundaysrc +7 -0
- package/dist/templates/frontend-nextjs/app/globals.css +125 -0
- package/dist/templates/frontend-nextjs/app/layout.tsx +45 -0
- package/dist/templates/frontend-nextjs/app/page.tsx +5 -0
- package/dist/templates/frontend-nextjs/components/project-generator.tsx +558 -0
- package/dist/templates/frontend-nextjs/components/theme-provider.tsx +11 -0
- package/dist/templates/frontend-nextjs/components/ui/accordion.tsx +66 -0
- package/dist/templates/frontend-nextjs/components/ui/alert-dialog.tsx +157 -0
- package/dist/templates/frontend-nextjs/components/ui/alert.tsx +66 -0
- package/dist/templates/frontend-nextjs/components/ui/aspect-ratio.tsx +11 -0
- package/dist/templates/frontend-nextjs/components/ui/avatar.tsx +53 -0
- package/dist/templates/frontend-nextjs/components/ui/badge.tsx +46 -0
- package/dist/templates/frontend-nextjs/components/ui/breadcrumb.tsx +109 -0
- package/dist/templates/frontend-nextjs/components/ui/button-group.tsx +83 -0
- package/dist/templates/frontend-nextjs/components/ui/button.tsx +60 -0
- package/dist/templates/frontend-nextjs/components/ui/calendar.tsx +213 -0
- package/dist/templates/frontend-nextjs/components/ui/card.tsx +92 -0
- package/dist/templates/frontend-nextjs/components/ui/carousel.tsx +241 -0
- package/dist/templates/frontend-nextjs/components/ui/chart.tsx +353 -0
- package/dist/templates/frontend-nextjs/components/ui/checkbox.tsx +32 -0
- package/dist/templates/frontend-nextjs/components/ui/collapsible.tsx +33 -0
- package/dist/templates/frontend-nextjs/components/ui/command.tsx +184 -0
- package/dist/templates/frontend-nextjs/components/ui/context-menu.tsx +252 -0
- package/dist/templates/frontend-nextjs/components/ui/dialog.tsx +143 -0
- package/dist/templates/frontend-nextjs/components/ui/drawer.tsx +135 -0
- package/dist/templates/frontend-nextjs/components/ui/dropdown-menu.tsx +257 -0
- package/dist/templates/frontend-nextjs/components/ui/empty.tsx +104 -0
- package/dist/templates/frontend-nextjs/components/ui/field.tsx +244 -0
- package/dist/templates/frontend-nextjs/components/ui/form.tsx +167 -0
- package/dist/templates/frontend-nextjs/components/ui/hover-card.tsx +44 -0
- package/dist/templates/frontend-nextjs/components/ui/input-group.tsx +169 -0
- package/dist/templates/frontend-nextjs/components/ui/input-otp.tsx +77 -0
- package/dist/templates/frontend-nextjs/components/ui/input.tsx +21 -0
- package/dist/templates/frontend-nextjs/components/ui/item.tsx +193 -0
- package/dist/templates/frontend-nextjs/components/ui/kbd.tsx +28 -0
- package/dist/templates/frontend-nextjs/components/ui/label.tsx +24 -0
- package/dist/templates/frontend-nextjs/components/ui/menubar.tsx +276 -0
- package/dist/templates/frontend-nextjs/components/ui/navigation-menu.tsx +166 -0
- package/dist/templates/frontend-nextjs/components/ui/pagination.tsx +127 -0
- package/dist/templates/frontend-nextjs/components/ui/popover.tsx +48 -0
- package/dist/templates/frontend-nextjs/components/ui/progress.tsx +31 -0
- package/dist/templates/frontend-nextjs/components/ui/radio-group.tsx +45 -0
- package/dist/templates/frontend-nextjs/components/ui/resizable.tsx +56 -0
- package/dist/templates/frontend-nextjs/components/ui/scroll-area.tsx +58 -0
- package/dist/templates/frontend-nextjs/components/ui/select.tsx +185 -0
- package/dist/templates/frontend-nextjs/components/ui/separator.tsx +28 -0
- package/dist/templates/frontend-nextjs/components/ui/sheet.tsx +139 -0
- package/dist/templates/frontend-nextjs/components/ui/sidebar.tsx +726 -0
- package/dist/templates/frontend-nextjs/components/ui/skeleton.tsx +13 -0
- package/dist/templates/frontend-nextjs/components/ui/slider.tsx +63 -0
- package/dist/templates/frontend-nextjs/components/ui/sonner.tsx +25 -0
- package/dist/templates/frontend-nextjs/components/ui/spinner.tsx +16 -0
- package/dist/templates/frontend-nextjs/components/ui/switch.tsx +31 -0
- package/dist/templates/frontend-nextjs/components/ui/table.tsx +116 -0
- package/dist/templates/frontend-nextjs/components/ui/tabs.tsx +66 -0
- package/dist/templates/frontend-nextjs/components/ui/textarea.tsx +18 -0
- package/dist/templates/frontend-nextjs/components/ui/toast.tsx +129 -0
- package/dist/templates/frontend-nextjs/components/ui/toaster.tsx +35 -0
- package/dist/templates/frontend-nextjs/components/ui/toggle-group.tsx +73 -0
- package/dist/templates/frontend-nextjs/components/ui/toggle.tsx +47 -0
- package/dist/templates/frontend-nextjs/components/ui/tooltip.tsx +61 -0
- package/dist/templates/frontend-nextjs/components/ui/use-mobile.tsx +19 -0
- package/dist/templates/frontend-nextjs/components/ui/use-toast.ts +191 -0
- package/dist/templates/frontend-nextjs/components.json +21 -0
- package/dist/templates/frontend-nextjs/gitignore +17 -0
- package/dist/templates/frontend-nextjs/hooks/use-mobile.ts +19 -0
- package/dist/templates/frontend-nextjs/hooks/use-toast.ts +191 -0
- package/dist/templates/frontend-nextjs/lib/utils.ts +6 -0
- package/dist/templates/frontend-nextjs/next.config.mjs +11 -0
- package/dist/templates/frontend-nextjs/postcss.config.mjs +8 -0
- package/dist/templates/frontend-nextjs/public/apple-icon.png +0 -0
- package/dist/templates/frontend-nextjs/public/icon-dark-32x32.png +0 -0
- package/dist/templates/frontend-nextjs/public/icon-light-32x32.png +0 -0
- package/dist/templates/frontend-nextjs/public/icon.svg +26 -0
- package/dist/templates/frontend-nextjs/public/placeholder-logo.png +0 -0
- package/dist/templates/frontend-nextjs/public/placeholder-logo.svg +1 -0
- package/dist/templates/frontend-nextjs/public/placeholder-user.jpg +0 -0
- package/dist/templates/frontend-nextjs/public/placeholder.jpg +0 -0
- package/dist/templates/frontend-nextjs/public/placeholder.svg +1 -0
- package/dist/templates/frontend-nextjs/styles/globals.css +125 -0
- package/dist/templates/frontend-nextjs/tsconfig.json +27 -0
- package/package.json +1 -1
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect } from "react"
|
|
4
|
+
import { motion, AnimatePresence } from "framer-motion"
|
|
5
|
+
import {
|
|
6
|
+
Sparkles,
|
|
7
|
+
Send,
|
|
8
|
+
Check,
|
|
9
|
+
Circle,
|
|
10
|
+
Loader2,
|
|
11
|
+
Code2,
|
|
12
|
+
Rocket,
|
|
13
|
+
Globe,
|
|
14
|
+
FileCode,
|
|
15
|
+
Database,
|
|
16
|
+
Shield,
|
|
17
|
+
Layout,
|
|
18
|
+
Palette,
|
|
19
|
+
Copy,
|
|
20
|
+
ChevronRight
|
|
21
|
+
} from "lucide-react"
|
|
22
|
+
import { Button } from "@/components/ui/button"
|
|
23
|
+
import { cn } from "@/lib/utils"
|
|
24
|
+
|
|
25
|
+
type PipelineStage = "idle" | "design" | "scaffold" | "deploy" | "live"
|
|
26
|
+
|
|
27
|
+
interface Message {
|
|
28
|
+
id: string
|
|
29
|
+
type: "user" | "assistant"
|
|
30
|
+
content: string
|
|
31
|
+
stack?: StackItem[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface StackItem {
|
|
35
|
+
icon: React.ReactNode
|
|
36
|
+
label: string
|
|
37
|
+
description: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface BlueprintSpec {
|
|
41
|
+
title: string
|
|
42
|
+
items: string[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const EXAMPLE_PROMPTS = [
|
|
46
|
+
"Sistema de facturación multi-tenant con auth JWT y generación de PDFs",
|
|
47
|
+
"E-commerce con carrito, pagos Stripe y panel admin",
|
|
48
|
+
"Dashboard de analytics con gráficos en tiempo real",
|
|
49
|
+
"App de gestión de tareas con colaboración en equipo"
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
export function ProjectGenerator() {
|
|
53
|
+
const [messages, setMessages] = useState<Message[]>([])
|
|
54
|
+
const [inputValue, setInputValue] = useState("")
|
|
55
|
+
const [isGenerating, setIsGenerating] = useState(false)
|
|
56
|
+
const [pipelineStage, setPipelineStage] = useState<PipelineStage>("idle")
|
|
57
|
+
const [activeTab, setActiveTab] = useState<"blueprint" | "pipeline">("blueprint")
|
|
58
|
+
const [blueprint, setBlueprint] = useState<BlueprintSpec[] | null>(null)
|
|
59
|
+
const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
60
|
+
const inputRef = useRef<HTMLTextAreaElement>(null)
|
|
61
|
+
|
|
62
|
+
const scrollToBottom = () => {
|
|
63
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
scrollToBottom()
|
|
68
|
+
}, [messages])
|
|
69
|
+
|
|
70
|
+
const simulateGeneration = async (prompt: string) => {
|
|
71
|
+
setIsGenerating(true)
|
|
72
|
+
|
|
73
|
+
// Add user message
|
|
74
|
+
const userMessage: Message = {
|
|
75
|
+
id: Date.now().toString(),
|
|
76
|
+
type: "user",
|
|
77
|
+
content: prompt
|
|
78
|
+
}
|
|
79
|
+
setMessages(prev => [...prev, userMessage])
|
|
80
|
+
|
|
81
|
+
// Simulate AI thinking
|
|
82
|
+
await new Promise(resolve => setTimeout(resolve, 1500))
|
|
83
|
+
|
|
84
|
+
// Add assistant response with stack
|
|
85
|
+
const assistantMessage: Message = {
|
|
86
|
+
id: (Date.now() + 1).toString(),
|
|
87
|
+
type: "assistant",
|
|
88
|
+
content: "Perfecto. He analizado tu requerimiento y propongo el siguiente stack técnico:",
|
|
89
|
+
stack: [
|
|
90
|
+
{ icon: <Database className="h-4 w-4" />, label: "Multi-tenant", description: "Arquitectura aislada por cliente" },
|
|
91
|
+
{ icon: <Shield className="h-4 w-4" />, label: "Auth JWT", description: "Autenticación segura con tokens" },
|
|
92
|
+
{ icon: <FileCode className="h-4 w-4" />, label: "CRUD completo", description: "Operaciones de datos estándar" },
|
|
93
|
+
{ icon: <Layout className="h-4 w-4" />, label: "Dashboard", description: "Panel de administración" },
|
|
94
|
+
{ icon: <Palette className="h-4 w-4" />, label: "UI Components", description: "Componentes shadcn/ui" },
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
setMessages(prev => [...prev, assistantMessage])
|
|
98
|
+
|
|
99
|
+
// Generate blueprint
|
|
100
|
+
setBlueprint([
|
|
101
|
+
{
|
|
102
|
+
title: "Arquitectura",
|
|
103
|
+
items: ["Next.js 16 App Router", "TypeScript strict mode", "Tailwind CSS + shadcn/ui", "PostgreSQL + Prisma ORM"]
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
title: "Autenticación",
|
|
107
|
+
items: ["JWT con refresh tokens", "Middleware de protección", "Roles y permisos", "Sessions seguras"]
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
title: "Features",
|
|
111
|
+
items: ["CRUD dinámico", "Filtros y búsqueda", "Paginación optimizada", "Exportación de datos"]
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
title: "Infraestructura",
|
|
115
|
+
items: ["Vercel deployment", "Edge functions", "CDN global", "Monitoreo integrado"]
|
|
116
|
+
}
|
|
117
|
+
])
|
|
118
|
+
|
|
119
|
+
setIsGenerating(false)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const handleGenerate = async () => {
|
|
123
|
+
if (pipelineStage !== "idle") return
|
|
124
|
+
|
|
125
|
+
setPipelineStage("design")
|
|
126
|
+
setActiveTab("pipeline")
|
|
127
|
+
|
|
128
|
+
// Simulate pipeline stages
|
|
129
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
130
|
+
setPipelineStage("scaffold")
|
|
131
|
+
await new Promise(resolve => setTimeout(resolve, 2500))
|
|
132
|
+
setPipelineStage("deploy")
|
|
133
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
134
|
+
setPipelineStage("live")
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
138
|
+
e.preventDefault()
|
|
139
|
+
if (!inputValue.trim() || isGenerating) return
|
|
140
|
+
|
|
141
|
+
await simulateGeneration(inputValue)
|
|
142
|
+
setInputValue("")
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const handleExampleClick = (prompt: string) => {
|
|
146
|
+
setInputValue(prompt)
|
|
147
|
+
inputRef.current?.focus()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const pipelineStages = [
|
|
151
|
+
{ id: "design", label: "Design", icon: Palette, description: "Generando especificaciones" },
|
|
152
|
+
{ id: "scaffold", label: "Scaffold", icon: Code2, description: "Creando estructura del proyecto" },
|
|
153
|
+
{ id: "deploy", label: "Deploy", icon: Rocket, description: "Desplegando a producción" },
|
|
154
|
+
{ id: "live", label: "Live", icon: Globe, description: "¡Tu proyecto está en vivo!" }
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
const getStageStatus = (stageId: string) => {
|
|
158
|
+
const stageOrder = ["design", "scaffold", "deploy", "live"]
|
|
159
|
+
const currentIndex = stageOrder.indexOf(pipelineStage)
|
|
160
|
+
const stageIndex = stageOrder.indexOf(stageId)
|
|
161
|
+
|
|
162
|
+
if (pipelineStage === "idle") return "pending"
|
|
163
|
+
if (stageIndex < currentIndex) return "completed"
|
|
164
|
+
if (stageIndex === currentIndex) return "active"
|
|
165
|
+
return "pending"
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div className="h-screen w-full bg-background flex">
|
|
170
|
+
{/* Main Chat Area */}
|
|
171
|
+
<div className="flex-1 flex flex-col min-w-0">
|
|
172
|
+
{/* Header */}
|
|
173
|
+
<header className="flex items-center gap-3 px-6 py-4 border-b border-border/50">
|
|
174
|
+
<div className="flex items-center justify-center w-9 h-9 rounded-lg bg-primary/10">
|
|
175
|
+
<Sparkles className="h-5 w-5 text-primary" />
|
|
176
|
+
</div>
|
|
177
|
+
<div>
|
|
178
|
+
<h1 className="text-lg font-semibold text-foreground">Project Generator</h1>
|
|
179
|
+
<p className="text-xs text-muted-foreground">Describe tu proyecto y genera el scaffolding completo</p>
|
|
180
|
+
</div>
|
|
181
|
+
</header>
|
|
182
|
+
|
|
183
|
+
{/* Messages Area */}
|
|
184
|
+
<div className="flex-1 overflow-y-auto px-6 py-6">
|
|
185
|
+
{messages.length === 0 ? (
|
|
186
|
+
<div className="h-full flex flex-col items-center justify-center">
|
|
187
|
+
<motion.div
|
|
188
|
+
initial={{ opacity: 0, y: 20 }}
|
|
189
|
+
animate={{ opacity: 1, y: 0 }}
|
|
190
|
+
className="text-center max-w-lg"
|
|
191
|
+
>
|
|
192
|
+
<div className="w-16 h-16 rounded-2xl bg-primary/10 flex items-center justify-center mx-auto mb-6">
|
|
193
|
+
<Sparkles className="h-8 w-8 text-primary" />
|
|
194
|
+
</div>
|
|
195
|
+
<h2 className="text-2xl font-semibold text-foreground mb-3">
|
|
196
|
+
¿Qué quieres construir hoy?
|
|
197
|
+
</h2>
|
|
198
|
+
<p className="text-muted-foreground mb-8">
|
|
199
|
+
Describe tu proyecto y generaré la especificación técnica completa con el stack óptimo para tu caso de uso.
|
|
200
|
+
</p>
|
|
201
|
+
|
|
202
|
+
<div className="grid gap-2">
|
|
203
|
+
<p className="text-xs text-muted-foreground uppercase tracking-wider mb-2">Ejemplos</p>
|
|
204
|
+
{EXAMPLE_PROMPTS.map((prompt, index) => (
|
|
205
|
+
<motion.button
|
|
206
|
+
key={index}
|
|
207
|
+
initial={{ opacity: 0, x: -20 }}
|
|
208
|
+
animate={{ opacity: 1, x: 0 }}
|
|
209
|
+
transition={{ delay: index * 0.1 }}
|
|
210
|
+
onClick={() => handleExampleClick(prompt)}
|
|
211
|
+
className="group flex items-center gap-3 px-4 py-3 rounded-xl bg-card hover:bg-secondary/80 border border-border/50 hover:border-primary/30 transition-all text-left"
|
|
212
|
+
>
|
|
213
|
+
<ChevronRight className="h-4 w-4 text-muted-foreground group-hover:text-primary transition-colors" />
|
|
214
|
+
<span className="text-sm text-foreground/80 group-hover:text-foreground transition-colors">{prompt}</span>
|
|
215
|
+
</motion.button>
|
|
216
|
+
))}
|
|
217
|
+
</div>
|
|
218
|
+
</motion.div>
|
|
219
|
+
</div>
|
|
220
|
+
) : (
|
|
221
|
+
<div className="max-w-3xl mx-auto space-y-6">
|
|
222
|
+
<AnimatePresence mode="popLayout">
|
|
223
|
+
{messages.map((message) => (
|
|
224
|
+
<motion.div
|
|
225
|
+
key={message.id}
|
|
226
|
+
initial={{ opacity: 0, y: 20 }}
|
|
227
|
+
animate={{ opacity: 1, y: 0 }}
|
|
228
|
+
exit={{ opacity: 0, y: -20 }}
|
|
229
|
+
className={cn(
|
|
230
|
+
"flex gap-4",
|
|
231
|
+
message.type === "user" && "justify-end"
|
|
232
|
+
)}
|
|
233
|
+
>
|
|
234
|
+
{message.type === "assistant" && (
|
|
235
|
+
<div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
|
|
236
|
+
<Sparkles className="h-4 w-4 text-primary" />
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
<div className={cn(
|
|
240
|
+
"max-w-[80%]",
|
|
241
|
+
message.type === "user" && "bg-primary text-primary-foreground px-4 py-3 rounded-2xl rounded-tr-md"
|
|
242
|
+
)}>
|
|
243
|
+
<p className={cn(
|
|
244
|
+
"text-sm leading-relaxed",
|
|
245
|
+
message.type === "assistant" && "text-foreground"
|
|
246
|
+
)}>
|
|
247
|
+
{message.content}
|
|
248
|
+
</p>
|
|
249
|
+
|
|
250
|
+
{message.stack && (
|
|
251
|
+
<div className="mt-4 space-y-2">
|
|
252
|
+
<p className="text-xs text-muted-foreground uppercase tracking-wider mb-3">Stack confirmado</p>
|
|
253
|
+
<div className="grid gap-2">
|
|
254
|
+
{message.stack.map((item, index) => (
|
|
255
|
+
<motion.div
|
|
256
|
+
key={index}
|
|
257
|
+
initial={{ opacity: 0, x: -10 }}
|
|
258
|
+
animate={{ opacity: 1, x: 0 }}
|
|
259
|
+
transition={{ delay: index * 0.1 }}
|
|
260
|
+
className="flex items-center gap-3 px-3 py-2 rounded-lg bg-card border border-border/50"
|
|
261
|
+
>
|
|
262
|
+
<div className="w-7 h-7 rounded-md bg-primary/10 flex items-center justify-center text-primary">
|
|
263
|
+
{item.icon}
|
|
264
|
+
</div>
|
|
265
|
+
<div>
|
|
266
|
+
<p className="text-sm font-medium text-foreground">{item.label}</p>
|
|
267
|
+
<p className="text-xs text-muted-foreground">{item.description}</p>
|
|
268
|
+
</div>
|
|
269
|
+
</motion.div>
|
|
270
|
+
))}
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
</motion.div>
|
|
276
|
+
))}
|
|
277
|
+
</AnimatePresence>
|
|
278
|
+
|
|
279
|
+
{isGenerating && (
|
|
280
|
+
<motion.div
|
|
281
|
+
initial={{ opacity: 0 }}
|
|
282
|
+
animate={{ opacity: 1 }}
|
|
283
|
+
className="flex gap-4"
|
|
284
|
+
>
|
|
285
|
+
<div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
|
|
286
|
+
<Sparkles className="h-4 w-4 text-primary" />
|
|
287
|
+
</div>
|
|
288
|
+
<div className="flex items-center gap-2 text-muted-foreground">
|
|
289
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
290
|
+
<span className="text-sm">Analizando requerimientos...</span>
|
|
291
|
+
</div>
|
|
292
|
+
</motion.div>
|
|
293
|
+
)}
|
|
294
|
+
|
|
295
|
+
<div ref={messagesEndRef} />
|
|
296
|
+
</div>
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
{/* Generate Button (when stack is confirmed) */}
|
|
301
|
+
{messages.length > 0 && !isGenerating && pipelineStage === "idle" && (
|
|
302
|
+
<motion.div
|
|
303
|
+
initial={{ opacity: 0, y: 20 }}
|
|
304
|
+
animate={{ opacity: 1, y: 0 }}
|
|
305
|
+
className="px-6 pb-4 flex justify-center"
|
|
306
|
+
>
|
|
307
|
+
<Button
|
|
308
|
+
onClick={handleGenerate}
|
|
309
|
+
size="lg"
|
|
310
|
+
className="gap-2 px-8 bg-primary hover:bg-primary/90 text-primary-foreground shadow-lg shadow-primary/25"
|
|
311
|
+
>
|
|
312
|
+
<Rocket className="h-4 w-4" />
|
|
313
|
+
Generar Proyecto
|
|
314
|
+
</Button>
|
|
315
|
+
</motion.div>
|
|
316
|
+
)}
|
|
317
|
+
|
|
318
|
+
{/* Input Area */}
|
|
319
|
+
<div className="border-t border-border/50 bg-card/30 backdrop-blur-sm px-6 py-4">
|
|
320
|
+
<form onSubmit={handleSubmit} className="max-w-3xl mx-auto">
|
|
321
|
+
<div className="relative flex items-end gap-3">
|
|
322
|
+
<div className="flex-1 relative">
|
|
323
|
+
<textarea
|
|
324
|
+
ref={inputRef}
|
|
325
|
+
value={inputValue}
|
|
326
|
+
onChange={(e) => setInputValue(e.target.value)}
|
|
327
|
+
onKeyDown={(e) => {
|
|
328
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
329
|
+
e.preventDefault()
|
|
330
|
+
handleSubmit(e)
|
|
331
|
+
}
|
|
332
|
+
}}
|
|
333
|
+
placeholder="Describe tu proyecto..."
|
|
334
|
+
rows={1}
|
|
335
|
+
className="w-full resize-none rounded-xl bg-input border border-border/50 focus:border-primary/50 focus:ring-1 focus:ring-primary/30 px-4 py-3 pr-12 text-sm text-foreground placeholder:text-muted-foreground outline-none transition-all"
|
|
336
|
+
style={{ minHeight: "48px", maxHeight: "150px" }}
|
|
337
|
+
/>
|
|
338
|
+
<div className="absolute right-2 bottom-2">
|
|
339
|
+
<Button
|
|
340
|
+
type="submit"
|
|
341
|
+
size="icon"
|
|
342
|
+
disabled={!inputValue.trim() || isGenerating}
|
|
343
|
+
className="h-8 w-8 rounded-lg bg-primary hover:bg-primary/90 disabled:opacity-50"
|
|
344
|
+
>
|
|
345
|
+
<Send className="h-4 w-4" />
|
|
346
|
+
</Button>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
<p className="text-xs text-muted-foreground text-center mt-2">
|
|
351
|
+
Enter para enviar · Shift+Enter para nueva línea
|
|
352
|
+
</p>
|
|
353
|
+
</form>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
{/* Preview Panel */}
|
|
358
|
+
<div className="w-[380px] border-l border-border/50 bg-card/30 flex flex-col">
|
|
359
|
+
{/* Panel Tabs */}
|
|
360
|
+
<div className="flex border-b border-border/50">
|
|
361
|
+
<button
|
|
362
|
+
onClick={() => setActiveTab("blueprint")}
|
|
363
|
+
className={cn(
|
|
364
|
+
"flex-1 px-4 py-3 text-sm font-medium transition-colors relative",
|
|
365
|
+
activeTab === "blueprint"
|
|
366
|
+
? "text-foreground"
|
|
367
|
+
: "text-muted-foreground hover:text-foreground"
|
|
368
|
+
)}
|
|
369
|
+
>
|
|
370
|
+
Blueprint
|
|
371
|
+
{activeTab === "blueprint" && (
|
|
372
|
+
<motion.div
|
|
373
|
+
layoutId="activeTab"
|
|
374
|
+
className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"
|
|
375
|
+
/>
|
|
376
|
+
)}
|
|
377
|
+
</button>
|
|
378
|
+
<button
|
|
379
|
+
onClick={() => setActiveTab("pipeline")}
|
|
380
|
+
className={cn(
|
|
381
|
+
"flex-1 px-4 py-3 text-sm font-medium transition-colors relative",
|
|
382
|
+
activeTab === "pipeline"
|
|
383
|
+
? "text-foreground"
|
|
384
|
+
: "text-muted-foreground hover:text-foreground"
|
|
385
|
+
)}
|
|
386
|
+
>
|
|
387
|
+
Pipeline
|
|
388
|
+
{activeTab === "pipeline" && (
|
|
389
|
+
<motion.div
|
|
390
|
+
layoutId="activeTab"
|
|
391
|
+
className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"
|
|
392
|
+
/>
|
|
393
|
+
)}
|
|
394
|
+
</button>
|
|
395
|
+
</div>
|
|
396
|
+
|
|
397
|
+
{/* Panel Content */}
|
|
398
|
+
<div className="flex-1 overflow-y-auto p-5">
|
|
399
|
+
<AnimatePresence mode="wait">
|
|
400
|
+
{activeTab === "blueprint" ? (
|
|
401
|
+
<motion.div
|
|
402
|
+
key="blueprint"
|
|
403
|
+
initial={{ opacity: 0, x: 20 }}
|
|
404
|
+
animate={{ opacity: 1, x: 0 }}
|
|
405
|
+
exit={{ opacity: 0, x: -20 }}
|
|
406
|
+
className="space-y-4"
|
|
407
|
+
>
|
|
408
|
+
{blueprint ? (
|
|
409
|
+
<>
|
|
410
|
+
<div className="flex items-center justify-between">
|
|
411
|
+
<h3 className="text-sm font-semibold text-foreground">Especificación Técnica</h3>
|
|
412
|
+
<Button variant="ghost" size="sm" className="h-7 gap-1.5 text-xs">
|
|
413
|
+
<Copy className="h-3 w-3" />
|
|
414
|
+
Copiar
|
|
415
|
+
</Button>
|
|
416
|
+
</div>
|
|
417
|
+
|
|
418
|
+
{blueprint.map((section, index) => (
|
|
419
|
+
<motion.div
|
|
420
|
+
key={index}
|
|
421
|
+
initial={{ opacity: 0, y: 10 }}
|
|
422
|
+
animate={{ opacity: 1, y: 0 }}
|
|
423
|
+
transition={{ delay: index * 0.1 }}
|
|
424
|
+
className="rounded-xl bg-secondary/50 border border-border/50 p-4"
|
|
425
|
+
>
|
|
426
|
+
<h4 className="text-xs font-semibold text-primary uppercase tracking-wider mb-3">
|
|
427
|
+
{section.title}
|
|
428
|
+
</h4>
|
|
429
|
+
<ul className="space-y-2">
|
|
430
|
+
{section.items.map((item, itemIndex) => (
|
|
431
|
+
<li key={itemIndex} className="flex items-center gap-2 text-sm text-foreground/80">
|
|
432
|
+
<div className="w-1.5 h-1.5 rounded-full bg-primary/60" />
|
|
433
|
+
{item}
|
|
434
|
+
</li>
|
|
435
|
+
))}
|
|
436
|
+
</ul>
|
|
437
|
+
</motion.div>
|
|
438
|
+
))}
|
|
439
|
+
</>
|
|
440
|
+
) : (
|
|
441
|
+
<div className="h-full flex flex-col items-center justify-center py-16 text-center">
|
|
442
|
+
<div className="w-12 h-12 rounded-xl bg-secondary/50 flex items-center justify-center mb-4">
|
|
443
|
+
<FileCode className="h-6 w-6 text-muted-foreground" />
|
|
444
|
+
</div>
|
|
445
|
+
<p className="text-sm text-muted-foreground">
|
|
446
|
+
Sin blueprint todavía
|
|
447
|
+
</p>
|
|
448
|
+
<p className="text-xs text-muted-foreground/60 mt-1">
|
|
449
|
+
Describe tu proyecto para generar las especificaciones
|
|
450
|
+
</p>
|
|
451
|
+
</div>
|
|
452
|
+
)}
|
|
453
|
+
</motion.div>
|
|
454
|
+
) : (
|
|
455
|
+
<motion.div
|
|
456
|
+
key="pipeline"
|
|
457
|
+
initial={{ opacity: 0, x: 20 }}
|
|
458
|
+
animate={{ opacity: 1, x: 0 }}
|
|
459
|
+
exit={{ opacity: 0, x: -20 }}
|
|
460
|
+
className="space-y-1"
|
|
461
|
+
>
|
|
462
|
+
{pipelineStages.map((stage, index) => {
|
|
463
|
+
const status = getStageStatus(stage.id)
|
|
464
|
+
const Icon = stage.icon
|
|
465
|
+
|
|
466
|
+
return (
|
|
467
|
+
<motion.div
|
|
468
|
+
key={stage.id}
|
|
469
|
+
initial={{ opacity: 0, x: 20 }}
|
|
470
|
+
animate={{ opacity: 1, x: 0 }}
|
|
471
|
+
transition={{ delay: index * 0.1 }}
|
|
472
|
+
className={cn(
|
|
473
|
+
"flex items-center gap-4 p-4 rounded-xl transition-all",
|
|
474
|
+
status === "active" && "bg-primary/10 border border-primary/30",
|
|
475
|
+
status === "completed" && "opacity-70"
|
|
476
|
+
)}
|
|
477
|
+
>
|
|
478
|
+
<div className={cn(
|
|
479
|
+
"w-10 h-10 rounded-xl flex items-center justify-center transition-colors",
|
|
480
|
+
status === "active" && "bg-primary text-primary-foreground",
|
|
481
|
+
status === "completed" && "bg-primary/20 text-primary",
|
|
482
|
+
status === "pending" && "bg-secondary text-muted-foreground"
|
|
483
|
+
)}>
|
|
484
|
+
{status === "completed" ? (
|
|
485
|
+
<Check className="h-5 w-5" />
|
|
486
|
+
) : status === "active" ? (
|
|
487
|
+
<Loader2 className="h-5 w-5 animate-spin" />
|
|
488
|
+
) : (
|
|
489
|
+
<Icon className="h-5 w-5" />
|
|
490
|
+
)}
|
|
491
|
+
</div>
|
|
492
|
+
<div className="flex-1 min-w-0">
|
|
493
|
+
<p className={cn(
|
|
494
|
+
"text-sm font-medium",
|
|
495
|
+
status === "active" ? "text-foreground" : "text-muted-foreground"
|
|
496
|
+
)}>
|
|
497
|
+
{stage.label}
|
|
498
|
+
</p>
|
|
499
|
+
{status === "active" && (
|
|
500
|
+
<motion.p
|
|
501
|
+
initial={{ opacity: 0 }}
|
|
502
|
+
animate={{ opacity: 1 }}
|
|
503
|
+
className="text-xs text-muted-foreground mt-0.5"
|
|
504
|
+
>
|
|
505
|
+
{stage.description}
|
|
506
|
+
</motion.p>
|
|
507
|
+
)}
|
|
508
|
+
</div>
|
|
509
|
+
{status === "completed" && (
|
|
510
|
+
<div className="w-2 h-2 rounded-full bg-primary" />
|
|
511
|
+
)}
|
|
512
|
+
</motion.div>
|
|
513
|
+
)
|
|
514
|
+
})}
|
|
515
|
+
|
|
516
|
+
{pipelineStage === "idle" && (
|
|
517
|
+
<div className="h-full flex flex-col items-center justify-center py-16 text-center">
|
|
518
|
+
<div className="w-12 h-12 rounded-xl bg-secondary/50 flex items-center justify-center mb-4">
|
|
519
|
+
<Rocket className="h-6 w-6 text-muted-foreground" />
|
|
520
|
+
</div>
|
|
521
|
+
<p className="text-sm text-muted-foreground">
|
|
522
|
+
Pipeline inactivo
|
|
523
|
+
</p>
|
|
524
|
+
<p className="text-xs text-muted-foreground/60 mt-1">
|
|
525
|
+
Genera un proyecto para ver el progreso aquí
|
|
526
|
+
</p>
|
|
527
|
+
</div>
|
|
528
|
+
)}
|
|
529
|
+
|
|
530
|
+
{pipelineStage === "live" && (
|
|
531
|
+
<motion.div
|
|
532
|
+
initial={{ opacity: 0, scale: 0.95 }}
|
|
533
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
534
|
+
className="mt-6 p-4 rounded-xl bg-primary/10 border border-primary/30"
|
|
535
|
+
>
|
|
536
|
+
<div className="flex items-center gap-3 mb-3">
|
|
537
|
+
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center">
|
|
538
|
+
<Check className="h-4 w-4 text-primary-foreground" />
|
|
539
|
+
</div>
|
|
540
|
+
<div>
|
|
541
|
+
<p className="text-sm font-semibold text-foreground">¡Proyecto generado!</p>
|
|
542
|
+
<p className="text-xs text-muted-foreground">Listo para descargar</p>
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
<Button className="w-full gap-2 bg-primary hover:bg-primary/90">
|
|
546
|
+
<Globe className="h-4 w-4" />
|
|
547
|
+
Ver Proyecto en Vivo
|
|
548
|
+
</Button>
|
|
549
|
+
</motion.div>
|
|
550
|
+
)}
|
|
551
|
+
</motion.div>
|
|
552
|
+
)}
|
|
553
|
+
</AnimatePresence>
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
</div>
|
|
557
|
+
)
|
|
558
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import {
|
|
5
|
+
ThemeProvider as NextThemesProvider,
|
|
6
|
+
type ThemeProviderProps,
|
|
7
|
+
} from 'next-themes'
|
|
8
|
+
|
|
9
|
+
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
|
10
|
+
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
|
11
|
+
}
|
|
@@ -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 }
|