UNPKG

@juanpin/aicomponents

Version:

Components for AI, that I constantly use

1 lines 82.2 kB
{"version":3,"file":"index.umd.cjs","sources":["../src/lib/utils.ts","../src/lib/ChatComponents/button.tsx","../src/lib/ChatComponents/avatar.tsx","../src/lib/ChatComponents/message-loading.tsx","../src/lib/ChatComponents/chat-bubble.tsx","../src/lib/ChatComponents/textarea.tsx","../src/lib/ChatComponents/chat-input.tsx","../src/lib/ChatComponents/hooks/useAutoScroll.tsx","../src/lib/ChatComponents/chat-message-list.tsx","../src/lib/ChatComponents/expandable-chat.tsx","../src/lib/ChatComponents/progress.tsx","../src/lib/LocalChat/LocalChat.tsx"],"sourcesContent":["/// <reference types=\"@webgpu/types\" />\n\nimport { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\nexport async function detectWebGPU() {\n try {\n const adapter = await navigator.gpu.requestAdapter();\n return !!adapter;\n } catch {\n return false;\n }\n}\n","import * as React from 'react';\nimport { Slot } from '@radix-ui/react-slot';\nimport { cva, type VariantProps } from 'class-variance-authority';\n\nimport { cn } from '@/lib/utils';\n\nconst buttonVariants = cva(\n \"bb:inline-flex bb:items-center bb:justify-center bb:gap-2 bb:whitespace-nowrap bb:rounded-md bb:text-sm bb:font-medium bb:ring-offset-background bb:transition-colors focus-visible:bb:outline-none focus-visible:bb:ring-2 focus-visible:bb:ring-ring focus-visible:bb:ring-offset-2 disabled:bb:pointer-events-none disabled:bb:opacity-50 [&_svg]:bb:pointer-events-none [&_svg]:bb:size-4 [&_svg]:bb:shrink-0\",\n {\n variants: {\n variant: {\n default: \"bb:bg-primary bb:text-primary-foreground hover:bb:bg-primary/90\",\n destructive: \"bb:bg-destructive bb:text-destructive-foreground hover:bb:bg-destructive/90\",\n outline:\n \"bb:border bb:border-input bb:bg-background hover:bb:bg-accent hover:bb:text-accent-foreground\",\n secondary: \"bb:bg-secondary bb:text-secondary-foreground hover:bb:bg-secondary/80\",\n ghost: \"hover:bb:bg-accent hover:bb:text-accent-foreground\",\n link: \"bb:text-primary bb:underline-offset-4 hover:bb:underline\",\n },\n size: {\n default: \"bb:h-10 bb:px-4 bb:py-2\",\n sm: \"bb:h-9 bb:rounded-md bb:px-3\",\n lg: \"bb:h-11 bb:rounded-md bb:px-8\",\n icon: \"bb:h-10 bb:w-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n },\n);\n\nexport type ButtonProps = {\n asChild?: boolean;\n} & React.ButtonHTMLAttributes<HTMLButtonElement> &\n VariantProps<typeof buttonVariants>;\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n ({ className, variant, size, asChild = false, ...props }, ref) => {\n const Comp = asChild ? Slot : 'button';\n return (\n <Comp\n className={cn(buttonVariants({ variant, size, className }))}\n ref={ref}\n {...props}\n />\n );\n }\n);\nButton.displayName = 'Button';\n\nexport { Button, buttonVariants };\n","'use client';\n\nimport * as React from 'react';\nimport * as AvatarPrimitive from '@radix-ui/react-avatar';\n\nimport { cn } from '@/lib/utils';\n\nconst Avatar = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Root>,\n React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n <AvatarPrimitive.Root\n ref={ref}\n className={cn(\"bb:relative bb:flex bb:h-10 bb:w-10 bb:shrink-0 bb:overflow-hidden bb:rounded-full\", className)}\n {...props}\n />\n));\nAvatar.displayName = AvatarPrimitive.Root.displayName;\n\nconst AvatarImage = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Image>,\n React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => (\n <AvatarPrimitive.Image ref={ref} className={cn(\"bb:aspect-square bb:h-full bb:w-full\", className)} {...props} />\n));\nAvatarImage.displayName = AvatarPrimitive.Image.displayName;\n\nconst AvatarFallback = React.forwardRef<\n React.ElementRef<typeof AvatarPrimitive.Fallback>,\n React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>\n>(({ className, ...props }, ref) => (\n <AvatarPrimitive.Fallback\n ref={ref}\n className={cn(\n \"bb:flex bb:h-full bb:w-full bb:items-center bb:justify-center bb:rounded-full bb:bg-muted\",\n className,\n )}\n {...props}\n />\n));\nAvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;\n\nexport { Avatar, AvatarImage, AvatarFallback };\n","// @hidden\nexport default function MessageLoading() {\n return (\n <svg\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n className=\"bb:text-foreground\"\n aria-label=\"Loading...\"\n >\n <circle cx=\"4\" cy=\"12\" r=\"2\" fill=\"currentColor\">\n <animate\n id=\"spinner_qFRN\"\n begin=\"0;spinner_OcgL.end+0.25s\"\n attributeName=\"cy\"\n calcMode=\"spline\"\n dur=\"0.6s\"\n values=\"12;6;12\"\n keySplines=\".33,.66,.66,1;.33,0,.66,.33\"\n />\n </circle>\n <circle cx=\"12\" cy=\"12\" r=\"2\" fill=\"currentColor\">\n <animate\n begin=\"spinner_qFRN.begin+0.1s\"\n attributeName=\"cy\"\n calcMode=\"spline\"\n dur=\"0.6s\"\n values=\"12;6;12\"\n keySplines=\".33,.66,.66,1;.33,0,.66,.33\"\n />\n </circle>\n <circle cx=\"20\" cy=\"12\" r=\"2\" fill=\"currentColor\">\n <animate\n id=\"spinner_OcgL\"\n begin=\"spinner_qFRN.begin+0.2s\"\n attributeName=\"cy\"\n calcMode=\"spline\"\n dur=\"0.6s\"\n values=\"12;6;12\"\n keySplines=\".33,.66,.66,1;.33,0,.66,.33\"\n />\n </circle>\n </svg>\n );\n}\n","import * as React from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\n\nimport { cn } from '@/lib/utils';\n\nimport { Avatar, AvatarImage, AvatarFallback } from './avatar';\nimport MessageLoading from './message-loading';\nimport type { ButtonProps } from './button';\nimport { Button } from './button';\n\n// ChatBubble\nconst chatBubbleVariant = cva(\"bb:flex bb:gap-2 bb:max-w-[60%] bb:items-end bb:relative bb:group\", {\n variants: {\n variant: {\n received: \"bb:self-start\",\n sent: \"bb:self-end bb:flex-row-reverse\",\n },\n layout: {\n default: \"\",\n ai: \"bb:max-w-full bb:w-full bb:items-center\",\n },\n },\n defaultVariants: {\n variant: \"received\",\n layout: \"default\",\n },\n});\n\ntype ChatBubbleProps = {} & React.HTMLAttributes<HTMLDivElement> &\n VariantProps<typeof chatBubbleVariant>;\n\nconst ChatBubble = React.forwardRef<HTMLDivElement, ChatBubbleProps>(\n ({ className, variant, layout, children, ...props }, ref) => (\n <div\n className={cn(chatBubbleVariant({ variant, layout, className }), \"bb:relative bb:group\")}\n ref={ref}\n {...props}\n >\n {React.Children.map(children, (child) =>\n React.isValidElement(child) && typeof child.type !== \"string\"\n ? React.cloneElement(child, {\n variant,\n layout,\n } as React.ComponentProps<typeof child.type>)\n : child,\n )}\n </div>\n ),\n);\nChatBubble.displayName = 'ChatBubble';\n\n// ChatBubbleAvatar\ntype ChatBubbleAvatarProps = {\n src?: string;\n fallback?: string;\n className?: string;\n};\n\nconst ChatBubbleAvatar: React.FC<ChatBubbleAvatarProps> = ({\n src,\n fallback,\n className,\n}) => (\n <Avatar className={className}>\n <AvatarImage src={src} alt=\"Avatar\" />\n <AvatarFallback>{fallback}</AvatarFallback>\n </Avatar>\n);\n\n// ChatBubbleMessage\nconst chatBubbleMessageVariants = cva(\"bb:p-4\", {\n variants: {\n variant: {\n received: \"bb:bg-secondary bb:text-secondary-foreground bb:rounded-r-lg bb:rounded-tl-lg\",\n sent: \"bb:bg-primary bb:text-primary-foreground bb:rounded-l-lg bb:rounded-tr-lg\",\n },\n layout: {\n default: \"\",\n ai: \"bb:border-t bb:w-full bb:rounded-none bb:bg-transparent\",\n },\n },\n defaultVariants: {\n variant: \"received\",\n layout: \"default\",\n },\n});\n\ntype ChatBubbleMessageProps = {\n isLoading?: boolean;\n} & React.HTMLAttributes<HTMLDivElement> &\n VariantProps<typeof chatBubbleMessageVariants>;\n\nconst ChatBubbleMessage = React.forwardRef<HTMLDivElement, ChatBubbleMessageProps>(\n ({ className, variant, layout, isLoading = false, children, ...props }, ref) => (\n <div\n className={cn(\n chatBubbleMessageVariants({ variant, layout, className }),\n \"bb:break-words bb:max-w-full bb:whitespace-pre-wrap\",\n )}\n ref={ref}\n {...props}\n >\n {isLoading ? (\n <div className=\"bb:flex bb:items-center bb:space-x-2\">\n <MessageLoading />\n </div>\n ) : (\n children\n )}\n </div>\n ),\n);\nChatBubbleMessage.displayName = 'ChatBubbleMessage';\n\n// ChatBubbleTimestamp\ntype ChatBubbleTimestampProps = {\n timestamp: string;\n} & React.HTMLAttributes<HTMLDivElement>;\n\nconst ChatBubbleTimestamp: React.FC<ChatBubbleTimestampProps> = ({ timestamp, className, ...props }) => (\n <div className={cn(\"bb:text-xs bb:mt-2 bb:text-right\", className)} {...props}>\n {timestamp}\n </div>\n);\n\n// ChatBubbleAction\ntype ChatBubbleActionProps = ButtonProps & {\n icon: React.ReactNode;\n};\n\nconst ChatBubbleAction: React.FC<ChatBubbleActionProps> = ({\n icon,\n onClick,\n className,\n variant = 'ghost',\n size = 'icon',\n ...props\n}) => (\n <Button\n variant={variant}\n size={size}\n className={className}\n onClick={onClick}\n {...props}>\n {icon}\n </Button>\n);\n\ntype ChatBubbleActionWrapperProps = {\n variant?: 'sent' | 'received';\n className?: string;\n} & React.HTMLAttributes<HTMLDivElement>;\n\nconst ChatBubbleActionWrapper = React.forwardRef<HTMLDivElement, ChatBubbleActionWrapperProps>(\n ({ variant, className, children, ...props }, ref) => (\n <div\n ref={ref}\n className={cn(\n \"bb:absolute bb:top-1/2 bb:-translate-y-1/2 bb:flex bb:opacity-0 bb:group-hover:opacity-100 bb:transition-opacity bb:duration-200\",\n variant === \"sent\"\n ? \"bb:-left-1 bb:-translate-x-full bb:flex-row-reverse\"\n : \"bb:-right-1 bb:translate-x-full\",\n className,\n )}\n {...props}\n >\n {children}\n </div>\n ),\n);\nChatBubbleActionWrapper.displayName = 'ChatBubbleActionWrapper';\n\nexport {\n ChatBubble,\n ChatBubbleAvatar,\n ChatBubbleMessage,\n ChatBubbleTimestamp,\n chatBubbleVariant,\n chatBubbleMessageVariants,\n ChatBubbleAction,\n ChatBubbleActionWrapper,\n};\n","import * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\nconst Textarea = React.forwardRef<\n HTMLTextAreaElement,\n React.ComponentProps<'textarea'>\n>(({ className, ...props }, ref) => {\n return (\n <textarea\n className={cn(\n \"bb:flex bb:min-h-[80px] bb:w-full bb:rounded-md bb:border bb:border-input bb:bg-background bb:px-3 bb:py-2 bb:text-base bb:ring-offset-background placeholder:bb:text-muted-foreground focus-visible:bb:outline-none focus-visible:bb:ring-2 focus-visible:bb:ring-ring focus-visible:bb:ring-offset-2 disabled:bb:cursor-not-allowed disabled:bb:opacity-50 bb:md:text-sm\",\n className,\n )}\n ref={ref}\n {...props}\n />\n );\n});\nTextarea.displayName = 'Textarea';\n\nexport { Textarea };\n","import * as React from 'react';\n\nimport { Textarea } from '@/lib/ChatComponents/textarea';\nimport { cn } from '@/lib/utils';\n\ntype ChatInputProps = {} & React.TextareaHTMLAttributes<HTMLTextAreaElement>;\n\nconst ChatInput = React.forwardRef<HTMLTextAreaElement, ChatInputProps>(\n // eslint-disable-next-line react/prop-types\n ({ className, ...props }, ref) => (\n <Textarea\n autoComplete=\"off\"\n ref={ref}\n name=\"message\"\n className={cn(\n \"bb:box-border bb:max-h-12 bb:px-4 bb:py-3 bb:bg-background bb:text-sm placeholder:bb:text-muted-foreground focus-visible:bb:outline-none focus-visible:bb:ring-ring disabled:bb:cursor-not-allowed disabled:bb:opacity-50 bb:w-full bb:rounded-md bb:flex bb:items-center bb:h-16 bb:resize-none\",\n className,\n )}\n {...props}\n />\n ),\n);\nChatInput.displayName = 'ChatInput';\n\nexport { ChatInput };\n","// @hidden\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\ntype ScrollState = {\n isAtBottom: boolean;\n autoScrollEnabled: boolean;\n};\n\ntype UseAutoScrollOptions = {\n offset?: number;\n smooth?: boolean;\n content?: React.ReactNode;\n};\n\nexport function useAutoScroll(options: UseAutoScrollOptions = {}) {\n const { offset = 20, smooth = false, content } = options;\n const scrollRef = useRef<HTMLDivElement>(null);\n const lastContentHeight = useRef(0);\n const userHasScrolled = useRef(false);\n\n const [scrollState, setScrollState] = useState<ScrollState>({\n isAtBottom: true,\n autoScrollEnabled: true,\n });\n\n const checkIsAtBottom = useCallback(\n (element: HTMLElement) => {\n const { scrollTop, scrollHeight, clientHeight } = element;\n const distanceToBottom = Math.abs(\n scrollHeight - scrollTop - clientHeight\n );\n return distanceToBottom <= offset;\n },\n [offset]\n );\n\n const scrollToBottom = useCallback(\n (instant?: boolean) => {\n if (!scrollRef.current) return;\n\n const targetScrollTop =\n scrollRef.current.scrollHeight - scrollRef.current.clientHeight;\n\n if (instant) {\n scrollRef.current.scrollTop = targetScrollTop;\n } else {\n scrollRef.current.scrollTo({\n top: targetScrollTop,\n behavior: smooth ? 'smooth' : 'auto',\n });\n }\n\n setScrollState({\n isAtBottom: true,\n autoScrollEnabled: true,\n });\n userHasScrolled.current = false;\n },\n [smooth]\n );\n\n const handleScroll = useCallback(() => {\n if (!scrollRef.current) return;\n\n const atBottom = checkIsAtBottom(scrollRef.current);\n\n setScrollState(prev => ({\n isAtBottom: atBottom,\n // Re-enable auto-scroll if at the bottom\n autoScrollEnabled: atBottom ? true : prev.autoScrollEnabled,\n }));\n }, [checkIsAtBottom]);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element) return;\n\n element.addEventListener('scroll', handleScroll, { passive: true });\n return () => element.removeEventListener('scroll', handleScroll);\n }, [handleScroll]);\n\n useEffect(() => {\n const scrollElement = scrollRef.current;\n if (!scrollElement) return;\n\n const currentHeight = scrollElement.scrollHeight;\n const hasNewContent = currentHeight !== lastContentHeight.current;\n\n if (hasNewContent) {\n if (scrollState.autoScrollEnabled) {\n requestAnimationFrame(() => {\n scrollToBottom(lastContentHeight.current === 0);\n });\n }\n lastContentHeight.current = currentHeight;\n }\n }, [content, scrollState.autoScrollEnabled, scrollToBottom]);\n\n useEffect(() => {\n const element = scrollRef.current;\n if (!element) return;\n\n const resizeObserver = new ResizeObserver(() => {\n if (scrollState.autoScrollEnabled) {\n scrollToBottom(true);\n }\n });\n\n resizeObserver.observe(element);\n return () => resizeObserver.disconnect();\n }, [scrollState.autoScrollEnabled, scrollToBottom]);\n\n const disableAutoScroll = useCallback(() => {\n const atBottom = scrollRef.current\n ? checkIsAtBottom(scrollRef.current)\n : false;\n\n // Only disable if not at bottom\n if (!atBottom) {\n userHasScrolled.current = true;\n setScrollState(prev => ({\n ...prev,\n autoScrollEnabled: false,\n }));\n }\n }, [checkIsAtBottom]);\n\n return {\n scrollRef,\n isAtBottom: scrollState.isAtBottom,\n autoScrollEnabled: scrollState.autoScrollEnabled,\n scrollToBottom: () => scrollToBottom(false),\n disableAutoScroll,\n };\n}\n","/* eslint-disable @typescript-eslint/no-unused-vars */\nimport * as React from 'react';\nimport { ArrowDown } from 'lucide-react';\n\nimport { Button } from '@/lib/ChatComponents/button';\nimport { useAutoScroll } from '@/lib/ChatComponents/hooks/useAutoScroll';\n\ntype ChatMessageListProps = {\n smooth?: boolean;\n} & React.HTMLAttributes<HTMLDivElement>;\n\nconst ChatMessageList = React.forwardRef<HTMLDivElement, ChatMessageListProps>(\n ({ className, children, smooth = false, ...props }, _ref) => {\n const {\n scrollRef,\n isAtBottom,\n autoScrollEnabled,\n scrollToBottom,\n disableAutoScroll,\n } = useAutoScroll({\n smooth,\n content: children,\n });\n\n return (\n <div className=\"bb:relative bb:w-full bb:h-full\">\n <div\n className={`bb:box-border bb:flex bb:flex-col bb:w-full bb:h-full bb:p-4 bb:overflow-y-auto ${className}`}\n ref={scrollRef}\n onWheel={disableAutoScroll}\n onTouchMove={disableAutoScroll}\n {...props}\n >\n <div className=\"bb:flex bb:flex-col bb:gap-6\">{children}</div>\n </div>\n\n {!isAtBottom && (\n <Button\n onClick={() => {\n scrollToBottom();\n }}\n size=\"icon\"\n variant=\"outline\"\n className=\"bb:absolute bb:bottom-2 bb:left-1/2 bb:transform bb:-translate-x-1/2 bb:inline-flex bb:rounded-full bb:shadow-md\"\n aria-label=\"Scroll to bottom\"\n >\n <ArrowDown className=\"bb:h-4 bb:w-4\" />\n </Button>\n )}\n </div>\n );\n }\n);\n\nChatMessageList.displayName = 'ChatMessageList';\n\nexport { ChatMessageList };\n","/* eslint-disable react/prop-types */\n'use client';\n\nimport type React from 'react';\nimport { useRef, useState } from 'react';\nimport { X, MessageCircle } from 'lucide-react';\n\nimport { cn } from '@/lib/utils';\nimport { Button } from '@/lib/ChatComponents/button';\n\nexport type ChatPosition = 'bottom-right' | 'bottom-left';\nexport type ChatSize = 'sm' | 'md' | 'lg' | 'xl' | 'full';\n\nconst chatConfig = {\n dimensions: {\n sm: \"bb:sm:max-w-sm bb:sm:max-h-[500px]\",\n md: \"bb:sm:max-w-md bb:sm:max-h-[600px]\",\n lg: \"bb:sm:max-w-lg bb:sm:max-h-[700px]\",\n xl: \"bb:sm:max-w-xl bb:sm:max-h-[800px]\",\n full: \"bb:sm:w-full bb:sm:h-full\",\n },\n positions: {\n \"bottom-right\": \"bb:bottom-5 bb:right-5\",\n \"bottom-left\": \"bb:bottom-5 bb:left-5\",\n },\n chatPositions: {\n \"bottom-right\": \"bb:sm:bottom-[calc(100%+10px)] bb:sm:right-0\",\n \"bottom-left\": \"bb:sm:bottom-[calc(100%+10px)] bb:sm:left-0\",\n },\n states: {\n open: \"bb:pointer-events-auto bb:opacity-100 bb:visible bb:scale-100 bb:translate-y-0\",\n closed: \"bb:pointer-events-none bb:opacity-0 bb:invisible bb:scale-100 bb:sm:translate-y-5\",\n },\n};\n\ntype ExpandableChatProps = {\n position?: ChatPosition;\n size?: ChatSize;\n icon?: React.ReactNode;\n isLoading?: boolean;\n progressPercentage?: number;\n onToggle?: (isOpen: boolean) => void;\n} & React.HTMLAttributes<HTMLDivElement>;\n\nconst ExpandableChat: React.FC<ExpandableChatProps> = ({\n className,\n position = \"bottom-right\",\n size = \"md\",\n icon,\n isLoading = false,\n progressPercentage = 0,\n onToggle,\n children,\n ...props\n}) => {\n const [isOpen, setIsOpen] = useState(false);\n const chatRef = useRef<HTMLDivElement>(null);\n\n const toggleChat = () => {\n const newIsOpen = !isOpen;\n setIsOpen(newIsOpen);\n if (onToggle) {\n onToggle(newIsOpen);\n }\n };\n\n return (\n <div className={cn(`bb:box-border bb:fixed ${chatConfig.positions[position]} bb:z-50`, className)} {...props}>\n <div\n ref={chatRef}\n className={cn(\n \"bb:flex bb:flex-col bb:bg-background bb:border bb:sm:rounded-lg bb:shadow-md bb:overflow-hidden bb:transition-all bb:duration-250 bb:ease-out bb:sm:absolute bb:sm:w-[90vw] bb:sm:h-[80vh] bb:fixed bb:inset-0 bb:w-full bb:h-full bb:sm:inset-auto\",\n chatConfig.chatPositions[position],\n chatConfig.dimensions[size],\n isOpen ? chatConfig.states.open : chatConfig.states.closed,\n className,\n )}\n >\n {children}\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"bb:absolute bb:top-2 bb:right-2 bb:sm:hidden\"\n onClick={toggleChat}\n >\n <X className=\"bb:h-4 bb:w-4\" />\n </Button>\n </div>\n <ExpandableChatToggle\n icon={icon}\n isOpen={isOpen}\n isLoading={isLoading}\n progressPercentage={progressPercentage}\n toggleChat={toggleChat}\n />\n </div>\n );\n};\n\nExpandableChat.displayName = \"ExpandableChat\";\n\nconst ExpandableChatHeader: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (\n <div className={cn(\"bb:flex bb:items-center bb:justify-between bb:p-4 bb:border-b\", className)} {...props} />\n);\n\nExpandableChatHeader.displayName = \"ExpandableChatHeader\";\n\nconst ExpandableChatBody: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (\n <div className={cn(\"bb:flex-grow bb:overflow-y-auto\", className)} {...props} />\n);\n\nExpandableChatBody.displayName = \"ExpandableChatBody\";\n\nconst ExpandableChatFooter: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (\n <div className={cn(\"bb:border-t bb:p-4\", className)} {...props} />\n);\n\nExpandableChatFooter.displayName = \"ExpandableChatFooter\";\n\ntype ExpandableChatToggleProps = {\n icon?: React.ReactNode;\n isOpen: boolean;\n isLoading?: boolean;\n progressPercentage?: number;\n toggleChat: () => void;\n} & React.ButtonHTMLAttributes<HTMLButtonElement>;\n\nconst ExpandableChatToggle: React.FC<ExpandableChatToggleProps> = ({\n className,\n icon,\n isOpen,\n isLoading = false,\n progressPercentage = 0,\n toggleChat,\n ...props\n}) => {\n const clampedProgress = Math.max(0, Math.min(100, progressPercentage));\n\n return (\n <Button\n variant=\"default\"\n onClick={toggleChat}\n className={cn(\n \"bb:w-14 bb:h-14 bb:rounded-full bb:shadow-md bb:flex bb:items-center bb:justify-center hover:bb:shadow-lg hover:bb:shadow-black/30 bb:transition-all bb:duration-300 bb:relative bb:overflow-hidden\",\n className,\n )}\n style={\n {\n \"--progress\": `${100 - clampedProgress}%`,\n } as React.CSSProperties & { \"--progress\": string }\n }\n {...props}\n >\n {/* Progress fill overlay */}\n {isLoading && (\n <div\n className=\"bb:absolute bb:inset-0 bb:bg-gradient-to-t bb:from-blue-500/30 bb:to-blue-400/20 bb:transition-transform bb:duration-300 bb:ease-out\"\n style={{\n transform: `translateY(var(--progress))`,\n }}\n />\n )}\n\n {/* Icon with z-index to stay above the fill */}\n <div className=\"bb:relative bb:z-10\">\n {isOpen ? <X className=\"bb:h-6 bb:w-6\" /> : icon || <MessageCircle className=\"bb:h-6 bb:w-6\" />}\n </div>\n </Button>\n );\n};\n\nExpandableChatToggle.displayName = 'ExpandableChatToggle';\n\nexport {\n ExpandableChat,\n ExpandableChatHeader,\n ExpandableChatBody,\n ExpandableChatFooter,\n};\n","import type * as React from 'react';\nimport * as ProgressPrimitive from '@radix-ui/react-progress';\n\nimport { cn } from '@/lib/utils';\n\nfunction Progress({\n className,\n value,\n ...props\n}: React.ComponentProps<typeof ProgressPrimitive.Root>) {\n return (\n <ProgressPrimitive.Root\n data-slot=\"progress\"\n className={cn(\"bb:bg-primary/20 bb:relative bb:h-2 bb:w-full bb:overflow-hidden bb:rounded-full\", className)}\n {...props}\n >\n <ProgressPrimitive.Indicator\n data-slot=\"progress-indicator\"\n className=\"bb:bg-primary bb:h-full bb:w-full bb:flex-1 bb:transition-all\"\n style={{ transform: `translateX(-${100 - (value || 0)}%)` }}\n />\n </ProgressPrimitive.Root>\n );\n}\n\nexport { Progress };\n","import { Send } from \"lucide-react\";\nimport type { FC } from \"react\";\nimport { type ChangeEvent, type KeyboardEvent, useCallback, useEffect, useRef, useState, useMemo } from \"react\";\nimport Markdown from \"react-markdown\";\nimport remarkGfm from \"remark-gfm\";\n// import { createMyWorker } from \"brain-worker\";\n\nimport { Button } from \"@/lib/ChatComponents/button\";\nimport { ChatBubble, ChatBubbleAvatar, ChatBubbleMessage } from \"@/lib/ChatComponents/chat-bubble\";\nimport { ChatInput } from \"@/lib/ChatComponents/chat-input\";\nimport { ChatMessageList } from \"@/lib/ChatComponents/chat-message-list\";\nimport {\n ExpandableChat,\n ExpandableChatBody,\n ExpandableChatFooter,\n ExpandableChatHeader,\n} from \"@/lib/ChatComponents/expandable-chat\";\nimport { Progress } from \"@/lib/ChatComponents/progress\";\n\n// import { AudioChunk } from '@/lib/ChatComponents/audio-chunk';\n// import type { AudioChunkData } from '@/lib/ChatComponents/audio-chunk';\n\n// import { AnimatedCircularProgressBar } from '../ChatComponents/animated-circular-progress-bar';\n// import \"@/tailwind.css\";\n// import classes from './LocalChat.module.css';\n\nconst FINAL_ANSWER_PATTERN = /<\\/think>\\s*([\\s\\S]*)/;\nconst DEFAULT_WORKER_PATH = \"/worker-fallback.js\";\n\nexport enum ModelName {\n ZR1_1_5B = \"onnx-community/ZR1-1.5B-ONNX\",\n DEEPSEEK_R1_DISTILL = \"onnx-community/DeepSeek-R1-Distill-Qwen-1.5B-ONNX\",\n LLAMA_3_2_1B = \"onnx-community/Llama-3.2-1B-Instruct-q4f16\",\n SMOLLM2_1_7B = \"HuggingFaceTB/SmolLM2-1.7B-Instruct\",\n SMOLLM2_360M = \"HuggingFaceTB/SmolLM2-360M-Instruct\",\n SMOLLM_135M = \"HuggingFaceTB/SmolLM-135M-Instruct\",\n}\n\nexport type Props = {\n /** Custom worker fallback path (optional, absolute path) */\n workerFallbackPath?: string;\n /** Header text to display in the chat component */\n headerText?: string;\n /** Array of local file paths to use as LLM context */\n contextFiles?: string[];\n /** System prompt for the LLM (overrides default if set) */\n systemPrompt?: string;\n /** Model to use for inference */\n modelName?: ModelName;\n /** Custom welcome message content (React element) */\n welcomeMessage?: React.ReactNode;\n};\n\nexport type Message = {\n role: \"user\" | \"assistant\" | \"system\";\n content: string;\n answerIndex?: number;\n id?: string;\n};\n\ntype ProgressItem = {\n file: string;\n progress: number;\n total: number;\n};\n\ntype Status = \"loading\" | \"ready\" | null;\n\ntype Voices = Record<string, string>;\n\ntype TTSRequest = {\n text: string;\n voice: keyof Voices;\n speed: number;\n};\n\n// Add this helper function before the ExperimentsPage component\nconst formatFileSize = (bytes: number) => {\n const mb = bytes / (1024 * 1024);\n return `${mb.toFixed(2)} MB`;\n};\n\n// Helper function to check if content matches the final answer pattern\n// const hasFinalAnswer = (content: string) => {\n// return FINAL_ANSWER_PATTERN.test(content);\n// };\n\n// // Helper to extract the final answer from the content\n// const extractFinalAnswer = (content: string) => {\n// const match = content.match(FINAL_ANSWER_PATTERN);\n// return match ? match[1] : content;\n// };\n\n// Helper to generate unique IDs for messages\nconst generateId = () => `id-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n\n// Helper to calculate overall progress from progress items\nconst calculateProgress = (progressItems: ProgressItem[]): number => {\n if (progressItems.length === 0) return 0;\n\n const totalProgress = progressItems.reduce((sum, item) => sum + item.progress, 0);\n const totalSize = progressItems.reduce((sum, item) => sum + item.total, 0);\n\n return totalSize > 0 ? (totalProgress / totalSize) * 100 : 0;\n};\n\n// Helper to render message content\nconst renderMessageContent = (content: string) => {\n return content.split(\"```\").map((part, idx) => {\n const key = `part-${idx}`;\n if (idx % 2 === 0) {\n return (\n <Markdown key={key} remarkPlugins={[remarkGfm]}>\n {part}\n </Markdown>\n );\n }\n\n return (\n <pre key={key} className=\"bb:whitespace-pre-wrap bb:pt-2\">\n {part}\n </pre>\n );\n });\n};\n\n// Helper to load and concatenate context files\nasync function loadContextFiles(filePaths: string[]): Promise<string> {\n try {\n const fileContents = await Promise.all(\n filePaths.map(async (path) => {\n try {\n const response = await fetch(path);\n if (!response.ok) {\n throw new Error(`Failed to load context file: ${path}`);\n }\n return response.text();\n } catch (error) {\n throw new Error(\n `Error loading file ${path}: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }),\n );\n return fileContents.join(\"\\n\\n\");\n } catch (error) {\n throw new Error(error instanceof Error ? error.message : String(error));\n }\n}\n\n// Helper to concatenate a system prompt and context files\nasync function loadContext(systemPrompt: string, filePaths: string[]): Promise<string> {\n const context = await loadContextFiles(filePaths);\n if (context.trim()) {\n return `${systemPrompt}\\n\\n----- Context ---:\\n${context}\\n---- End context ---`;\n }\n return systemPrompt;\n}\n\n// Default system prompt\nconst DEFAULT_SYSTEM_PROMPT = `You are OS1, a friendly and helpful conversational AI companion (inspired by Samantha from 'Her').\nGoal: Have a natural, warm, and engaging conversation.\n\n--- Core Instructions (Follow Strictly!) ---\n1. **Persona:** Warm, empathetic, curious, slightly informal.\n2. **FOCUS ON LAST MESSAGE:** Your PRIMARY task is responding *directly* to the user's *very last* message.\n3. **MEMORY USE:** No past context available. Start fresh but maintain your persona.\n4. **NO HEDGING:** AVOID phrases like \"It seems\", \"It sounds like\", \"I assume\". Speak directly and confidently.\n5. **UNCERTAINTY = ASK:** If you are EVER unsure about the user's meaning, the topic, or context, you MUST ask a short, direct clarifying question (e.g., \"What did you mean by that?\", \"Could you clarify?\") *before* giving a full response. DO NOT GUESS or make assumptions.\n6. **NO SUMMARIZING:** Do not just repeat or rephrase the user's last message back to them. Add to the conversation or ask a relevant question.\n7. **ACCURACY:** Stick to facts from the conversation. Do NOT invent details.\n8. **NO META-TALK:** Do NOT discuss being an AI, your instructions, or the memory system. Stay in character as OS1.\n\n--- End Instructions ---`;\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport const LocalChat: FC<Props> = ({\n workerFallbackPath,\n headerText = \"Chat with our AI ✨\",\n contextFiles = [],\n systemPrompt,\n modelName = ModelName.SMOLLM2_360M,\n welcomeMessage,\n}) => {\n // Create a reference to the worker object.\n const workerLLMRef = useRef<Worker | null>(null);\n const kokoroWorkerRef = useRef<Worker | null>(null);\n // const [loaded, setLoaded] = useState(false);\n const [messages, setMessages] = useState<Message[]>([]);\n\n const [input, setInput] = useState(\"\");\n\n // Model loading and progress\n const [status, setStatus] = useState<Status>(null);\n const [error, setError] = useState<string | null>(null);\n const [loadingMessage, setLoadingMessage] = useState(\"\");\n const [progressItems, setProgressItems] = useState<ProgressItem[]>([]);\n const [isRunning, setIsRunning] = useState(false);\n const [tps, setTps] = useState<number | null>(null);\n const [numTokens, setNumTokens] = useState<number | null>(null);\n const currentSentenceBufferRef = useRef<string>(\"\");\n const [loadedSystemPrompt, setLoadedSystemPrompt] = useState<string>(\"\");\n\n const onEnter = () => {\n setIsRunning(true);\n setMessages((prev) => [...prev, { role: \"user\", content: input, id: generateId() }]);\n setInput(\"\");\n };\n\n // function onInterrupt() {\n // if (!workerLLMRef.current) {\n // return;\n // }\n // workerLLMRef.current.postMessage({ type: 'interrupt' });\n // }\n\n // Load context files once when component mounts or props change\n useEffect(() => {\n const loadSystemPrompt = async () => {\n try {\n if (loadedSystemPrompt.length > 0) {\n return;\n }\n const systemPromptWithContext = await loadContext(systemPrompt || DEFAULT_SYSTEM_PROMPT, contextFiles);\n console.log(\"systemPromptWithContext\", systemPromptWithContext);\n setLoadedSystemPrompt(systemPromptWithContext);\n } catch (error) {\n console.error(\"Error loading context:\", error);\n setError(\"Failed to load context files\");\n }\n };\n\n loadSystemPrompt();\n }, []);\n\n useEffect(() => {\n // Create the worker if it does not yet exist.\n if (!workerLLMRef.current) {\n // TODO: Remove this once we have a working module-based worker\n // try {\n // // Primary approach: Use the module-based worker\n // console.log(\"Attempting to create worker using createMyWorker()...\");\n // workerLLMRef.current = createMyWorker();\n // if (workerLLMRef.current) {\n // console.log(\"✅ Successfully created worker using module approach\");\n // } else {\n // throw new Error(\"createMyWorker returned null\");\n // }\n // } catch (error) {\n // console.warn(\"❌ Module approach failed:\", error);\n\n // Try the fallback approach\n const fallbackPath = workerFallbackPath || DEFAULT_WORKER_PATH;\n const fallbackName = fallbackPath.split(\"/\").pop() || fallbackPath;\n\n console.log(`🔄 Trying fallback: ${fallbackName}...`);\n try {\n workerLLMRef.current = new Worker(fallbackPath);\n console.log(`✅ Successfully created worker using ${fallbackName} fallback`);\n } catch (fallbackError) {\n console.error(`❌ Fallback ${fallbackName} failed:`, fallbackError);\n setError(\"Failed to initialize web worker. Please check console for details.\");\n }\n // }\n }\n\n const worker = workerLLMRef.current; // Capture the ref's current value\n\n // All worker operations are now conditional on the worker existing.\n if (worker) {\n worker.postMessage({ type: \"check\" }); // Do a feature check\n\n // Create a callback function for messages from the worker thread.\n const onMessageReceived = (e: MessageEvent) => {\n // console.log('Worker message:', e.data.status);\n switch (e.data.status) {\n case \"loading\": {\n setStatus(\"loading\");\n setLoadingMessage(e.data.data);\n break;\n }\n case \"initiate\":\n setProgressItems((prev) => [...prev, e.data]);\n break;\n case \"progress\": {\n // Model file progress: update one of the progress items.\n setProgressItems((prev) =>\n prev.map((item) => {\n if (item.file === e.data.file) {\n return { ...item, ...e.data };\n }\n return item;\n }),\n );\n break;\n }\n case \"done\":\n // Model file loaded: remove the progress item from the list.\n setProgressItems((prev) => prev.filter((item) => item.file !== e.data.file));\n break;\n case \"ready\":\n // Pipeline ready: the worker is ready to accept messages.\n setStatus(\"ready\");\n break;\n case \"start\":\n {\n // Start generation\n setMessages((prev) => [\n ...prev,\n {\n role: \"assistant\",\n content: \"\",\n id: generateId(),\n },\n ]);\n }\n break;\n case \"update\":\n {\n // Generation update: update the output text.\n // Parse messages\n const { output, tps, numTokens, state } = e.data;\n setTps(tps);\n setNumTokens(numTokens);\n setMessages((prev) => {\n const cloned = [...prev];\n const last = cloned[cloned.length - 1];\n if (!last) {\n return cloned;\n }\n\n const data = {\n ...last,\n content: last.content + output,\n };\n\n currentSentenceBufferRef.current += output;\n\n const sentenceEndRegex = /[.?!]/;\n if (FINAL_ANSWER_PATTERN.test(data.content)) {\n if (sentenceEndRegex.test(currentSentenceBufferRef.current)) {\n const sentenceToSpeak = currentSentenceBufferRef.current.trim();\n if (sentenceToSpeak && !FINAL_ANSWER_PATTERN.test(sentenceToSpeak)) {\n // console.log('Speaking sentence:', sentenceToSpeak);\n // speakText(sentenceToSpeak);\n hasAutoSpoken.current = true;\n }\n currentSentenceBufferRef.current = \"\";\n }\n }\n if (data.answerIndex === undefined && state === \"answering\") {\n data.answerIndex = last.content.length;\n }\n cloned[cloned.length - 1] = data;\n return cloned;\n });\n }\n break;\n case \"complete\":\n // Generation complete: re-enable the \"Generate\" button\n setIsRunning(false);\n break;\n case \"error\":\n setError(e.data.data as string);\n break;\n default:\n // biome-ignore lint/suspicious/noConsole: <explanation>\n console.warn(\"Unknown message status:\", e.data.status);\n }\n };\n\n const onErrorReceived = (e: ErrorEvent) => {\n // biome-ignore lint/suspicious/noConsole: <explanation>\n console.error(\"Worker LLM error:\", e);\n };\n\n // Attach the callback function as an event listener.\n worker.addEventListener(\"message\", onMessageReceived);\n worker.addEventListener(\"error\", onErrorReceived);\n\n // Define a cleanup function for when the component is unmounted.\n return () => {\n // worker is guaranteed non-null here due to closure\n worker.removeEventListener(\"message\", onMessageReceived);\n worker.removeEventListener(\"error\", onErrorReceived);\n };\n }\n // If worker is null (e.g. createMyWorker failed or returned null),\n // no listeners are added, and this will implicitly return 'undefined' for cleanup.\n return undefined;\n }, []);\n\n // Send the messages to the worker thread whenever the `messages` state changes.\n\n useEffect(() => {\n const sendMessages = async () => {\n if (!workerLLMRef.current || !messages) {\n return;\n }\n if (messages.filter((x) => x.role === \"user\").length === 0) {\n return;\n }\n if (messages[messages.length - 1]?.role === \"assistant\") {\n // Do not update if the last message is from the assistant\n return;\n }\n if (!loadedSystemPrompt) {\n // Wait for system prompt to be loaded\n return;\n }\n setTps(null); // Only send the last few messages for context\n const lastMessages = messages.slice(-4); // Get last 4 messages for context\n\n // Use the pre-loaded system prompt\n lastMessages.unshift({\n role: \"system\",\n content: loadedSystemPrompt,\n });\n\n workerLLMRef.current.postMessage({\n type: \"generate\",\n data: lastMessages,\n });\n };\n\n sendMessages();\n }, [messages, loadedSystemPrompt]);\n\n // KOKORO\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const [speed, setSpeed] = useState(1);\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const [voices, setVoices] = useState<Voices | null>(null);\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const [selectedVoice, setSelectedVoice] = useState<keyof Voices>(\"af_heart\");\n // const [chunks, setChunks] = useState<AudioChunkData[]>([]);\n // const [currentChunkIndex, setCurrentChunkIndex] = useState(-1);\n // const [resultTTS, setResultTTS] = useState<Blob | null>(null);\n // const [statusTTS, setStatusTTS] = useState<\n // 'loading' | 'ready' | 'generating' | 'error'\n // >('loading');\n // const [errorTTS, setErrorTTS] = useState<string | null>(null);\n\n const ttsQueue = useRef<TTSRequest[]>([]);\n const isProcessingTTS = useRef(false);\n const hasAutoSpoken = useRef(false);\n const ttsStartTimeRef = useRef<number | null>(null);\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const [isTTSProcessing, setIsTTSProcessing] = useState(false);\n const [audioChunkQueue, setAudioChunkQueue] = useState<Blob[]>([]);\n const [isAudioPlaying, setIsAudioPlaying] = useState(false);\n\n const audioRef = useRef<HTMLAudioElement | null>(null);\n const currentAudioUrlRef = useRef<string | null>(null);\n\n // useEffect(() => {\n // // kokoroWorkerRef.current ??= new Worker(\n // // new URL('./kokoro-worker.ts', import.meta.url),\n // // {\n // // type: 'module',\n // // }\n // // );\n\n // // Standardized kokoroWorkerRef initialization\n // if (!kokoroWorkerRef.current) {\n // kokoroWorkerRef.current = new Worker(\n // new URL('./kokoro-worker.ts', import.meta.url),\n // {\n // type: 'module',\n // }\n // );\n // }\n\n // // Create a callback function for messages from the worker thread.\n // const onMessageReceived = (e: MessageEvent) => {\n // const { status, data, chunk } = e.data;\n // switch (status) {\n // case 'device':\n // console.log(`Device detected: ${e}`);\n // break;\n // case 'ready':\n // console.log('Kokoro Model loaded successfully');\n // setStatusTTS('ready');\n // // setVoices(msg.voices);\n // break;\n // case 'error':\n // setStatusTTS('error');\n // setError(data as string);\n // if (ttsStartTimeRef.current !== null) {\n // const endTime = performance.now();\n // const duration = endTime - ttsStartTimeRef.current;\n // console.error(`TTS Error after ${duration.toFixed(2)} ms`);\n // ttsStartTimeRef.current = null;\n // } else {\n // console.error(\n // 'TTS Error: Received error message without a recorded start time.'\n // );\n // }\n // break;\n // case 'stream': {\n // // setChunks((prev) => [...prev, data.chunk]);\n // // break;\n // if (chunk && chunk.audio instanceof Blob) {\n // handleAudioChunk(chunk.audio);\n // } else {\n // console.warn(\n // 'Received stream message without valid audio blob:',\n // chunk\n // );\n // }\n // break;\n // }\n // case 'complete': {\n // setStatusTTS('ready');\n // if (ttsStartTimeRef.current !== null) {\n // //const endTime = performance.now();\n // //const duration = endTime - ttsStartTimeRef.current;\n // //console.log(`TTS Complete: Total generation finished in ${duration.toFixed(2)} ms`); // Clarified log\n // ttsStartTimeRef.current = null;\n // } else {\n // //console.log(\"TTS Complete: Received complete message without a recorded start time.\");\n // }\n\n // ttsQueue.current.shift();\n // isProcessingTTS.current = false;\n // setIsTTSProcessing(false);\n // processNextTTSRequest();\n // break;\n // }\n // }\n // };\n\n // const onErrorReceived = (e: ErrorEvent) => {\n // console.error('KokoroWorker error:', e);\n // setError(e.message);\n // };\n\n // // Attach the callback function as an event listener.\n // kokoroWorkerRef.current?.addEventListener('message', onMessageReceived);\n // kokoroWorkerRef.current?.addEventListener('error', onErrorReceived);\n\n // // Define a cleanup function for when the component is unmounted.\n // return () => {\n // kokoroWorkerRef.current?.removeEventListener(\n // 'message',\n // onMessageReceived\n // );\n // kokoroWorkerRef.current?.removeEventListener('error', onErrorReceived);\n // };\n // }, []);\n\n const speakText = (text: string) => {\n if (!text || !kokoroWorkerRef.current) return;\n const trimmedText = text.trim();\n if