initial agent

This commit is contained in:
Anibal Angulo
2025-11-07 11:19:43 -06:00
parent af9b5fed01
commit c9a63e129d
6 changed files with 1169 additions and 460 deletions

View File

@@ -0,0 +1,283 @@
import React from "react";
import {
AlertTriangle,
CheckCircle,
XCircle,
FileText,
Building,
Calendar,
AlertCircle,
TrendingUp,
Shield,
} from "lucide-react";
import { cn } from "@/lib/utils";
type Severity = "Pass" | "Warning" | "Error";
interface AuditFinding {
check_id: string;
category: string;
severity: Severity;
message: string;
mitigation?: string;
confidence: number;
}
interface AuditSectionSummary {
section: string;
severity: Severity;
summary: string;
confidence: number;
}
interface AuditReportData {
organisation_ein: string;
organisation_name: string;
year?: number;
overall_severity: Severity;
findings: AuditFinding[];
sections: AuditSectionSummary[];
overall_summary?: string;
notes?: string;
}
interface AuditReportProps {
data: AuditReportData;
}
const getSeverityIcon = (severity: Severity) => {
switch (severity) {
case "Pass":
return <CheckCircle className="w-5 h-5 text-green-600" />;
case "Warning":
return <AlertTriangle className="w-5 h-5 text-yellow-600" />;
case "Error":
return <XCircle className="w-5 h-5 text-red-600" />;
default:
return <AlertCircle className="w-5 h-5 text-gray-600" />;
}
};
const getSeverityColor = (severity: Severity) => {
switch (severity) {
case "Pass":
return "text-green-700 bg-green-50 border-green-200";
case "Warning":
return "text-yellow-700 bg-yellow-50 border-yellow-200";
case "Error":
return "text-red-700 bg-red-50 border-red-200";
default:
return "text-gray-700 bg-gray-50 border-gray-200";
}
};
const getConfidenceColor = (confidence: number) => {
if (confidence >= 0.8) return "text-green-600";
if (confidence >= 0.6) return "text-yellow-600";
return "text-red-600";
};
export const AuditReport: React.FC<AuditReportProps> = ({ data }) => {
const {
organisation_ein,
organisation_name,
year,
overall_severity,
findings,
sections,
overall_summary,
notes,
} = data;
const severityStats = {
Pass: findings.filter((f) => f.severity === "Pass").length,
Warning: findings.filter((f) => f.severity === "Warning").length,
Error: findings.filter((f) => f.severity === "Error").length,
};
return (
<div className="w-full bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden">
<div>
{/* Header */}
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 border-b border-gray-200 p-3">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-100 rounded-lg">
<Shield className="w-6 h-6 text-blue-600" />
</div>
<div>
<h2 className="text-xl font-semibold text-gray-900 flex items-center gap-2">
<Building className="w-5 h-5" />
{organisation_name}
</h2>
<div className="flex items-center gap-4 mt-1 text-sm text-gray-600">
<span className="flex items-center gap-1">
<FileText className="w-4 h-4" />
EIN: {organisation_ein}
</span>
{year && (
<span className="flex items-center gap-1">
<Calendar className="w-4 h-4" />
{year}
</span>
)}
</div>
</div>
</div>
{/* Overall Status */}
<div
className={cn(
"flex items-center gap-2 px-4 py-2 rounded-full border",
getSeverityColor(overall_severity),
)}
>
{getSeverityIcon(overall_severity)}
<span className="font-medium">{overall_severity}</span>
</div>
</div>
</div>
{/* Statistics Bar */}
<div className="bg-gray-50 px-4 py-2 border-b border-gray-200">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-gray-700">Audit Summary</h3>
<div className="flex items-center gap-6 text-sm">
<div className="flex items-center gap-1">
<CheckCircle className="w-4 h-4 text-green-600" />
<span className="text-green-700 font-medium">
{severityStats.Pass}
</span>
<span className="text-gray-600">Passed</span>
</div>
<div className="flex items-center gap-1">
<AlertTriangle className="w-4 h-4 text-yellow-600" />
<span className="text-yellow-700 font-medium">
{severityStats.Warning}
</span>
<span className="text-gray-600">Warnings</span>
</div>
<div className="flex items-center gap-1">
<XCircle className="w-4 h-4 text-red-600" />
<span className="text-red-700 font-medium">
{severityStats.Error}
</span>
<span className="text-gray-600">Errors</span>
</div>
</div>
</div>
</div>
{/* Overall Summary */}
{overall_summary && (
<div className="p-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900 mb-3">
Overall Assessment
</h3>
<p className="text-gray-700 leading-relaxed">{overall_summary}</p>
</div>
)}
{/* Section Summaries */}
{sections.length > 0 && (
<div className="p-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900 mb-4">
Section Analysis
</h3>
<div className="grid gap-2 sm:grid-cols-2">
{sections.map((section, index) => (
<div
key={index}
className={cn(
"border rounded-lg p-3",
getSeverityColor(section.severity),
)}
>
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium">{section.section}</h4>
<div className="flex items-center gap-2">
{getSeverityIcon(section.severity)}
<span
className={cn(
"text-xs font-medium",
getConfidenceColor(section.confidence),
)}
>
{Math.round(section.confidence * 100)}%
</span>
</div>
</div>
<p className="text-sm opacity-90">{section.summary}</p>
</div>
))}
</div>
</div>
)}
{/* Detailed Findings */}
<div className="p-4">
<h3 className="text-lg font-medium text-gray-900 mb-4">
Detailed Findings ({findings.length})
</h3>
<div className="space-y-3">
{findings.map((finding, index) => (
<div
key={index}
className={cn(
"border rounded-lg p-3",
getSeverityColor(finding.severity),
)}
>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-2">
{getSeverityIcon(finding.severity)}
<div>
<span className="font-medium">{finding.category}</span>
<span className="text-sm text-gray-500 ml-2">
#{finding.check_id}
</span>
</div>
</div>
<div className="flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-gray-400" />
<span
className={cn(
"text-sm font-medium",
getConfidenceColor(finding.confidence),
)}
>
{Math.round(finding.confidence * 100)}% confidence
</span>
</div>
</div>
<p className="text-sm mb-2 leading-relaxed">
{finding.message}
</p>
{finding.mitigation && (
<div className="bg-white bg-opacity-50 rounded p-2 border border-current border-opacity-20">
<h5 className="font-medium text-sm mb-1">
Recommended Action:
</h5>
<p className="text-sm">{finding.mitigation}</p>
</div>
)}
</div>
))}
</div>
</div>
{/* Notes */}
{notes && (
<div className="bg-gray-50 p-3 border-t border-gray-200">
<h3 className="text-sm font-medium text-gray-700 mb-2">
Additional Notes
</h3>
<p className="text-sm text-gray-600 italic">{notes}</p>
</div>
)}
</div>
</div>
);
};

