diff --git a/surfsense_web/components/chat/AnimatedEmptyState.tsx b/surfsense_web/components/chat/AnimatedEmptyState.tsx index 2e1d7a8..7b23ddb 100644 --- a/surfsense_web/components/chat/AnimatedEmptyState.tsx +++ b/surfsense_web/components/chat/AnimatedEmptyState.tsx @@ -1,45 +1,137 @@ "use client"; + import { cn } from "@/lib/utils"; import { Manrope } from "next/font/google"; -import React, { useRef, useEffect, useState } from "react"; +import React, { + useRef, + useEffect, + useReducer, + useMemo +} from "react"; import { RoughNotation, RoughNotationGroup } from "react-rough-notation"; import { useInView } from "framer-motion"; import { useSidebar } from "@/components/ui/sidebar"; -const manrope = Manrope({ subsets: ["latin"], weight: ["400", "700"] }); +// Font configuration - could be moved to a global font config file +const manrope = Manrope({ + subsets: ["latin"], + weight: ["400", "700"], + display: "swap", // Optimize font loading + variable: "--font-manrope" +}); + +// Constants for timing - makes it easier to adjust and more maintainable +const TIMING = { + SIDEBAR_TRANSITION: 300, // Wait for sidebar transition + buffer + LAYOUT_SETTLE: 100, // Small delay to ensure layout is fully settled +} as const; + +// Animation configuration +const ANIMATION_CONFIG = { + HIGHLIGHT: { + type: "highlight" as const, + animationDuration: 2000, + iterations: 3, + color: "#3b82f680", + multiline: true, + }, + UNDERLINE: { + type: "underline" as const, + animationDuration: 2000, + iterations: 3, + color: "#10b981", + }, +} as const; + +// State management with useReducer for better organization +interface HighlightState { + shouldShowHighlight: boolean; + layoutStable: boolean; +} + +type HighlightAction = + | { type: "SIDEBAR_CHANGED" } + | { type: "LAYOUT_STABILIZED" } + | { type: "SHOW_HIGHLIGHT" } + | { type: "HIDE_HIGHLIGHT" }; + +const highlightReducer = ( + state: HighlightState, + action: HighlightAction +): HighlightState => { + switch (action.type) { + case "SIDEBAR_CHANGED": + return { + shouldShowHighlight: false, + layoutStable: false, + }; + case "LAYOUT_STABILIZED": + return { + ...state, + layoutStable: true, + }; + case "SHOW_HIGHLIGHT": + return { + ...state, + shouldShowHighlight: true, + }; + case "HIDE_HIGHLIGHT": + return { + ...state, + shouldShowHighlight: false, + }; + default: + return state; + } +}; + +const initialState: HighlightState = { + shouldShowHighlight: false, + layoutStable: true, +}; export function AnimatedEmptyState() { - const ref = useRef(null); + const ref = useRef(null); const isInView = useInView(ref); - const { state } = useSidebar(); - const [shouldShowHighlight, setShouldShowHighlight] = useState(false); - const [layoutStable, setLayoutStable] = useState(true); + const { state: sidebarState } = useSidebar(); + const [{ shouldShowHighlight, layoutStable }, dispatch] = useReducer( + highlightReducer, + initialState + ); - // Track sidebar state changes and manage highlight visibility + // Memoize class names to prevent unnecessary recalculations + const headingClassName = useMemo(() => cn( + "text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight text-neutral-900 dark:text-neutral-50 mb-6", + manrope.className, + ), []); + + const paragraphClassName = useMemo(() => + "text-lg sm:text-xl text-neutral-600 dark:text-neutral-300 mb-8 max-w-2xl mx-auto", + []); + + // Handle sidebar state changes useEffect(() => { - // Set layout as unstable when sidebar state changes - setLayoutStable(false); - setShouldShowHighlight(false); + dispatch({ type: "SIDEBAR_CHANGED" }); - // Wait for layout to stabilize after sidebar transition const stabilizeTimer = setTimeout(() => { - setLayoutStable(true); - }, 300); // Wait for sidebar transition (200ms) + buffer + dispatch({ type: "LAYOUT_STABILIZED" }); + }, TIMING.SIDEBAR_TRANSITION); return () => clearTimeout(stabilizeTimer); - }, [state]); + }, [sidebarState]); - // Re-enable highlights after layout stabilizes and component is in view + // Handle highlight visibility based on layout stability and viewport visibility useEffect(() => { - if (layoutStable && isInView) { - const showTimer = setTimeout(() => { - setShouldShowHighlight(true); - }, 100); // Small delay to ensure layout is fully settled - - return () => clearTimeout(showTimer); - } else { - setShouldShowHighlight(false); + if (!layoutStable || !isInView) { + dispatch({ type: "HIDE_HIGHLIGHT" }); + return; } + + const showTimer = setTimeout(() => { + dispatch({ type: "SHOW_HIGHLIGHT" }); + }, TIMING.LAYOUT_SETTLE); + + return () => clearTimeout(showTimer); }, [layoutStable, isInView]); return ( @@ -49,30 +141,14 @@ export function AnimatedEmptyState() { >
-

