From 21f31a0c35cf24198f75b1b0f7b4da5c577e1ba9 Mon Sep 17 00:00:00 2001 From: Isaac Aracena Date: Fri, 8 May 2026 10:31:17 -0400 Subject: [PATCH] =?UTF-8?q?Actualizar=20flujo=20CDC=20Brief:=20campa=C3=B1?= =?UTF-8?q?a=20existente=20y=20revisi=C3=B3n=20final?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 17 +-- src/components/InputArea.tsx | 222 +++++++++++++++++++++++++++++++---- src/types.ts | 1 + 3 files changed, 210 insertions(+), 30 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 1bf4acc..7e03dbe 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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([ { @@ -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
{[ - '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) => ( diff --git a/src/components/InputArea.tsx b/src/components/InputArea.tsx index cf265fe..83945f2 100644 --- a/src/components/InputArea.tsx +++ b/src/components/InputArea.tsx @@ -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 = ({ onSendMessage, isProcessin const [recordingDuration, setRecordingDuration] = useState(0); const [audioRecording, setAudioRecording] = useState(null); const [currentStep, setCurrentStep] = useState('audio'); + const [hasCampaign, setHasCampaign] = useState<'yes' | 'no' | null>(null); const [validationMessage, setValidationMessage] = useState(null); const fileInputRef = useRef(null); @@ -84,7 +99,11 @@ export const InputArea: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ onSendMessage, isProcessin />
-
+
{STEP_DEFINITIONS.map((step) => { const status = getStepStatus(step.id); return ( @@ -335,7 +393,7 @@ export const InputArea: React.FC = ({ 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 = ({ onSendMessage, isProcessin
)}
-

{step.title}

-

{step.hint}

+

{step.title}

+

{step.hint}

); })} @@ -489,6 +547,67 @@ export const InputArea: React.FC = ({ onSendMessage, isProcessin
)} + {currentStep === 'campaign' && ( +
+

+ ¿Ya existe campaña o elementos gráficos para esta solicitud? (logos, key visuals, piezas previas, etc.) +

+
+ + +
+ + {hasCampaign === 'yes' && ( + +
+ + Debes adjuntar al menos un archivo de la campaña. +
+ +
+ )} +
+ )} + {currentStep === 'links' && (

@@ -510,6 +629,57 @@ export const InputArea: React.FC = ({ onSendMessage, isProcessin

)} + {currentStep === 'review' && ( +
+

+ Revisa los detalles de tu solicitud antes de enviarla. +

+ +
+
+

Contenido Principal

+
+
+ Nota de Voz: + + {audioRecording ? "Cargada" : "Faltante"} + +
+
+ Imágenes: + 0 ? "text-emerald-400" : "text-amber-400")}> + {imageAttachments.length} archivo(s) + +
+
+
+ +
+

Archivos Adicionales

+
+
+ Documentos: + {documentAttachments.length} archivo(s) +
+
+ Campaña Previa: + + {hasCampaign === 'yes' ? `${campaignAttachments.length} archivo(s)` : 'No aplica'} + +
+
+
+
+ +
+

Enlaces y Referencias

+

+ {text.trim() ? text : "Sin enlaces agregados"} +

+
+
+ )} + = ({ onSendMessage, isProcessin
- {currentStep !== 'links' ? ( + {currentStep !== 'review' ? ( - ) : null} - - + ) : ( + + )}
@@ -584,6 +754,12 @@ export const InputArea: React.FC = ({ onSendMessage, isProcessin Documentos {documentAttachments.length} +
+ Campaña + + {hasCampaign === 'yes' ? `${campaignAttachments.length} archivo(s)` : hasCampaign === 'no' ? 'No aplica' : 'Pendiente'} + +
Enlaces {text.split('\n').map(line => line.trim()).filter(Boolean).length} diff --git a/src/types.ts b/src/types.ts index 239ad90..da85988 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,6 +5,7 @@ export interface Attachment { file: File; type: AttachmentType; url: string; // Object URL for preview + category?: 'general' | 'campaign'; } export interface AudioRecording {