Álvaro Valenzuela Valdes commited on
Commit
7a29176
·
1 Parent(s): e3da83f

feat: add Voice Command Mode and Anexos Express generator

Browse files
frontend/components/AgentAnalysis.tsx CHANGED
@@ -37,10 +37,40 @@ export default function AgentAnalysis({ tender, companyProfile, analysis, onAnal
37
  // Multiple Files Support (The Corral)
38
  const [corral, setCorral] = useState<Array<{ file: File, text: string, analysis: AnalysisResult | null, id: string }>>([]);
39
  const [activeAnimalId, setActiveAnimalId] = useState<string | null>(null);
 
 
40
 
41
  // Removed auto-scroll to keep user at the top during demo recordings
42
 
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
45
  if (event.target.files && event.target.files[0]) {
46
  const newFile = event.target.files[0];
@@ -412,17 +442,24 @@ export default function AgentAnalysis({ tender, companyProfile, analysis, onAnal
412
  <span className="text-[10px] text-purple-300 font-bold">{corral.find(a => a.id === activeAnimalId)?.file.name || tender?.name}</span>
413
  </div>
414
  </div>
415
- <div className="flex flex-col items-end gap-3">
416
  <div className={`rounded-2xl px-6 py-3 text-[10px] font-black uppercase tracking-widest shadow-lg ${activeAnalysis.decision === 'Recommended' ? 'bg-green-500/20 text-green-400 border border-green-500/30 shadow-green-500/10' : 'bg-amber-500/20 text-amber-400 border border-amber-500/30 shadow-amber-500/10'}`}>
417
  {activeAnalysis.decision}
418
  </div>
419
  <div className="flex gap-2">
420
- <button
421
- onClick={() => window.print()}
422
- className="px-4 py-2 rounded-xl bg-white/5 border border-white/10 text-[10px] font-bold text-slate-400 hover:text-white hover:bg-white/10 transition uppercase tracking-[0.2em]"
423
- >
424
- Export PDF
425
- </button>
 
 
 
 
 
 
 
426
  <button
427
  onClick={() => alert("Report sent to executive committee via REW Secure Channel.")}
428
  className="px-4 py-2 rounded-xl bg-white/5 border border-white/10 text-xs text-slate-400 hover:text-white hover:bg-white/10 transition"
@@ -561,6 +598,45 @@ export default function AgentAnalysis({ tender, companyProfile, analysis, onAnal
561
  ))}
562
  </div>
563
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
  </div>
565
  </div>
566
  )}
 
37
  // Multiple Files Support (The Corral)
38
  const [corral, setCorral] = useState<Array<{ file: File, text: string, analysis: AnalysisResult | null, id: string }>>([]);
39
  const [activeAnimalId, setActiveAnimalId] = useState<string | null>(null);
40
+ const [generatedAnnexes, setGeneratedAnnexes] = useState<Array<{ name: string, content: string }>>([]);
41
+ const [isGeneratingAnnexes, setIsGeneratingAnnexes] = useState(false);
42
 
43
  // Removed auto-scroll to keep user at the top during demo recordings
44
 
45
 
46
+ const generateAnnexes = async () => {
47
+ if (!tender) return;
48
+ setIsGeneratingAnnexes(true);
49
+ // Simulate AI generating specific annexes based on tender data
50
+ setTimeout(() => {
51
+ const annexes = [
52
+ {
53
+ name: "Anexo 1: Identificación del Oferente",
54
+ content: `# ANEXO N°1\nIDENTIFICACIÓN DEL OFERENTE\n\n**Licitación:** ${tender.name}\n**ID:** ${tender.code}\n\n**RAZÓN SOCIAL:** ${companyProfile.name}\n**RUT:** 77.345.123-K\n**REPRESENTANTE LEGAL:** Álvaro Pérez\n**DOMICILIO:** Av. Apoquindo 4500, Las Condes, Santiago.\n**GIRO:** ${companyProfile.industry}\n\n*Documento generado automáticamente por AndesOps AI.*`
55
+ },
56
+ {
57
+ name: "Anexo 2: Declaración Jurada Simple",
58
+ content: `# ANEXO N°2\nDECLARACIÓN JURADA SIMPLE\n\nYo, Álvaro Pérez, en representación de ${companyProfile.name}, declaro bajo juramento que mi representada no se encuentra afecta a ninguna de las inhabilidades previstas en el artículo 92 de la Ley N° 19.886.\n\n**Fecha:** ${new Date().toLocaleDateString()}\n\n__________________________\nFirma Representante Legal`
59
+ },
60
+ {
61
+ name: "Anexo 3: Experiencia del Oferente",
62
+ content: `# ANEXO N°3\nEXPERIENCIA DEL OFERENTE\n\n**Empresa:** ${companyProfile.name}\n**Años de Experiencia:** ${companyProfile.experience}\n\n**Principales Servicios:**\n${companyProfile.services.map(s => `- ${s}`).join('\n')}\n\n**Certificaciones:**\n${companyProfile.certifications.map(c => `- ${c}`).join('\n')}\n\n*Validado por AndesOps AI Intelligence.*`
63
+ }
64
+ ];
65
+ setGeneratedAnnexes(annexes);
66
+ setIsGeneratingAnnexes(false);
67
+ // Smooth scroll to annexes
68
+ setTimeout(() => {
69
+ document.getElementById('annexes-section')?.scrollIntoView({ behavior: 'smooth' });
70
+ }, 100);
71
+ }, 2000);
72
+ };
73
+
74
  const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
75
  if (event.target.files && event.target.files[0]) {
76
  const newFile = event.target.files[0];
 
442
  <span className="text-[10px] text-purple-300 font-bold">{corral.find(a => a.id === activeAnimalId)?.file.name || tender?.name}</span>
443
  </div>
444
  </div>
445
+ <div className="flex flex-col items-end gap-3">
446
  <div className={`rounded-2xl px-6 py-3 text-[10px] font-black uppercase tracking-widest shadow-lg ${activeAnalysis.decision === 'Recommended' ? 'bg-green-500/20 text-green-400 border border-green-500/30 shadow-green-500/10' : 'bg-amber-500/20 text-amber-400 border border-amber-500/30 shadow-amber-500/10'}`}>
447
  {activeAnalysis.decision}
448
  </div>
449
  <div className="flex gap-2">
450
+ <button
451
+ onClick={() => window.print()}
452
+ className="px-4 py-2 rounded-xl bg-white/5 border border-white/10 text-[10px] font-bold text-slate-400 hover:text-white hover:bg-white/10 transition uppercase tracking-[0.2em]"
453
+ >
454
+ Export PDF
455
+ </button>
456
+ <button
457
+ onClick={generateAnnexes}
458
+ disabled={isGeneratingAnnexes}
459
+ className={`px-4 py-2 rounded-xl border text-[10px] font-bold transition uppercase tracking-[0.2em] ${isGeneratingAnnexes ? 'bg-purple-500/20 border-purple-500/50 text-purple-300 animate-pulse' : 'bg-purple-500/10 border-purple-500/20 text-purple-400 hover:bg-purple-500/20'}`}
460
+ >
461
+ {isGeneratingAnnexes ? 'Generating...' : '✨ Anexos Express'}
462
+ </button>
463
  <button
464
  onClick={() => alert("Report sent to executive committee via REW Secure Channel.")}
465
  className="px-4 py-2 rounded-xl bg-white/5 border border-white/10 text-xs text-slate-400 hover:text-white hover:bg-white/10 transition"
 
598
  ))}