- +

+ SurfSense

-

- +

+ Let's Start Surfing {" "} through your knowledge base. diff --git a/surfsense_web/components/chat/CodeBlock.tsx b/surfsense_web/components/chat/CodeBlock.tsx index f547400..965b4b0 100644 --- a/surfsense_web/components/chat/CodeBlock.tsx +++ b/surfsense_web/components/chat/CodeBlock.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useMemo, useCallback } from "react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { oneLight, @@ -9,13 +9,66 @@ import { import { Check, Copy } from "lucide-react"; import { useTheme } from "next-themes"; -// Code block component with syntax highlighting and copy functionality -export const CodeBlock = ({ - children, - language, -}: { +// Constants for styling and configuration +const COPY_TIMEOUT = 2000; + +const BASE_CUSTOM_STYLE = { + margin: 0, + borderRadius: "0.375rem", + fontSize: "0.75rem", + lineHeight: "1.5rem", + border: "none", +} as const; + +const LINE_PROPS_STYLE = { + wordBreak: "break-all" as const, + whiteSpace: "pre-wrap" as const, + border: "none", + borderBottom: "none", + paddingLeft: 0, + paddingRight: 0, + margin: "0.25rem 0", +} as const; + +const CODE_TAG_PROPS = { + className: "font-mono", + style: { + border: "none", + background: "var(--syntax-bg)", + }, +} as const; + +// TypeScript interfaces +interface CodeBlockProps { children: string; language: string; +} + +interface LanguageRenderer { + (props: { code: string }): React.JSX.Element; +} + +interface SyntaxStyle { + [key: string]: React.CSSProperties; +} + +// Memoized fallback component for SSR/hydration +const FallbackCodeBlock = React.memo(({ children }: { children: string }) => ( +

