Á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 |
-
|
| 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 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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}
|