wip chat
This commit is contained in:
241
frontend/src/components/ChatExample.tsx
Normal file
241
frontend/src/components/ChatExample.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
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;
|
||||
@@ -1,12 +1,94 @@
|
||||
import { MessageCircle, Send, Bot, User } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
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,
|
||||
PromptInputHeader,
|
||||
type PromptInputMessage,
|
||||
PromptInputSubmit,
|
||||
PromptInputTextarea,
|
||||
PromptInputFooter,
|
||||
PromptInputTools,
|
||||
} from "@/components/ai-elements/prompt-input";
|
||||
import { Action, Actions } from "@/components/ai-elements/actions";
|
||||
import { Fragment, useState, useEffect } from "react";
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { Response } from "@/components/ai-elements/response";
|
||||
import {
|
||||
CopyIcon,
|
||||
RefreshCcwIcon,
|
||||
MessageCircle,
|
||||
Bot,
|
||||
AlertCircle,
|
||||
PaperclipIcon,
|
||||
} from "lucide-react";
|
||||
import { Loader } from "@/components/ai-elements/loader";
|
||||
import { DefaultChatTransport } from "ai";
|
||||
|
||||
interface ChatTabProps {
|
||||
selectedTema: string | null;
|
||||
}
|
||||
|
||||
export function ChatTab({ selectedTema }: ChatTabProps) {
|
||||
const [input, setInput] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
messages,
|
||||
sendMessage,
|
||||
status,
|
||||
regenerate,
|
||||
error: chatError,
|
||||
} = useChat({
|
||||
transport: new DefaultChatTransport({
|
||||
api: "/api/v1/agent/chat",
|
||||
}),
|
||||
onError: (error) => {
|
||||
setError(`Error en el chat: ${error.message}`);
|
||||
},
|
||||
});
|
||||
|
||||
// Clear error when starting new conversation
|
||||
useEffect(() => {
|
||||
if (status === "streaming") {
|
||||
setError(null);
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
const handleSubmit = (message: PromptInputMessage) => {
|
||||
const hasText = Boolean(message.text?.trim());
|
||||
const hasAttachments = Boolean(message.files?.length);
|
||||
|
||||
if (!(hasText || hasAttachments)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
sendMessage(
|
||||
{
|
||||
text: message.text || "Enviado con archivos adjuntos",
|
||||
files: message.files,
|
||||
},
|
||||
{
|
||||
body: {
|
||||
dataroom: selectedTema,
|
||||
context: `Usuario está consultando sobre el dataroom: ${selectedTema}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
setInput("");
|
||||
};
|
||||
|
||||
if (!selectedTema) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-64">
|
||||
@@ -37,55 +119,137 @@ export function ChatTab({ selectedTema }: ChatTabProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Chat Messages Area */}
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<div className="max-w-4xl mx-auto space-y-4">
|
||||
{/* Welcome Message */}
|
||||
<div className="flex items-start gap-3">
|
||||
<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>
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* Placeholder for future messages */}
|
||||
<div className="text-center py-8">
|
||||
<MessageCircle className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||
<h4 className="text-lg font-medium text-gray-900 mb-2">
|
||||
Chat Inteligente
|
||||
</h4>
|
||||
<p className="text-gray-500 max-w-md mx-auto">
|
||||
El chat estará disponible próximamente. Podrás hacer preguntas sobre los
|
||||
documentos y obtener respuestas basadas en el contenido del dataroom.
|
||||
</p>
|
||||
</div>
|
||||
</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 Input Area */}
|
||||
<div className="border-t border-gray-200 p-6">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
{/* 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>
|
||||
);
|
||||
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
|
||||
className="w-full"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<Button disabled className="gap-2">
|
||||
<Send className="w-4 h-4" />
|
||||
Enviar
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
Esta funcionalidad estará disponible próximamente
|
||||
</p>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,91 @@
|
||||
import { FileText, Users, Database, Activity } from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
FileText,
|
||||
Database,
|
||||
Activity,
|
||||
TrendingUp,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import { api } from "@/services/api";
|
||||
|
||||
interface DashboardTabProps {
|
||||
selectedTema: string | null;
|
||||
}
|
||||
|
||||
interface DataroomInfo {
|
||||
name: string;
|
||||
collection: string;
|
||||
storage: string;
|
||||
file_count: number;
|
||||
total_size_bytes: number;
|
||||
total_size_mb: number;
|
||||
collection_exists: boolean;
|
||||
vector_count: number | null;
|
||||
collection_info: {
|
||||
vectors_count: number;
|
||||
indexed_vectors_count: number;
|
||||
points_count: number;
|
||||
segments_count: number;
|
||||
status: string;
|
||||
} | null;
|
||||
file_types: Record<string, number>;
|
||||
recent_files: Array<{
|
||||
name: string;
|
||||
size_mb: number;
|
||||
last_modified: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function DashboardTab({ selectedTema }: DashboardTabProps) {
|
||||
const [dataroomInfo, setDataroomInfo] = useState<DataroomInfo | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTema) {
|
||||
fetchDataroomInfo();
|
||||
}
|
||||
}, [selectedTema]);
|
||||
|
||||
const fetchDataroomInfo = async () => {
|
||||
if (!selectedTema) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const info = await api.getDataroomInfo(selectedTema);
|
||||
setDataroomInfo(info);
|
||||
} catch (err) {
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : "Error desconocido";
|
||||
setError(`Error cargando información: ${errorMessage}`);
|
||||
console.error("Error fetching dataroom info:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatFileTypes = (fileTypes: Record<string, number>) => {
|
||||
const entries = Object.entries(fileTypes);
|
||||
if (entries.length === 0) return "Sin archivos";
|
||||
|
||||
return entries
|
||||
.sort(([, a], [, b]) => b - a) // Sort by count descending
|
||||
.slice(0, 3) // Take top 3
|
||||
.map(([ext, count]) => `${ext.toUpperCase()}: ${count}`)
|
||||
.join(", ");
|
||||
};
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (bytes === 0) return "0 MB";
|
||||
const mb = bytes / (1024 * 1024);
|
||||
if (mb < 1) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${mb.toFixed(1)} MB`;
|
||||
};
|
||||
|
||||
if (!selectedTema) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-64">
|
||||
@@ -16,6 +97,40 @@ export function DashboardTab({ selectedTema }: DashboardTabProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-64">
|
||||
<Loader2 className="w-8 h-8 text-blue-600 animate-spin mb-4" />
|
||||
<p className="text-gray-600">Cargando métricas...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 flex items-center gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-red-800">Error</p>
|
||||
<p className="text-sm text-red-600">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!dataroomInfo) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-64">
|
||||
<AlertCircle className="w-12 h-12 text-gray-400 mb-4" />
|
||||
<p className="text-gray-500">
|
||||
No se pudo cargar la información del dataroom
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
@@ -36,7 +151,12 @@ export function DashboardTab({ selectedTema }: DashboardTabProps) {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Archivos</p>
|
||||
<p className="text-2xl font-bold text-gray-900">--</p>
|
||||
<p className="text-2xl font-bold text-gray-900">
|
||||
{dataroomInfo.file_count}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{formatFileTypes(dataroomInfo.file_types)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,8 +168,15 @@ export function DashboardTab({ selectedTema }: DashboardTabProps) {
|
||||
<Database className="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Almacenamiento</p>
|
||||
<p className="text-2xl font-bold text-gray-900">--</p>
|
||||
<p className="text-sm font-medium text-gray-600">
|
||||
Almacenamiento
|
||||
</p>
|
||||
<p className="text-2xl font-bold text-gray-900">
|
||||
{dataroomInfo.total_size_mb.toFixed(1)} MB
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{formatBytes(dataroomInfo.total_size_bytes)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,37 +189,139 @@ export function DashboardTab({ selectedTema }: DashboardTabProps) {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Vectores</p>
|
||||
<p className="text-2xl font-bold text-gray-900">--</p>
|
||||
<p className="text-2xl font-bold text-gray-900">
|
||||
{dataroomInfo.vector_count ?? 0}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{dataroomInfo.collection_exists
|
||||
? "Vectores indexados"
|
||||
: "Sin vectores"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Activity Card */}
|
||||
{/* Collection Status Card */}
|
||||
<div className="bg-white border border-gray-200 rounded-lg p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-orange-100 rounded-lg">
|
||||
<Users className="w-5 h-5 text-orange-600" />
|
||||
<TrendingUp className="w-5 h-5 text-orange-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Actividad</p>
|
||||
<p className="text-2xl font-bold text-gray-900">--</p>
|
||||
<p className="text-sm font-medium text-gray-600">Estado</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-2xl font-bold text-gray-900">
|
||||
{dataroomInfo.collection_exists ? "Activo" : "Inactivo"}
|
||||
</p>
|
||||
{dataroomInfo.collection_exists ? (
|
||||
<CheckCircle className="w-6 h-6 text-green-600" />
|
||||
) : (
|
||||
<AlertCircle className="w-6 h-6 text-yellow-600" />
|
||||
)}
|
||||
</div>
|
||||
{dataroomInfo.collection_info ? (
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{dataroomInfo.collection_info.indexed_vectors_count}/
|
||||
{dataroomInfo.collection_info.vectors_count} vectores
|
||||
indexados
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
{dataroomInfo.collection_exists
|
||||
? "Colección sin datos"
|
||||
: "Sin colección"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Coming Soon Message */}
|
||||
<div className="mt-8 bg-gray-50 border border-gray-200 rounded-lg p-6">
|
||||
<div className="text-center">
|
||||
<Activity className="w-8 h-8 text-gray-400 mx-auto mb-3" />
|
||||
<h4 className="text-sm font-medium text-gray-900 mb-2">
|
||||
Panel de Métricas
|
||||
{/* Recent Files Section */}
|
||||
{dataroomInfo.recent_files.length > 0 && (
|
||||
<div className="mt-8">
|
||||
<h4 className="text-md font-semibold text-gray-900 mb-4">
|
||||
Archivos Recientes
|
||||
</h4>
|
||||
<p className="text-sm text-gray-500">
|
||||
Este panel se llenará con métricas detalladas y gráficos interactivos próximamente.
|
||||
</p>
|
||||
<div className="bg-white border border-gray-200 rounded-lg overflow-hidden">
|
||||
<div className="divide-y divide-gray-200">
|
||||
{dataroomInfo.recent_files.map((file, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="p-4 flex items-center justify-between hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="w-4 h-4 text-gray-400" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{file.name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{new Date(file.last_modified).toLocaleDateString(
|
||||
"es-ES",
|
||||
{
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-gray-600">
|
||||
{file.size_mb.toFixed(2)} MB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,11 +5,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { FilesTab } from "./FilesTab";
|
||||
import { DashboardTab } from "./DashboardTab";
|
||||
import { ChatTab } from "./ChatTab";
|
||||
import {
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
|
||||
interface DataroomViewProps {
|
||||
onProcessingChange?: (isProcessing: boolean) => void;
|
||||
@@ -18,133 +13,27 @@ interface DataroomViewProps {
|
||||
export function DataroomView({ onProcessingChange }: DataroomViewProps = {}) {
|
||||
const { selectedTema, files } = useFileStore();
|
||||
|
||||
// Collection status states
|
||||
const [isCheckingCollection, setIsCheckingCollection] = useState(false);
|
||||
const [collectionExists, setCollectionExists] = useState<boolean | null>(
|
||||
null,
|
||||
);
|
||||
const [collectionError, setCollectionError] = useState<string | null>(null);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
|
||||
// Check collection status when tema changes
|
||||
useEffect(() => {
|
||||
checkCollectionStatus();
|
||||
}, [selectedTema]);
|
||||
|
||||
// Load files when tema changes
|
||||
useEffect(() => {
|
||||
loadFiles();
|
||||
}, [selectedTema]);
|
||||
|
||||
const checkCollectionStatus = async () => {
|
||||
if (!selectedTema) {
|
||||
setCollectionExists(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCheckingCollection(true);
|
||||
setCollectionError(null);
|
||||
|
||||
try {
|
||||
const result = await api.checkCollectionExists(selectedTema);
|
||||
setCollectionExists(result.exists);
|
||||
} catch (err) {
|
||||
console.error("Error checking collection:", err);
|
||||
setCollectionError(
|
||||
err instanceof Error ? err.message : "Error al verificar colección",
|
||||
);
|
||||
setCollectionExists(null);
|
||||
} finally {
|
||||
setIsCheckingCollection(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateCollection = async () => {
|
||||
if (!selectedTema) return;
|
||||
|
||||
setIsCheckingCollection(true);
|
||||
setCollectionError(null);
|
||||
|
||||
try {
|
||||
const result = await api.createCollection(selectedTema);
|
||||
if (result.success) {
|
||||
setCollectionExists(true);
|
||||
console.log(`Collection "${selectedTema}" created successfully`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error creating collection:", err);
|
||||
setCollectionError(
|
||||
err instanceof Error ? err.message : "Error al crear colección",
|
||||
);
|
||||
} finally {
|
||||
setIsCheckingCollection(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadFiles = async () => {
|
||||
// This will be handled by FilesTab component
|
||||
};
|
||||
|
||||
const handleProcessingChange = (isProcessing: boolean) => {
|
||||
setProcessing(isProcessing);
|
||||
onProcessingChange?.(isProcessing);
|
||||
};
|
||||
|
||||
const totalFiles = files.length;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-white">
|
||||
<div className="border-b border-gray-200 px-6 py-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h2 className="text-2xl font-semibold text-gray-900">
|
||||
{selectedTema
|
||||
? `Dataroom: ${selectedTema}`
|
||||
: "Selecciona un dataroom"}
|
||||
</h2>
|
||||
{/* Collection Status Indicator */}
|
||||
{selectedTema && (
|
||||
<div className="flex items-center gap-2">
|
||||
{isCheckingCollection ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin text-gray-500" />
|
||||
<span className="text-xs text-gray-500">
|
||||
Verificando...
|
||||
</span>
|
||||
</>
|
||||
) : collectionExists === true ? (
|
||||
<>
|
||||
<CheckCircle2 className="w-4 h-4 text-green-600" />
|
||||
<span className="text-xs text-green-600">
|
||||
Colección disponible
|
||||
</span>
|
||||
</>
|
||||
) : collectionExists === false ? (
|
||||
<>
|
||||
<AlertCircle className="w-4 h-4 text-yellow-600" />
|
||||
<button
|
||||
onClick={handleCreateCollection}
|
||||
className="text-xs text-yellow-600 hover:text-yellow-700 underline"
|
||||
>
|
||||
Crear colección
|
||||
</button>
|
||||
</>
|
||||
) : collectionError ? (
|
||||
<>
|
||||
<AlertCircle className="w-4 h-4 text-red-600" />
|
||||
<span className="text-xs text-red-600">
|
||||
Error de conexión
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold text-gray-900 mb-2">
|
||||
{selectedTema
|
||||
? `Dataroom: ${selectedTema}`
|
||||
: "Selecciona un dataroom"}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600">
|
||||
{selectedTema
|
||||
? `${totalFiles} archivo${totalFiles !== 1 ? "s" : ""}`
|
||||
: "Selecciona un dataroom de la barra lateral para ver sus archivos"}
|
||||
? "Gestiona archivos, consulta métricas y chatea con IA sobre el contenido"
|
||||
: "Selecciona un dataroom de la barra lateral para comenzar"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useFileStore } from "@/stores/fileStore";
|
||||
import { api } from "@/services/api";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -80,6 +80,11 @@ export function FilesTab({
|
||||
const [chunkingFileTema, setChunkingFileTema] = useState("");
|
||||
const [chunkingCollectionName, setChunkingCollectionName] = useState("");
|
||||
|
||||
// Load files when component mounts or selectedTema changes
|
||||
useEffect(() => {
|
||||
loadFiles();
|
||||
}, [selectedTema]);
|
||||
|
||||
const loadFiles = async () => {
|
||||
// Don't load files if no dataroom is selected
|
||||
if (!selectedTema) {
|
||||
|
||||
@@ -273,14 +273,11 @@ export function Sidebar({
|
||||
collapsed ? "justify-center" : "justify-between",
|
||||
)}
|
||||
>
|
||||
<h2
|
||||
className={cn(
|
||||
"text-sm font-medium text-slate-300",
|
||||
collapsed && "text-xs text-center",
|
||||
)}
|
||||
>
|
||||
{collapsed ? "Rooms" : "Datarooms"}
|
||||
</h2>
|
||||
{!collapsed && (
|
||||
<h2 className="text-sm font-medium text-slate-300">
|
||||
Datarooms
|
||||
</h2>
|
||||
)}
|
||||
{renderWithTooltip(
|
||||
"Crear dataroom",
|
||||
<Button
|
||||
|
||||
@@ -39,6 +39,30 @@ interface DataroomsResponse {
|
||||
}>;
|
||||
}
|
||||
|
||||
interface DataroomInfo {
|
||||
name: string;
|
||||
collection: string;
|
||||
storage: string;
|
||||
file_count: number;
|
||||
total_size_bytes: number;
|
||||
total_size_mb: number;
|
||||
collection_exists: boolean;
|
||||
vector_count: number | null;
|
||||
collection_info: {
|
||||
vectors_count: number;
|
||||
indexed_vectors_count: number;
|
||||
points_count: number;
|
||||
segments_count: number;
|
||||
status: string;
|
||||
} | null;
|
||||
file_types: Record<string, number>;
|
||||
recent_files: Array<{
|
||||
name: string;
|
||||
size_mb: number;
|
||||
last_modified: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface CreateDataroomRequest {
|
||||
name: string;
|
||||
collection?: string;
|
||||
@@ -100,6 +124,15 @@ export const api = {
|
||||
return response.json();
|
||||
},
|
||||
|
||||
// Obtener información detallada de un dataroom
|
||||
getDataroomInfo: async (dataroomName: string): Promise<DataroomInfo> => {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/dataroom/${encodeURIComponent(dataroomName)}/info`,
|
||||
);
|
||||
if (!response.ok) throw new Error("Error fetching dataroom info");
|
||||
return response.json();
|
||||
},
|
||||
|
||||
// Obtener archivos (todos o por tema)
|
||||
getFiles: async (tema?: string): Promise<FileListResponse> => {
|
||||
const url = tema
|
||||
|
||||
Reference in New Issue
Block a user