+
+      
+        {children}
+      
+    
+
+)); + +FallbackCodeBlock.displayName = "FallbackCodeBlock"; + +// Code block component with syntax highlighting and copy functionality +export const CodeBlock = React.memo(({ + children, + language, }) => { const [copied, setCopied] = useState(false); const { resolvedTheme, theme } = useTheme(); @@ -26,15 +79,62 @@ export const CodeBlock = ({ setMounted(true); }, []); - const handleCopy = async () => { - await navigator.clipboard.writeText(children); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; + // Memoize theme detection + const isDarkTheme = useMemo(() => + mounted && (resolvedTheme === "dark" || theme === "dark"), + [mounted, resolvedTheme, theme] + ); - // Choose theme based on current system/user preference - const isDarkTheme = mounted && (resolvedTheme === "dark" || theme === "dark"); - const syntaxTheme = isDarkTheme ? oneDark : oneLight; + // Memoize syntax theme selection + const syntaxTheme = useMemo(() => + isDarkTheme ? oneDark : oneLight, + [isDarkTheme] + ); + + // Memoize enhanced style with theme-specific modifications + const enhancedStyle = useMemo(() => ({ + ...syntaxTheme, + 'pre[class*="language-"]': { + ...syntaxTheme['pre[class*="language-"]'], + margin: 0, + border: "none", + borderRadius: "0.375rem", + background: "var(--syntax-bg)", + }, + 'code[class*="language-"]': { + ...syntaxTheme['code[class*="language-"]'], + border: "none", + background: "var(--syntax-bg)", + }, + }), [syntaxTheme]); + + // Memoize custom style with background + const customStyle = useMemo(() => ({ + ...BASE_CUSTOM_STYLE, + backgroundColor: "var(--syntax-bg)", + }), []); + + // Memoized copy handler + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(children); + setCopied(true); + const timeoutId = setTimeout(() => setCopied(false), COPY_TIMEOUT); + return () => clearTimeout(timeoutId); + } catch (error) { + console.warn("Failed to copy code to clipboard:", error); + } + }, [children]); + + // Memoized line props with style + const lineProps = useMemo(() => ({ + style: LINE_PROPS_STYLE, + }), []); + + // Early return for non-mounted state + if (!mounted) { + return {children}; + } return (
@@ -43,6 +143,7 @@ export const CodeBlock = ({ onClick={handleCopy} className="p-1.5 rounded-md bg-background/80 hover:bg-background border border-border flex items-center justify-center transition-colors" aria-label="Copy code" + type="button" > {copied ? ( @@ -51,103 +152,43 @@ export const CodeBlock = ({ )}
- {mounted ? ( - - {children} - - ) : ( -
-
-            
-              {children}
-            
-          
-
- )} + + {children} +

); -}; +}); -// Create language renderer function -const createLanguageRenderer = (lang: string) => - ({ code }: { code: string }) => ( +CodeBlock.displayName = "CodeBlock"; + +// Optimized language renderer factory with memoization +const createLanguageRenderer = (lang: string): LanguageRenderer => { + const renderer = ({ code }: { code: string }) => ( {code} ); + renderer.displayName = `LanguageRenderer(${lang})`; + return renderer; +}; -// Define language renderers for common programming languages -export const languageRenderers = { - "javascript": createLanguageRenderer("javascript"), - "typescript": createLanguageRenderer("typescript"), - "python": createLanguageRenderer("python"), - "java": createLanguageRenderer("java"), - "csharp": createLanguageRenderer("csharp"), - "cpp": createLanguageRenderer("cpp"), - "c": createLanguageRenderer("c"), - "php": createLanguageRenderer("php"), - "ruby": createLanguageRenderer("ruby"), - "go": createLanguageRenderer("go"), - "rust": createLanguageRenderer("rust"), - "swift": createLanguageRenderer("swift"), - "kotlin": createLanguageRenderer("kotlin"), - "scala": createLanguageRenderer("scala"), - "sql": createLanguageRenderer("sql"), - "json": createLanguageRenderer("json"), - "xml": createLanguageRenderer("xml"), - "yaml": createLanguageRenderer("yaml"), - "bash": createLanguageRenderer("bash"), - "shell": createLanguageRenderer("shell"), - "powershell": createLanguageRenderer("powershell"), - "dockerfile": createLanguageRenderer("dockerfile"), - "html": createLanguageRenderer("html"), - "css": createLanguageRenderer("css"), - "scss": createLanguageRenderer("scss"), - "less": createLanguageRenderer("less"), - "markdown": createLanguageRenderer("markdown"), - "text": createLanguageRenderer("text"), -}; \ No newline at end of file +// Pre-defined supported languages for better maintainability +const SUPPORTED_LANGUAGES = [ + "javascript", "typescript", "python", "java", "csharp", "cpp", "c", + "php", "ruby", "go", "rust", "swift", "kotlin", "scala", "sql", + "json", "xml", "yaml", "bash", "shell", "powershell", "dockerfile", + "html", "css", "scss", "less", "markdown", "text" +] as const; + +// Generate language renderers efficiently +export const languageRenderers: Record = + Object.fromEntries( + SUPPORTED_LANGUAGES.map(lang => [lang, createLanguageRenderer(lang)]) + ); \ No newline at end of file