"use client"; import "@assistant-ui/react-markdown/styles/dot.css"; import { ArrowDownIcon, ArrowUpIcon, BotIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, CopyIcon, LoaderIcon, PaperclipIcon, PencilIcon, RefreshCwIcon, SquareIcon, ThumbsDownIcon, ThumbsUpIcon, UserIcon, Volume2Icon, } from "lucide-react"; import { ActionBarPrimitive, AuiIf, BranchPickerPrimitive, ComposerPrimitive, MessagePrimitive, ThreadPrimitive, useMessagePartText, } from "@assistant-ui/react"; import { type FC, createContext, useContext, useMemo, memo, useState, useEffect, } from "react"; import { useTheme } from "next-themes"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { ReasoningRoot, ReasoningTrigger, ReasoningContent, ReasoningText, } from "@/components/assistant-ui/reasoning"; import { Source, SourceIcon, SourceTitle, } from "@/components/assistant-ui/sources"; import { type CodeHeaderProps, MarkdownTextPrimitive, unstable_memoizeMarkdownComponents as memoizeMarkdownComponents, useIsMarkdownCodeBlock, type SyntaxHighlighterProps, } from "@assistant-ui/react-markdown"; import remarkGfm from "remark-gfm"; import ShikiHighlighter from "react-shiki"; import { SHIKI_THEME_MAP, DEFAULT_COLORS, type BuilderConfig, type CodeHighlightTheme, type ThemeColor, } from "./types"; import { BORDER_RADIUS_CLASS, MESSAGE_SPACING_CLASS, isLightColor, } from "@/lib/builder-utils"; interface BuilderPreviewContextValue { config: BuilderConfig; isDark: boolean; accentColor: string; } const BuilderPreviewContext = createContext( null, ); function useBuilderPreviewContext() { const context = useContext(BuilderPreviewContext); if (!context) { throw new Error( "useBuilderPreviewContext must be used within BuilderPreviewProvider", ); } return context; } const UserMessageWrapper: FC = () => { const { config } = useBuilderPreviewContext(); return ; }; const AssistantMessageWrapper: FC = () => { const { config } = useBuilderPreviewContext(); return ; }; const PlainText: FC = () => { const { text } = useMessagePartText(); return

{text}

; }; const MarkdownTextWrapper: FC = () => { const { config } = useBuilderPreviewContext(); return ( ); }; interface BuilderPreviewProps { config: BuilderConfig; } // Hook to detect page theme from document.documentElement.classList function usePageTheme() { const { resolvedTheme } = useTheme(); const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); // Return false during SSR/hydration to avoid mismatch, then update on client if (!mounted) return false; return resolvedTheme === "dark"; } export function BuilderPreview({ config }: BuilderPreviewProps) { const { components, styles } = config; // Always follow page theme so user can preview both light and dark variants const isDark = usePageTheme(); // Helper to get color value based on current theme const getColor = ( color: ThemeColor | undefined, fallback: ThemeColor, ): string => { const c = color ?? fallback; return isDark ? c.dark : c.light; }; const { colors } = styles; const accentColor = getColor(colors.accent, DEFAULT_COLORS.accent); // Define CSS variables with theme-aware values const cssVars = { "--aui-thread-max-width": styles.maxWidth, "--aui-accent-color": accentColor, "--aui-background": getColor(colors.background, DEFAULT_COLORS.background), "--aui-foreground": getColor(colors.foreground, DEFAULT_COLORS.foreground), "--aui-muted": getColor(colors.muted, DEFAULT_COLORS.muted), "--aui-muted-foreground": getColor( colors.mutedForeground, DEFAULT_COLORS.mutedForeground, ), "--aui-border": getColor(colors.border, DEFAULT_COLORS.border), "--aui-user-message-background": getColor( colors.userMessage, DEFAULT_COLORS.userMessage, ), "--aui-assistant-message-background": colors.assistantMessage ? getColor(colors.assistantMessage, DEFAULT_COLORS.background) : undefined, "--aui-composer-background": getColor( colors.composer, DEFAULT_COLORS.composer, ), "--aui-user-avatar-background": getColor( colors.userAvatar, DEFAULT_COLORS.userAvatar, ), "--aui-assistant-avatar-background": getColor( colors.assistantAvatar, DEFAULT_COLORS.assistantAvatar, ), "--aui-suggestion-background": getColor( colors.suggestion, DEFAULT_COLORS.suggestion, ), "--aui-suggestion-border": getColor( colors.suggestionBorder, DEFAULT_COLORS.suggestionBorder, ), fontFamily: styles.fontFamily, } as React.CSSProperties; return (
{config.customCSS && ( )} {components.threadWelcome && ( s.thread.isEmpty}> )} {!components.threadWelcome && ( s.thread.isEmpty}>
)} {({ message }) => { if (message.composer.isEditing) return ; if (message.role === "user") return ; return ; }} {components.scrollToBottom && }
); } interface ThreadWelcomeProps { config: BuilderConfig; } const ThreadWelcome: FC = ({ config }) => { const { components, styles } = config; return (

Hello there!

How can I help you today?

{components.suggestions && }
); }; const SUGGESTIONS = [ { title: "What's the weather", label: "in San Francisco?", prompt: "What's the weather in San Francisco?", }, { title: "Explain React hooks", label: "like useState and useEffect", prompt: "Explain React hooks like useState and useEffect", }, ] as const; interface ThreadSuggestionsProps { config: BuilderConfig; } const ThreadSuggestions: FC = ({ config }) => { const { styles } = config; return (
{SUGGESTIONS.map((suggestion, index) => (
))}
); }; interface ComposerProps { config: BuilderConfig; } const Composer: FC = ({ config }) => { const { styles } = config; return ( ); }; interface ComposerActionProps { config: BuilderConfig; } const ComposerAction: FC = ({ config }) => { const { accentColor } = useBuilderPreviewContext(); const { components } = config; return (
{components.attachments ? ( ) : (
)} !s.thread.isRunning}> s.thread.isRunning}>
); }; const ThreadScrollToBottom: FC = () => { return ( ); }; interface UserMessageProps { config: BuilderConfig; } const UserMessage: FC = ({ config }) => { const { components, styles } = config; const isLeftAligned = styles.userMessagePosition === "left"; const messageSpacingClass = MESSAGE_SPACING_CLASS[styles.messageSpacing]; // For left-aligned, use flex layout like before // For right-aligned (default), use grid layout like thread.tsx if (isLeftAligned) { return ( {components.avatar && (
)}
{components.editMessage && (
)}
{components.branchPicker && ( )}
); } // Right-aligned (default) - use grid layout like thread.tsx return ( *)]:col-start-2", styles.animations && "fade-in slide-in-from-bottom-1 animate-in duration-150", )} data-role="user" > {components.avatar && (
)}
{components.editMessage && (
)}
{components.branchPicker && ( )}
); }; const UserActionBar: FC = () => { return ( ); }; interface AssistantMessageProps { config: BuilderConfig; } const AssistantMessage: FC = ({ config }) => { const { components, styles } = config; const messageSpacingClass = MESSAGE_SPACING_CLASS[styles.messageSpacing]; const TextComponent = components.markdown ? MarkdownTextWrapper : PlainText; return (
{components.avatar && (
)}
{components.reasoning && (

Let me analyze this step by step. First, I'll consider the key points of your question...

)}
{({ part }) => { if (part.type === "text") return ; return null; }} {components.loadingIndicator !== "none" && ( thread.isRunning && message.content.length === 0 } >
{components.loadingIndicator === "text" && ( {components.loadingText} )}
)}
{components.sources && (
React Documentation Next.js
)}
{components.branchPicker && }
{components.followUpSuggestions && ( !s.thread.isRunning}> )}
); }; const FollowUpSuggestions: FC = () => { return (
Tell me more Explain differently
); }; interface AssistantActionBarProps { config: BuilderConfig; } const AssistantActionBar: FC = ({ config }) => { const { components } = config; const { actionBar } = components; if ( !actionBar.copy && !actionBar.reload && !actionBar.speak && !actionBar.feedback ) { return null; } return ( {actionBar.copy && ( s.message.isCopied}> !s.message.isCopied}> )} {actionBar.reload && ( )} {actionBar.speak && ( )} {actionBar.feedback && ( <> )} ); }; interface BranchPickerProps { className?: string; } const BranchPicker: FC = ({ className }) => { return ( / ); }; const EditComposer: FC = () => { return (
); }; const MarkdownCodeHeader: FC = ({ language }) => (
{language}
); const MarkdownH1: FC> = ({ className, ...props }) => (

); const MarkdownH2: FC> = ({ className, ...props }) => (

); const MarkdownH3: FC> = ({ className, ...props }) => (

); const MarkdownP: FC> = ({ className, ...props }) => (

); const MarkdownUl: FC> = ({ className, ...props }) => (

    li]:mt-1", className, )} {...props} /> ); const MarkdownOl: FC> = ({ className, ...props }) => (
      li]:mt-1", className, )} {...props} /> ); const MarkdownPre: FC> = ({ className, ...props }) => (
      );
      
      const MarkdownCode: FC> = ({
        className,
        ...props
      }) => {
        const isCodeBlock = useIsMarkdownCodeBlock();
        return (
          
        );
      };
      
      const MarkdownLi: FC> = ({
        className,
        ...props
      }) => 
    1. ; const baseMarkdownComponents = { h1: MarkdownH1, h2: MarkdownH2, h3: MarkdownH3, p: MarkdownP, ul: MarkdownUl, ol: MarkdownOl, li: MarkdownLi, pre: MarkdownPre, code: MarkdownCode, CodeHeader: MarkdownCodeHeader, }; const createSyntaxHighlighter = ( theme: Exclude, ): FC => { const SyntaxHighlighter: FC = ({ code, language, }) => ( {code} ); return SyntaxHighlighter; }; interface ConfigurableMarkdownTextProps { codeHighlightTheme: CodeHighlightTheme; } const ConfigurableMarkdownText: FC = memo( ({ codeHighlightTheme }) => { const components = useMemo(() => { if (codeHighlightTheme === "none") { return memoizeMarkdownComponents(baseMarkdownComponents); } return memoizeMarkdownComponents({ ...baseMarkdownComponents, SyntaxHighlighter: createSyntaxHighlighter(codeHighlightTheme), }); }, [codeHighlightTheme]); return ( ); }, ); ConfigurableMarkdownText.displayName = "ConfigurableMarkdownText";