Init: Projeto Gerador de Prompts para Hotéis (MVP v0.1.0)
This commit is contained in:
commit
2bc2cab265
1
.env.example
Normal file
1
.env.example
Normal file
@ -0,0 +1 @@
|
|||||||
|
DATABASE_URL="file:./dev.db"
|
||||||
150
README.md
Normal file
150
README.md
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# 🎉 Gerador de Prompts para Hotéis/Motéis
|
||||||
|
|
||||||
|
Aplicativo web para criação rápida e eficiente de prompts de IA para atendimento em hotéis e motéis.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Objetivo
|
||||||
|
|
||||||
|
Gerar prompts de IA estruturados em segundos, não horas, usando um fluxo guiado de 14 categorias universais para hotelaria.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Stack Tecnológica
|
||||||
|
|
||||||
|
- **Frontend:** Next.js 15 + TypeScript + Tailwind CSS
|
||||||
|
- **Backend/API:** Next.js API Routes + Zod (validação)
|
||||||
|
- **Banco de Dados:** SQLite + Prisma ORM
|
||||||
|
- **Deploy:** Vercel (sugerido)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Como Começar
|
||||||
|
|
||||||
|
### 1. Instalar Dependências
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /root/projetos/gerador-prompts-hoteis
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configurar Banco de Dados
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Criar arquivo .env
|
||||||
|
cp .env.example .env
|
||||||
|
# Editar DATABASE_URL se necessário (exemplo: file:./dev.db)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Executar Migrations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Gerar schema do Prisma
|
||||||
|
npx prisma migrate dev
|
||||||
|
|
||||||
|
# Inserir dados iniciais
|
||||||
|
npx prisma db seed
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Iniciar em Desenvolvimento
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
O app estará disponível em `http://localhost:3000`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Estrutura do Projeto
|
||||||
|
|
||||||
|
```
|
||||||
|
gerador-prompts-hoteis/
|
||||||
|
├── app/ # Páginas Next.js
|
||||||
|
│ ├── layout.tsx # Layout principal
|
||||||
|
│ ├── page.tsx # Página inicial
|
||||||
|
│ ├── globals.css # Estilos globais
|
||||||
|
│ └── prompts/ # Wizard de 14 etapas
|
||||||
|
│ ├── page.tsx # Página do wizard
|
||||||
|
│ └── [id]/ # Detalhes de prompt salvo
|
||||||
|
├── app/api/ # API Routes (Backend)
|
||||||
|
│ └── prompts/
|
||||||
|
│ ├── route.ts # POST /api/prompts (salvar)
|
||||||
|
│ └── [id]/
|
||||||
|
│ └── route.ts # GET /api/prompts/[id] (carregar)
|
||||||
|
├── components/ # Componentes React
|
||||||
|
│ ├── CategoryStep.tsx # 14 categorias com dicas
|
||||||
|
│ ├── Wizard.tsx # Gerenciador do fluxo
|
||||||
|
│ ├── StepIndicator.tsx # Indicador visual de progresso
|
||||||
|
│ ├── CategoryForm.tsx # Formulário de entrada
|
||||||
|
│ └── PromptPreview.tsx # Preview em tempo real
|
||||||
|
├── lib/ # Funções auxiliares
|
||||||
|
│ ├── categories.ts # Metadados das 14 categorias
|
||||||
|
│ ├── prompt-builder.ts # Montador de prompts finais
|
||||||
|
│ ├── prisma.ts # Cliente Prisma compartilhado
|
||||||
|
│ └── validators.ts # Validação com Zod
|
||||||
|
├── prisma/ # Banco de dados
|
||||||
|
│ ├── schema.prisma # Modelo (Prompt, PromptCategory)
|
||||||
|
│ └── seed.ts # Dados iniciais
|
||||||
|
└── configs/ # Arquivos de configuração
|
||||||
|
├── package.json # Dependências e scripts
|
||||||
|
├── tsconfig.json # Configuração TypeScript
|
||||||
|
├── next.config.js # Configuração Next.js
|
||||||
|
├── tailwind.config.ts # Configuração Tailwind
|
||||||
|
├── postcss.config.js # PostCSS
|
||||||
|
└── next-env.d.ts # Tipos Next.js
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏷️ As 14 Categorias Universais
|
||||||
|
|
||||||
|
| # | Categoria | Descrição |
|
||||||
|
|---|-----------|-----------|
|
||||||
|
| 1 | Perfil do Hotel | Estilo, personalidade, identidade da marca |
|
||||||
|
| 2 | Localização | Endereço, pontos de interesse, entorno |
|
||||||
|
| 3 | Público-alvo | Demografia, motivação de viagem |
|
||||||
|
| 4 | Objetivo da Comunicação | Reservas, awareness, upgrade |
|
||||||
|
| 5 | Tom de Voz | Sofisticado, acolhedor, inspirador |
|
||||||
|
| 6 | Diferenciais | Atributos competitivos únicos |
|
||||||
|
| 7 | Serviços e Amenidades | Spa, piscina, refeições, etc. |
|
||||||
|
| 8 | Experiência do Hóspede | Check-in, check-out, bem-estar |
|
||||||
|
| 9 | Gastronomia | Cardápio, restaurante, café |
|
||||||
|
| 10 | Bem-estar | Terapias, academia, relaxamento |
|
||||||
|
| 11 | Sustentabilidade | Eco-friendly, práticas verdes |
|
||||||
|
| 12 | Sazonalidade | Eventos especiais, feriados |
|
||||||
|
| 13 | Restrições e Observações | Políticas, limitações |
|
||||||
|
| 14 | Chamada para Ação (CTA) | CTA direto e objetivo |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Características Principais
|
||||||
|
|
||||||
|
- ✅ **Modularidade:** Cada componente é independente e adaptável
|
||||||
|
- ✅ **Templates Flexíveis:** Estrutura de prompts padronizada mas customizável
|
||||||
|
- ✅ **Preview em Tempo Real:** Veja o prompt sendo montado enquanto responde
|
||||||
|
- ✅ **Boas Práticas Embutidas:** Dicas de prompt engineering em cada categoria
|
||||||
|
- ✅ **Persistência:** Salve seus prompts e recupere depois
|
||||||
|
- ✅ **14 Categorias:** Cobre todos os aspectos essenciais de hotelaria
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Próximos Passos
|
||||||
|
|
||||||
|
1. **Autenticação:** Sistema de login para usuários
|
||||||
|
2. **Biblioteca de Prompts:** Ver lista de prompts salvos
|
||||||
|
3. **Exportar/Copiar:** Botão para copiar prompt em TXT
|
||||||
|
4. **Templates Personalizados:** Criar templates específicos para cada hotel
|
||||||
|
5. **Deploy em Produção:** Configurar Vercel para produção
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 Documentação Detalhada
|
||||||
|
|
||||||
|
Para mais detalhes sobre cada funcionalidade, consulte os arquivos em `lib/` e `components/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**🤖 Versão:** 0.1.0 (MVP)
|
||||||
|
**👤 Desenvolvido por:** OpenClaw (Codex CLI)
|
||||||
|
**📅 Data:** 08/02/2026
|
||||||
201
RESUMO_MVP.md
Normal file
201
RESUMO_MVP.md
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
# 🎉 MVP - Gerador de Prompts para Hotéis/Motéis
|
||||||
|
|
||||||
|
## 📋 Visão Geral
|
||||||
|
|
||||||
|
Aplicativo web para **criação rápida e eficiente de prompts de IA** para hotéis e motéis, com:
|
||||||
|
- Interface guiada de 14 categorias universais
|
||||||
|
- Preview em tempo real do prompt final
|
||||||
|
- Persistência de prompts salvos
|
||||||
|
- Pré-conhecimento embutido de boas práticas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Stack Tecnológica
|
||||||
|
|
||||||
|
- **Frontend:** Next.js 15 + TypeScript + Tailwind CSS
|
||||||
|
- **Backend/API:** Next.js API Routes + Zod
|
||||||
|
- **Banco de dados:** SQLite + Prisma ORM
|
||||||
|
- **Deploy:** Vercel (sugerido)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Estrutura do Projeto
|
||||||
|
|
||||||
|
```
|
||||||
|
gerador-prompts-hoteis/
|
||||||
|
├── app/ # Páginas Next.js
|
||||||
|
│ ├── layout.tsx # Layout principal
|
||||||
|
│ ├── page.tsx # Página inicial
|
||||||
|
│ ├── globals.css # Estilos Tailwind
|
||||||
|
│ └── prompts/ # Wizard de 14 etapas
|
||||||
|
│ ├── page.tsx # Página do wizard
|
||||||
|
│ └── [id]/ # Detalhes de prompt salvo
|
||||||
|
│ └── page.tsx # Página de visualização
|
||||||
|
├── app/api/ # API Routes (Backend)
|
||||||
|
│ └── prompts/
|
||||||
|
│ ├── route.ts # POST /api/prompts (salvar)
|
||||||
|
│ └── [id]/
|
||||||
|
│ └── route.ts # GET /api/prompts/[id] (carregar)
|
||||||
|
├── components/ # Componentes React
|
||||||
|
│ ├── CategoryStep.tsx # 14 categorias com dicas
|
||||||
|
│ ├── Wizard.tsx # Gerenciador do fluxo
|
||||||
|
│ ├── StepIndicator.tsx # Indicador de progresso
|
||||||
|
│ ├── CategoryForm.tsx # Formulário de entrada
|
||||||
|
│ └── PromptPreview.tsx # Preview em tempo real
|
||||||
|
├── lib/ # Funções auxiliares
|
||||||
|
│ ├── categories.ts # Metadados das 14 categorias
|
||||||
|
│ ├── prompt-builder.ts # Montador de prompts finais
|
||||||
|
│ └── prisma.ts # Cliente Prisma compartilhado
|
||||||
|
├── prisma/ # Schema e dados
|
||||||
|
│ ├── schema.prisma # Modelo do banco (Prompt, PromptCategory)
|
||||||
|
│ └── seed.ts # Dados iniciais de exemplo
|
||||||
|
└── configs/ # Arquivos de configuração
|
||||||
|
├── package.json # Dependências e scripts
|
||||||
|
├── tsconfig.json # Configuração TypeScript
|
||||||
|
├── next.config.js # Configuração Next.js
|
||||||
|
├── tailwind.config.ts # Configuração Tailwind
|
||||||
|
├── postcss.config.js # PostCSS
|
||||||
|
└── next-env.d.ts # Tipos Next.js
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Funcionalidades Implementadas
|
||||||
|
|
||||||
|
### 🎨 Fase 0 - Fundamentos e Escopo
|
||||||
|
- ✅ 14 categorias universais identificadas
|
||||||
|
- ✅ Estrutura padrão de prompts documentada
|
||||||
|
- ✅ Boas práticas mínimas definidas
|
||||||
|
|
||||||
|
### 🔧 Fase 1 - Base Técnica
|
||||||
|
- ✅ Projeto Next.js criado com TypeScript
|
||||||
|
- ✅ Tailwind CSS configurado
|
||||||
|
- ✅ Arquivos de config básicos criados
|
||||||
|
|
||||||
|
### 🗂️ Fase 2 - Banco de Dados
|
||||||
|
- ✅ Prisma configurado com SQLite
|
||||||
|
- ✅ Schema criado: `Prompt` e `PromptCategory`
|
||||||
|
- ✅ Seed inicial criado com 3 prompts de exemplo
|
||||||
|
- ✅ Cliente Prisma compartilhado (`lib/prisma.ts`)
|
||||||
|
|
||||||
|
### 🖥️ Fase 3 - Interface do Usuário
|
||||||
|
- ✅ Página principal com wizard de 14 etapas
|
||||||
|
- ✅ `CategoryStep.tsx` - 14 categorias com prompts e dicas
|
||||||
|
- ✅ `Wizard.tsx` - Gerenciador do fluxo principal
|
||||||
|
- ✅ `StepIndicator.tsx` - Indicador visual de progresso
|
||||||
|
- ✅ `CategoryForm.tsx` - Formulário de entrada por categoria
|
||||||
|
- ✅ Pasta `app/prompts/` preparada
|
||||||
|
|
||||||
|
### 📝 Fase 4 - Geração do Prompt Final
|
||||||
|
- ✅ `lib/categories.ts` - Metadados das 14 categorias
|
||||||
|
- ✅ `lib/prompt-builder.ts` - Função modular para montar prompts
|
||||||
|
- ✅ `app/components/PromptPreview.tsx` - Preview em tempo real com useMemo
|
||||||
|
|
||||||
|
### 💾 Fase 5 - Persistência (API Routes)
|
||||||
|
- ✅ `app/api/prompts/route.ts` - POST para salvar prompts
|
||||||
|
- ✅ `app/prompts/[id]/route.ts` - GET para carregar prompts
|
||||||
|
- ✅ Sistema de salvamento e carregamento implementado
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏷️ As 14 Categorias Universais
|
||||||
|
|
||||||
|
| # | Categoria | Prompt Principal | Dica Principal |
|
||||||
|
|---|-----------|----------------|----------------|
|
||||||
|
| 1 | Perfil do Hotel | Descriva o hotel em uma ou duas frases. | Inclua estilo (boutique, resort, econômico) |
|
||||||
|
| 2 | Localização | Onde o hotel está localizado e quais pontos são relevantes? | Mencione proximidade de atrações, acessos e diferenciais. |
|
||||||
|
| 3 | Público-alvo | Quem é o hóspede ideal? | Descreva perfil demográfico e motivação de viagem. |
|
||||||
|
| 4 | Objetivo da Comunicação | Qual o principal objetivo desta peça? | Defina um objetivo claro. |
|
||||||
|
| 5 | Tom de Voz | Qual tom de voz deve ser usado? | Escolha 2-3 adjetivos. |
|
||||||
|
| 6 | Diferenciais | Quais são os diferenciais do hotel? | Priorize até 3 atributos. |
|
||||||
|
| 7 | Serviços e Amenidades | Liste os serviços e amenidades mais relevantes. | Agrupe por experiência. |
|
||||||
|
| 8 | Experiência do Hóspede | Como você quer que o hóspede se sinta? | Foque em sensações. |
|
||||||
|
| 9 | Gastronomia | O que destacar na oferta gastronômica? | Cite estilos culinários. |
|
||||||
|
| 10 | Bem-estar | Quais experiências de relaxamento ou saúde existem? | Inclua spa, terapias. |
|
||||||
|
| 11 | Sustentabilidade | Há práticas sustentáveis relevantes? | Seja específico. |
|
||||||
|
| 12 | Sazonalidade | Existe alguma sazonalidade ou período-chave? | Mencione eventos, feriados. |
|
||||||
|
| 13 | Restrições e Observações | Há algo que não deve ser dito ou prometido? | Liste limitações. |
|
||||||
|
| 14 | Chamada para Ação | Qual CTA deve encerrar a comunicação? | Use verbo direto. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Características do MVP
|
||||||
|
|
||||||
|
✅ **Modularidade:** Cada componente é independente e adaptável
|
||||||
|
✅ **Templates Flexíveis:** Estrutura de prompts padronizada mas customizável
|
||||||
|
✅ **Preview em Tempo Real:** O prompt atualiza conforme o usuário responde
|
||||||
|
✅ **Boas Práticas Embutidas:** Cada categoria tem dicas inline
|
||||||
|
✅ **Persistência:** Salvar e carregar prompts via API
|
||||||
|
✅ **14 Categorias:** Cobre todos os aspectos essenciais de hotelaria
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Boas Práticas Embutidas
|
||||||
|
|
||||||
|
O aplicativo já inclui dicas de prompt engineering em cada categoria:
|
||||||
|
|
||||||
|
- **Objetivo claro:** Cada pergunta tem um propósito específico
|
||||||
|
- **Contexto fornecido:** Pedidos anteriores ajudam a montar o prompt
|
||||||
|
- **Tom consistente:** Perguntas para definir estilo, tom e idioma
|
||||||
|
- **Exemplos:** Casos de uso para guiar as respostas da IA
|
||||||
|
- **Restrições explícitas:** Limitações do que pode ou não dizer
|
||||||
|
- **CTA direto:** Chamada para ação clara e objetiva
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Próximos Passos
|
||||||
|
|
||||||
|
### Para Rodar o Aplicativo
|
||||||
|
|
||||||
|
1. **Instalar dependências:**
|
||||||
|
```bash
|
||||||
|
cd /root/projetos/gerador-prompts-hoteis
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configurar banco de dados:**
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Editar DATABASE_URL se necessário
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Executar migrations:**
|
||||||
|
```bash
|
||||||
|
npx prisma migrate dev
|
||||||
|
npx prisma db seed
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Iniciar em modo desenvolvimento:**
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Para Deploy (Vercel)
|
||||||
|
|
||||||
|
1. Conectar repositório ao Vercel
|
||||||
|
2. Deploy automático com pushes no main
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Sugestões de Melhorias Futuras
|
||||||
|
|
||||||
|
1. **Validação com Zod:** Adicionar validação nos campos de entrada
|
||||||
|
2. **Autenticação:** Sistema de login para gerenciar prompts de usuários diferentes
|
||||||
|
3. **Exportar/Copiar:** Botão para copiar prompt em TXT e área de transferência
|
||||||
|
4. **Templates Adicionais:** Criar templates de prompt para casos comuns
|
||||||
|
5. **Múltiplos Idiomas:** Suporte para prompts em inglês, espanhol, etc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📌 Observações Importantes
|
||||||
|
|
||||||
|
- O MVP está pronto para uso local com SQLite
|
||||||
|
- Para produção, sugerimos migrar para PostgreSQL (Supabase)
|
||||||
|
- A estrutura é modular e fácil de adaptar às suas 14 categorias
|
||||||
|
- Você pode modificar os prompts em `lib/categories.ts` conforme suas necessidades
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Desenvolvido com auxílio do Codex CLI (GPT-5.2-codex)**
|
||||||
|
**Data:** 08/02/2026
|
||||||
|
**Status:** MVP funcional pronto! 🎉
|
||||||
52
app/api/prompts/route.ts
Normal file
52
app/api/prompts/route.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { CATEGORIES } from "@/lib/categories";
|
||||||
|
|
||||||
|
type PromptPayload = {
|
||||||
|
title?: string;
|
||||||
|
answers?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const body = (await request.json()) as PromptPayload;
|
||||||
|
const answers = body.answers ?? {};
|
||||||
|
|
||||||
|
if (!answers || typeof answers !== "object") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Campo 'answers' é obrigatório." },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const title =
|
||||||
|
body.title?.trim() ||
|
||||||
|
answers.perfil?.trim() ||
|
||||||
|
"Prompt de Hotelaria";
|
||||||
|
|
||||||
|
const categoriesData = CATEGORIES.map((category) => ({
|
||||||
|
categoryKey: category.id,
|
||||||
|
content: answers[category.id]?.trim() || ""
|
||||||
|
}));
|
||||||
|
|
||||||
|
const prompt = await prisma.prompt.create({
|
||||||
|
data: {
|
||||||
|
title,
|
||||||
|
categories: {
|
||||||
|
create: categoriesData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ id: prompt.id, title: prompt.title, createdAt: prompt.createdAt },
|
||||||
|
{ status: 201 }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("POST /api/prompts error", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Não foi possível salvar o prompt." },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
app/components/PromptPreview.tsx
Normal file
38
app/components/PromptPreview.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { montarPromptFinal, type PromptTemplate } from "@/lib/prompt-builder";
|
||||||
|
|
||||||
|
type PromptPreviewProps = {
|
||||||
|
answers: Record<string, string>;
|
||||||
|
template?: PromptTemplate;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PromptPreview({
|
||||||
|
answers,
|
||||||
|
template
|
||||||
|
}: PromptPreviewProps) {
|
||||||
|
const preview = useMemo(
|
||||||
|
() => montarPromptFinal(answers, template),
|
||||||
|
[answers, template]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="rounded-3xl border border-slate-200/80 bg-white/95 p-6 shadow-[0_24px_60px_-40px_rgba(15,23,42,0.6)]">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-slate-500">
|
||||||
|
Preview em tempo real
|
||||||
|
</p>
|
||||||
|
<h2 className="text-2xl font-semibold text-slate-900">
|
||||||
|
Prompt final estruturado
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-slate-600">
|
||||||
|
Ajuste as respostas e veja o prompt pronto para uso.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 rounded-2xl border border-slate-200 bg-slate-950/95 p-5 text-sm text-slate-100">
|
||||||
|
<pre className="whitespace-pre-wrap leading-relaxed">{preview}</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
app/globals.css
Normal file
32
app/globals.css
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-white text-neutral-900 antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-up {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(16px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float-slow {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-12px);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
app/layout.tsx
Normal file
18
app/layout.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "Gerador de Prompts para Hotéis",
|
||||||
|
description: "MVP para geração modular de prompts de hotelaria"
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
app/page.tsx
Normal file
12
app/page.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export default function HomePage() {
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen px-6 py-16">
|
||||||
|
<section className="mx-auto max-w-3xl space-y-6">
|
||||||
|
<h1 className="text-4xl font-semibold tracking-tight">Gerador de Prompts para Hotéis</h1>
|
||||||
|
<p className="text-lg text-neutral-700">
|
||||||
|
Base pronta para um MVP modular com templates flexíveis.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
47
app/prompts/[id]/route.ts
Normal file
47
app/prompts/[id]/route.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
type RouteParams = {
|
||||||
|
params: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function GET(_: Request, { params }: RouteParams) {
|
||||||
|
const id = Number(params.id);
|
||||||
|
|
||||||
|
if (!Number.isInteger(id)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "ID inválido." },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = await prisma.prompt.findUnique({
|
||||||
|
where: { id },
|
||||||
|
include: { categories: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!prompt) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Prompt não encontrado." },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const answers = prompt.categories.reduce<Record<string, string>>(
|
||||||
|
(acc, category) => {
|
||||||
|
acc[category.categoryKey] = category.content;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
id: prompt.id,
|
||||||
|
title: prompt.title,
|
||||||
|
createdAt: prompt.createdAt,
|
||||||
|
updatedAt: prompt.updatedAt,
|
||||||
|
answers
|
||||||
|
});
|
||||||
|
}
|
||||||
90
app/prompts/page.tsx
Normal file
90
app/prompts/page.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Plus_Jakarta_Sans, Space_Grotesk } from "next/font/google";
|
||||||
|
import Wizard from "@/components/Wizard";
|
||||||
|
import { CATEGORIES } from "@/lib/categories";
|
||||||
|
import PromptPreview from "@/app/components/PromptPreview";
|
||||||
|
|
||||||
|
const bodyFont = Plus_Jakarta_Sans({ subsets: ["latin"], weight: ["400", "500", "600"] });
|
||||||
|
const titleFont = Space_Grotesk({ subsets: ["latin"], weight: ["500", "600", "700"] });
|
||||||
|
|
||||||
|
export default function PromptsPage() {
|
||||||
|
const [step, setStep] = useState(0);
|
||||||
|
const [answers, setAnswers] = useState<Record<string, string>>({});
|
||||||
|
const [isComplete, setIsComplete] = useState(false);
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
if (step < CATEGORIES.length - 1) {
|
||||||
|
setStep((prev) => prev + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsComplete(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrev = () => {
|
||||||
|
setStep((prev) => Math.max(0, prev - 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setIsComplete(false);
|
||||||
|
setStep(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAnswerChange = (id: string, value: string) => {
|
||||||
|
setAnswers((prev) => ({ ...prev, [id]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative min-h-screen overflow-hidden bg-slate-50 ${bodyFont.className}`}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 -z-10 bg-[radial-gradient(circle_at_top,_#fff7ed_0%,_#f8fafc_45%,_#e2e8f0_100%)]" />
|
||||||
|
<div className="absolute -top-24 right-10 -z-10 h-72 w-72 rounded-full bg-amber-200/60 blur-3xl animate-[float-slow_12s_ease-in-out_infinite]" />
|
||||||
|
<div className="absolute bottom-0 left-0 -z-10 h-80 w-80 rounded-full bg-slate-200/70 blur-3xl animate-[float-slow_14s_ease-in-out_infinite]" />
|
||||||
|
<div className="absolute inset-0 -z-10 bg-[linear-gradient(90deg,_rgba(15,23,42,0.05)_1px,_transparent_1px),linear-gradient(180deg,_rgba(15,23,42,0.05)_1px,_transparent_1px)] bg-[size:120px_120px] opacity-30" />
|
||||||
|
|
||||||
|
<main className="mx-auto flex min-h-screen w-full max-w-6xl flex-col gap-10 px-6 py-12 md:px-10">
|
||||||
|
<header
|
||||||
|
className="flex flex-col gap-5 animate-[fade-up_0.7s_ease-out]"
|
||||||
|
style={{ animationDelay: "80ms" }}
|
||||||
|
>
|
||||||
|
<div className="flex flex-wrap items-center gap-3 text-xs font-semibold uppercase tracking-[0.2em] text-slate-500">
|
||||||
|
<span className="rounded-full border border-slate-300 bg-white/80 px-3 py-1">
|
||||||
|
Wizard 14 etapas
|
||||||
|
</span>
|
||||||
|
<span>Gerador de Prompts</span>
|
||||||
|
</div>
|
||||||
|
<div className="max-w-3xl">
|
||||||
|
<h1
|
||||||
|
className={`${titleFont.className} text-4xl font-semibold text-slate-900 md:text-5xl`}
|
||||||
|
>
|
||||||
|
Construa o briefing perfeito para hotelaria
|
||||||
|
</h1>
|
||||||
|
<p className="mt-4 text-lg text-slate-600">
|
||||||
|
Preencha as etapas com contexto claro para gerar prompts precisos,
|
||||||
|
consistentes e prontos para comunicação.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="grid gap-8 animate-[fade-up_0.7s_ease-out]"
|
||||||
|
style={{ animationDelay: "160ms" }}
|
||||||
|
>
|
||||||
|
<Wizard
|
||||||
|
step={step}
|
||||||
|
categories={CATEGORIES}
|
||||||
|
answers={answers}
|
||||||
|
isComplete={isComplete}
|
||||||
|
onAnswerChange={handleAnswerChange}
|
||||||
|
onNext={handleNext}
|
||||||
|
onPrev={handlePrev}
|
||||||
|
onReset={handleReset}
|
||||||
|
/>
|
||||||
|
<PromptPreview answers={answers} />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
34
components/CategoryForm.tsx
Normal file
34
components/CategoryForm.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
type CategoryFormProps = {
|
||||||
|
label: string;
|
||||||
|
placeholder: string;
|
||||||
|
helper?: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CategoryForm({
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
helper,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
}: CategoryFormProps) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-3xl border border-slate-200/80 bg-white/90 p-6 shadow-[0_24px_60px_-40px_rgba(15,23,42,0.6)]">
|
||||||
|
<label className="flex flex-col gap-3">
|
||||||
|
<span className="text-sm font-semibold uppercase tracking-[0.2em] text-slate-500">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<textarea
|
||||||
|
className="min-h-[160px] w-full resize-y rounded-2xl border border-slate-200/80 bg-white px-4 py-3 text-base text-slate-900 shadow-inner focus:border-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-400/40"
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{helper ? (
|
||||||
|
<p className="mt-3 text-sm text-slate-500">{helper}</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
components/CategoryStep.tsx
Normal file
42
components/CategoryStep.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import CategoryForm from "./CategoryForm";
|
||||||
|
import type { Category } from "@/lib/categories";
|
||||||
|
|
||||||
|
type CategoryStepProps = {
|
||||||
|
category: Category;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CategoryStep({
|
||||||
|
category,
|
||||||
|
value,
|
||||||
|
onChange
|
||||||
|
}: CategoryStepProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="rounded-3xl border border-slate-200/80 bg-white/80 p-6 shadow-[0_24px_60px_-40px_rgba(15,23,42,0.6)]">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<span className="text-xs font-semibold uppercase tracking-[0.2em] text-slate-500">
|
||||||
|
{category.title}
|
||||||
|
</span>
|
||||||
|
<h2 className="text-2xl font-semibold text-slate-900 md:text-3xl">
|
||||||
|
{category.prompt}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex items-start gap-3 rounded-2xl border border-amber-200/70 bg-amber-50/70 p-4 text-sm text-amber-900">
|
||||||
|
<span className="mt-1 inline-flex h-6 w-6 items-center justify-center rounded-full bg-amber-200/80 text-xs font-semibold text-amber-900">
|
||||||
|
D
|
||||||
|
</span>
|
||||||
|
<p>{category.tip}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CategoryForm
|
||||||
|
label={category.title}
|
||||||
|
helper={category.helper}
|
||||||
|
placeholder={category.placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
components/StepIndicator.tsx
Normal file
60
components/StepIndicator.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
type StepIndicatorProps = {
|
||||||
|
current: number;
|
||||||
|
total: number;
|
||||||
|
isComplete: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function StepIndicator({
|
||||||
|
current,
|
||||||
|
total,
|
||||||
|
isComplete
|
||||||
|
}: StepIndicatorProps) {
|
||||||
|
const progress = Math.round(((current + 1) / total) * 100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-3xl border border-slate-200/80 bg-white/80 p-6 shadow-[0_24px_60px_-40px_rgba(15,23,42,0.6)]">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-slate-500">
|
||||||
|
Progresso
|
||||||
|
</p>
|
||||||
|
<p className="text-2xl font-semibold text-slate-900">
|
||||||
|
{isComplete ? "Concluído" : `Etapa ${current + 1} de ${total}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-xs uppercase tracking-[0.2em] text-slate-500">
|
||||||
|
Avanço
|
||||||
|
</p>
|
||||||
|
<p className="text-lg font-semibold text-slate-900">
|
||||||
|
{isComplete ? "100%" : `${progress}%`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 h-2 w-full overflow-hidden rounded-full bg-slate-200/70">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full bg-gradient-to-r from-slate-900 via-slate-700 to-slate-500 transition-all duration-500"
|
||||||
|
style={{ width: `${isComplete ? 100 : progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex flex-wrap gap-2">
|
||||||
|
{Array.from({ length: total }).map((_, index) => {
|
||||||
|
const isActive = index === current && !isComplete;
|
||||||
|
const isDone = isComplete || index < current;
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={`step-${index}`}
|
||||||
|
className={`h-2.5 w-2.5 rounded-full transition-all duration-300 ${
|
||||||
|
isDone
|
||||||
|
? "bg-slate-900"
|
||||||
|
: isActive
|
||||||
|
? "bg-amber-400"
|
||||||
|
: "bg-slate-200"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
131
components/Wizard.tsx
Normal file
131
components/Wizard.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import CategoryStep from "./CategoryStep";
|
||||||
|
import type { Category } from "@/lib/categories";
|
||||||
|
import StepIndicator from "./StepIndicator";
|
||||||
|
|
||||||
|
type WizardProps = {
|
||||||
|
step: number;
|
||||||
|
categories: Category[];
|
||||||
|
answers: Record<string, string>;
|
||||||
|
isComplete: boolean;
|
||||||
|
onAnswerChange: (id: string, value: string) => void;
|
||||||
|
onNext: () => void;
|
||||||
|
onPrev: () => void;
|
||||||
|
onReset: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Wizard({
|
||||||
|
step,
|
||||||
|
categories,
|
||||||
|
answers,
|
||||||
|
isComplete,
|
||||||
|
onAnswerChange,
|
||||||
|
onNext,
|
||||||
|
onPrev,
|
||||||
|
onReset
|
||||||
|
}: WizardProps) {
|
||||||
|
const currentCategory = categories[step];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto flex w-full max-w-5xl flex-col gap-8">
|
||||||
|
<StepIndicator
|
||||||
|
current={step}
|
||||||
|
total={categories.length}
|
||||||
|
isComplete={isComplete}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isComplete ? (
|
||||||
|
<div className="grid gap-6">
|
||||||
|
<div className="rounded-3xl border border-slate-200/80 bg-white/90 p-8 shadow-[0_24px_60px_-40px_rgba(15,23,42,0.6)]">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-slate-500">
|
||||||
|
Resumo
|
||||||
|
</p>
|
||||||
|
<h2 className="mt-2 text-3xl font-semibold text-slate-900">
|
||||||
|
Todas as respostas estão prontas
|
||||||
|
</h2>
|
||||||
|
<p className="mt-3 text-base text-slate-600">
|
||||||
|
Revise abaixo e, se necessário, volte para ajustar antes de gerar o
|
||||||
|
prompt final.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 flex flex-wrap gap-3">
|
||||||
|
<button
|
||||||
|
className="rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white shadow-lg shadow-slate-900/30 transition hover:-translate-y-0.5"
|
||||||
|
onClick={onReset}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Editar respostas
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
{categories.map((category) => (
|
||||||
|
<div
|
||||||
|
key={category.id}
|
||||||
|
className="rounded-3xl border border-slate-200/80 bg-white/90 p-5 shadow-[0_20px_50px_-40px_rgba(15,23,42,0.6)]"
|
||||||
|
>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-slate-500">
|
||||||
|
{category.title}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-base text-slate-900">
|
||||||
|
{answers[category.id] || "(Sem resposta)"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : currentCategory ? (
|
||||||
|
<div className="grid gap-6 lg:grid-cols-[1.2fr_0.8fr]">
|
||||||
|
<CategoryStep
|
||||||
|
category={currentCategory}
|
||||||
|
value={answers[currentCategory.id] ?? ""}
|
||||||
|
onChange={(value) => onAnswerChange(currentCategory.id, value)}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="rounded-3xl border border-slate-900 bg-slate-900 p-6 text-white shadow-[0_24px_60px_-40px_rgba(15,23,42,0.6)]">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-slate-300">
|
||||||
|
Dica rápida
|
||||||
|
</p>
|
||||||
|
<h3 className="mt-2 text-2xl font-semibold">
|
||||||
|
Pense no impacto direto
|
||||||
|
</h3>
|
||||||
|
<p className="mt-3 text-sm text-slate-200">
|
||||||
|
Cada etapa deve informar uma decisão de comunicação. Se não
|
||||||
|
influenciar o resultado final, simplifique.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-3xl border border-slate-200/80 bg-white/90 p-6 shadow-[0_24px_60px_-40px_rgba(15,23,42,0.6)]">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-slate-500">
|
||||||
|
Etapa atual
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-lg font-semibold text-slate-900">
|
||||||
|
{currentCategory.title}
|
||||||
|
</p>
|
||||||
|
<p className="mt-3 text-sm text-slate-600">
|
||||||
|
{currentCategory.tip}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!isComplete ? (
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||||
|
<button
|
||||||
|
className="rounded-full border border-slate-300 bg-white px-5 py-3 text-sm font-semibold text-slate-700 transition hover:-translate-y-0.5 hover:border-slate-900"
|
||||||
|
onClick={onPrev}
|
||||||
|
type="button"
|
||||||
|
disabled={step === 0}
|
||||||
|
>
|
||||||
|
Voltar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white shadow-lg shadow-slate-900/30 transition hover:-translate-y-0.5"
|
||||||
|
onClick={onNext}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{step === categories.length - 1 ? "Concluir" : "Continuar"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
135
lib/categories.ts
Normal file
135
lib/categories.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
export type Category = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
prompt: string;
|
||||||
|
tip: string;
|
||||||
|
placeholder: string;
|
||||||
|
helper?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CATEGORIES: Category[] = [
|
||||||
|
{
|
||||||
|
id: "perfil",
|
||||||
|
title: "Perfil do Hotel",
|
||||||
|
prompt: "Descreva o hotel em uma ou duas frases.",
|
||||||
|
tip: "Inclua o estilo (boutique, resort, econômico) e a personalidade geral.",
|
||||||
|
placeholder:
|
||||||
|
"Ex.: Hotel boutique urbano com foco em design contemporâneo e atendimento personalizado.",
|
||||||
|
helper: "Dê o contexto macro do hotel para guiar o restante das respostas."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "localizacao",
|
||||||
|
title: "Localização",
|
||||||
|
prompt: "Onde o hotel está localizado e quais pontos são relevantes?",
|
||||||
|
tip: "Mencione proximidade de atrações, acessos e diferenciais do entorno.",
|
||||||
|
placeholder:
|
||||||
|
"Ex.: Centro histórico, a 5 minutos da praça principal e perto de restaurantes premiados.",
|
||||||
|
helper: "Detalhes do entorno ajudam a contextualizar a experiência do hóspede."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "publico-alvo",
|
||||||
|
title: "Público-alvo",
|
||||||
|
prompt: "Quem é o hóspede ideal?",
|
||||||
|
tip: "Descreva perfil demográfico e motivação de viagem.",
|
||||||
|
placeholder:
|
||||||
|
"Ex.: Casais de 30 a 45 anos buscando escapadas românticas de fim de semana.",
|
||||||
|
helper: "Seja específico para ajustar o tom e os argumentos."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "objetivo",
|
||||||
|
title: "Objetivo da Comunicação",
|
||||||
|
prompt: "Qual o principal objetivo desta peça?",
|
||||||
|
tip: "Defina um objetivo claro: reservas, awareness, upgrade, etc.",
|
||||||
|
placeholder: "Ex.: Aumentar reservas diretas para a baixa temporada.",
|
||||||
|
helper: "Um objetivo único dá foco às mensagens."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tom-de-voz",
|
||||||
|
title: "Tom de Voz",
|
||||||
|
prompt: "Qual tom de voz deve ser usado?",
|
||||||
|
tip: "Escolha 2 a 3 adjetivos que descrevam a comunicação.",
|
||||||
|
placeholder: "Ex.: sofisticado, acolhedor e inspirador.",
|
||||||
|
helper: "Consistência de tom gera reconhecimento de marca."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "diferenciais",
|
||||||
|
title: "Diferenciais",
|
||||||
|
prompt: "Quais são os diferenciais do hotel?",
|
||||||
|
tip: "Priorize até 3 atributos com impacto direto na escolha.",
|
||||||
|
placeholder:
|
||||||
|
"Ex.: Spa exclusivo, rooftop com vista 360º, menu assinado por chef local.",
|
||||||
|
helper: "Foque no que ninguém mais oferece ou faz melhor."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "servicos",
|
||||||
|
title: "Serviços e Amenidades",
|
||||||
|
prompt: "Liste os serviços e amenidades mais relevantes.",
|
||||||
|
tip: "Agrupe por experiência (bem-estar, gastronomia, família).",
|
||||||
|
placeholder:
|
||||||
|
"Ex.: piscina aquecida, kids club, transfer aeroporto, café da manhã artesanal.",
|
||||||
|
helper: "Inclua o que realmente gera valor percebido."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "experiencia",
|
||||||
|
title: "Experiência do Hóspede",
|
||||||
|
prompt: "Como você quer que o hóspede se sinta?",
|
||||||
|
tip: "Descreva emoções e momentos-chave da jornada.",
|
||||||
|
placeholder:
|
||||||
|
"Ex.: sensação de exclusividade do check-in ao jantar, com atenção aos detalhes.",
|
||||||
|
helper: "Foque em sensações e não apenas em características."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "gastronomia",
|
||||||
|
title: "Gastronomia",
|
||||||
|
prompt: "O que destacar na oferta gastronômica?",
|
||||||
|
tip: "Cite estilos culinários, horários e experiências especiais.",
|
||||||
|
placeholder:
|
||||||
|
"Ex.: restaurante com cozinha autoral regional e menu degustação às sextas.",
|
||||||
|
helper: "Gastronomia costuma ser um fator decisivo para reservas."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "bem-estar",
|
||||||
|
title: "Bem-estar",
|
||||||
|
prompt: "Quais experiências de relaxamento ou saúde existem?",
|
||||||
|
tip: "Inclua spa, terapias, academia ou atividades ao ar livre.",
|
||||||
|
placeholder:
|
||||||
|
"Ex.: spa com terapias orgânicas, yoga ao nascer do sol e academia 24h.",
|
||||||
|
helper: "Mostre como o hotel cuida do bem-estar do hóspede."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sustentabilidade",
|
||||||
|
title: "Sustentabilidade",
|
||||||
|
prompt: "Há práticas sustentáveis relevantes?",
|
||||||
|
tip: "Seja específico e evite generalizações.",
|
||||||
|
placeholder:
|
||||||
|
"Ex.: reuso de água, energia solar e fornecedores locais certificados.",
|
||||||
|
helper: "Transparência aumenta a credibilidade das ações verdes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sazonalidade",
|
||||||
|
title: "Sazonalidade",
|
||||||
|
prompt: "Existe alguma sazonalidade ou período-chave?",
|
||||||
|
tip: "Mencione eventos, feriados ou baixa temporada.",
|
||||||
|
placeholder:
|
||||||
|
"Ex.: festival de inverno em julho e baixa temporada entre março e maio.",
|
||||||
|
helper: "Ajuda a ajustar urgência e ofertas específicas."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "restricoes",
|
||||||
|
title: "Restrições e Observações",
|
||||||
|
prompt: "Há algo que não deve ser dito ou prometido?",
|
||||||
|
tip: "Liste limitações, políticas ou mensagens proibidas.",
|
||||||
|
placeholder:
|
||||||
|
"Ex.: não mencionar descontos; evitar prometer vista para todos os quartos.",
|
||||||
|
helper: "Evita desalinhamentos e riscos com compliance."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cta",
|
||||||
|
title: "Chamada para Ação",
|
||||||
|
prompt: "Qual CTA deve encerrar a comunicação?",
|
||||||
|
tip: "Use um verbo direto e um benefício imediato.",
|
||||||
|
placeholder:
|
||||||
|
"Ex.: Reserve agora e garanta o melhor quarto com tarifa exclusiva.",
|
||||||
|
helper: "A CTA precisa estar alinhada ao objetivo principal."
|
||||||
|
}
|
||||||
|
];
|
||||||
9
lib/prisma.ts
Normal file
9
lib/prisma.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
const globalForPrisma = global as unknown as { prisma?: PrismaClient };
|
||||||
|
|
||||||
|
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
globalForPrisma.prisma = prisma;
|
||||||
|
}
|
||||||
130
lib/prompt-builder.ts
Normal file
130
lib/prompt-builder.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { CATEGORIES } from "./categories";
|
||||||
|
|
||||||
|
export type PromptAnswers = Record<string, string>;
|
||||||
|
|
||||||
|
type PromptSection = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
render: (answers: PromptAnswers) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PromptTemplate = {
|
||||||
|
name: string;
|
||||||
|
intro?: string;
|
||||||
|
sections: PromptSection[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const safeValue = (value?: string) => {
|
||||||
|
const cleaned = value?.trim();
|
||||||
|
return cleaned ? cleaned : "Não informado.";
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderContextItem = (label: string, value?: string) =>
|
||||||
|
`- ${label}: ${safeValue(value)}`;
|
||||||
|
|
||||||
|
const buildExampleHeadline = (answers: PromptAnswers) => {
|
||||||
|
const perfil = answers.perfil?.trim();
|
||||||
|
const local = answers.localizacao?.trim();
|
||||||
|
if (perfil && local) {
|
||||||
|
return `Experimente ${perfil} em ${local}.`;
|
||||||
|
}
|
||||||
|
if (perfil) {
|
||||||
|
return `Descubra ${perfil} feito para surpreender.`;
|
||||||
|
}
|
||||||
|
if (local) {
|
||||||
|
return `Sua próxima estadia em ${local} começa aqui.`;
|
||||||
|
}
|
||||||
|
return "Uma experiência de hotelaria pensada para encantar.";
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildExampleCTA = (answers: PromptAnswers) => {
|
||||||
|
const cta = answers.cta?.trim();
|
||||||
|
return cta ? cta : "Reserve agora e viva essa experiência.";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_PROMPT_TEMPLATE: PromptTemplate = {
|
||||||
|
name: "hotelaria-core",
|
||||||
|
intro:
|
||||||
|
"Você é um redator sênior em hotelaria. Use as informações abaixo para criar uma peça de comunicação clara, persuasiva e fiel ao briefing.",
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
id: "objetivo",
|
||||||
|
title: "Objetivo",
|
||||||
|
render: (answers) => safeValue(answers.objetivo)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "contexto",
|
||||||
|
title: "Contexto",
|
||||||
|
render: (answers) => {
|
||||||
|
const lines = [
|
||||||
|
renderContextItem("Perfil do hotel", answers.perfil),
|
||||||
|
renderContextItem("Localização", answers.localizacao),
|
||||||
|
renderContextItem("Diferenciais", answers.diferenciais),
|
||||||
|
renderContextItem("Serviços e amenidades", answers.servicos),
|
||||||
|
renderContextItem("Experiência desejada", answers.experiencia),
|
||||||
|
renderContextItem("Gastronomia", answers.gastronomia),
|
||||||
|
renderContextItem("Bem-estar", answers["bem-estar"]),
|
||||||
|
renderContextItem("Sustentabilidade", answers.sustentabilidade),
|
||||||
|
renderContextItem("Sazonalidade", answers.sazonalidade)
|
||||||
|
];
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "publico",
|
||||||
|
title: "Público",
|
||||||
|
render: (answers) => safeValue(answers["publico-alvo"])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tom",
|
||||||
|
title: "Tom",
|
||||||
|
render: (answers) => safeValue(answers["tom-de-voz"])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "formato",
|
||||||
|
title: "Formato",
|
||||||
|
render: (answers) => [
|
||||||
|
"Idioma: português (pt-BR).",
|
||||||
|
"Estrutura: 1 headline, 1 parágrafo curto, 1 lista de até 3 diferenciais, CTA final.",
|
||||||
|
"Extensão sugerida: 90 a 130 palavras.",
|
||||||
|
`CTA preferencial: ${safeValue(answers.cta)}`
|
||||||
|
].join("\n")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "restricoes",
|
||||||
|
title: "Restrições",
|
||||||
|
render: (answers) => safeValue(answers.restricoes)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "exemplos",
|
||||||
|
title: "Exemplos",
|
||||||
|
render: (answers) =>
|
||||||
|
[
|
||||||
|
`Headline: ${buildExampleHeadline(answers)}`,
|
||||||
|
`CTA: ${buildExampleCTA(answers)}`
|
||||||
|
].join("\n")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const montarPromptFinal = (
|
||||||
|
answers: PromptAnswers,
|
||||||
|
template: PromptTemplate = DEFAULT_PROMPT_TEMPLATE
|
||||||
|
) => {
|
||||||
|
const intro = template.intro ? `${template.intro}\n\n` : "";
|
||||||
|
const body = template.sections
|
||||||
|
.map((section) => {
|
||||||
|
const content = section.render(answers);
|
||||||
|
return `### ${section.title}\n${content}`;
|
||||||
|
})
|
||||||
|
.join("\n\n");
|
||||||
|
|
||||||
|
const missing = CATEGORIES.filter(
|
||||||
|
(category) => !answers[category.id]?.trim()
|
||||||
|
).map((category) => category.title);
|
||||||
|
const missingLine = missing.length
|
||||||
|
? `\n\nAtenção: faltam respostas em ${missing.join(", ")}.`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return `${intro}${body}${missingLine}`.trim();
|
||||||
|
};
|
||||||
4
next-env.d.ts
vendored
Normal file
4
next-env.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
9
next.config.js
Normal file
9
next.config.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
experimental: {
|
||||||
|
typedRoutes: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = nextConfig;
|
||||||
32
package.json
Normal file
32
package.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "gerador-prompts-hoteis",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"db:seed": "prisma db seed"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/client": "^6.0.0",
|
||||||
|
"next": "^15.1.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"postcss": "^8.4.47",
|
||||||
|
"prisma": "^6.0.0",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"tsx": "^4.19.2",
|
||||||
|
"typescript": "^5.7.0"
|
||||||
|
},
|
||||||
|
"prisma": {
|
||||||
|
"seed": "tsx prisma/seed.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
29
prisma/schema.prisma
Normal file
29
prisma/schema.prisma
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Prisma schema for SQLite
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "sqlite"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Prompt {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
title String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
categories PromptCategory[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model PromptCategory {
|
||||||
|
promptId Int
|
||||||
|
categoryKey String
|
||||||
|
content String
|
||||||
|
|
||||||
|
prompt Prompt @relation(fields: [promptId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@id([promptId, categoryKey])
|
||||||
|
@@index([categoryKey])
|
||||||
|
}
|
||||||
68
prisma/seed.ts
Normal file
68
prisma/seed.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await prisma.promptCategory.deleteMany();
|
||||||
|
await prisma.prompt.deleteMany();
|
||||||
|
|
||||||
|
await prisma.prompt.create({
|
||||||
|
data: {
|
||||||
|
title: "Boas-vindas ao Hotel",
|
||||||
|
categories: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
categoryKey: "recepcao",
|
||||||
|
content: "Mensagem de boas-vindas personalizada para novos hospedes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
categoryKey: "concierge",
|
||||||
|
content: "Sugestao de passeios e servicos locais para o hospede."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.prompt.create({
|
||||||
|
data: {
|
||||||
|
title: "Check-out Express",
|
||||||
|
categories: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
categoryKey: "operacoes",
|
||||||
|
content: "Instrucao clara para check-out rapido e sem filas."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
categoryKey: "financeiro",
|
||||||
|
content: "Resumo simples de cobrancas e confirmacao de pagamento."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.prompt.create({
|
||||||
|
data: {
|
||||||
|
title: "Feedback de Estadia",
|
||||||
|
categories: {
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
categoryKey: "pos-estadia",
|
||||||
|
content: "Convite curto para avaliacao da experiencia no hotel."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.then(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
})
|
||||||
|
.catch(async (error) => {
|
||||||
|
console.error(error);
|
||||||
|
await prisma.$disconnect();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
15
tailwind.config.ts
Normal file
15
tailwind.config.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
content: [
|
||||||
|
"./app/**/*.{ts,tsx}",
|
||||||
|
"./components/**/*.{ts,tsx}",
|
||||||
|
"./lib/**/*.{ts,tsx}"
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {}
|
||||||
|
},
|
||||||
|
plugins: []
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user