diff --git a/.gitignore b/.gitignore index 1bbb7b5..13f85ba 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,8 @@ pids *.seed *.pid.lock *.bak +backups/ +storage/ # Coverage directory used by tools like istanbul coverage/ diff --git a/client/src/App.tsx b/client/src/App.tsx index c2374c1..17a0bc3 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,27 +1,35 @@ import { Toaster } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; import NotFound from "@/pages/NotFound"; +import { lazy, Suspense } from "react"; import { Route, Switch } from "wouter"; import ErrorBoundary from "./components/ErrorBoundary"; import { ThemeProvider } from "./contexts/ThemeContext"; -import Home from "./pages/Home"; -import Login from "./pages/Login"; -import Dashboard from "./pages/Dashboard"; -import Upload from "./pages/Upload"; -import Invoices from "./pages/Invoices"; -import InvoicesBAP from "./pages/InvoicesBAP"; -import InvoiceDetail from "./pages/InvoiceDetail"; -import Settings from "./pages/Settings"; -import ImportSettings from "./pages/ImportSettings"; -import History from "./pages/History"; -import Users from "./pages/Users"; -import ListsAdmin from "./pages/ListsAdmin"; -import AutomationRules from "./pages/AutomationRules"; -import BapHistory from "./pages/BapHistory"; -import ImportReport from "./pages/ImportReport"; -import LearningSettings from "./pages/LearningSettings"; -import VentilationFreePro from "./pages/VentilationFreePro"; -import WebImportSources from "./pages/WebImportSources"; + +// Pages chargées à la demande : l'écran de connexion reste léger et chaque +// module métier ne télécharge son code qu'au moment où l'utilisateur l'ouvre. +const Home = lazy(() => import("./pages/Home")); +const Login = lazy(() => import("./pages/Login")); +const Dashboard = lazy(() => import("./pages/Dashboard")); +const Upload = lazy(() => import("./pages/Upload")); +const Invoices = lazy(() => import("./pages/Invoices")); +const InvoicesBAP = lazy(() => import("./pages/InvoicesBAP")); +const InvoiceDetail = lazy(() => import("./pages/InvoiceDetail")); +const Settings = lazy(() => import("./pages/Settings")); +const ImportSettings = lazy(() => import("./pages/ImportSettings")); +const History = lazy(() => import("./pages/History")); +const Users = lazy(() => import("./pages/Users")); +const ListsAdmin = lazy(() => import("./pages/ListsAdmin")); +const AutomationRules = lazy(() => import("./pages/AutomationRules")); +const BapHistory = lazy(() => import("./pages/BapHistory")); +const ImportReport = lazy(() => import("./pages/ImportReport")); +const LearningSettings = lazy(() => import("./pages/LearningSettings")); +const VentilationFreePro = lazy(() => import("./pages/VentilationFreePro")); +const WebImportSources = lazy(() => import("./pages/WebImportSources")); + +function RouteFallback() { + return
; +} function Router() { return ( @@ -56,7 +64,9 @@ function App() { - + }> + + diff --git a/client/src/components/AIChatBox.tsx b/client/src/components/AIChatBox.tsx deleted file mode 100644 index 1c00871..0000000 --- a/client/src/components/AIChatBox.tsx +++ /dev/null @@ -1,335 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { Textarea } from "@/components/ui/textarea"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { cn } from "@/lib/utils"; -import { Loader2, Send, User, Sparkles } from "lucide-react"; -import { useState, useEffect, useRef } from "react"; -import { Streamdown } from "streamdown"; - -/** - * Message type matching server-side LLM Message interface - */ -export type Message = { - role: "system" | "user" | "assistant"; - content: string; -}; - -export type AIChatBoxProps = { - /** - * Messages array to display in the chat. - * Should match the format used by invokeLLM on the server. - */ - messages: Message[]; - - /** - * Callback when user sends a message. - * Typically you'll call a tRPC mutation here to invoke the LLM. - */ - onSendMessage: (content: string) => void; - - /** - * Whether the AI is currently generating a response - */ - isLoading?: boolean; - - /** - * Placeholder text for the input field - */ - placeholder?: string; - - /** - * Custom className for the container - */ - className?: string; - - /** - * Height of the chat box (default: 600px) - */ - height?: string | number; - - /** - * Empty state message to display when no messages - */ - emptyStateMessage?: string; - - /** - * Suggested prompts to display in empty state - * Click to send directly - */ - suggestedPrompts?: string[]; -}; - -/** - * A ready-to-use AI chat box component that integrates with the LLM system. - * - * Features: - * - Matches server-side Message interface for seamless integration - * - Markdown rendering with Streamdown - * - Auto-scrolls to latest message - * - Loading states - * - Uses global theme colors from index.css - * - * @example - * ```tsx - * const ChatPage = () => { - * const [messages, setMessages] = useState([ - * { role: "system", content: "You are a helpful assistant." } - * ]); - * - * const chatMutation = trpc.ai.chat.useMutation({ - * onSuccess: (response) => { - * // Assuming your tRPC endpoint returns the AI response as a string - * setMessages(prev => [...prev, { - * role: "assistant", - * content: response - * }]); - * }, - * onError: (error) => { - * console.error("Chat error:", error); - * // Optionally show error message to user - * } - * }); - * - * const handleSend = (content: string) => { - * const newMessages = [...messages, { role: "user", content }]; - * setMessages(newMessages); - * chatMutation.mutate({ messages: newMessages }); - * }; - * - * return ( - * - * ); - * }; - * ``` - */ -export function AIChatBox({ - messages, - onSendMessage, - isLoading = false, - placeholder = "Type your message...", - className, - height = "600px", - emptyStateMessage = "Start a conversation with AI", - suggestedPrompts, -}: AIChatBoxProps) { - const [input, setInput] = useState(""); - const scrollAreaRef = useRef(null); - const containerRef = useRef(null); - const inputAreaRef = useRef(null); - const textareaRef = useRef(null); - - // Filter out system messages - const displayMessages = messages.filter((msg) => msg.role !== "system"); - - // Calculate min-height for last assistant message to push user message to top - const [minHeightForLastMessage, setMinHeightForLastMessage] = useState(0); - - useEffect(() => { - if (containerRef.current && inputAreaRef.current) { - const containerHeight = containerRef.current.offsetHeight; - const inputHeight = inputAreaRef.current.offsetHeight; - const scrollAreaHeight = containerHeight - inputHeight; - - // Reserve space for: - // - padding (p-4 = 32px top+bottom) - // - user message: 40px (item height) + 16px (margin-top from space-y-4) = 56px - // Note: margin-bottom is not counted because it naturally pushes the assistant message down - const userMessageReservedHeight = 56; - const calculatedHeight = scrollAreaHeight - 32 - userMessageReservedHeight; - - setMinHeightForLastMessage(Math.max(0, calculatedHeight)); - } - }, []); - - // Scroll to bottom helper function with smooth animation - const scrollToBottom = () => { - const viewport = scrollAreaRef.current?.querySelector( - '[data-radix-scroll-area-viewport]' - ) as HTMLDivElement; - - if (viewport) { - requestAnimationFrame(() => { - viewport.scrollTo({ - top: viewport.scrollHeight, - behavior: 'smooth' - }); - }); - } - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - const trimmedInput = input.trim(); - if (!trimmedInput || isLoading) return; - - onSendMessage(trimmedInput); - setInput(""); - - // Scroll immediately after sending - scrollToBottom(); - - // Keep focus on input - textareaRef.current?.focus(); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - handleSubmit(e); - } - }; - - return ( -
- {/* Messages Area */} -
- {displayMessages.length === 0 ? ( -
-
-
- -

{emptyStateMessage}

-
- - {suggestedPrompts && suggestedPrompts.length > 0 && ( -
- {suggestedPrompts.map((prompt, index) => ( - - ))} -
- )} -
-
- ) : ( - -
- {displayMessages.map((message, index) => { - // Apply min-height to last message only if NOT loading (when loading, the loading indicator gets it) - const isLastMessage = index === displayMessages.length - 1; - const shouldApplyMinHeight = - isLastMessage && !isLoading && minHeightForLastMessage > 0; - - return ( -
- {message.role === "assistant" && ( -
- -
- )} - -
- {message.role === "assistant" ? ( -
- {message.content} -
- ) : ( -

- {message.content} -

- )} -
- - {message.role === "user" && ( -
- -
- )} -
- ); - })} - - {isLoading && ( -
0 - ? { minHeight: `${minHeightForLastMessage}px` } - : undefined - } - > -
- -
-
- -
-
- )} -
-
- )} -
- - {/* Input Area */} -
-