{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chatbot-window",
  "description": "Resizable AI chatbot interface with collapsible sidebar",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "avatar",
    "select",
    "textarea",
    "resizable",
    "@delta/ai-elements"
  ],
  "files": [
    {
      "path": "blocks/chatbot-window/page.tsx",
      "content": "\"use client\"\n\nimport { ChatbotWindow } from \"@/components/chatbot-window\"\n\nexport default function ChatbotWindowPage() {\n  return (\n    <div className=\"bg-background h-screen w-full\">\n      <ChatbotWindow defaultOpen={false}>\n        <div className=\"flex items-center justify-center\">\n          <div className=\"w-full max-w-lg space-y-20 px-5 text-justify\">\n            <div className=\"mb-12 text-center\">\n              <h2 className=\"font-heading mb-4 text-3xl font-bold\">\n                Welcome to Your App\n              </h2>\n              <p className=\"text-muted-foreground text-lg\">\n                Use the chat window on the right to interact with our AI\n                assistant. Click the expand button to open the chat interface.\n              </p>\n            </div>\n            {Array.from({ length: 2 }).map((_, index) => (\n              <div key={index}>\n                Lorem ipsum dolor sit amet consectetur adipisicing elit.\n                Obcaecati, reiciendis eum vitae nostrum, temporibus repudiandae\n                voluptatibus, natus iure ipsa velit odit quibusdam illum.\n                Quaerat cumque laudantium libero reprehenderit perferendis quo\n                nulla voluptate? Repellat tenetur labore exercitationem dicta\n                libero voluptate suscipit, iusto ea assumenda. Ipsa enim, quidem\n                atque modi error eaque, debitis perferendis, hic iste libero\n                dignissimos ea! Quod inventore beatae aspernatur nulla rem\n                perferendis aperiam at debitis delectus odit quia animi ex\n                mollitia vero molestias itaque deleniti, quos exercitationem\n                consequatur assumenda dolor? Quod reiciendis in similique\n                reprehenderit commodi quo blanditiis nobis hic ea optio illum\n                placeat officia alias quasi autem earum quos obcaecati,\n                voluptatum corporis quisquam. Quisquam iste, quas explicabo\n                omnis harum aut quam adipisci, voluptatem saepe accusantium\n                doloribus repellendus amet culpa magnam ex et dolores accusamus\n                commodi facere aliquam voluptatum alias? Officia expedita ut\n                vel? Beatae deserunt sequi id eos libero suscipit totam cum, sed\n                architecto atque quisquam et incidunt quod fuga ullam repellat\n                assumenda quos ab, voluptatum sint nesciunt? Ad sapiente est\n                laborum quam sint eius sequi. Eum, veniam dignissimos.\n              </div>\n            ))}\n          </div>\n        </div>\n      </ChatbotWindow>\n    </div>\n  )\n}\n",
      "type": "registry:page",
      "target": "app/chatbot-window/page.tsx"
    },
    {
      "path": "blocks/chatbot-window/components/chatbot-window.tsx",
      "content": "\"use client\"\n\nimport { useCallback, useRef, useState } from \"react\"\nimport type { FileUIPart, ToolUIPart } from \"ai\"\nimport {\n  GlobeIcon,\n  PanelRightClose,\n  PanelRightOpen,\n  ThumbsDownIcon,\n  ThumbsUpIcon,\n} from \"lucide-react\"\nimport { toast } from \"sonner\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Icons } from \"@/components/icons\"\nimport {\n  Action,\n  Actions,\n  CopyAction,\n} from \"@/components/ui/ai-elements/actions\"\nimport {\n  Branch,\n  BranchMessages,\n  BranchNext,\n  BranchPage,\n  BranchPrevious,\n  BranchSelector,\n} from \"@/components/ui/ai-elements/branch\"\nimport {\n  Conversation,\n  ConversationContent,\n  ConversationScrollButton,\n} from \"@/components/ui/ai-elements/conversation\"\nimport { Loader } from \"@/components/ui/ai-elements/loader\"\nimport {\n  Message,\n  MessageContent,\n} from \"@/components/ui/ai-elements/message\"\nimport {\n  PromptInput,\n  PromptInputActionAddAttachments,\n  PromptInputActionMenu,\n  PromptInputActionMenuContent,\n  PromptInputActionMenuTrigger,\n  PromptInputAttachment,\n  PromptInputAttachments,\n  PromptInputBody,\n  PromptInputButton,\n  PromptInputFooter,\n  PromptInputModelSelect,\n  PromptInputModelSelectContent,\n  PromptInputModelSelectItem,\n  PromptInputModelSelectTrigger,\n  PromptInputModelSelectValue,\n  PromptInputSpeechButton,\n  PromptInputSubmit,\n  PromptInputTextarea,\n  PromptInputTools,\n  type PromptInputMessage,\n} from \"@/components/ui/ai-elements/prompt-input\"\nimport {\n  Reasoning,\n  ReasoningContent,\n  ReasoningTrigger,\n} from \"@/components/ui/ai-elements/reasoning\"\nimport { Response } from \"@/components/ui/ai-elements/response\"\nimport {\n  Source,\n  Sources,\n  SourcesContent,\n  SourcesTrigger,\n} from \"@/components/ui/ai-elements/sources\"\nimport {\n  ResizableHandle,\n  ResizablePanel,\n  ResizablePanelGroup,\n} from \"@/components/ui/resizable\"\n\ntype MessageType = {\n  key: string\n  from: \"user\" | \"assistant\"\n  sources?: { href: string; title: string }[]\n  versions: {\n    id: string\n    content: string\n  }[]\n  reasoning?: {\n    content: string\n    duration: number\n  }\n  tools?: {\n    name: string\n    description: string\n    status: ToolUIPart[\"state\"]\n    parameters: Record<string, unknown>\n    result: string | undefined\n    error: string | undefined\n  }[]\n  files?: { name: string; url: string; type: string }[]\n  avatar: string\n  name: string\n}\n\nconst initialMessages: MessageType[] = []\n\nconst models = [\n  { id: \"gpt-4\", name: \"GPT-4\" },\n  { id: \"gpt-3.5-turbo\", name: \"GPT-3.5 Turbo\" },\n  { id: \"claude-2\", name: \"Claude 2\" },\n  { id: \"claude-instant\", name: \"Claude Instant\" },\n  { id: \"palm-2\", name: \"PaLM 2\" },\n  { id: \"llama-2-70b\", name: \"Llama 2 70B\" },\n  { id: \"llama-2-13b\", name: \"Llama 2 13B\" },\n  { id: \"cohere-command\", name: \"Command\" },\n  { id: \"mistral-7b\", name: \"Mistral 7B\" },\n]\n\nconst mockResponses = [\n  \"Hello! I'm an AI assistant built with Delta Components. I can help you with coding questions, explain concepts, or just have a conversation. What would you like to talk about?\",\n  \"That's a great question! Let me help you understand this concept better. The key thing to remember is that proper implementation requires careful consideration of the underlying principles and best practices in the field.\",\n  \"I'd be happy to explain this topic in detail. From my understanding, there are several important factors to consider when approaching this problem. Let me break it down step by step for you.\",\n  \"This is an interesting topic that comes up frequently. The solution typically involves understanding the core concepts and applying them in the right context. Here's what I recommend...\",\n  \"Great choice of topic! This is something that many developers encounter. The approach I'd suggest is to start with the fundamentals and then build up to more complex scenarios.\",\n]\n\ninterface ChatbotProps {\n  onClose?: () => void\n}\n\nfunction Chatbot({}: ChatbotProps) {\n  const [model, setModel] = useState<string>(models[0].id)\n  const [text, setText] = useState<string>(\"\")\n  const [useWebSearch, setUseWebSearch] = useState<boolean>(false)\n  const [status, setStatus] = useState<\n    \"submitted\" | \"streaming\" | \"ready\" | \"error\"\n  >(\"ready\")\n  const [messages, setMessages] = useState<MessageType[]>(initialMessages)\n  const [streamingMessageId, setStreamingMessageId] = useState<string | null>(\n    null\n  )\n  const [touchedMessages, setTouchedMessages] = useState<Set<string>>(new Set())\n  const shouldCancelRef = useRef<boolean>(false)\n  const addMessageTimeoutRef = useRef<NodeJS.Timeout | null>(null)\n  const textareaRef = useRef<HTMLTextAreaElement>(null)\n\n  const stop = useCallback(() => {\n    console.log(\"Stopping generation...\")\n\n    // Set cancellation flag\n    shouldCancelRef.current = true\n\n    // Clear timeout for adding assistant message\n    if (addMessageTimeoutRef.current) {\n      clearTimeout(addMessageTimeoutRef.current)\n      addMessageTimeoutRef.current = null\n    }\n\n    setStatus(\"ready\")\n    setStreamingMessageId(null)\n  }, [])\n\n  const streamResponse = useCallback(\n    async (messageId: string, content: string) => {\n      setStatus(\"streaming\")\n      setStreamingMessageId(messageId)\n      shouldCancelRef.current = false\n\n      const words = content.split(\" \")\n      let currentContent = \"\"\n\n      for (let i = 0; i < words.length; i++) {\n        // Check if streaming should be cancelled\n        if (shouldCancelRef.current) {\n          setStatus(\"ready\")\n          setStreamingMessageId(null)\n          return\n        }\n\n        currentContent += (i > 0 ? \" \" : \"\") + words[i]\n\n        setMessages((prev) =>\n          prev.map((msg) => {\n            if (msg.versions.some((v) => v.id === messageId)) {\n              return {\n                ...msg,\n                versions: msg.versions.map((v) =>\n                  v.id === messageId ? { ...v, content: currentContent } : v\n                ),\n              }\n            }\n            return msg\n          })\n        )\n\n        await new Promise((resolve) =>\n          setTimeout(resolve, Math.random() * 100 + 50)\n        )\n      }\n\n      setStatus(\"ready\")\n      setStreamingMessageId(null)\n    },\n    []\n  )\n\n  const handleMessageTouch = useCallback((messageKey: string) => {\n    setTouchedMessages((prev) => new Set(prev).add(messageKey))\n  }, [])\n\n  const addUserMessage = useCallback(\n    (\n      content: string,\n      files?: { name: string; url: string; type: string }[]\n    ) => {\n      const userMessage: MessageType = {\n        key: `user-${Date.now()}`,\n        from: \"user\",\n        versions: [\n          {\n            id: `user-${Date.now()}`,\n            content,\n          },\n        ],\n        files,\n        avatar: \"https://patrickprunty.com/icon.webp\",\n        name: \"User\",\n      }\n\n      setMessages((prev) => [...prev, userMessage])\n\n      addMessageTimeoutRef.current = setTimeout(() => {\n        const assistantMessageId = `assistant-${Date.now()}`\n        const randomResponse =\n          mockResponses[Math.floor(Math.random() * mockResponses.length)]\n\n        const assistantMessage: MessageType = {\n          key: `assistant-${Date.now()}`,\n          from: \"assistant\",\n          versions: [\n            {\n              id: assistantMessageId,\n              content: \"\",\n            },\n          ],\n          avatar: \"https://github.com/openai.png\",\n          name: \"Assistant\",\n        }\n\n        setMessages((prev) => [...prev, assistantMessage])\n        streamResponse(assistantMessageId, randomResponse)\n        addMessageTimeoutRef.current = null\n      }, 500)\n    },\n    [streamResponse]\n  )\n\n  const handleSubmit = (message: PromptInputMessage) => {\n    // If currently streaming or submitted, stop instead of submitting\n    if (status === \"streaming\" || status === \"submitted\") {\n      stop()\n      return\n    }\n\n    const hasText = Boolean(message.text)\n    const hasAttachments = Boolean(message.files?.length)\n\n    if (!(hasText || hasAttachments)) {\n      return\n    }\n\n    setStatus(\"submitted\")\n\n    let processedFiles:\n      | { name: string; url: string; type: string }[]\n      | undefined\n\n    if (message.files?.length) {\n      processedFiles = message.files.map((file) => ({\n        name: \"name\" in file ? (file as any).name : \"Unknown file\",\n        url: \"url\" in file ? (file as any).url : \"\",\n        type: \"type\" in file ? (file as any).type : \"application/octet-stream\",\n      }))\n\n      toast.success(\"Files attached\", {\n        description: `${message.files.length} file(s) attached to message`,\n      })\n    }\n\n    addUserMessage(message.text || \"\", processedFiles)\n    setText(\"\")\n  }\n\n  return (\n    <div className=\"flex h-full flex-col\">\n      <Conversation className=\"flex-1\" showGradient={false}>\n        <ConversationContent>\n          {messages.map(({ versions, ...message }) => {\n            const assistantMessages = messages.filter(\n              (m) => m.from === \"assistant\"\n            )\n            const isLastAssistantMessage =\n              message.from === \"assistant\" &&\n              assistantMessages.length > 0 &&\n              assistantMessages[assistantMessages.length - 1].key ===\n                message.key\n\n            return (\n              <Branch defaultBranch={0} key={message.key}>\n                <BranchMessages>\n                  {versions.map((version) => (\n                    <Message\n                      from={message.from}\n                      key={`${message.key}-${version.id}`}\n                      className={cn(\n                        message.from === \"user\"\n                          ? \"items-end justify-end\"\n                          : undefined,\n                        \"group/message\"\n                      )}\n                      onTouchStart={() => handleMessageTouch(message.key)}\n                    >\n                      <div>\n                        {message.sources?.length && (\n                          <Sources>\n                            <SourcesTrigger count={message.sources.length} />\n                            <SourcesContent>\n                              {message.sources.map((source) => (\n                                <Source\n                                  href={source.href}\n                                  key={source.href}\n                                  title={source.title}\n                                />\n                              ))}\n                            </SourcesContent>\n                          </Sources>\n                        )}\n                        {message.reasoning && (\n                          <Reasoning duration={message.reasoning.duration}>\n                            <ReasoningTrigger />\n                            <ReasoningContent>\n                              {message.reasoning.content}\n                            </ReasoningContent>\n                          </Reasoning>\n                        )}\n                        <MessageContent\n                          className={cn(\n                            message.from === \"assistant\" ? \"max-w-full\" : \"\",\n                            message.from === \"user\" && \"ml-auto w-fit\"\n                          )}\n                        >\n                          {message.files?.length && (\n                            <div className=\"mb-3 space-y-2\">\n                              {message.files.map((file, index) => (\n                                <div key={index} className=\"max-w-sm\">\n                                  {file.type.startsWith(\"image/\") ? (\n                                    <div className=\"relative\">\n                                      <img\n                                        src={file.url}\n                                        alt={file.name}\n                                        className=\"h-auto max-w-full rounded-lg\"\n                                        style={{ maxHeight: \"300px\" }}\n                                      />\n                                      <div className=\"absolute bottom-2 left-2 rounded bg-black/70 px-2 py-1 text-xs text-white\">\n                                        {file.name}\n                                      </div>\n                                    </div>\n                                  ) : (\n                                    <div className=\"bg-muted flex items-center gap-2 rounded-lg p-3\">\n                                      <div className=\"bg-primary/10 flex h-8 w-8 items-center justify-center rounded\">\n                                        📄\n                                      </div>\n                                      <div className=\"min-w-0 flex-1\">\n                                        <div className=\"truncate text-sm font-medium\">\n                                          {file.name}\n                                        </div>\n                                        <div className=\"text-muted-foreground text-xs\">\n                                          {file.type}\n                                        </div>\n                                      </div>\n                                    </div>\n                                  )}\n                                </div>\n                              ))}\n                            </div>\n                          )}\n                          {version.content && (\n                            <div className=\"text-base leading-[1.65rem]\">\n                              <Response>{version.content}</Response>\n                            </div>\n                          )}\n                        </MessageContent>\n                        {message.from === \"assistant\" && (\n                          <div className=\"flex items-center justify-between\">\n                            <div className=\"flex items-center\">\n                              {status === \"streaming\" &&\n                                streamingMessageId === version.id && (\n                                  <Loader\n                                    size={16}\n                                    className=\"text-muted-foreground ml-1\"\n                                  />\n                                )}\n                            </div>\n                            {status !== \"streaming\" &&\n                              streamingMessageId !== version.id && (\n                                <Actions\n                                  className={cn(\n                                    \"justify-end\",\n                                    isLastAssistantMessage ||\n                                      touchedMessages.has(message.key)\n                                      ? \"opacity-100\"\n                                      : \"opacity-0 group-hover/message:opacity-100\"\n                                  )}\n                                >\n                                  <CopyAction\n                                    value={version.content}\n                                    tooltip=\"Copy message\"\n                                  />\n                                  <Action\n                                    tooltip=\"Good response\"\n                                    onClick={() =>\n                                      toast.success(\"Feedback recorded\")\n                                    }\n                                  >\n                                    <ThumbsUpIcon className=\"h-4 w-4\" />\n                                  </Action>\n                                  <Action\n                                    tooltip=\"Poor response\"\n                                    onClick={() =>\n                                      toast.success(\"Feedback recorded\")\n                                    }\n                                  >\n                                    <ThumbsDownIcon className=\"h-4 w-4\" />\n                                  </Action>\n                                  <Action\n                                    tooltip=\"Regenerate response\"\n                                    onClick={() =>\n                                      toast.info(\"Regenerating response...\")\n                                    }\n                                    className=\"text-muted-foreground hover:text-foreground hover:bg-accent relative size-9 h-9 w-auto min-w-0 rounded-md p-1.5 px-2 text-sm font-medium transition-colors duration-200\"\n                                  >\n                                    Retry\n                                  </Action>\n                                </Actions>\n                              )}\n                          </div>\n                        )}\n                      </div>\n                    </Message>\n                  ))}\n                </BranchMessages>\n                {versions.length > 1 && (\n                  <BranchSelector from={message.from}>\n                    <BranchPrevious />\n                    <BranchPage />\n                    <BranchNext />\n                  </BranchSelector>\n                )}\n              </Branch>\n            )\n          })}\n        </ConversationContent>\n        <ConversationScrollButton />\n      </Conversation>\n      <div className=\"grid shrink-0 gap-4\">\n        <div className=\"w-full px-4 pb-4\">\n          <PromptInput globalDrop multiple onSubmit={handleSubmit}>\n            <PromptInputBody>\n              <PromptInputAttachments>\n                {(attachment: any) => (\n                  <PromptInputAttachment data={attachment} />\n                )}\n              </PromptInputAttachments>\n              <PromptInputTextarea\n                onChange={(event: any) => setText(event.target.value)}\n                ref={textareaRef}\n                value={text}\n                className=\"text-base leading-[1.65rem]\"\n              />\n            </PromptInputBody>\n            <PromptInputFooter>\n              <PromptInputTools>\n                <PromptInputActionMenu>\n                  <PromptInputActionMenuTrigger />\n                  <PromptInputActionMenuContent>\n                    <PromptInputActionAddAttachments />\n                  </PromptInputActionMenuContent>\n                </PromptInputActionMenu>\n                <PromptInputSpeechButton\n                  onTranscriptionChange={setText}\n                  textareaRef={textareaRef}\n                />\n                <PromptInputButton\n                  onClick={() => setUseWebSearch(!useWebSearch)}\n                  variant={useWebSearch ? \"default\" : \"ghost\"}\n                >\n                  <GlobeIcon size={16} />\n                  <span>Search</span>\n                </PromptInputButton>\n              </PromptInputTools>\n              <PromptInputTools>\n                <PromptInputModelSelect onValueChange={setModel} value={model}>\n                  <PromptInputModelSelectTrigger>\n                    <PromptInputModelSelectValue />\n                  </PromptInputModelSelectTrigger>\n                  <PromptInputModelSelectContent>\n                    {models.map((model: any) => (\n                      <PromptInputModelSelectItem\n                        key={model.id || model.name}\n                        value={model.id || model.name}\n                      >\n                        {model.name || model.id}\n                      </PromptInputModelSelectItem>\n                    ))}\n                  </PromptInputModelSelectContent>\n                </PromptInputModelSelect>\n                <PromptInputSubmit\n                  disabled={(!text.trim() && !status) || status === \"streaming\"}\n                  status={status}\n                />\n              </PromptInputTools>\n            </PromptInputFooter>\n          </PromptInput>\n        </div>\n      </div>\n    </div>\n  )\n}\n\ninterface ChatbotWindowProps extends React.HTMLAttributes<HTMLDivElement> {\n  defaultOpen?: boolean\n  children?: React.ReactNode\n}\n\nexport function ChatbotWindow({\n  className,\n  defaultOpen = false,\n  children,\n  ...props\n}: ChatbotWindowProps) {\n  const [isOpen, setIsOpen] = useState(defaultOpen)\n\n  return (\n    <div className={cn(\"flex h-screen w-full flex-col\", className)} {...props}>\n      {/* Header */}\n      <div className=\"shrink-0 border-b p-4\">\n        <div className=\"flex items-center\">\n          <Icons.logo className=\"size-5\" />\n        </div>\n      </div>\n\n      {/* Content area with sidebar */}\n      <div className=\"flex min-h-0 flex-1\">\n        <ResizablePanelGroup direction=\"horizontal\" className=\"h-full\">\n          {/* Main content area */}\n          <ResizablePanel defaultSize={isOpen ? 70 : 100} minSize={30}>\n            <div className=\"no-scrollbar h-full overflow-auto\">\n              <div className=\"p-8 pt-16 pb-16\">{children}</div>\n            </div>\n          </ResizablePanel>\n\n          {isOpen && (\n            <>\n              <ResizableHandle\n                className=\"group bg-border hover:bg-muted-foreground/20 relative transition-colors\"\n                onClick={() => setIsOpen(false)}\n              >\n                <div className=\"absolute inset-0 z-10 flex items-center justify-center opacity-0 transition-opacity group-hover:opacity-100\">\n                  <button\n                    className=\"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 hover:text-accent-foreground dark:hover:bg-accent/50 bg-background border-border hover:bg-muted inline-flex size-8 shrink-0 items-center justify-center gap-1 rounded-md border p-0 text-sm font-medium whitespace-nowrap shadow-sm transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 has-[>svg]:px-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n                    data-slot=\"button\"\n                    type=\"button\"\n                    title=\"Toggle Chat Sidebar\"\n                    onClick={() => setIsOpen(false)}\n                  >\n                    <PanelRightClose className=\"h-3 w-3\" aria-hidden=\"true\" />\n                  </button>\n                </div>\n              </ResizableHandle>\n              <ResizablePanel defaultSize={30} minSize={20} maxSize={80}>\n                <aside className=\"bg-muted h-full min-w-0 overflow-hidden\">\n                  <Chatbot onClose={() => setIsOpen(false)} />\n                </aside>\n              </ResizablePanel>\n            </>\n          )}\n\n          {/* Collapsed sidebar toggle */}\n          {!isOpen && (\n            <div className=\"border-border relative flex w-10 shrink-0 items-center justify-center border-l\">\n              <button\n                onClick={() => setIsOpen(true)}\n                className=\"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 hover:text-accent-foreground dark:hover:bg-accent/50 bg-background hover:bg-muted inline-flex h-8 w-8 shrink-0 items-center justify-center gap-1 rounded-md p-0 text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 has-[>svg]:px-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n                data-slot=\"button\"\n                type=\"button\"\n                title=\"Expand Chat Sidebar\"\n              >\n                <PanelRightOpen className=\"h-4 w-4\" aria-hidden=\"true\" />\n              </button>\n            </div>\n          )}\n        </ResizablePanelGroup>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/chatbot-window.tsx"
    }
  ],
  "meta": {
    "iframeHeight": "800px",
    "container": "w-full h-screen",
    "mobile": "component"
  },
  "categories": [
    "ai-elements",
    "featured"
  ],
  "type": "registry:block"
}