599
  </div>
600
  </div>
601
+
602
+ {/* Anexos Express Section */}
603
+ {generatedAnnexes.length > 0 && (
604
+ <div id="annexes-section" className="mt-8 glass-card rounded-3xl p-10 bg-purple-500/[0.03] border border-purple-500/20 animate-in fade-in slide-in-from-bottom-8 duration-700">
605
+ <div className="flex items-center gap-4 mb-8">
606
+ <div className="w-12 h-12 rounded-2xl bg-purple-500/20 flex items-center justify-center text-2xl shadow-lg shadow-purple-500/20">📄</div>
607
+ <div>
608
+ <h4 className="text-2xl font-black text-white tracking-tight">Compliance: Anexos Express</h4>
609
+ <p className="text-slate-500 text-sm">Official annexes pre-filled with company data and tender requirements.</p>
610
+ </div>
611
+ </div>
612
+
613
+ <div className="grid gap-6 md:grid-cols-1 lg:grid-cols-3">
614
+ {generatedAnnexes.map((annex, i) => (
615
+ <div key={i} className="group rounded-3xl bg-white/[0.02] border border-white/5 p-6 hover:border-purple-500/40 transition-all">
616
+ <div className="text-[10px] font-bold uppercase text-purple-400 mb-3 tracking-widest">Template Generated</div>
617
+ <h5 className="text-white font-bold mb-4 line-clamp-1">{annex.name}</h5>
618
+ <div className="bg-black/40 rounded-xl p-4 text-[9px] font-mono text-slate-500 mb-4 h-32 overflow-hidden relative">
619
+ <pre className="whitespace-pre-wrap">{annex.content}</pre>
620
+ <div className="absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-black/60 to-transparent" />
621
+ </div>
622
+ <button
623
+ onClick={() => {
624
+ const blob = new Blob([annex.content], { type: 'text/markdown' });
625
+ const url = window.URL.createObjectURL(blob);
626
+ const a = document.createElement('a');
627
+ a.href = url;
628
+ a.download = `${annex.name.replace(/ /g, '_')}.md`;
629
+ a.click();
630
+ }}
631
+ className="w-full py-2.5 rounded-xl bg-white/5 border border-white/10 text-[10px] font-bold text-slate-400 hover:text-white hover:bg-white/10 transition uppercase tracking-widest"
632
+ >
633
+ Download .md 📥
634
+ </button>
635
+ </div>
636
+ ))}
637
+ </div>
638
+ </div>
639
+ )}
640
  </div>
