Actualizar flujo CDC Brief: campaña existente y revisión final
This commit is contained in:
+10
-7
@@ -20,17 +20,19 @@ Este portal funciona por pasos y no como un chat libre.
|
||||
|
||||
1. **Nota de voz** _(obligatorio)_
|
||||
2. **Imágenes de referencia** _(obligatorio)_
|
||||
3. **Documentos relacionados y archivos de apoyo** _(opcional)_
|
||||
4. **Enlaces de referencia online** _(opcional)_
|
||||
3. **Documentos relacionados** _(opcional)_
|
||||
4. **Campaña o elementos gráficos existentes** _(opcional)_
|
||||
5. **Enlaces de referencia online** _(opcional)_
|
||||
6. **Revisión y envío**
|
||||
|
||||
**Qué hace el sistema con cada entrada:**
|
||||
|
||||
- **Nota de voz:** se analiza
|
||||
- **Imágenes:** se analizan
|
||||
- **PDF, Excel, PowerPoint, Word y otros archivos:** se guardan en la carpeta del proyecto
|
||||
- **Documentos y Campaña:** se guardan en la carpeta del proyecto
|
||||
- **Enlaces online:** se listan dentro del brief
|
||||
|
||||
Cuando completes los pasos obligatorios, podrás enviar la solicitud.`;
|
||||
Cuando completes los pasos obligatorios, podrás enviar la solicitud en el paso final.`;
|
||||
|
||||
const [messages, setMessages] = useState<Message[]>([
|
||||
{
|
||||
@@ -145,7 +147,8 @@ Cuando completes los pasos obligatorios, podrás enviar la solicitud.`;
|
||||
formData.append('timestamp', new Date().toISOString());
|
||||
|
||||
attachments.forEach((att, index) => {
|
||||
formData.append(`file_${index}`, att.file);
|
||||
const key = att.category === 'campaign' ? `campaign_file_${index}` : `file_${index}`;
|
||||
formData.append(key, att.file);
|
||||
});
|
||||
|
||||
if (audio) {
|
||||
@@ -288,8 +291,8 @@ Verifica que la URL productiva esté correcta en el entorno y que el endpoint es
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
{[
|
||||
'La nota de voz y las imágenes sí se analizan automáticamente.',
|
||||
'PDF, Excel, PowerPoint, Word y otros adjuntos se guardan en la carpeta del proyecto.',
|
||||
'La nota de voz, las imágenes y los PDFs se analizan automáticamente.',
|
||||
'Excel, PowerPoint, Word y otros adjuntos se guardan como soporte en la carpeta del proyecto.',
|
||||
'Los enlaces online se registran para alimentar el brief final.',
|
||||
'El usuario no necesita usar el portal como chat libre: todo queda guiado.',
|
||||
].map((item) => (
|
||||
|
||||
+191
-15
@@ -23,7 +23,7 @@ interface InputAreaProps {
|
||||
isProcessing: boolean;
|
||||
}
|
||||
|
||||
type GuidedStep = 'audio' | 'images' | 'documents' | 'links';
|
||||
type GuidedStep = 'audio' | 'images' | 'documents' | 'campaign' | 'links' | 'review';
|
||||
|
||||
interface StepDefinition {
|
||||
id: GuidedStep;
|
||||
@@ -52,14 +52,28 @@ const STEP_DEFINITIONS: StepDefinition[] = [
|
||||
id: 'documents',
|
||||
number: 3,
|
||||
title: 'Documentos relacionados',
|
||||
hint: 'Opcional. Adjunta PDF, Excel, PowerPoint, Word u otros archivos de apoyo.',
|
||||
hint: 'Opcional. Adjunta archivos de apoyo.',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'campaign',
|
||||
number: 4,
|
||||
title: 'Campaña o elementos gráficos',
|
||||
hint: 'Opcional. Si ya existen piezas gráficas o logos.',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'links',
|
||||
number: 4,
|
||||
title: 'Enlaces de referencia online',
|
||||
hint: 'Opcional. Pega enlaces, uno por línea.',
|
||||
number: 5,
|
||||
title: 'Enlaces de referencia',
|
||||
hint: 'Opcional. Pega enlaces online.',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'review',
|
||||
number: 6,
|
||||
title: 'Revisión y envío',
|
||||
hint: 'Finaliza tu solicitud aquí.',
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
@@ -71,6 +85,7 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
|
||||
const [recordingDuration, setRecordingDuration] = useState(0);
|
||||
const [audioRecording, setAudioRecording] = useState<AudioRecording | null>(null);
|
||||
const [currentStep, setCurrentStep] = useState<GuidedStep>('audio');
|
||||
const [hasCampaign, setHasCampaign] = useState<'yes' | 'no' | null>(null);
|
||||
const [validationMessage, setValidationMessage] = useState<string | null>(null);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -84,7 +99,11 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
|
||||
[attachments],
|
||||
);
|
||||
const documentAttachments = useMemo(
|
||||
() => attachments.filter((att) => att.type !== 'image'),
|
||||
() => attachments.filter((att) => att.type !== 'image' && att.category !== 'campaign'),
|
||||
[attachments],
|
||||
);
|
||||
const campaignAttachments = useMemo(
|
||||
() => attachments.filter((att) => att.category === 'campaign'),
|
||||
[attachments],
|
||||
);
|
||||
|
||||
@@ -109,8 +128,14 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
|
||||
return imageAttachments.length > 0 ? 'done' : currentStep === 'images' ? 'active' : 'pending';
|
||||
case 'documents':
|
||||
return documentAttachments.length > 0 ? 'done' : currentStep === 'documents' ? 'active' : 'pending';
|
||||
case 'campaign':
|
||||
if (hasCampaign === 'no') return 'done';
|
||||
if (hasCampaign === 'yes' && campaignAttachments.length > 0) return 'done';
|
||||
return currentStep === 'campaign' ? 'active' : 'pending';
|
||||
case 'links':
|
||||
return text.trim() ? 'done' : currentStep === 'links' ? 'active' : 'pending';
|
||||
case 'review':
|
||||
return currentStep === 'review' ? 'active' : 'pending';
|
||||
default:
|
||||
return 'pending';
|
||||
}
|
||||
@@ -158,6 +183,7 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
|
||||
file,
|
||||
type: inferAttachmentType(file),
|
||||
url: URL.createObjectURL(file),
|
||||
category: currentStep === 'campaign' ? 'campaign' : 'general',
|
||||
}));
|
||||
|
||||
setAttachments((prev) => [...prev, ...newAttachments]);
|
||||
@@ -254,9 +280,27 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
|
||||
}
|
||||
|
||||
if (currentStep === 'documents') {
|
||||
advanceTo('campaign');
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentStep === 'campaign') {
|
||||
if (hasCampaign === null) {
|
||||
setValidationMessage('Por favor responde si ya existe campaña o elementos gráficos.');
|
||||
return;
|
||||
}
|
||||
if (hasCampaign === 'yes' && campaignAttachments.length === 0) {
|
||||
setValidationMessage('Si seleccionaste que sí existe campaña, debes adjuntar al menos un archivo.');
|
||||
return;
|
||||
}
|
||||
advanceTo('links');
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentStep === 'links') {
|
||||
advanceTo('review');
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const buildSubmissionText = () => {
|
||||
@@ -268,11 +312,19 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
|
||||
|
||||
const summaryLines = [
|
||||
'Solicitud enviada desde el portal guiado del Brief CDC.',
|
||||
`Nota de voz: ${audioRecording ? 'sí' : 'no'}`,
|
||||
`Nota de voz: ${audioRecording ? 'Sí' : 'No'}`,
|
||||
`Imágenes de referencia: ${imageAttachments.length}`,
|
||||
`Documentos relacionados: ${documentAttachments.length}`,
|
||||
`¿Existe campaña o elementos gráficos?: ${hasCampaign === 'yes' ? 'Sí' : 'No'}`,
|
||||
];
|
||||
|
||||
if (hasCampaign === 'yes') {
|
||||
summaryLines.push(`Archivos de campaña adjuntos: ${campaignAttachments.length}`);
|
||||
campaignAttachments.forEach((att) => {
|
||||
summaryLines.push(`- ${att.file.name}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (linksText) {
|
||||
summaryLines.push('', 'Enlaces de referencia:', linksText);
|
||||
}
|
||||
@@ -281,8 +333,13 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
|
||||
};
|
||||
|
||||
const handleSend = () => {
|
||||
if (currentStep !== 'review') {
|
||||
advanceTo('review');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!requiredCompleted) {
|
||||
setValidationMessage('Completa primero la nota de voz y al menos una imagen de referencia.');
|
||||
setValidationMessage('Completa primero los pasos obligatorios (voz e imágenes).');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -290,6 +347,7 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
|
||||
onSendMessage(buildSubmissionText(), attachments, audioRecording || undefined);
|
||||
setText('');
|
||||
setAttachments([]);
|
||||
setHasCampaign(null);
|
||||
if (audioRecording?.url) {
|
||||
URL.revokeObjectURL(audioRecording.url);
|
||||
}
|
||||
@@ -326,7 +384,7 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 xl:grid-cols-4">
|
||||
<div className="grid gap-3 xl:grid-cols-6">
|
||||
{STEP_DEFINITIONS.map((step) => {
|
||||
const status = getStepStatus(step.id);
|
||||
return (
|
||||
@@ -335,7 +393,7 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
|
||||
type="button"
|
||||
onClick={() => advanceTo(step.id)}
|
||||
className={cn(
|
||||
'group relative overflow-hidden rounded-[24px] border p-4 text-left transition-all',
|
||||
'group relative flex h-full flex-col overflow-hidden rounded-[24px] border p-4 text-left transition-all',
|
||||
status === 'done' && 'border-emerald-400/25 bg-emerald-500/10',
|
||||
status === 'active' && 'border-indigo-400/30 bg-[linear-gradient(180deg,rgba(99,102,241,0.18),rgba(59,130,246,0.08))] shadow-[0_18px_40px_rgba(79,70,229,0.18)]',
|
||||
status === 'pending' && 'border-white/10 bg-black/20 hover:border-white/20 hover:bg-black/25'
|
||||
@@ -353,8 +411,8 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
|
||||
<div className="h-3 w-3 rounded-full bg-zinc-600" />
|
||||
)}
|
||||
</div>
|
||||
<h4 className="text-sm font-semibold text-white">{step.title}</h4>
|
||||
<p className="mt-2 text-xs leading-6 text-zinc-400">{step.hint}</p>
|
||||
<h4 className="mb-2 min-h-[2.5rem] text-sm font-semibold text-white">{step.title}</h4>
|
||||
<p className="min-h-[3rem] text-xs leading-5 text-zinc-400">{step.hint}</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -489,6 +547,67 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'campaign' && (
|
||||
<div className="rounded-[24px] border border-white/10 bg-white/5 p-4">
|
||||
<p className="mb-5 text-sm leading-7 text-zinc-300">
|
||||
¿Ya existe campaña o elementos gráficos para esta solicitud? (logos, key visuals, piezas previas, etc.)
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4 mb-6">
|
||||
<button
|
||||
onClick={() => {
|
||||
setHasCampaign('yes');
|
||||
setValidationMessage(null);
|
||||
}}
|
||||
className={cn(
|
||||
"px-6 py-3 rounded-2xl font-semibold transition-all border",
|
||||
hasCampaign === 'yes'
|
||||
? "bg-indigo-500/20 border-indigo-400 text-white shadow-[0_0_20px_rgba(99,102,241,0.2)]"
|
||||
: "bg-black/20 border-white/10 text-zinc-400 hover:border-white/20"
|
||||
)}
|
||||
>
|
||||
Sí, ya existen
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setHasCampaign('no');
|
||||
// Remove any campaign attachments if user switches to 'no'
|
||||
setAttachments(prev => prev.filter(a => a.category !== 'campaign'));
|
||||
setValidationMessage(null);
|
||||
}}
|
||||
className={cn(
|
||||
"px-6 py-3 rounded-2xl font-semibold transition-all border",
|
||||
hasCampaign === 'no'
|
||||
? "bg-indigo-500/20 border-indigo-400 text-white shadow-[0_0_20px_rgba(99,102,241,0.2)]"
|
||||
: "bg-black/20 border-white/10 text-zinc-400 hover:border-white/20"
|
||||
)}
|
||||
>
|
||||
No, empezar de cero
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{hasCampaign === 'yes' && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="space-y-4"
|
||||
>
|
||||
<div className="flex items-center gap-3 text-sm text-indigo-300 mb-2">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<span>Debes adjuntar al menos un archivo de la campaña.</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="inline-flex items-center gap-2 rounded-2xl bg-[linear-gradient(135deg,#6366f1_0%,#2563eb_100%)] px-5 py-3.5 text-sm font-semibold text-white shadow-[0_18px_40px_rgba(79,70,229,0.25)] transition hover:brightness-110 disabled:opacity-50"
|
||||
disabled={isProcessing || isRecording}
|
||||
>
|
||||
<Paperclip className="h-4 w-4" />
|
||||
Adjuntar elementos de campaña
|
||||
</button>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'links' && (
|
||||
<div className="rounded-[24px] border border-white/10 bg-white/5 p-4">
|
||||
<p className="mb-5 text-sm leading-7 text-zinc-300">
|
||||
@@ -510,6 +629,57 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'review' && (
|
||||
<div className="rounded-[24px] border border-white/10 bg-white/5 p-4">
|
||||
<p className="mb-5 text-sm leading-7 text-zinc-300">
|
||||
Revisa los detalles de tu solicitud antes de enviarla.
|
||||
</p>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="rounded-2xl border border-white/10 bg-black/20 p-4">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-widest text-zinc-500 mb-3">Contenido Principal</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-zinc-400">Nota de Voz:</span>
|
||||
<span className={cn("font-medium", audioRecording ? "text-emerald-400" : "text-amber-400")}>
|
||||
{audioRecording ? "Cargada" : "Faltante"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-zinc-400">Imágenes:</span>
|
||||
<span className={cn("font-medium", imageAttachments.length > 0 ? "text-emerald-400" : "text-amber-400")}>
|
||||
{imageAttachments.length} archivo(s)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-white/10 bg-black/20 p-4">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-widest text-zinc-500 mb-3">Archivos Adicionales</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-zinc-400">Documentos:</span>
|
||||
<span className="text-zinc-200 font-medium">{documentAttachments.length} archivo(s)</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-zinc-400">Campaña Previa:</span>
|
||||
<span className="text-zinc-200 font-medium">
|
||||
{hasCampaign === 'yes' ? `${campaignAttachments.length} archivo(s)` : 'No aplica'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 rounded-2xl border border-white/10 bg-black/20 p-4">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-widest text-zinc-500 mb-2">Enlaces y Referencias</h4>
|
||||
<p className="text-sm text-zinc-300 italic whitespace-pre-wrap">
|
||||
{text.trim() ? text : "Sin enlaces agregados"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
@@ -534,7 +704,7 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{currentStep !== 'links' ? (
|
||||
{currentStep !== 'review' ? (
|
||||
<button
|
||||
onClick={handleContinue}
|
||||
disabled={isProcessing}
|
||||
@@ -543,8 +713,7 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
|
||||
Continuar
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
) : (
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={isProcessing || !requiredCompleted}
|
||||
@@ -553,6 +722,7 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
|
||||
{isProcessing ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
||||
Finalizar y enviar
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -584,6 +754,12 @@ export const InputArea: React.FC<InputAreaProps> = ({ onSendMessage, isProcessin
|
||||
<span>Documentos</span>
|
||||
<span className="font-semibold text-white">{documentAttachments.length}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>Campaña</span>
|
||||
<span className={cn('font-semibold', hasCampaign === 'yes' ? 'text-emerald-300' : 'text-zinc-500')}>
|
||||
{hasCampaign === 'yes' ? `${campaignAttachments.length} archivo(s)` : hasCampaign === 'no' ? 'No aplica' : 'Pendiente'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>Enlaces</span>
|
||||
<span className="font-semibold text-white">{text.split('\n').map(line => line.trim()).filter(Boolean).length}</span>
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface Attachment {
|
||||
file: File;
|
||||
type: AttachmentType;
|
||||
url: string; // Object URL for preview
|
||||
category?: 'general' | 'campaign';
|
||||
}
|
||||
|
||||
export interface AudioRecording {
|
||||
|
||||
Reference in New Issue
Block a user