View File

@@ -32,6 +32,7 @@ import {
AlertCircle,
PaperclipIcon,
} from "lucide-react";
import { AuditReport } from "./AuditReport";
import { Loader } from "@/components/ai-elements/loader";
import { DefaultChatTransport } from "ai";
@@ -101,9 +102,9 @@ export function ChatTab({ selectedTema }: ChatTabProps) {
}
return (
<div className="flex flex-col h-full">
<div className="flex flex-col h-[638px] max-h-[638px]">
{/* Chat Header */}
<div className="border-b border-gray-200 px-6 py-4">
<div className="border-b border-gray-200 px-6 py-4 flex-shrink-0">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-100 rounded-lg">
<MessageCircle className="w-5 h-5 text-blue-600" />
@@ -120,138 +121,178 @@ export function ChatTab({ selectedTema }: ChatTabProps) {
</div>
{/* Chat Content */}
<div className="flex-1 flex flex-col">
<Conversation className="flex-1">
<ConversationContent className="p-6">
<div className="max-w-4xl mx-auto w-full">
{/* Welcome Message */}
{messages.length === 0 && (
<div className="flex items-start gap-3 mb-6">
<div className="p-2 bg-blue-100 rounded-full">
<Bot className="w-4 h-4 text-blue-600" />
</div>
<div className="flex-1 bg-gray-50 rounded-lg p-4">
<p className="text-sm text-gray-800">
¡Hola! Soy tu asistente de IA para el dataroom{" "}
<strong>{selectedTema}</strong>. Puedes hacerme preguntas
sobre los documentos almacenados aquí.
</p>
</div>
</div>
)}
<div className="flex-1 min-h-0 overflow-y-auto">
<div className="max-w-4xl mx-auto w-full space-y-6 p-6">
{/* Welcome Message */}
{messages.length === 0 && (
<div className="flex items-start gap-3 mb-6">
<div className="p-2 bg-blue-100 rounded-full">
<Bot className="w-4 h-4 text-blue-600" />
</div>
<div className="flex-1 bg-gray-50 rounded-lg p-4">
<p className="text-sm text-gray-800">
¡Hola! Soy tu asistente de IA para el dataroom{" "}
<strong>{selectedTema}</strong>. Puedes hacerme preguntas
sobre los documentos almacenados aquí.
</p>
</div>
</div>
)}
{/* Error Message */}
{error && (
<div className="flex items-start gap-3 mb-4">
<div className="p-2 bg-red-100 rounded-full">
<AlertCircle className="w-4 h-4 text-red-600" />
</div>
<div className="flex-1 bg-red-50 rounded-lg p-4 border border-red-200">
<p className="text-sm text-red-800">{error}</p>
</div>
</div>
)}
{/* Error Message */}
{error && (
<div className="flex items-start gap-3 mb-4">
<div className="p-2 bg-red-100 rounded-full">
<AlertCircle className="w-4 h-4 text-red-600" />
</div>
<div className="flex-1 bg-red-50 rounded-lg p-4 border border-red-200">
<p className="text-sm text-red-800">{error}</p>
</div>
</div>
)}
{/* Chat Messages */}
{messages.map((message) => (
<div key={message.id}>
{message.parts.map((part, i) => {
switch (part.type) {
case "text":
{/* Chat Messages */}
{messages.map((message) => (
<div key={message.id}>
{message.parts.map((part, i) => {
switch (part.type) {
case "text":
return (
<Fragment key={`${message.id}-${i}`}>
<Message from={message.role} className="max-w-none">
<MessageContent>
<Response>{part.text}</Response>
</MessageContent>
</Message>
{message.role === "assistant" &&
i === message.parts.length - 1 && (
<Actions className="mt-2">
<Action
onClick={() => regenerate()}
label="Regenerar"
disabled={status === "streaming"}
>
<RefreshCcwIcon className="size-3" />
</Action>
<Action
onClick={() =>
navigator.clipboard.writeText(part.text)
}
label="Copiar"
>
<CopyIcon className="size-3" />
</Action>
</Actions>
)}
</Fragment>
);
case "tool-build_audit_report":
switch (part.state) {
case "input-available":
return (
<Fragment key={`${message.id}-${i}`}>
<Message from={message.role} className="max-w-none">
<MessageContent>
<Response>{part.text}</Response>
</MessageContent>
</Message>
{message.role === "assistant" &&
i === message.parts.length - 1 && (
<Actions className="mt-2">
<Action
onClick={() => regenerate()}
label="Regenerar"
disabled={status === "streaming"}
>
<RefreshCcwIcon className="size-3" />
</Action>
<Action
onClick={() =>
navigator.clipboard.writeText(part.text)
}
label="Copiar"
>
<CopyIcon className="size-3" />
</Action>
</Actions>
)}
</Fragment>
<div
key={`${message.id}-${i}`}
className="flex items-center gap-2 p-4 bg-blue-50 rounded-lg border border-blue-200"
>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600"></div>
<span className="text-sm text-blue-700">
Generando reporte de auditoría...
</span>
</div>
);
case "output-available":
return (
<div
key={`${message.id}-${i}`}
className="mt-4 w-full"
>
<div className="max-w-full overflow-hidden">
<AuditReport data={part.output} />
</div>
</div>
);
case "output-error":
return (
<div
key={`${message.id}-${i}`}
className="p-4 bg-red-50 border border-red-200 rounded-lg"
>
<div className="flex items-center gap-2">
<AlertCircle className="w-4 h-4 text-red-600" />
<span className="text-sm font-medium text-red-800">
Error generando reporte de auditoría
</span>
</div>
<p className="text-sm text-red-600 mt-1">
{part.errorText}
</p>
</div>
);
default:
return null;
}
})}
</div>
))}
{status === "streaming" && <Loader />}
{status === "loading" && <Loader />}
</div>
</ConversationContent>
<ConversationScrollButton />
</Conversation>
{/* Chat Input */}
<div className="border-t border-gray-200 p-6 bg-gray-50/50">
<PromptInput
onSubmit={handleSubmit}
className="max-w-4xl mx-auto border-2 border-gray-200 rounded-xl focus-within:border-slate-500 transition-colors duration-200 bg-white"
globalDrop
multiple
>
<PromptInputHeader className="p-2 pb-0">
<PromptInputAttachments>
{(attachment) => <PromptInputAttachment data={attachment} />}
</PromptInputAttachments>
</PromptInputHeader>
<PromptInputBody>
<PromptInputTextarea
onChange={(e) => setInput(e.target.value)}
value={input}
placeholder={`Pregunta algo sobre ${selectedTema}...`}
disabled={status === "streaming" || status === "loading"}
className="min-h-[60px] resize-none border-0 focus:ring-0 transition-all duration-200 text-base px-4 py-3 bg-white rounded-xl"
/>
</PromptInputBody>
<PromptInputFooter className="mt-3 flex justify-between items-center">
<PromptInputTools>
<PromptInputActionMenu>
<PromptInputActionMenuTrigger>
<PaperclipIcon className="size-4" />
</PromptInputActionMenuTrigger>
<PromptInputActionMenuContent>
<PromptInputActionAddAttachments />
</PromptInputActionMenuContent>
</PromptInputActionMenu>
</PromptInputTools>
<PromptInputSubmit
disabled={
(!input.trim() && !status) ||
status === "streaming" ||
status === "loading"
default:
return null;
}
status={status}
className={`rounded-full px-6 py-2 font-medium transition-all duration-200 flex items-center gap-2 ${
(!input.trim() && !status) ||
status === "streaming" ||
status === "loading"
? "bg-gray-300 cursor-not-allowed text-gray-500"
: "bg-blue-600 hover:bg-blue-700 text-white"
}`}
/>
</PromptInputFooter>
</PromptInput>
})}
</div>
))}
{status === "streaming" && <Loader />}
{status === "loading" && <Loader />}
</div>
</div>
{/* Chat Input */}
<div className="border-t border-gray-200 p-3 bg-gray-50/50 flex-shrink-0">
<PromptInput
onSubmit={handleSubmit}
className="max-w-4xl mx-auto border-2 border-gray-200 rounded-xl focus-within:border-slate-500 transition-colors duration-200 bg-white"
globalDrop
multiple
>
<PromptInputHeader className="p-2 pb-0">
<PromptInputAttachments>
{(attachment) => <PromptInputAttachment data={attachment} />}
</PromptInputAttachments>
</PromptInputHeader>
<PromptInputBody>
<PromptInputTextarea
onChange={(e) => setInput(e.target.value)}
value={input}
placeholder={`Pregunta algo sobre ${selectedTema}...`}
disabled={status === "streaming" || status === "loading"}
className="min-h-[60px] resize-none border-0 focus:ring-0 transition-all duration-200 text-base px-4 py-3 bg-white rounded-xl"
/>
</PromptInputBody>
<PromptInputFooter className="mt-3 flex justify-between items-center">
<PromptInputTools>
<PromptInputActionMenu>
<PromptInputActionMenuTrigger>
<PaperclipIcon className="size-4" />
</PromptInputActionMenuTrigger>
<PromptInputActionMenuContent>
<PromptInputActionAddAttachments />
</PromptInputActionMenuContent>
</PromptInputActionMenu>
</PromptInputTools>
<PromptInputSubmit
disabled={
(!input.trim() && !status) ||
status === "streaming" ||
status === "loading"
}
status={status}
className={`rounded-full px-6 py-2 font-medium transition-all duration-200 flex items-center gap-2 ${
(!input.trim() && !status) ||
status === "streaming" ||
status === "loading"
? "bg-gray-300 cursor-not-allowed text-gray-500"
: "bg-blue-600 hover:bg-blue-700 text-white"
}`}
/>
</PromptInputFooter>
</PromptInput>
</div>
</div>
);
}