add agent context

This commit is contained in:
Anibal Angulo
2025-11-09 08:35:01 -06:00
parent a23f45ca6d
commit 77a11ef32e
16 changed files with 1227 additions and 946 deletions

View File

@@ -1,16 +1,15 @@
import React from "react";
import React, { useState } from "react";
import {
AlertTriangle,
CheckCircle,
XCircle,
FileText,
Building,
Calendar,
AlertCircle,
TrendingUp,
ChevronDown,
ChevronRight,
Shield,
Info,
} from "lucide-react";
import { cn } from "@/lib/utils";
type Severity = "Pass" | "Warning" | "Error";
@@ -45,42 +44,45 @@ interface AuditReportProps {
data: AuditReportData;
}
const getSeverityIcon = (severity: Severity) => {
const getSeverityIcon = (severity: Severity, size = "w-4 h-4") => {
switch (severity) {
case "Pass":
return <CheckCircle className="w-5 h-5 text-green-600" />;
return <CheckCircle className={`${size} text-green-600`} />;
case "Warning":
return <AlertTriangle className="w-5 h-5 text-yellow-600" />;
return <AlertTriangle className={`${size} text-yellow-600`} />;
case "Error":
return <XCircle className="w-5 h-5 text-red-600" />;
return <XCircle className={`${size} text-red-600`} />;
default:
return <AlertCircle className="w-5 h-5 text-gray-600" />;
return <AlertCircle className={`${size} 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 getSeverityBadge = (severity: Severity) => {
const colors = {
Pass: "bg-green-100 text-green-800 border-green-200",
Warning: "bg-yellow-100 text-yellow-800 border-yellow-200",
Error: "bg-red-100 text-red-800 border-red-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";
return (
<span
className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium border ${colors[severity]}`}
>
{getSeverityIcon(severity, "w-3 h-3")}
{severity}
</span>
);
};
export const AuditReport: React.FC<AuditReportProps> = ({ data }) => {
const [expandedSections, setExpandedSections] = useState<Set<string>>(
new Set(),
);
const [showAllFindings, setShowAllFindings] = useState(false);
const {
organisation_ein,
organisation_name,
organisation_ein,
year,
overall_severity,
findings,
@@ -95,186 +97,227 @@ export const AuditReport: React.FC<AuditReportProps> = ({ data }) => {
Error: findings.filter((f) => f.severity === "Error").length,
};
const toggleSection = (section: string) => {
const newExpanded = new Set(expandedSections);
if (newExpanded.has(section)) {
newExpanded.delete(section);
} else {
newExpanded.add(section);
}
setExpandedSections(newExpanded);
};
const criticalFindings = findings.filter((f) => f.severity === "Error");
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" />
{/* Compact Header */}
<div className="bg-linear-to-r from-blue-50 to-indigo-50 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Shield className="w-6 h-6 text-blue-600" />
<div>
<h3 className="font-semibold text-gray-900">
{organisation_name}
</h3>
<div className="flex items-center gap-3 text-xs text-gray-600">
<span>EIN: {organisation_ein}</span>
{year && <span>{year}</span>}
</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>
{getSeverityBadge(overall_severity)}
</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">
{/* Quick Stats */}
<div className="bg-gray-50 px-4 py-3 border-b">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-700">
Audit Results
</span>
<div className="flex items-center gap-4 text-sm">
{severityStats.Pass > 0 && (
<div className="flex items-center gap-1">
<CheckCircle className="w-4 h-4 text-green-600" />
<span className="text-green-700 font-medium">
<CheckCircle className="w-3 h-3 text-green-600" />
<span className="font-medium text-green-700">
{severityStats.Pass}
</span>
<span className="text-gray-600">Passed</span>
</div>
)}
{severityStats.Warning > 0 && (
<div className="flex items-center gap-1">
<AlertTriangle className="w-4 h-4 text-yellow-600" />
<span className="text-yellow-700 font-medium">
<AlertTriangle className="w-3 h-3 text-yellow-600" />
<span className="font-medium text-yellow-700">
{severityStats.Warning}
</span>
<span className="text-gray-600">Warnings</span>
</div>
)}
{severityStats.Error > 0 && (
<div className="flex items-center gap-1">
<XCircle className="w-4 h-4 text-red-600" />
<span className="text-red-700 font-medium">
<XCircle className="w-3 h-3 text-red-600" />
<span className="font-medium text-red-700">
{severityStats.Error}
</span>
<span className="text-gray-600">Errors</span>
</div>
)}
</div>
</div>
</div>
<div className="p-4 space-y-4">
{/* Summary */}
{overall_summary && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<div className="flex items-start gap-2">
<Info className="w-4 h-4 text-blue-600 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-blue-900 text-sm mb-1">
Summary
</h4>
<p className="text-sm text-blue-800">{overall_summary}</p>
</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>
{/* Critical Issues (if any) */}
{criticalFindings.length > 0 && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
<h4 className="font-medium text-red-900 text-sm mb-2 flex items-center gap-2">
<XCircle className="w-4 h-4" />
Critical Issues ({criticalFindings.length})
</h4>
<div className="space-y-2">
{criticalFindings.slice(0, 2).map((finding, index) => (
<div key={index} className="text-sm text-red-800">
<span className="font-medium">{finding.category}:</span>{" "}
{finding.message}
</div>
))}
{criticalFindings.length > 2 && (
<button
onClick={() => setShowAllFindings(!showAllFindings)}
className="text-xs text-red-700 hover:text-red-800 font-medium"
>
{showAllFindings
? "Show less"
: `+${criticalFindings.length - 2} more issues`}
</button>
)}
</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}
{/* Sections Overview */}
{sections.length > 0 && (
<div>
<button
onClick={() => toggleSection("sections")}
className="flex items-center gap-2 w-full text-left p-2 hover:bg-gray-50 rounded-lg"
>
{expandedSections.has("sections") ? (
<ChevronDown className="w-4 h-4" />
) : (
<ChevronRight className="w-4 h-4" />
)}
<span className="font-medium text-sm">
Section Analysis ({sections.length})
</span>
</button>
{expandedSections.has("sections") && (
<div className="mt-2 grid gap-2 sm:grid-cols-2">
{sections.map((section, index) => (
<div key={index} className="border rounded-lg p-3 bg-gray-50">
<div className="flex items-center justify-between mb-1">
<span className="font-medium text-sm">
{section.section}
</span>
<div className="flex items-center gap-1">
{getSeverityIcon(section.severity)}
<span className="text-xs text-gray-600">
{Math.round(section.confidence * 100)}%
</span>
</div>
</div>
<p className="text-xs text-gray-700">{section.summary}</p>
</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),
)}
>
))}
</div>
)}
</div>
)}
{/* All Findings */}
<div>
<button
onClick={() => toggleSection("findings")}
className="flex items-center gap-2 w-full text-left p-2 hover:bg-gray-50 rounded-lg"
>
{expandedSections.has("findings") ? (
<ChevronDown className="w-4 h-4" />
) : (
<ChevronRight className="w-4 h-4" />
)}
<span className="font-medium text-sm">
All Findings ({findings.length})
</span>
</button>
{expandedSections.has("findings") && (
<div className="mt-2 space-y-2">
{findings.map((finding, index) => (
<div key={index} className="border rounded-lg p-3 bg-gray-50">
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
{getSeverityIcon(finding.severity)}
<div>
<span className="font-medium text-sm">
{finding.category}
</span>
<span className="text-xs text-gray-500 ml-1">
#{finding.check_id}
</span>
</div>
</div>
<span className="text-xs text-gray-600">
{Math.round(finding.confidence * 100)}% confidence
</span>
</div>
<p className="text-sm text-gray-700 mb-2">
{finding.message}
</p>
{finding.mitigation && (
<div className="bg-white rounded p-2 border">
<span className="text-xs font-medium text-gray-600">
Recommended:
</span>
<p className="text-xs text-gray-700 mt-1">
{finding.mitigation}
</p>
</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>
)}
</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 className="border-t pt-3">
<div className="flex items-start gap-2">
<FileText className="w-4 h-4 text-gray-400 mt-0.5 shrink-0" />
<div>
<h4 className="font-medium text-gray-700 text-sm mb-1">
Notes
</h4>
<p className="text-sm text-gray-600 italic">{notes}</p>
</div>
</div>
</div>
)}
</div>

View File

@@ -1,241 +0,0 @@
import {
Conversation,
ConversationContent,
ConversationScrollButton,
} from "@/components/ai-elements/conversation";
import { Message, MessageContent } from "@/components/ai-elements/message";
import {
PromptInput,
PromptInputActionAddAttachments,
PromptInputActionMenu,
PromptInputActionMenuContent,
PromptInputActionMenuTrigger,
PromptInputAttachment,
PromptInputAttachments,
PromptInputBody,
PromptInputButton,
PromptInputHeader,
type PromptInputMessage,
PromptInputSelect,
PromptInputSelectContent,
PromptInputSelectItem,
PromptInputSelectTrigger,
PromptInputSelectValue,
PromptInputSubmit,
PromptInputTextarea,
PromptInputFooter,
PromptInputTools,
} from "@/components/ai-elements/prompt-input";
import { Action, Actions } from "@/components/ai-elements/actions";
import { Fragment, useState } from "react";
import { useChat } from "@ai-sdk/react";
import { Response } from "@/components/ai-elements/response";
import { CopyIcon, GlobeIcon, RefreshCcwIcon } from "lucide-react";
import {
Source,
Sources,
SourcesContent,
SourcesTrigger,
} from "@/components/ai-elements/sources";
import {
Reasoning,
ReasoningContent,
ReasoningTrigger,
} from "@/components/ai-elements/reasoning";
import { Loader } from "@/components/ai-elements/loader";
import { DefaultChatTransport } from "ai";
const models = [
{
name: "GPT 4o",
value: "openai/gpt-4o",
},
{
name: "Deepseek R1",
value: "deepseek/deepseek-r1",
},
];
const ChatBotDemo = () => {
const [input, setInput] = useState("");
const [model, setModel] = useState<string>(models[0].value);
const [webSearch, setWebSearch] = useState(false);
const { messages, sendMessage, status, regenerate } = useChat({
transport: new DefaultChatTransport({
api: "/api/v1/chat",
}),
});
const handleSubmit = (message: PromptInputMessage) => {
const hasText = Boolean(message.text);
const hasAttachments = Boolean(message.files?.length);
if (!(hasText || hasAttachments)) {
return;
}
sendMessage(
{
text: message.text || "Sent with attachments",
files: message.files,
},
{
body: {
model: model,
webSearch: webSearch,
},
},
);
setInput("");
};
return (
<div className="max-w-4xl mx-auto p-6 relative size-full h-screen">
<div className="flex flex-col h-full">
<Conversation className="h-full">
<ConversationContent>
{messages.map((message) => (
<div key={message.id}>
{message.role === "assistant" &&
message.parts.filter((part) => part.type === "source-url")
.length > 0 && (
<Sources>
<SourcesTrigger
count={
message.parts.filter(
(part) => part.type === "source-url",
).length
}
/>
{message.parts
.filter((part) => part.type === "source-url")
.map((part, i) => (
<SourcesContent key={`${message.id}-${i}`}>
<Source
key={`${message.id}-${i}`}
href={part.url}
title={part.url}
/>
</SourcesContent>
))}
</Sources>
)}
{message.parts.map((part, i) => {
switch (part.type) {
case "text":
return (
<Fragment key={`${message.id}-${i}`}>
<Message from={message.role}>
<MessageContent>
<Response>{part.text}</Response>
</MessageContent>
</Message>
{message.role === "assistant" &&
i === messages.length - 1 && (
<Actions className="mt-2">
<Action
onClick={() => regenerate()}
label="Retry"
>
<RefreshCcwIcon className="size-3" />
</Action>
<Action
onClick={() =>
navigator.clipboard.writeText(part.text)
}
label="Copy"
>
<CopyIcon className="size-3" />
</Action>
</Actions>
)}
</Fragment>
);
case "reasoning":
return (
<Reasoning
key={`${message.id}-${i}`}
className="w-full"
isStreaming={
status === "streaming" &&
i === message.parts.length - 1 &&
message.id === messages.at(-1)?.id
}
>
<ReasoningTrigger />
<ReasoningContent>{part.text}</ReasoningContent>
</Reasoning>
);
default:
return null;
}
})}
</div>
))}
{status === "submitted" && <Loader />}
</ConversationContent>
<ConversationScrollButton />
</Conversation>
<PromptInput
onSubmit={handleSubmit}
className="mt-4"
globalDrop
multiple
>
<PromptInputHeader>
<PromptInputAttachments>
{(attachment) => <PromptInputAttachment data={attachment} />}
</PromptInputAttachments>
</PromptInputHeader>
<PromptInputBody>
<PromptInputTextarea
onChange={(e) => setInput(e.target.value)}
value={input}
/>
</PromptInputBody>
<PromptInputFooter>
<PromptInputTools>
<PromptInputActionMenu>
<PromptInputActionMenuTrigger />
<PromptInputActionMenuContent>
<PromptInputActionAddAttachments />
</PromptInputActionMenuContent>
</PromptInputActionMenu>
<PromptInputButton
variant={webSearch ? "default" : "ghost"}
onClick={() => setWebSearch(!webSearch)}
>
<GlobeIcon size={16} />
<span>Search</span>
</PromptInputButton>
<PromptInputSelect
onValueChange={(value) => {
setModel(value);
}}
value={model}
>
<PromptInputSelectTrigger>
<PromptInputSelectValue />
</PromptInputSelectTrigger>
<PromptInputSelectContent>
{models.map((model) => (
<PromptInputSelectItem
key={model.value}
value={model.value}
>
{model.name}
</PromptInputSelectItem>
))}
</PromptInputSelectContent>
</PromptInputSelect>
</PromptInputTools>
<PromptInputSubmit disabled={!input && !status} status={status} />
</PromptInputFooter>
</PromptInput>
</div>
</div>
);
};
export default ChatBotDemo;

View File

@@ -1,8 +1,3 @@
import {
Conversation,
ConversationContent,
ConversationScrollButton,
} from "@/components/ai-elements/conversation";
import { Message, MessageContent } from "@/components/ai-elements/message";
import {
PromptInput,
@@ -31,8 +26,10 @@ import {
Bot,
AlertCircle,
PaperclipIcon,
User,
} from "lucide-react";
import { AuditReport } from "./AuditReport";
import { WebSearchResults } from "./WebSearchResults";
import { Loader } from "@/components/ai-elements/loader";
import { DefaultChatTransport } from "ai";
@@ -53,6 +50,9 @@ export function ChatTab({ selectedTema }: ChatTabProps) {
} = useChat({
transport: new DefaultChatTransport({
api: "/api/v1/agent/chat",
headers: {
tema: selectedTema || "",
},
}),
onError: (error) => {
setError(`Error en el chat: ${error.message}`);
@@ -103,23 +103,6 @@ export function ChatTab({ selectedTema }: ChatTabProps) {
return (
<div className="flex flex-col h-[638px] max-h-[638px]">
{/* Chat Header */}
<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" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">
Chat con {selectedTema}
</h3>
<p className="text-sm text-gray-600">
Haz preguntas sobre los documentos de este dataroom
</p>
</div>
</div>
</div>
{/* Chat Content */}
<div className="flex-1 min-h-0 overflow-y-auto">
<div className="max-w-4xl mx-auto w-full space-y-6 p-6">
@@ -159,30 +142,60 @@ export function ChatTab({ selectedTema }: ChatTabProps) {
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 === "user" ? (
<div className="flex items-start gap-3 justify-end">
<div className="flex-1">
<Message
from={message.role}
className="max-w-none"
>
<MessageContent>
<Response>{part.text}</Response>
</MessageContent>
</Message>
</div>
<div className="p-2 rounded-full flex-shrink-0 mt-1 bg-gray-100">
<User className="w-4 h-4 text-gray-600" />
</div>
</div>
) : (
<div className="flex items-start gap-3">
<div className="p-2 rounded-full flex-shrink-0 mt-1 bg-blue-100">
<Bot className="w-4 h-4 text-blue-600" />
</div>
<div className="flex-1">
<Message
from={message.role}
className="max-w-none"
>
<MessageContent>
<Response>{part.text}</Response>
</MessageContent>
</Message>
</div>
</div>
)}
{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>
<div className="ml-12">
<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>
</div>
)}
</Fragment>
);
@@ -231,6 +244,51 @@ export function ChatTab({ selectedTema }: ChatTabProps) {
default:
return null;
}
case "tool-search_web_information":
switch (part.state) {
case "input-available":
return (
<div
key={`${message.id}-${i}`}
className="flex items-center gap-2 p-4 bg-green-50 rounded-lg border border-green-200"
>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-green-600"></div>
<span className="text-sm text-green-700">
Searching the web...
</span>
</div>
);
case "output-available":
return (
<div
key={`${message.id}-${i}`}
className="mt-4 w-full"
>
<div className="max-w-full overflow-hidden">
<WebSearchResults 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 searching the web
</span>
</div>
<p className="text-sm text-red-600 mt-1">
{part.errorText}
</p>
</div>
);
default:
return null;
}
default:
return null;
}

View File

@@ -281,47 +281,6 @@ export function DashboardTab({ selectedTema }: DashboardTabProps) {
</div>
</div>
)}
{/* Collection Details */}
{dataroomInfo.collection_info && (
<div className="mt-8">
<h4 className="text-md font-semibold text-gray-900 mb-4">
Detalles de la Colección
</h4>
<div className="bg-white border border-gray-200 rounded-lg p-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<p className="text-sm font-medium text-gray-600">
Total Vectores
</p>
<p className="text-lg font-bold text-gray-900">
{dataroomInfo.collection_info.vectors_count}
</p>
</div>
<div>
<p className="text-sm font-medium text-gray-600">
Vectores Indexados
</p>
<p className="text-lg font-bold text-gray-900">
{dataroomInfo.collection_info.indexed_vectors_count}
</p>
</div>
<div>
<p className="text-sm font-medium text-gray-600">Puntos</p>
<p className="text-lg font-bold text-gray-900">
{dataroomInfo.collection_info.points_count}
</p>
</div>
<div>
<p className="text-sm font-medium text-gray-600">Segmentos</p>
<p className="text-lg font-bold text-gray-900">
{dataroomInfo.collection_info.segments_count}
</p>
</div>
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,7 +1,14 @@
import { useEffect, useState } from "react";
import { useState } from "react";
import { useFileStore } from "@/stores/fileStore";
import { api } from "@/services/api";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Expand, Minimize2 } from "lucide-react";
import { FilesTab } from "./FilesTab";
import { DashboardTab } from "./DashboardTab";
import { ChatTab } from "./ChatTab";
@@ -11,15 +18,69 @@ interface DataroomViewProps {
}
export function DataroomView({ onProcessingChange }: DataroomViewProps = {}) {
const { selectedTema, files } = useFileStore();
const { selectedTema } = useFileStore();
const [processing, setProcessing] = useState(false);
const [fullscreenTab, setFullscreenTab] = useState<string | null>(null);
const [currentTab, setCurrentTab] = useState("overview");
const handleProcessingChange = (isProcessing: boolean) => {
setProcessing(isProcessing);
onProcessingChange?.(isProcessing);
};
const openFullscreen = (tabValue: string) => {
setFullscreenTab(tabValue);
};
const closeFullscreen = () => {
setFullscreenTab(null);
};
const renderTabContent = (tabValue: string, isFullscreen = false) => {
const className = isFullscreen ? "h-[calc(100vh-8rem)] flex flex-col" : "";
switch (tabValue) {
case "overview":
return (
<div className={className}>
<DashboardTab selectedTema={selectedTema} />
</div>
);
case "files":
return (
<div className={className}>
<FilesTab
selectedTema={selectedTema}
processing={processing}
onProcessingChange={handleProcessingChange}
/>
</div>
);
case "chat":
return (
<div className={className}>
<ChatTab selectedTema={selectedTema} />
</div>
);
default:
return null;
}
};
const getTabTitle = (tabValue: string) => {
switch (tabValue) {
case "overview":
return "Overview";
case "files":
return "Files";
case "chat":
return "Chat";
default:
return "";
}
};
return (
<div className="flex flex-col h-full bg-white">
<div className="border-b border-gray-200 px-6 py-4">
@@ -39,46 +100,86 @@ export function DataroomView({ onProcessingChange }: DataroomViewProps = {}) {
</div>
</div>
<Tabs defaultValue="files" className="flex flex-col flex-1">
<Tabs
value={currentTab}
onValueChange={setCurrentTab}
className="flex flex-col flex-1"
>
<div className="border-b border-gray-200 px-6 py-2">
<TabsList className="flex h-10 w-full items-center gap-2 bg-transparent p-0 justify-start">
<TabsTrigger
value="overview"
className="rounded-md px-4 py-2 text-sm font-medium text-gray-600 transition data-[state=active]:bg-gray-900 data-[state=active]:text-white data-[state=active]:shadow"
<TabsList className="flex h-10 w-full items-center gap-2 bg-transparent p-0 justify-between">
<div className="flex items-center gap-2">
<TabsTrigger
value="overview"
className="rounded-md px-4 py-2 text-sm font-medium text-gray-600 transition data-[state=active]:bg-gray-900 data-[state=active]:text-white data-[state=active]:shadow"
>
Overview
</TabsTrigger>
<TabsTrigger
value="files"
className="rounded-md px-4 py-2 text-sm font-medium text-gray-600 transition data-[state=active]:bg-gray-900 data-[state=active]:text-white data-[state=active]:shadow"
>
Files
</TabsTrigger>
<TabsTrigger
value="chat"
className="rounded-md px-4 py-2 text-sm font-medium text-gray-600 transition data-[state=active]:bg-gray-900 data-[state=active]:text-white data-[state=active]:shadow"
>
Chat
</TabsTrigger>
</div>
<Button
variant="outline"
size="sm"
onClick={() => openFullscreen(currentTab)}
className="ml-auto"
>
Overview
</TabsTrigger>
<TabsTrigger
value="files"
className="rounded-md px-4 py-2 text-sm font-medium text-gray-600 transition data-[state=active]:bg-gray-900 data-[state=active]:text-white data-[state=active]:shadow"
>
Files
</TabsTrigger>
<TabsTrigger
value="chat"
className="rounded-md px-4 py-2 text-sm font-medium text-gray-600 transition data-[state=active]:bg-gray-900 data-[state=active]:text-white data-[state=active]:shadow"
>
Chat
</TabsTrigger>
<Expand className="h-4 w-4" />
<span className="sr-only">Open fullscreen</span>
</Button>
</TabsList>
</div>
<TabsContent value="overview" className="mt-0 flex-1">
<DashboardTab selectedTema={selectedTema} />
{renderTabContent("overview")}
</TabsContent>
<TabsContent value="files" className="mt-0 flex flex-1 flex-col">
<FilesTab
selectedTema={selectedTema}
processing={processing}
onProcessingChange={handleProcessingChange}
/>
{renderTabContent("files")}
</TabsContent>
<TabsContent value="chat" className="mt-0 flex-1">
<ChatTab selectedTema={selectedTema} />
{renderTabContent("chat")}
</TabsContent>
</Tabs>
<Dialog
open={fullscreenTab !== null}
onOpenChange={(open: boolean) => !open && closeFullscreen()}
>
<DialogContent className="max-w-[100vw] max-h-[100vh] w-[100vw] h-[100vh] m-0 rounded-none [&>button]:hidden">
<DialogHeader className="flex flex-row items-center justify-between space-y-0 pb-4">
<DialogTitle className="text-xl font-semibold">
{selectedTema
? `${getTabTitle(fullscreenTab || "")} - ${selectedTema}`
: getTabTitle(fullscreenTab || "")}
</DialogTitle>
<Button
variant="outline"
size="sm"
onClick={closeFullscreen}
className="h-8 w-8 p-0"
>
<Minimize2 className="h-4 w-4" />
<span className="sr-only">Exit fullscreen</span>
</Button>
</DialogHeader>
<div className="flex-1 overflow-hidden">
{fullscreenTab && renderTabContent(fullscreenTab, true)}
</div>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -0,0 +1,206 @@
import React, { useState } from "react";
import {
Globe,
ExternalLink,
Search,
ChevronDown,
ChevronRight,
Info,
Star,
} from "lucide-react";
import { cn } from "@/lib/utils";
interface SearchResult {
title: string;
url: string;
content: string;
score?: number;
}
interface WebSearchData {
query: string;
results: SearchResult[];
summary: string;
total_results: number;
}
interface WebSearchResultsProps {
data: WebSearchData;
}
const getScoreColor = (score?: number) => {
if (!score) return "text-gray-500";
if (score >= 0.8) return "text-green-600";
if (score >= 0.6) return "text-yellow-600";
return "text-gray-500";
};
const getScoreStars = (score?: number) => {
if (!score) return 0;
return Math.round(score * 5);
};
const truncateContent = (content: string, maxLength: number = 200) => {
if (content.length <= maxLength) return content;
return content.slice(0, maxLength) + "...";
};
export const WebSearchResults: React.FC<WebSearchResultsProps> = ({ data }) => {
const [expandedResults, setExpandedResults] = useState<Set<number>>(new Set());
const [showAllResults, setShowAllResults] = useState(false);
const { query, results, summary, total_results } = data;
const toggleResult = (index: number) => {
const newExpanded = new Set(expandedResults);
if (newExpanded.has(index)) {
newExpanded.delete(index);
} else {
newExpanded.add(index);
}
setExpandedResults(newExpanded);
};
const visibleResults = showAllResults ? results : results.slice(0, 3);
return (
<div className="w-full bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden">
{/* Header */}
<div className="bg-gradient-to-r from-green-50 to-emerald-50 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Globe className="w-6 h-6 text-green-600" />
<div>
<h3 className="font-semibold text-gray-900">Web Search Results</h3>
<div className="flex items-center gap-2 text-xs text-gray-600">
<Search className="w-3 h-3" />
<span>"{query}"</span>
</div>
</div>
</div>
<div className="text-right">
<div className="text-sm font-medium text-gray-900">{results.length}</div>
<div className="text-xs text-gray-600">
of {total_results} results
</div>
</div>
</div>
</div>
<div className="p-4 space-y-4">
{/* Summary */}
{summary && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<div className="flex items-start gap-2">
<Info className="w-4 h-4 text-blue-600 mt-0.5 flex-shrink-0" />
<div>
<h4 className="font-medium text-blue-900 text-sm mb-1">
Summary
</h4>
<p className="text-sm text-blue-800">{summary}</p>
</div>
</div>
</div>
)}
{/* Search Results */}
<div className="space-y-3">
{visibleResults.map((result, index) => {
const isExpanded = expandedResults.has(index);
const stars = getScoreStars(result.score);
return (
<div key={index} className="border border-gray-200 rounded-lg overflow-hidden">
<div className="p-3">
<div className="flex items-start justify-between mb-2">
<div className="flex-1 min-w-0">
<h4 className="font-medium text-gray-900 text-sm mb-1 line-clamp-2">
{result.title}
</h4>
<div className="flex items-center gap-2">
<a
href={result.url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-600 hover:text-blue-800 flex items-center gap-1 truncate"
>
<ExternalLink className="w-3 h-3 flex-shrink-0" />
{new URL(result.url).hostname}
</a>
{result.score && (
<div className="flex items-center gap-1">
<div className="flex">
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={cn(
"w-3 h-3",
i < stars
? "text-yellow-400 fill-current"
: "text-gray-300"
)}
/>
))}
</div>
<span className={cn("text-xs font-medium", getScoreColor(result.score))}>
{Math.round((result.score || 0) * 100)}%
</span>
</div>
)}
</div>
</div>
</div>
<div className="text-sm text-gray-700">
{isExpanded ? result.content : truncateContent(result.content)}
</div>
{result.content.length > 200 && (
<button
onClick={() => toggleResult(index)}
className="mt-2 flex items-center gap-1 text-xs text-blue-600 hover:text-blue-800 font-medium"
>
{isExpanded ? (
<>
<ChevronDown className="w-3 h-3" />
Show less
</>
) : (
<>
<ChevronRight className="w-3 h-3" />
Read more
</>
)}
</button>
)}
</div>
</div>
);
})}
</div>
{/* Show More/Less Button */}
{results.length > 3 && (
<div className="text-center">
<button
onClick={() => setShowAllResults(!showAllResults)}
className="text-sm text-blue-600 hover:text-blue-800 font-medium px-4 py-2 rounded-lg hover:bg-blue-50 transition-colors"
>
{showAllResults
? "Show fewer results"
: `Show ${results.length - 3} more results`}
</button>
</div>
)}
{/* No Results */}
{results.length === 0 && (
<div className="text-center py-6">
<Search className="w-8 h-8 text-gray-400 mx-auto mb-2" />
<p className="text-sm text-gray-600">No results found for "{query}"</p>
</div>
)}
</div>
</div>
);
};