From 205b6061ef195a70cedcff46b309348e1a19460e Mon Sep 17 00:00:00 2001 From: Manus Date: Thu, 8 Jan 2026 05:47:56 -0500 Subject: [PATCH] Initial project bootstrap --- .gitignore | 107 + .prettierignore | 35 + .prettierrc | 15 + client/index.html | 26 + client/public/.gitkeep | 0 client/src/App.tsx | 42 + client/src/_core/hooks/useAuth.ts | 84 + client/src/components/AIChatBox.tsx | 335 + client/src/components/DashboardLayout.tsx | 264 + .../components/DashboardLayoutSkeleton.tsx | 46 + client/src/components/ErrorBoundary.tsx | 62 + client/src/components/ManusDialog.tsx | 89 + client/src/components/Map.tsx | 155 + client/src/components/ui/accordion.tsx | 64 + client/src/components/ui/alert-dialog.tsx | 155 + client/src/components/ui/alert.tsx | 66 + client/src/components/ui/aspect-ratio.tsx | 9 + client/src/components/ui/avatar.tsx | 51 + client/src/components/ui/badge.tsx | 46 + client/src/components/ui/breadcrumb.tsx | 109 + client/src/components/ui/button-group.tsx | 83 + client/src/components/ui/button.tsx | 60 + client/src/components/ui/calendar.tsx | 211 + client/src/components/ui/card.tsx | 92 + client/src/components/ui/carousel.tsx | 239 + client/src/components/ui/chart.tsx | 355 + client/src/components/ui/checkbox.tsx | 30 + client/src/components/ui/collapsible.tsx | 31 + client/src/components/ui/command.tsx | 184 + client/src/components/ui/context-menu.tsx | 250 + client/src/components/ui/dialog.tsx | 209 + client/src/components/ui/drawer.tsx | 133 + client/src/components/ui/dropdown-menu.tsx | 255 + client/src/components/ui/empty.tsx | 104 + client/src/components/ui/field.tsx | 242 + client/src/components/ui/form.tsx | 168 + client/src/components/ui/hover-card.tsx | 42 + client/src/components/ui/input-group.tsx | 168 + client/src/components/ui/input-otp.tsx | 75 + client/src/components/ui/input.tsx | 70 + client/src/components/ui/item.tsx | 193 + client/src/components/ui/kbd.tsx | 28 + client/src/components/ui/label.tsx | 22 + client/src/components/ui/menubar.tsx | 274 + client/src/components/ui/navigation-menu.tsx | 168 + client/src/components/ui/pagination.tsx | 127 + client/src/components/ui/popover.tsx | 46 + client/src/components/ui/progress.tsx | 29 + client/src/components/ui/radio-group.tsx | 43 + client/src/components/ui/resizable.tsx | 54 + client/src/components/ui/scroll-area.tsx | 56 + client/src/components/ui/select.tsx | 185 + client/src/components/ui/separator.tsx | 26 + client/src/components/ui/sheet.tsx | 139 + client/src/components/ui/sidebar.tsx | 734 ++ client/src/components/ui/skeleton.tsx | 13 + client/src/components/ui/slider.tsx | 61 + client/src/components/ui/sonner.tsx | 23 + client/src/components/ui/spinner.tsx | 16 + client/src/components/ui/switch.tsx | 29 + client/src/components/ui/table.tsx | 114 + client/src/components/ui/tabs.tsx | 64 + client/src/components/ui/textarea.tsx | 67 + client/src/components/ui/toggle-group.tsx | 73 + client/src/components/ui/toggle.tsx | 45 + client/src/components/ui/tooltip.tsx | 59 + client/src/const.ts | 17 + client/src/contexts/ThemeContext.tsx | 64 + client/src/hooks/useComposition.ts | 81 + client/src/hooks/useMobile.tsx | 21 + client/src/hooks/usePersistFn.ts | 20 + client/src/index.css | 177 + client/src/lib/trpc.ts | 4 + client/src/lib/utils.ts | 6 + client/src/main.tsx | 61 + client/src/pages/ComponentShowcase.tsx | 1437 +++ client/src/pages/Home.tsx | 31 + client/src/pages/NotFound.tsx | 52 + components.json | 19 + drizzle.config.ts | 15 + drizzle/meta/_journal.json | 5 + drizzle/migrations/.gitkeep | 0 drizzle/relations.ts | 1 + drizzle/schema.ts | 28 + package.json | 115 + patches/wouter@3.7.1.patch | 28 + pnpm-lock.yaml | 9022 +++++++++++++++++ server/_core/context.ts | 28 + server/_core/cookies.ts | 48 + server/_core/dataApi.ts | 64 + server/_core/env.ts | 10 + server/_core/imageGeneration.ts | 92 + server/_core/index.ts | 65 + server/_core/llm.ts | 332 + server/_core/map.ts | 319 + server/_core/notification.ts | 114 + server/_core/oauth.ts | 53 + server/_core/sdk.ts | 304 + server/_core/systemRouter.ts | 29 + server/_core/trpc.ts | 45 + server/_core/types/cookie.d.ts | 6 + server/_core/types/manusTypes.ts | 69 + server/_core/vite.ts | 67 + server/_core/voiceTranscription.ts | 284 + server/auth.logout.test.ts | 62 + server/db.ts | 92 + server/routers.ts | 28 + server/storage.ts | 102 + shared/_core/errors.ts | 19 + shared/const.ts | 5 + shared/types.ts | 7 + tsconfig.json | 23 + vite.config.ts | 44 + vitest.config.ts | 19 + 114 files changed, 20983 insertions(+) create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 client/index.html create mode 100644 client/public/.gitkeep create mode 100644 client/src/App.tsx create mode 100644 client/src/_core/hooks/useAuth.ts create mode 100644 client/src/components/AIChatBox.tsx create mode 100644 client/src/components/DashboardLayout.tsx create mode 100644 client/src/components/DashboardLayoutSkeleton.tsx create mode 100644 client/src/components/ErrorBoundary.tsx create mode 100644 client/src/components/ManusDialog.tsx create mode 100644 client/src/components/Map.tsx create mode 100644 client/src/components/ui/accordion.tsx create mode 100644 client/src/components/ui/alert-dialog.tsx create mode 100644 client/src/components/ui/alert.tsx create mode 100644 client/src/components/ui/aspect-ratio.tsx create mode 100644 client/src/components/ui/avatar.tsx create mode 100644 client/src/components/ui/badge.tsx create mode 100644 client/src/components/ui/breadcrumb.tsx create mode 100644 client/src/components/ui/button-group.tsx create mode 100644 client/src/components/ui/button.tsx create mode 100644 client/src/components/ui/calendar.tsx create mode 100644 client/src/components/ui/card.tsx create mode 100644 client/src/components/ui/carousel.tsx create mode 100644 client/src/components/ui/chart.tsx create mode 100644 client/src/components/ui/checkbox.tsx create mode 100644 client/src/components/ui/collapsible.tsx create mode 100644 client/src/components/ui/command.tsx create mode 100644 client/src/components/ui/context-menu.tsx create mode 100644 client/src/components/ui/dialog.tsx create mode 100644 client/src/components/ui/drawer.tsx create mode 100644 client/src/components/ui/dropdown-menu.tsx create mode 100644 client/src/components/ui/empty.tsx create mode 100644 client/src/components/ui/field.tsx create mode 100644 client/src/components/ui/form.tsx create mode 100644 client/src/components/ui/hover-card.tsx create mode 100644 client/src/components/ui/input-group.tsx create mode 100644 client/src/components/ui/input-otp.tsx create mode 100644 client/src/components/ui/input.tsx create mode 100644 client/src/components/ui/item.tsx create mode 100644 client/src/components/ui/kbd.tsx create mode 100644 client/src/components/ui/label.tsx create mode 100644 client/src/components/ui/menubar.tsx create mode 100644 client/src/components/ui/navigation-menu.tsx create mode 100644 client/src/components/ui/pagination.tsx create mode 100644 client/src/components/ui/popover.tsx create mode 100644 client/src/components/ui/progress.tsx create mode 100644 client/src/components/ui/radio-group.tsx create mode 100644 client/src/components/ui/resizable.tsx create mode 100644 client/src/components/ui/scroll-area.tsx create mode 100644 client/src/components/ui/select.tsx create mode 100644 client/src/components/ui/separator.tsx create mode 100644 client/src/components/ui/sheet.tsx create mode 100644 client/src/components/ui/sidebar.tsx create mode 100644 client/src/components/ui/skeleton.tsx create mode 100644 client/src/components/ui/slider.tsx create mode 100644 client/src/components/ui/sonner.tsx create mode 100644 client/src/components/ui/spinner.tsx create mode 100644 client/src/components/ui/switch.tsx create mode 100644 client/src/components/ui/table.tsx create mode 100644 client/src/components/ui/tabs.tsx create mode 100644 client/src/components/ui/textarea.tsx create mode 100644 client/src/components/ui/toggle-group.tsx create mode 100644 client/src/components/ui/toggle.tsx create mode 100644 client/src/components/ui/tooltip.tsx create mode 100644 client/src/const.ts create mode 100644 client/src/contexts/ThemeContext.tsx create mode 100644 client/src/hooks/useComposition.ts create mode 100644 client/src/hooks/useMobile.tsx create mode 100644 client/src/hooks/usePersistFn.ts create mode 100644 client/src/index.css create mode 100644 client/src/lib/trpc.ts create mode 100644 client/src/lib/utils.ts create mode 100644 client/src/main.tsx create mode 100644 client/src/pages/ComponentShowcase.tsx create mode 100644 client/src/pages/Home.tsx create mode 100644 client/src/pages/NotFound.tsx create mode 100644 components.json create mode 100644 drizzle.config.ts create mode 100644 drizzle/meta/_journal.json create mode 100644 drizzle/migrations/.gitkeep create mode 100644 drizzle/relations.ts create mode 100644 drizzle/schema.ts create mode 100644 package.json create mode 100644 patches/wouter@3.7.1.patch create mode 100644 pnpm-lock.yaml create mode 100644 server/_core/context.ts create mode 100644 server/_core/cookies.ts create mode 100644 server/_core/dataApi.ts create mode 100644 server/_core/env.ts create mode 100644 server/_core/imageGeneration.ts create mode 100644 server/_core/index.ts create mode 100644 server/_core/llm.ts create mode 100644 server/_core/map.ts create mode 100644 server/_core/notification.ts create mode 100644 server/_core/oauth.ts create mode 100644 server/_core/sdk.ts create mode 100644 server/_core/systemRouter.ts create mode 100644 server/_core/trpc.ts create mode 100644 server/_core/types/cookie.d.ts create mode 100644 server/_core/types/manusTypes.ts create mode 100644 server/_core/vite.ts create mode 100644 server/_core/voiceTranscription.ts create mode 100644 server/auth.logout.test.ts create mode 100644 server/db.ts create mode 100644 server/routers.ts create mode 100644 server/storage.ts create mode 100644 shared/_core/errors.ts create mode 100644 shared/const.ts create mode 100644 shared/types.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c1dbd8b --- /dev/null +++ b/.gitignore @@ -0,0 +1,107 @@ +# Dependencies +**/node_modules +.pnpm-store/ + +# Build outputs +dist/ +build/ +*.dist + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock +*.bak + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt + +# Gatsby files +.cache/ + +# Storybook build outputs +.out +.storybook-out + +# Temporary folders +tmp/ +temp/ + +# Database +*.db +*.sqlite +*.sqlite3 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..7284259 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,35 @@ +# Dependencies +node_modules/ +.pnpm-store/ + +# Build outputs +dist/ +build/ +*.dist + +# Generated files +*.tsbuildinfo +coverage/ + +# Package files +package-lock.json +pnpm-lock.yaml + +# Database +*.db +*.sqlite +*.sqlite3 + +# Logs +*.log + +# Environment files +.env* + +# IDE files +.vscode/ +.idea/ + +# OS files +.DS_Store +Thumbs.db diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..67c0bc8 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,15 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": false, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "avoid", + "endOfLine": "lf", + "quoteProps": "as-needed", + "jsxSingleQuote": false, + "proseWrap": "preserve" +} diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..fa9739e --- /dev/null +++ b/client/index.html @@ -0,0 +1,26 @@ + + + + + + + Invoice Analyzer - Dématérialisation de Facturation + + + + +
+ + + + + diff --git a/client/public/.gitkeep b/client/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 0000000..5c7a610 --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,42 @@ +import { Toaster } from "@/components/ui/sonner"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import NotFound from "@/pages/NotFound"; +import { Route, Switch } from "wouter"; +import ErrorBoundary from "./components/ErrorBoundary"; +import { ThemeProvider } from "./contexts/ThemeContext"; +import Home from "./pages/Home"; + +function Router() { + // make sure to consider if you need authentication for certain routes + return ( + + + + {/* Final fallback route */} + + + ); +} + +// NOTE: About Theme +// - First choose a default theme according to your design style (dark or light bg), than change color palette in index.css +// to keep consistent foreground/background color across components +// - If you want to make theme switchable, pass `switchable` ThemeProvider and use `useTheme` hook + +function App() { + return ( + + + + + + + + + ); +} + +export default App; diff --git a/client/src/_core/hooks/useAuth.ts b/client/src/_core/hooks/useAuth.ts new file mode 100644 index 0000000..dcef9bd --- /dev/null +++ b/client/src/_core/hooks/useAuth.ts @@ -0,0 +1,84 @@ +import { getLoginUrl } from "@/const"; +import { trpc } from "@/lib/trpc"; +import { TRPCClientError } from "@trpc/client"; +import { useCallback, useEffect, useMemo } from "react"; + +type UseAuthOptions = { + redirectOnUnauthenticated?: boolean; + redirectPath?: string; +}; + +export function useAuth(options?: UseAuthOptions) { + const { redirectOnUnauthenticated = false, redirectPath = getLoginUrl() } = + options ?? {}; + const utils = trpc.useUtils(); + + const meQuery = trpc.auth.me.useQuery(undefined, { + retry: false, + refetchOnWindowFocus: false, + }); + + const logoutMutation = trpc.auth.logout.useMutation({ + onSuccess: () => { + utils.auth.me.setData(undefined, null); + }, + }); + + const logout = useCallback(async () => { + try { + await logoutMutation.mutateAsync(); + } catch (error: unknown) { + if ( + error instanceof TRPCClientError && + error.data?.code === "UNAUTHORIZED" + ) { + return; + } + throw error; + } finally { + utils.auth.me.setData(undefined, null); + await utils.auth.me.invalidate(); + } + }, [logoutMutation, utils]); + + const state = useMemo(() => { + localStorage.setItem( + "manus-runtime-user-info", + JSON.stringify(meQuery.data) + ); + return { + user: meQuery.data ?? null, + loading: meQuery.isLoading || logoutMutation.isPending, + error: meQuery.error ?? logoutMutation.error ?? null, + isAuthenticated: Boolean(meQuery.data), + }; + }, [ + meQuery.data, + meQuery.error, + meQuery.isLoading, + logoutMutation.error, + logoutMutation.isPending, + ]); + + useEffect(() => { + if (!redirectOnUnauthenticated) return; + if (meQuery.isLoading || logoutMutation.isPending) return; + if (state.user) return; + if (typeof window === "undefined") return; + if (window.location.pathname === redirectPath) return; + + window.location.href = redirectPath + }, [ + redirectOnUnauthenticated, + redirectPath, + logoutMutation.isPending, + meQuery.isLoading, + state.user, + ]); + + return { + ...state, + refresh: () => meQuery.refetch(), + logout, + }; +} diff --git a/client/src/components/AIChatBox.tsx b/client/src/components/AIChatBox.tsx new file mode 100644 index 0000000..1c00871 --- /dev/null +++ b/client/src/components/AIChatBox.tsx @@ -0,0 +1,335 @@ +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 */} +
+