641
  </div>
642
  )}
frontend/components/AgentChat.tsx CHANGED
@@ -36,10 +36,35 @@ export default function AgentChat({ tender, companyProfile }: Props) {
36
  const [isLoading, setIsLoading] = useState(false);
37
  const [isTyping, setIsTyping] = useState(false);
38
  const [isUploading, setIsUploading] = useState(false);
 
39
  const [contextText, setContextText] = useState("");
40
  const scrollRef = useRef<HTMLDivElement>(null);
41
  const fileInputRef = useRef<HTMLInputElement>(null);
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  const suggestedQuestions = [
44
  "Summarize the main requirements",
45
  "Identify legal risks for my company",
@@ -262,6 +287,19 @@ export default function AgentChat({ tender, companyProfile }: Props) {
262
  className="flex-1 bg-black/40 border border-white/10 rounded-2xl px-4 md:px-5 py-3 text-white text-sm placeholder:text-slate-600 focus:outline-none focus:ring-2 focus:ring-purple-500/40 transition-all disabled:opacity-50"
263
  />
264
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  <button
266
  onClick={() => handleSend()}
267
  disabled={!input.trim() || isLoading || isUploading}
 
36
  const [isLoading, setIsLoading] = useState(false);
37
  const [isTyping, setIsTyping] = useState(false);
38
  const [isUploading, setIsUploading] = useState(false);
39
+ const [isListening, setIsListening] = useState(false);
40
  const [contextText, setContextText] = useState("");
41
  const scrollRef = useRef<HTMLDivElement>(null);
42
  const fileInputRef = useRef<HTMLInputElement>(null);
43
 
44
+ const startSpeechRecognition = () => {
45
+ const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
46
+ if (!SpeechRecognition) {
47
+ alert("Speech recognition not supported in this browser.");
48
+ return;
49
+ }
50
+
51
+ const recognition = new SpeechRecognition();
52
+ recognition.lang = "es-CL";
53
+ recognition.interimResults = false;
54
+
55
+ recognition.onstart = () => setIsListening(true);
56
+ recognition.onend = () => setIsListening(false);
57
+
58
+ recognition.onresult = (event: any) => {
59
+ const transcript = event.results[0][0].transcript;
60
+ setInput(transcript);
61
+ // Optional: Auto-send after voice command
62
+ // handleSend(transcript);
63
+ };
64
+
65
+ recognition.start();
66
+ };
67
+
68
  const suggestedQuestions = [
69
  "Summarize the main requirements",
70
  "Identify legal risks for my company",
 
287
  className="flex-1 bg-black/40 border border-white/10 rounded-2xl px-4 md:px-5 py-3 text-white text-sm placeholder:text-slate-600 focus:outline-none focus:ring-2 focus:ring-purple-500/40 transition-all disabled:opacity-50"
288
  />
289
 
290
+ <button
291
+ onClick={startSpeechRecognition}
292
+ disabled={isListening || isLoading || isUploading}
293
+ className={`w-10 h-10 md:w-12 md:h-12 rounded-2xl flex items-center justify-center transition-all active:scale-95 disabled:opacity-30 border ${
294
+ isListening
295
+ ? 'bg-red-500/20 border-red-500 shadow-[0_0_20px_rgba(239,68,68,0.4)] animate-pulse'
296
+ : 'bg-white/5 border-white/10 text-slate-400 hover:bg-white/10'
297
+ }`}
298
+ title="Voice Command"
299
+ >
300
+ <span className="text-xl">{isListening ? '🛑' : '🎙️'}</span>
301
+ </button>
302
+
303
  <button
304
  onClick={() => handleSend()}
305
  disabled={!input.trim() || isLoading || isUploading}