diff --git a/frontend/src/api/ai.ts b/frontend/src/api/ai.ts index 1aba362..ab333d5 100644 --- a/frontend/src/api/ai.ts +++ b/frontend/src/api/ai.ts @@ -61,6 +61,14 @@ export interface AIAgent { updated_at?: string; } +export interface ChatFolder { + id: string; + name: string; + parent_id: string | null; + user_id: string; + created_at?: string; +} + export interface ChatSession { id: string; user_id: string; @@ -68,6 +76,7 @@ export interface ChatSession { title: string; is_pinned: boolean; is_sidebar: boolean; + folder_id: string | null; created_at?: string; updated_at?: string; } @@ -84,6 +93,15 @@ export interface ChatMessage { created_at?: string; } +export interface ChatAttachment { + id: string; + message_id: string | null; + session_id: string; + filename: string; + mime_type: string; + size_bytes: number; +} + export interface AITool { name: string; description: string; @@ -126,11 +144,18 @@ export const deleteAgent = (id: string) => apiDelete(`/ai/agents/${id}`); export const fetchTools = () => apiGet('/ai/tools'); +// ─── Folders ─── + +export const fetchFolders = () => apiGet('/ai/folders'); +export const createFolder = (data: { name: string; parent_id?: string }) => apiPost('/ai/folders', data); +export const updateFolder = (id: string, data: Partial) => apiPut(`/ai/folders/${id}`, data); +export const deleteFolder = (id: string) => apiDelete(`/ai/folders/${id}`); + // ─── Sessions ─── export const fetchSessions = (isSidebar?: boolean) => apiGet('/ai/sessions', { params: isSidebar !== undefined ? { is_sidebar: isSidebar } : {} }); -export const createSession = (data: { title?: string; agent_id?: string; is_sidebar?: boolean }) => +export const createSession = (data: { title?: string; agent_id?: string; is_sidebar?: boolean; folder_id?: string }) => apiPost('/ai/sessions', data); export const updateSession = (id: string, data: Partial) => apiPut(`/ai/sessions/${id}`, data); @@ -141,6 +166,31 @@ export const deleteSession = (id: string) => apiDelete(`/ai/sessions/${id}`); export const fetchMessages = (sessionId: string) => apiGet(`/ai/sessions/${sessionId}/messages`); +// ─── Attachments ─── + +export const fetchAttachments = (sessionId: string) => + apiGet(`/ai/sessions/${sessionId}/attachments`); + +export async function uploadAttachment(sessionId: string, file: File): Promise { + const formData = new FormData(); + formData.append('file', file); + const csrfToken = sessionStorage.getItem('leocrm_csrf_token'); + const response = await fetch(`/api/v1/ai/sessions/${sessionId}/attachments`, { + method: 'POST', + headers: { + ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}), + }, + credentials: 'include', + body: formData, + }); + if (!response.ok) throw new Error(`Upload failed: ${response.status}`); + return response.json(); +} + +export function getAttachmentDownloadUrl(attachmentId: string): string { + return `/api/v1/ai/attachments/${attachmentId}/download`; +} + // ─── Streaming Chat ─── export interface StreamEvent { diff --git a/frontend/src/components/ai/ChatWindow.tsx b/frontend/src/components/ai/ChatWindow.tsx index d2ac8b0..003913e 100644 --- a/frontend/src/components/ai/ChatWindow.tsx +++ b/frontend/src/components/ai/ChatWindow.tsx @@ -1,6 +1,9 @@ import React, { useState, useRef, useEffect } from 'react'; import clsx from 'clsx'; -import { streamChat, fetchMessages, type ChatMessage } from '@/api/ai'; +import { + streamChat, fetchMessages, fetchAttachments, uploadAttachment, getAttachmentDownloadUrl, + type ChatMessage, type ChatAttachment, +} from '@/api/ai'; interface ChatWindowProps { sessionId: string; @@ -9,23 +12,33 @@ interface ChatWindowProps { compact?: boolean; } +function formatSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; +} + export function ChatWindow({ sessionId, agentId, className, compact }: ChatWindowProps) { const [messages, setMessages] = useState([]); + const [attachments, setAttachments] = useState([]); const [input, setInput] = useState(''); const [isStreaming, setIsStreaming] = useState(false); const [streamingContent, setStreamingContent] = useState(''); const [toolStatus, setToolStatus] = useState(null); const [error, setError] = useState(null); + const [pendingFiles, setPendingFiles] = useState([]); + const [uploadingFile, setUploadingFile] = useState(false); const scrollRef = useRef(null); const inputRef = useRef(null); + const fileInputRef = useRef(null); useEffect(() => { if (!sessionId) return; setMessages([]); + setAttachments([]); setError(null); - fetchMessages(sessionId) - .then((msgs) => setMessages(msgs)) - .catch((e) => setError(e?.message || 'Failed to load messages')); + fetchMessages(sessionId).then(setMessages).catch((e) => setError(e?.message || 'Failed to load messages')); + fetchAttachments(sessionId).then(setAttachments).catch(() => {}); }, [sessionId]); useEffect(() => { @@ -34,6 +47,23 @@ export function ChatWindow({ sessionId, agentId, className, compact }: ChatWindo } }, [messages, streamingContent]); + const handleFileSelect = async (e: React.ChangeEvent) => { + const files = Array.from(e.target.files || []); + if (files.length === 0) return; + setUploadingFile(true); + try { + for (const file of files) { + const att = await uploadAttachment(sessionId, file); + setAttachments((prev) => [...prev, att]); + } + } catch (e) { + setError(e instanceof Error ? e.message : 'Upload failed'); + } finally { + setUploadingFile(false); + if (fileInputRef.current) fileInputRef.current.value = ''; + } + }; + const handleSend = async () => { const content = input.trim(); if (!content || isStreaming) return; @@ -94,6 +124,30 @@ export function ChatWindow({ sessionId, agentId, className, compact }: ChatWindo return (
+ {/* Attachments bar */} + {attachments.length > 0 && ( +
+
+ {attachments.map((att) => ( + + + + + {att.filename} + {formatSize(att.size_bytes)} + + ))} +
+
+ )} + + {/* Messages */}
{messages.length === 0 && !streamingContent && !error && (
@@ -128,8 +182,27 @@ export function ChatWindow({ sessionId, agentId, className, compact }: ChatWindo
)}
+ + {/* Input */}
+ +