From 80444bfdc6daecb6e83c021f798c1b9499c70c86 Mon Sep 17 00:00:00 2001 From: Manus Date: Sat, 7 Mar 2026 21:53:07 -0500 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20v3.0=20=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E7=89=88=E6=9C=AC=EF=BC=9A=E6=8E=A5=E5=85=A5BSC/ETH=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=E9=93=BE=E4=B8=8A=E6=95=B0=E6=8D=AE=E3=80=81TRC20?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=8F=91=E6=94=BE=E5=90=8E=E7=AB=AF=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E3=80=81=E4=B8=AD=E8=8B=B1=E6=96=87=E5=8F=8C=E8=AF=AD?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E3=80=81SSL=E8=AF=81=E4=B9=A6=E5=9F=9F?= =?UTF-8?q?=E5=90=8D=E5=8C=96=E9=83=A8=E7=BD=B2=E5=88=B0pre-sale.newassetc?= =?UTF-8?q?hain.io?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .prettierignore | 40 +- client/src/App.tsx | 1 + 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/ManusDialog.tsx | 6 +- client/src/lib/i18n.ts | 351 +++ client/src/lib/trpc.ts | 4 + client/src/main.tsx | 58 +- client/src/pages/ComponentShowcase.tsx | 1437 +++++++++ client/src/pages/Home.tsx | 471 +-- client/src/pages/NotFound.tsx | 5 +- drizzle.config.ts | 15 + drizzle/0000_noisy_squadron_sinister.sql | 13 + drizzle/0001_known_moira_mactaggert.sql | 25 + drizzle/meta/0000_snapshot.json | 110 + drizzle/meta/0001_snapshot.json | 278 ++ drizzle/meta/_journal.json | 20 + drizzle/migrations/.gitkeep | 0 drizzle/relations.ts | 1 + drizzle/schema.ts | 68 + package.json | 31 +- pnpm-lock.yaml | 2762 ++++++++++++++--- 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 | 69 + 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/onchain.ts | 252 ++ server/operator.test.ts | 17 + server/presale.test.ts | 85 + server/routers.ts | 44 + server/storage.ts | 102 + server/trc20Monitor.ts | 187 ++ shared/_core/errors.ts | 19 + shared/const.ts | 3 + shared/types.ts | 7 + vite.config.ts | 3 +- vitest.config.ts | 19 + 55 files changed, 8599 insertions(+), 652 deletions(-) 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/lib/i18n.ts create mode 100644 client/src/lib/trpc.ts create mode 100644 client/src/pages/ComponentShowcase.tsx create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_noisy_squadron_sinister.sql create mode 100644 drizzle/0001_known_moira_mactaggert.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/0001_snapshot.json 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 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/onchain.ts create mode 100644 server/operator.test.ts create mode 100644 server/presale.test.ts create mode 100644 server/routers.ts create mode 100644 server/storage.ts create mode 100644 server/trc20Monitor.ts create mode 100644 shared/_core/errors.ts create mode 100644 shared/types.ts create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore index d21f814..24ab932 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ pids *.pid *.seed *.pid.lock +*.bak # Coverage directory used by tools like istanbul coverage/ diff --git a/.prettierignore b/.prettierignore index 27a587d..7284259 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,35 @@ -dist -node_modules -.git -*.min.js -*.min.css +# 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/client/src/App.tsx b/client/src/App.tsx index 2e1d475..e075a06 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -7,6 +7,7 @@ import { ThemeProvider } from "./contexts/ThemeContext"; import Home from "./pages/Home"; function Router() { + // make sure to consider if you need authentication for certain routes return ( 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 */} +
+