diff --git a/examples/poly-explainer-bot/.env.example b/examples/poly-explainer-bot/.env.example
new file mode 100644
index 00000000..85d27328
--- /dev/null
+++ b/examples/poly-explainer-bot/.env.example
@@ -0,0 +1 @@
+NEXT_LB_PIPE_API_KEY=""
\ No newline at end of file
diff --git a/examples/poly-explainer-bot/.gitignore b/examples/poly-explainer-bot/.gitignore
new file mode 100644
index 00000000..54a02eee
--- /dev/null
+++ b/examples/poly-explainer-bot/.gitignore
@@ -0,0 +1,54 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env
+.env*.local
+.copy.local.env
+.copy.remote.env
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+# Supabase
+seed.sql
+xseed.sql
+xxseed.sql
+-seed.sql
+/supabase/seed.sql
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
+
+# No lock files.
+package-lock.json
+yarn.lock
+dist
diff --git a/examples/poly-explainer-bot/README.md b/examples/poly-explainer-bot/README.md
new file mode 100755
index 00000000..da7a7eb6
--- /dev/null
+++ b/examples/poly-explainer-bot/README.md
@@ -0,0 +1,79 @@
+![PolyExplainer Chatbot by ⌘ Langbase][cover]
+
+![License: MIT][mit] [![Fork to ⌘ Langbase][fork]][pipe]
+
+## Build an PolyExplainer Chatbot with Pipes — ⌘ Langbase
+
+This chatbot is built by using an AI Pipe on Langbase, it works with 30+ LLMs (OpenAI, Gemini, Mistral, Llama, Gemma, etc), any Data (10M+ context with Memory sets), and any Framework (standard web API you can use with any software).
+
+Check out the live demo [here][demo].
+
+## Features
+
+- 💬 [PolyExplainer Chatbot][demo] — Built with an [AI Pipe on ⌘ Langbase][pipe]
+- ⚡️ Streaming — Real-time chat experience with streamed responses
+- 🗣️ Q/A — Ask questions and get pre-defined answers with your preferred AI model and tone
+- 🔋 Responsive and open source — Works on all devices and platforms
+
+## Learn more
+
+1. Check the [PolyExplainer Chatbot Pipe on ⌘ Langbase][pipe]
+2. Read the [source code on GitHub][gh] for this example
+3. Go through Documentaion: [Pipe Quick Start][qs]
+4. Learn more about [Pipes & Memory features on ⌘ Langbase][docs]
+
+## Get started
+
+Let's get started with the project:
+
+To get started with Langbase, you'll need to [create a free personal account on Langbase.com][signup] and verify your email address. _Done? Cool, cool!_
+
+1. Fork the [PolyExplainer Chatbot][pipe] Pipe on ⌘ Langbase.
+2. Go to the API tab to copy the Pipe's API key (to be used on server-side only).
+3. Download the example project folder from [here][download] or clone the reppository.
+4. `cd` into the project directory and open it in your code editor.
+5. Duplicate the `.env.example` file in this project and rename it to `.env.local`.
+6. Add the following environment variables (.env.local):
+```
+    # Replace `PIPE_API_KEY` with the copied API key.
+    NEXT_LB_PIPE_API_KEY="PIPE_API_KEY"
+```    
+
+7. Issue the following in your CLI:
+```sh
+    # Install the dependencies using the following command:
+    npm install
+
+    # Run the project using the following command:
+    npm run dev
+``` 
+
+8. Your app template should now be running on [localhost:3000][local].
+
+> NOTE:
+> This is a Next.js project, so you can build and deploy it to any platform of your choice, like Vercel, Netlify, Cloudflare, etc.
+
+---
+
+## Authors
+
+This project is created by [Langbase][lb] team members, with contributions from:
+
+- Muhammad-Ali Danish - Software Engineer, [Langbase][lb] <br>
+**_Built by ⌘ [Langbase.com][lb] — Ship hyper-personalized AI assistants with memory!_**
+
+
+[demo]: https://poly-explainer-bot.langbase.dev
+[lb]: https://langbase.com
+[pipe]: https://beta.langbase.com/examples/poly-explainer-bot
+[gh]: https://github.com/LangbaseInc/langbase-examples/tree/main/examples/poly-explainer-bot
+[cover]:https://raw.githubusercontent.com/LangbaseInc/docs-images/main/examples/poly-explainer-bot/poly-explainer-bot.png
+[download]:https://download-directory.github.io/?url=https://github.com/LangbaseInc/poly-explainer-bot/tree/main/examples/poly-explainer-bot
+[signup]: https://langbase.fyi/io
+[qs]:https://langbase.com/docs/pipe/quickstart
+[docs]:https://langbase.com/docs
+[xaa]:https://x.com/MrAhmadAwais
+[xab]:https://x.com/AhmadBilalDev
+[local]:http://localhost:3000
+[mit]: https://img.shields.io/badge/license-MIT-blue.svg?style=for-the-badge&color=%23000000
+[fork]: https://img.shields.io/badge/FORK%20ON-%E2%8C%98%20Langbase-000000.svg?style=for-the-badge&logo=%E2%8C%98%20Langbase&logoColor=000000
diff --git a/examples/poly-explainer-bot/app/api/chat/route.ts b/examples/poly-explainer-bot/app/api/chat/route.ts
new file mode 100755
index 00000000..95af6ed7
--- /dev/null
+++ b/examples/poly-explainer-bot/app/api/chat/route.ts
@@ -0,0 +1,57 @@
+import { OpenAIStream, StreamingTextResponse } from 'ai'
+
+export const runtime = 'edge'
+
+/**
+ * Stream AI Chat Messages from Langbase
+ *
+ * @param req
+ * @returns
+ */
+export async function POST(req: Request) {
+  try {
+    if (!process.env.NEXT_LB_PIPE_API_KEY) {
+      throw new Error(
+        'Please set NEXT_LB_PIPE_API_KEY in your environment variables.'
+      )
+    }
+
+    const endpointUrl = 'https://api.langbase.com/beta/chat'
+
+    const headers = {
+      'Content-Type': 'application/json',
+      Authorization: `Bearer ${process.env.NEXT_LB_PIPE_API_KEY}`
+    }
+
+    // Get chat prompt messages and threadId from the client.
+    const body = await req.json()
+    const { messages, threadId } = body
+
+    const requestBody = {
+      messages,
+      ...(threadId && { threadId }) // Only include threadId if it exists
+    }
+
+    // Send the request to Langbase API.
+    const response = await fetch(endpointUrl, {
+      method: 'POST',
+      headers,
+      body: JSON.stringify(requestBody)
+    })
+
+    if (!response.ok) {
+      const res = await response.json()
+      throw new Error(`Error ${res.error.status}: ${res.error.message}`)
+    }
+
+    // Handle Langbase response, which is a stream in OpenAI format.
+    const stream = OpenAIStream(response)
+    // Respond with a text stream.
+    return new StreamingTextResponse(stream, {
+      headers: response.headers
+    })
+  } catch (error: any) {
+    console.error('Uncaught API Error:', error)
+    return new Response(JSON.stringify(error), { status: 500 })
+  }
+}
diff --git a/examples/poly-explainer-bot/app/favicon.ico b/examples/poly-explainer-bot/app/favicon.ico
new file mode 100644
index 00000000..7c6d4a8a
Binary files /dev/null and b/examples/poly-explainer-bot/app/favicon.ico differ
diff --git a/examples/poly-explainer-bot/app/globals.css b/examples/poly-explainer-bot/app/globals.css
new file mode 100755
index 00000000..36084b74
--- /dev/null
+++ b/examples/poly-explainer-bot/app/globals.css
@@ -0,0 +1,99 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* Zinc */
+/* --background: 240 10% 3.9%; */
+/* --muted: 240 3.7% 15.9%; */
+@layer base {
+  :root {
+    --background: 0 0% 100%;
+    --foreground: 240 10% 3.9%;
+    --card: 0 0% 100%;
+    --card-foreground: 240 10% 3.9%;
+    --popover: 0 0% 100%;
+    --popover-foreground: 240 10% 3.9%;
+    --primary: 240 5.9% 10%;
+    --primary-foreground: 0 0% 98%;
+    --secondary: 240 4.8% 95.9%;
+    --secondary-foreground: 240 5.9% 10%;
+    --muted: 240 4.8% 95.9%;
+    --muted-foreground: 240 3.8% 46.1%;
+    --accent: 240 4.8% 95.9%;
+    --accent-foreground: 240 5.9% 10%;
+    /* --destructive: 0 84.2% 60.2%; */
+    --destructive: 2.74 92.59% 62.94%;
+    --destructive-foreground: 0 0% 98%;
+    --warning: 46.38 70.61% 48.04%;
+    --warning-foreground: 120 12.5% 3.14%;
+    --border: 240 5.9% 90%;
+    --input: 240 5.9% 90%;
+    --ring: 240 5.9% 10%;
+    --radius: 6px;
+    --danger: 2.74 92.59% 62.94%;
+  }
+
+  .dark {
+    /* --background: 120 12.5% 3.14%; */
+    --background: 240, 3%, 9%;
+    --foreground: 0 0% 98%;
+    --card: 240 10% 3.9%;
+    --card-foreground: 0 0% 98%;
+    --popover: 240 10% 3.9%;
+    --popover-foreground: 0 0% 98%;
+    --primary: 0 0% 98%;
+    --primary-foreground: 240 5.9% 10%;
+    --secondary: 240 3.7% 15.9%;
+    --secondary-foreground: 0 0% 98%;
+    /* --muted: 165 10% 7.84%; */
+    --muted: 240 3.45% 11.37%;
+    --muted-foreground: 240 5% 64.9%;
+    --accent: 240 3.7% 15.9%;
+    --accent-foreground: 0 0% 98%;
+    /* --destructive: 0 62.8% 30.6%; */
+    --destructive: 356.18 70.61% 48.04%;
+    --destructive-foreground: 0 0% 98%;
+    --warning: 46.38 70.61% 48.04%;
+    --warning-foreground: 120 12.5% 3.14%;
+    /* --border: 240 3.7% 15.9%; */
+    --border: 240 2% 14%;
+    --border-muted: 240 2% 14%;
+    --input: 240 3.7% 15.9%;
+    --ring: 240 4.9% 83.9%;
+    --danger: 356.18 70.61% 48.04%;
+  }
+}
+
+@layer base {
+  * {
+    @apply border-border;
+  }
+  body {
+    @apply bg-background text-foreground;
+  }
+}
+
+::selection {
+  color: hsl(var(--background));
+  background: hsl(var(--foreground));
+}
+
+.google {
+  display: inline-block;
+  width: 20px;
+  height: 20px;
+  position: relative;
+  background-size: contain;
+  background-repeat: no-repeat;
+  background-position: 50%;
+  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 48 48'%3E%3Cdefs%3E%3Cpath id='a' d='M44.5 20H24v8.5h11.8C34.7 33.9 30.1 37 24 37c-7.2 0-13-5.8-13-13s5.8-13 13-13c3.1 0 5.9 1.1 8.1 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 11.8 2 2 11.8 2 24s9.8 22 22 22c11 0 21-8 21-22 0-1.3-.2-2.7-.5-4z'/%3E%3C/defs%3E%3CclipPath id='b'%3E%3Cuse xlink:href='%23a' overflow='visible'/%3E%3C/clipPath%3E%3Cpath clip-path='url(%23b)' fill='%23FBBC05' d='M0 37V11l17 13z'/%3E%3Cpath clip-path='url(%23b)' fill='%23EA4335' d='M0 11l17 13 7-6.1L48 14V0H0z'/%3E%3Cpath clip-path='url(%23b)' fill='%2334A853' d='M0 37l30-23 7.9 1L48 0v48H0z'/%3E%3Cpath clip-path='url(%23b)' fill='%234285F4' d='M48 48L17 24l-4-3 35-10z'/%3E%3C/svg%3E");
+}
+
+@keyframes spin {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
diff --git a/examples/poly-explainer-bot/app/layout.tsx b/examples/poly-explainer-bot/app/layout.tsx
new file mode 100755
index 00000000..325e73b8
--- /dev/null
+++ b/examples/poly-explainer-bot/app/layout.tsx
@@ -0,0 +1,36 @@
+import { Header } from '@/components/header'
+import cn from 'mxcn'
+import type { Metadata } from 'next'
+import { Inter } from 'next/font/google'
+import { Toaster } from 'sonner'
+import './globals.css'
+
+const inter = Inter({ subsets: ['latin'] })
+
+export const metadata: Metadata = {
+  title: 'PolyExplainer  Chatbot - Langbase',
+  description: 'Build a PolyExplainer  Chatbot with ⌘ Langbase using any LLM model.',
+  keywords: ['PolyExplainer', 'Chatbot', 'Langbase']
+}
+
+export default function RootLayout({
+  children
+}: Readonly<{
+  children: React.ReactNode
+}>) {
+  return (
+    <html lang="en">
+      <body className={cn(inter.className, 'dark bg-background')}>
+        <div className="flex min-h-screen flex-col px-3 pr-0 pt-6">
+          <div className="rounded-l-[calc(var(--radius)+2px)] border border-r-0 pb-1 pl-1">
+            <Toaster />
+            <Header />
+            <main className="rounded-l-[calc(var(--radius)+2px)] bg-muted">
+              {children}
+            </main>
+          </div>
+        </div>
+      </body>
+    </html>
+  )
+}
diff --git a/examples/poly-explainer-bot/app/page.tsx b/examples/poly-explainer-bot/app/page.tsx
new file mode 100755
index 00000000..f278d230
--- /dev/null
+++ b/examples/poly-explainer-bot/app/page.tsx
@@ -0,0 +1,7 @@
+import { Chatbot } from '@/components/chatbot-page'
+
+export const runtime = 'edge'
+
+export default function ChatPage() {
+  return <Chatbot />
+}
diff --git a/examples/poly-explainer-bot/components/chat-input.tsx b/examples/poly-explainer-bot/components/chat-input.tsx
new file mode 100755
index 00000000..569fbc6f
--- /dev/null
+++ b/examples/poly-explainer-bot/components/chat-input.tsx
@@ -0,0 +1,75 @@
+import { type UseChatHelpers } from 'ai/react'
+
+import { PromptForm } from '@/components/prompt-form'
+import { Button } from '@/components/ui/button'
+import { IconRegenerate, IconStop } from '@/components/ui/icons'
+
+export interface ChatInputProps
+  extends Pick<
+    UseChatHelpers,
+    | 'append'
+    | 'isLoading'
+    | 'reload'
+    | 'messages'
+    | 'stop'
+    | 'input'
+    | 'setInput'
+  > {
+  id?: string
+}
+
+export function ChatInput({
+  id,
+  isLoading,
+  stop,
+  append,
+  reload,
+  input,
+  setInput,
+  messages
+}: ChatInputProps) {
+  return (
+    <div className="fixed inset-x-0 bottom-0">
+      <div className="xbg-muted mx-auto max-w-3xl sm:max-w-4xl">
+        <div className="flex h-10 items-center justify-center">
+          {isLoading ? (
+            <Button
+              variant="outline"
+              onClick={() => stop()}
+              className="bg-background"
+              size={'sm'}
+            >
+              <IconStop className="text-muted-foreground/50 group-hover:text-background" />
+              Stop generating
+            </Button>
+          ) : (
+            messages?.length > 0 && (
+              <Button
+                variant="outline"
+                onClick={() => reload()}
+                className="bg-background"
+                size={'sm'}
+              >
+                <IconRegenerate className="size-4 text-muted-foreground/50 group-hover:text-background" />
+                Regenerate response
+              </Button>
+            )
+          )}
+        </div>
+        <div className="space-y-4 py-2 md:pb-4 md:pt-2">
+          <PromptForm
+            onSubmit={async value => {
+              await append({
+                content: value,
+                role: 'user'
+              })
+            }}
+            input={input}
+            setInput={setInput}
+            isLoading={isLoading}
+          />
+        </div>
+      </div>
+    </div>
+  )
+}
diff --git a/examples/poly-explainer-bot/components/chat-list.tsx b/examples/poly-explainer-bot/components/chat-list.tsx
new file mode 100755
index 00000000..32074928
--- /dev/null
+++ b/examples/poly-explainer-bot/components/chat-list.tsx
@@ -0,0 +1,27 @@
+import { type Message } from 'ai'
+
+import { Separator } from '@/components/ui/separator'
+import { ChatMessage } from '@/components/chat-message'
+
+export interface ChatList {
+  messages: Message[]
+}
+
+export function ChatList({ messages }: ChatList) {
+  if (!messages.length) {
+    return null
+  }
+
+  return (
+    <div className="relative mx-auto max-w-2xl px-4 pb-[100px]">
+      {messages.map((message, index) => (
+        <div key={index}>
+          <ChatMessage message={message} />
+          {index < messages.length - 1 && (
+            <Separator className="my-4 md:my-8" />
+          )}
+        </div>
+      ))}
+    </div>
+  )
+}
diff --git a/examples/poly-explainer-bot/components/chat-message-actions.tsx b/examples/poly-explainer-bot/components/chat-message-actions.tsx
new file mode 100755
index 00000000..37f68b2a
--- /dev/null
+++ b/examples/poly-explainer-bot/components/chat-message-actions.tsx
@@ -0,0 +1,40 @@
+'use client'
+
+import { type Message } from 'ai'
+
+import { Button } from '@/components/ui/button'
+import { IconCheck, IconCopy } from '@/components/ui/icons'
+import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
+import cn from 'mxcn'
+
+interface ChatMessageActionsProps extends React.ComponentProps<'div'> {
+  message: Message
+}
+
+export function ChatMessageActions({
+  message,
+  className,
+  ...props
+}: ChatMessageActionsProps) {
+  const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
+
+  const onCopy = () => {
+    if (isCopied) return
+    copyToClipboard(message.content)
+  }
+
+  return (
+    <div
+      className={cn(
+        'flex items-center justify-end transition-opacity group-hover:opacity-100 md:absolute md:-right-10 md:-top-2 md:opacity-0',
+        className
+      )}
+      {...props}
+    >
+      <Button variant="ghost" size="icon" onClick={onCopy}>
+        {isCopied ? <IconCheck /> : <IconCopy />}
+        <span className="sr-only">Copy message</span>
+      </Button>
+    </div>
+  )
+}
diff --git a/examples/poly-explainer-bot/components/chat-message.tsx b/examples/poly-explainer-bot/components/chat-message.tsx
new file mode 100755
index 00000000..c359618d
--- /dev/null
+++ b/examples/poly-explainer-bot/components/chat-message.tsx
@@ -0,0 +1,77 @@
+import { Message } from 'ai'
+import remarkGfm from 'remark-gfm'
+import remarkMath from 'remark-math'
+
+import { ChatMessageActions } from '@/components/chat-message-actions'
+import { MemoizedReactMarkdown } from '@/components/markdown'
+import { CodeBlock } from '@/components/ui/codeblock'
+import { IconSparkles, IconUser } from '@/components/ui/icons'
+import cn from 'mxcn'
+
+export interface ChatMessageProps {
+  message: Message
+}
+
+export function ChatMessage({ message, ...props }: ChatMessageProps) {
+  return (
+    <div
+      className={cn('group relative mb-4 flex items-start md:-ml-12')}
+      {...props}
+    >
+      <div
+        className={cn(
+          'flex h-8 w-8 shrink-0 select-none items-center justify-center rounded-xl border shadow',
+          message.role === 'user'
+            ? 'bg-background'
+            : 'bg-primary text-primary-foreground'
+        )}
+      >
+        {message.role === 'user' ? <IconUser /> : <IconSparkles />}
+      </div>
+      <div className="ml-4 flex-1 space-y-2 overflow-hidden px-1">
+        <MemoizedReactMarkdown
+          className="prose rounded-xl dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 break-words prose-pre:rounded-xl"
+          remarkPlugins={[remarkGfm, remarkMath]}
+          components={{
+            p({ children }) {
+              return <p className="mb-2 last:mb-0">{children}</p>
+            },
+            code({ node, inline, className, children, ...props }) {
+              if (children.length) {
+                if (children[0] == '▍') {
+                  return (
+                    <span className="mt-1 animate-pulse cursor-default">▍</span>
+                  )
+                }
+
+                children[0] = (children[0] as string).replace('`▍`', '▍')
+              }
+
+              const match = /language-(\w+)/.exec(className || '')
+
+              if (inline) {
+                return (
+                  <code className={className} {...props}>
+                    {children}
+                  </code>
+                )
+              }
+
+              return (
+                <CodeBlock
+                  key={Math.random()}
+                  language={(match && match[1]) || ''}
+                  value={String(children).replace(/\n$/, '')}
+                  {...props}
+                />
+              )
+            }
+          }}
+        >
+          {message.content}
+        </MemoizedReactMarkdown>
+        <ChatMessageActions message={message} />
+      </div>
+    </div>
+  )
+}
diff --git a/examples/poly-explainer-bot/components/chatbot-page.tsx b/examples/poly-explainer-bot/components/chatbot-page.tsx
new file mode 100755
index 00000000..80203378
--- /dev/null
+++ b/examples/poly-explainer-bot/components/chatbot-page.tsx
@@ -0,0 +1,57 @@
+'use client'
+
+import { ChatList } from '@/components/chat-list'
+import { useChat, type Message } from 'ai/react'
+import cn from 'mxcn'
+import { useState } from 'react'
+import { toast } from 'sonner'
+import { ChatInput } from './chat-input'
+import { Opening } from './opening'
+
+export interface ChatProps extends React.ComponentProps<'div'> {
+  id?: string // Optional: Thread ID if you want to persist the chat in a DB
+  initialMessages?: Message[] // Optional: Messages to pre-populate the chat from DB
+}
+
+export function Chatbot({ id, initialMessages, className }: ChatProps) {
+  const [threadId, setThreadId] = useState<null | string>(null)
+  const { messages, append, reload, stop, isLoading, input, setInput } =
+    useChat({
+      api: '/api/chat',
+      initialMessages,
+      body: { threadId },
+      onResponse(response) {
+        if (response.status !== 200) {
+          console.log('✨ ~ response:', response)
+          toast.error(response.statusText)
+        }
+
+        // Get Thread ID from response header
+        const lbThreadId = response.headers.get('lb-thread-id')
+        setThreadId(lbThreadId)
+      }
+    })
+  return (
+    <div className="min-h-screen">
+      <div className={cn('pb-36 pt-4 md:pt-10', className)}>
+        {messages.length ? (
+          <>
+            <ChatList messages={messages} />
+          </>
+        ) : (
+          <Opening />
+        )}
+      </div>
+      <ChatInput
+        id={id}
+        isLoading={isLoading}
+        stop={stop}
+        append={append}
+        reload={reload}
+        messages={messages}
+        input={input}
+        setInput={setInput}
+      />
+    </div>
+  )
+}
diff --git a/examples/poly-explainer-bot/components/header.tsx b/examples/poly-explainer-bot/components/header.tsx
new file mode 100755
index 00000000..51c3ff8f
--- /dev/null
+++ b/examples/poly-explainer-bot/components/header.tsx
@@ -0,0 +1,47 @@
+import { buttonVariants } from '@/components/ui/button'
+import cn from 'mxcn'
+import Link from 'next/link'
+import { IconFork, IconGitHub } from './ui/icons'
+
+export async function Header() {
+  return (
+    <header className="bg-background sticky top-0 z-50 flex h-16 w-full shrink-0 items-center justify-between px-4">
+      <div className="flex h-16 shrink-0 items-center">
+        <h1>
+          <Link href="/" className="font-bold">
+            <span
+              aria-hidden="true"
+              className="border-muted-foreground/10 bg-muted mr-1 select-none rounded-lg border px-[0.2rem] py-[0.1rem] text-sm font-bold shadow-2xl"
+            >
+              ⌘
+            </span>
+            Langbase
+          </Link>
+        </h1>
+      </div>
+
+      <div className="flex items-center justify-end space-x-2">
+        <a
+          target="_blank"
+          href="https://github.com/LangbaseInc/langbase-examples/tree/main/examples/poly-explainer-bot"
+          rel="noopener noreferrer"
+          className={cn(buttonVariants({ variant: 'outline' }))}
+        >
+          <IconGitHub />
+          <span className="hidden md:flex">GitHub</span>
+        </a>
+        <a
+          target="_blank"
+          href="https://beta.langbase.com/examples/poly-explainer-bot"
+          rel="noopener noreferrer"
+          className={cn(buttonVariants({ variant: 'default' }))}
+        >
+          <IconFork />
+          <span className="hidden md:flex gap-1">
+            Fork on <span className="font-bold">Langbase</span>
+          </span>
+        </a>
+      </div>
+    </header>
+  )
+}
diff --git a/examples/poly-explainer-bot/components/markdown.tsx b/examples/poly-explainer-bot/components/markdown.tsx
new file mode 100755
index 00000000..d4491467
--- /dev/null
+++ b/examples/poly-explainer-bot/components/markdown.tsx
@@ -0,0 +1,9 @@
+import { FC, memo } from 'react'
+import ReactMarkdown, { Options } from 'react-markdown'
+
+export const MemoizedReactMarkdown: FC<Options> = memo(
+  ReactMarkdown,
+  (prevProps, nextProps) =>
+    prevProps.children === nextProps.children &&
+    prevProps.className === nextProps.className
+)
diff --git a/examples/poly-explainer-bot/components/opening.tsx b/examples/poly-explainer-bot/components/opening.tsx
new file mode 100755
index 00000000..60e79d2a
--- /dev/null
+++ b/examples/poly-explainer-bot/components/opening.tsx
@@ -0,0 +1,81 @@
+import Link from 'next/link'
+
+export function Opening() {
+  return (
+    <div className="mx-auto max-w-3xl px-2 sm:max-w-4xl sm:px-0">
+      <div className="light:ring-ring:ring-border ring-ring/10 relative my-7 rounded-lg py-3.5 pl-[1.625rem] pr-4 ring-1 ring-inset [--callout-border:theme(colors.indigo.400)] [--callout-icon:theme(colors.indigo.400)] [--callout-title:theme(colors.indigo.400)] dark:[--callout-border:theme(colors.indigo.400)] dark:[--callout-icon:theme(colors.indigo.400)] dark:[--callout-title:theme(colors.indigo.400)] [&>*]:my-0 [&>*]:py-0">
+        <div className="absolute inset-y-2 left-2 w-0.5 rounded-full bg-[--callout-border]"></div>
+        <div className="mb-2 mt-0 flex items-center justify-start gap-1">
+          <span className="text-xs font-medium text-[--callout-title]">
+            Chatbot Example
+          </span>
+        </div>
+
+        <div className="mt-2">
+          <header className="mb-8">
+            <h4 className="text-foreground text-sm sm:text-base mt-4 flex gap-1 tracking-wide">
+              <span>PolyExplainer Chatbot by a</span>
+              <Link
+                target="_blank"
+                className="underline hover:text-indigo-400 mb-2"
+                href="https://beta.langbase.com/examples/poly-explainer-bot"
+              >
+                <span className="font-bold">pipe on ⌘ Langbase</span>
+              </Link>
+            </h4>
+            <h5 className="text-sm text-muted-foreground">
+              Ship hyper-personalized AI assistants with memory.
+            </h5>
+          </header>
+
+          <div className="mt-4 flex flex-col gap-4 text-sm [&>p]:my-0 [&>p]:py-0">
+            <p>Learn more by checking out:</p>
+            <div className="flex flex-col gap-4 mt-2 text-sm">
+              <Dlink href="https://beta.langbase.com/examples/poly-explainer-bot">
+                <span>1.</span>
+                <span>Fork this PolyExplainer Chatbot Pipe on ⌘ Langbase</span>
+              </Dlink>
+              <Dlink href="https://github.com/LangbaseInc/langbase-examples/tree/main/examples/poly-explainer-bot">
+                <span>2.</span>
+                <span>Use LangUI.dev's open source code components</span>
+              </Dlink>
+
+              <Dlink href="https://langbase.com/docs/pipe/quickstart">
+                <span>3.</span>
+                <span>Go through Documentaion: Pipe Quickstart </span>
+              </Dlink>
+              <Dlink href="https://langbase.com/docs">
+                <span>4.</span>
+                <span>
+                  Learn more about Pipes & Memory features on ⌘ Langbase
+                </span>
+              </Dlink>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  )
+}
+
+// Description Link
+function Dlink({
+  href,
+  children,
+  ...props
+}: {
+  href: string
+  children: React.ReactNode
+  [key: string]: any
+}) {
+  return (
+    <Link
+      href={href}
+      target="_blank"
+      className="flex hover:text-indigo-400 flex items-center gap-2 [&>span:first-child]:text-indigo-400"
+      {...props}
+    >
+      {children}
+    </Link>
+  )
+}
diff --git a/examples/poly-explainer-bot/components/prompt-form.tsx b/examples/poly-explainer-bot/components/prompt-form.tsx
new file mode 100755
index 00000000..49b6cb77
--- /dev/null
+++ b/examples/poly-explainer-bot/components/prompt-form.tsx
@@ -0,0 +1,95 @@
+import { Button } from '@/components/ui/button'
+import { IconChat, IconCommand, IconSpinner } from '@/components/ui/icons'
+import { useEnterSubmit } from '@/lib/hooks/use-enter-submit'
+import { UseChatHelpers } from 'ai/react'
+import * as React from 'react'
+import Textarea from 'react-textarea-autosize'
+
+export interface PromptProps
+  extends Pick<UseChatHelpers, 'input' | 'setInput'> {
+  onSubmit: (value: string) => Promise<void>
+  isLoading: boolean
+}
+
+export function PromptForm({
+  onSubmit,
+  input,
+  setInput,
+  isLoading
+}: PromptProps) {
+  const { formRef, onKeyDown } = useEnterSubmit()
+  const inputRef = React.useRef<HTMLTextAreaElement>(null)
+
+  React.useEffect(() => {
+    if (inputRef.current) {
+      inputRef.current.focus()
+    }
+  }, [])
+
+  return (
+    <form
+      onSubmit={async e => {
+        e.preventDefault()
+        if (!input?.trim()) {
+          return
+        }
+        setInput('')
+        await onSubmit(input)
+      }}
+      ref={formRef}
+    >
+      <div className="bg-background relative flex max-h-60 w-full grow flex-col overflow-hidden px-2 pb-2 sm:rounded-2xl sm:border">
+        <div className="flex w-full flex-col">
+          <label
+            htmlFor="playground"
+            className="text-config text-foreground flex justify-between gap-y-4 rounded-xl px-3 py-4 font-medium leading-6 md:flex-row md:items-center md:gap-y-0"
+          >
+            <div className="flex items-center gap-x-2">
+              <IconChat
+                className="text-muted-foreground/50 h-5 w-5"
+                aria-hidden="true"
+              />
+              <h3>Chat</h3>
+            </div>
+
+            <div className="flex items-center justify-center gap-2 md:justify-start">
+              {/* Reset chat */}
+              <Button
+                variant="ghost"
+                className="max-w-xs"
+                onClick={e => {
+                  e.preventDefault()
+                  location.reload()
+                }}
+              >
+                Reset
+              </Button>
+              {/* Send button */}
+              <Button type="submit" disabled={isLoading || input === ''}>
+                {isLoading ? (
+                  <IconSpinner />
+                ) : (
+                  <IconCommand className="size-4" />
+                )}
+                Send
+                <span className="sr-only">Send message</span>
+              </Button>
+            </div>
+          </label>
+        </div>
+        <Textarea
+          ref={inputRef}
+          tabIndex={0}
+          onKeyDown={onKeyDown}
+          rows={1}
+          maxRows={10}
+          value={input}
+          onChange={e => setInput(e.target.value)}
+          placeholder="Enter your prompt message..."
+          spellCheck={false}
+          className="bg-muted min-h-[60px] w-full resize-none rounded-lg px-4 py-[1.3rem] focus-within:outline-none sm:text-sm"
+        />
+      </div>
+    </form>
+  )
+}
diff --git a/examples/poly-explainer-bot/components/ui/button.tsx b/examples/poly-explainer-bot/components/ui/button.tsx
new file mode 100755
index 00000000..73643111
--- /dev/null
+++ b/examples/poly-explainer-bot/components/ui/button.tsx
@@ -0,0 +1,70 @@
+import { Slot } from '@radix-ui/react-slot'
+import { cva, type VariantProps } from 'class-variance-authority'
+import * as React from 'react'
+
+import cn from 'mxcn'
+
+const buttonVariants = cva(
+  'focus-visible:ring-ring-muted-foreground/25 inline-flex cursor-pointer select-none items-center justify-center rounded-lg text-sm font-medium transition-colors focus:ring-1 focus:ring-muted-foreground/25 focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50 gap-2 group',
+  {
+    variants: {
+      variant: {
+        default:
+          'border border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/90',
+        warn: 'bg-warning text-warning-foreground hover:bg-warning/90 shadow-sm',
+        destructive:
+          'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
+        'destructive-hover':
+          'border border-input bg-muted font-bold text-destructive shadow-sm hover:bg-destructive hover:text-destructive-foreground',
+        'outline-background':
+          'border border-input bg-background text-foreground shadow-sm transition-colors hover:bg-foreground hover:text-background',
+        'outline-inverse':
+          'border border-input bg-muted-foreground text-muted shadow-sm hover:bg-foreground hover:text-background',
+        outline:
+          'border border-input bg-transparent shadow-sm hover:bg-foreground hover:text-background',
+        'outline-muted':
+          'border border-input bg-muted text-foreground shadow-sm hover:bg-foreground hover:text-background',
+        secondary:
+          'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
+        ghost: 'hover:bg-accent hover:text-accent-foreground',
+        link: 'text-primary underline-offset-4 hover:underline',
+        green:
+          'rounded-lg bg-green-500 text-primary shadow-sm hover:bg-green-400 dark:bg-green-700 dark:hover:bg-green-800'
+      },
+      size: {
+        default: 'h-9 px-4 py-2',
+        xs: 'h-6 rounded-lg px-2 text-xs',
+        sm: 'h-8 rounded-lg px-3 text-xs',
+        lg: 'h-10 rounded-lg px-8',
+        xl: 'h-14 rounded-lg px-10',
+        icon: 'h-9 w-9'
+      }
+    },
+    defaultVariants: {
+      variant: 'default',
+      size: 'default'
+    }
+  }
+)
+
+export interface ButtonProps
+  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
+    VariantProps<typeof buttonVariants> {
+  asChild?: boolean
+}
+
+const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
+  ({ className, variant, size, asChild = false, ...props }, ref) => {
+    const Comp = asChild ? Slot : 'button'
+    return (
+      <Comp
+        className={cn(buttonVariants({ variant, size, className }))}
+        ref={ref}
+        {...props}
+      />
+    )
+  }
+)
+Button.displayName = 'Button'
+
+export { Button, buttonVariants }
diff --git a/examples/poly-explainer-bot/components/ui/codeblock.tsx b/examples/poly-explainer-bot/components/ui/codeblock.tsx
new file mode 100755
index 00000000..a2e266a6
--- /dev/null
+++ b/examples/poly-explainer-bot/components/ui/codeblock.tsx
@@ -0,0 +1,145 @@
+// Inspired by Chatbot-UI and modified to fit the needs of this project
+// @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Markdown/CodeBlock.tsx
+
+'use client'
+
+import { FC, memo } from 'react'
+import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
+import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
+
+import { Button } from '@/components/ui/button'
+import { IconCheck, IconCopy, IconDownload } from '@/components/ui/icons'
+import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
+
+interface Props {
+  language: string
+  value: string
+}
+
+interface languageMap {
+  [key: string]: string | undefined
+}
+
+export const programmingLanguages: languageMap = {
+  javascript: '.js',
+  python: '.py',
+  java: '.java',
+  c: '.c',
+  cpp: '.cpp',
+  'c++': '.cpp',
+  'c#': '.cs',
+  ruby: '.rb',
+  php: '.php',
+  swift: '.swift',
+  'objective-c': '.m',
+  kotlin: '.kt',
+  typescript: '.ts',
+  go: '.go',
+  perl: '.pl',
+  rust: '.rs',
+  scala: '.scala',
+  haskell: '.hs',
+  lua: '.lua',
+  shell: '.sh',
+  sql: '.sql',
+  html: '.html',
+  css: '.css'
+  // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
+}
+
+export const generateRandomString = (length: number, lowercase = false) => {
+  const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789' // excluding similar looking characters like Z, 2, I, 1, O, 0
+  let result = ''
+  for (let i = 0; i < length; i++) {
+    result += chars.charAt(Math.floor(Math.random() * chars.length))
+  }
+  return lowercase ? result.toLowerCase() : result
+}
+
+const CodeBlock: FC<Props> = memo(({ language, value }) => {
+  const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
+
+  const downloadAsFile = () => {
+    if (typeof window === 'undefined') {
+      return
+    }
+    const fileExtension = programmingLanguages[language] || '.file'
+    const suggestedFileName = `file-${generateRandomString(
+      3,
+      true
+    )}${fileExtension}`
+    const fileName = window.prompt('Enter file name' || '', suggestedFileName)
+
+    if (!fileName) {
+      // User pressed cancel on prompt.
+      return
+    }
+
+    const blob = new Blob([value], { type: 'text/plain' })
+    const url = URL.createObjectURL(blob)
+    const link = document.createElement('a')
+    link.download = fileName
+    link.href = url
+    link.style.display = 'none'
+    document.body.appendChild(link)
+    link.click()
+    document.body.removeChild(link)
+    URL.revokeObjectURL(url)
+  }
+
+  const onCopy = () => {
+    if (isCopied) return
+    copyToClipboard(value)
+  }
+
+  return (
+    <div className="codeblock relative rounded-xl w-full bg-zinc-950 font-sans">
+      <div className="flex w-full rounded-xl items-center justify-between bg-zinc-800 px-6 py-2 pr-4 text-zinc-100">
+        <span className="text-xs lowercase">{language}</span>
+        <div className="flex items-center space-x-1">
+          <Button
+            variant="ghost"
+            className="hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0"
+            onClick={downloadAsFile}
+            size="icon"
+          >
+            <IconDownload />
+            <span className="sr-only">Download</span>
+          </Button>
+          <Button
+            variant="ghost"
+            size="icon"
+            className="text-xs hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0"
+            onClick={onCopy}
+          >
+            {isCopied ? <IconCheck /> : <IconCopy />}
+            <span className="sr-only">Copy code</span>
+          </Button>
+        </div>
+      </div>
+      <SyntaxHighlighter
+        language={language}
+        style={coldarkDark}
+        PreTag="div"
+        showLineNumbers
+        customStyle={{
+          margin: 0,
+          width: '100%',
+          background: 'transparent',
+          padding: '1.5rem 1rem'
+        }}
+        codeTagProps={{
+          style: {
+            fontSize: '0.9rem',
+            fontFamily: 'var(--font-mono)'
+          }
+        }}
+      >
+        {value}
+      </SyntaxHighlighter>
+    </div>
+  )
+})
+CodeBlock.displayName = 'CodeBlock'
+
+export { CodeBlock }
diff --git a/examples/poly-explainer-bot/components/ui/icons.tsx b/examples/poly-explainer-bot/components/ui/icons.tsx
new file mode 100755
index 00000000..e07c2254
--- /dev/null
+++ b/examples/poly-explainer-bot/components/ui/icons.tsx
@@ -0,0 +1,339 @@
+'use client'
+
+import cn from 'mxcn'
+import * as React from 'react'
+
+function IconOpenAI({ className, ...props }: React.ComponentProps<'svg'>) {
+  return (
+    <svg
+      fill="currentColor"
+      viewBox="0 0 24 24"
+      role="img"
+      xmlns="http://www.w3.org/2000/svg"
+      className={cn('size-4', className)}
+      {...props}
+    >
+      <title>OpenAI icon</title>
+      <path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
+    </svg>
+  )
+}
+
+function IconGitHub({ className, ...props }: React.ComponentProps<'svg'>) {
+  return (
+    <svg
+      role="img"
+      viewBox="0 0 24 24"
+      xmlns="http://www.w3.org/2000/svg"
+      fill="currentColor"
+      className={cn('size-4', className)}
+      {...props}
+    >
+      <title>GitHub</title>
+      <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
+    </svg>
+  )
+}
+
+function IconArrowRight({ className, ...props }: React.ComponentProps<'svg'>) {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      viewBox="0 0 256 256"
+      fill="none"
+      className={cn('size-4', className)}
+      {...props}
+    >
+      <path d="m221.66 133.66-72 72a8 8 0 0 1-11.32-11.32L196.69 136H40a8 8 0 0 1 0-16h156.69l-58.35-58.34a8 8 0 0 1 11.32-11.32l72 72a8 8 0 0 1 0 11.32Z" />
+    </svg>
+  )
+}
+
+function IconUser(props: JSX.IntrinsicElements['svg']) {
+  return (
+    <svg
+      {...props}
+      xmlns="http://www.w3.org/2000/svg"
+      width="24"
+      height="24"
+      viewBox="0 0 24 24"
+      fill="none"
+    >
+      <path
+        d="M4 20V19C4 16.7909 5.79086 15 8 15H16C18.2091 15 20 16.7909 20 19V20M16.5 7.5C16.5 9.98528 14.4853 12 12 12C9.51472 12 7.5 9.98528 7.5 7.5C7.5 5.01472 9.51472 3 12 3C14.4853 3 16.5 5.01472 16.5 7.5Z"
+        stroke="currentColor"
+        strokeWidth="2"
+        strokeLinecap="round"
+      />
+    </svg>
+  )
+}
+
+function IconSpinner({ className, ...props }: React.ComponentProps<'svg'>) {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      viewBox="0 0 256 256"
+      fill="currentColor"
+      className={cn('size-4 animate-spin', className)}
+      {...props}
+    >
+      <path d="M232 128a104 104 0 0 1-208 0c0-41 23.81-78.36 60.66-95.27a8 8 0 0 1 6.68 14.54C60.15 61.59 40 93.27 40 128a88 88 0 0 0 176 0c0-34.73-20.15-66.41-51.34-80.73a8 8 0 0 1 6.68-14.54C208.19 49.64 232 87 232 128Z" />
+    </svg>
+  )
+}
+
+function IconMessage({ className, ...props }: React.ComponentProps<'svg'>) {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      viewBox="0 0 256 256"
+      fill="currentColor"
+      className={cn('size-4', className)}
+      {...props}
+    >
+      <path d="M216 48H40a16 16 0 0 0-16 16v160a15.84 15.84 0 0 0 9.25 14.5A16.05 16.05 0 0 0 40 240a15.89 15.89 0 0 0 10.25-3.78.69.69 0 0 0 .13-.11L82.5 208H216a16 16 0 0 0 16-16V64a16 16 0 0 0-16-16ZM40 224Zm176-32H82.5a16 16 0 0 0-10.3 3.75l-.12.11L40 224V64h176Z" />
+    </svg>
+  )
+}
+
+function IconRefresh({ className, ...props }: React.ComponentProps<'svg'>) {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      viewBox="0 0 256 256"
+      fill="currentColor"
+      className={cn('size-4', className)}
+      {...props}
+    >
+      <path d="M197.67 186.37a8 8 0 0 1 0 11.29C196.58 198.73 170.82 224 128 224c-37.39 0-64.53-22.4-80-39.85V208a8 8 0 0 1-16 0v-48a8 8 0 0 1 8-8h48a8 8 0 0 1 0 16H55.44C67.76 183.35 93 208 128 208c36 0 58.14-21.46 58.36-21.68a8 8 0 0 1 11.31.05ZM216 40a8 8 0 0 0-8 8v23.85C192.53 54.4 165.39 32 128 32c-42.82 0-68.58 25.27-69.66 26.34a8 8 0 0 0 11.3 11.34C69.86 69.46 92 48 128 48c35 0 60.24 24.65 72.56 40H168a8 8 0 0 0 0 16h48a8 8 0 0 0 8-8V48a8 8 0 0 0-8-8Z" />
+    </svg>
+  )
+}
+
+export function IconRegenerate(props: JSX.IntrinsicElements['svg']) {
+  return (
+    <svg
+      {...props}
+      xmlns="http://www.w3.org/2000/svg"
+      width="24"
+      height="24"
+      viewBox="0 0 24 24"
+      fill="none"
+    >
+      <path
+        fillRule="evenodd"
+        clipRule="evenodd"
+        d="M12 5C8.13401 5 5 8.13401 5 12C5 14.3776 6.18526 16.4797 8 17.7455V15C8 14.4477 8.44772 14 9 14C9.55228 14 10 14.4477 10 15V20C10 20.5523 9.55228 21 9 21H4C3.44772 21 3 20.5523 3 20C3 19.4477 3.44772 19 4 19H6.34293C4.30475 17.3508 3 14.8284 3 12C3 7.02944 7.02944 3 12 3C13.1917 3 14.3316 3.23213 15.3752 3.65458C15.8871 3.8618 16.1342 4.4448 15.9269 4.95673C15.7197 5.46867 15.1367 5.71568 14.6248 5.50845C13.8155 5.18088 12.9301 5 12 5Z"
+        fill="currentColor"
+      />
+      <path
+        d="M13 21C13.5523 21 14 20.5523 14 20C14 19.4477 13.5523 19 13 19C12.4477 19 12 19.4477 12 20C12 20.5523 12.4477 21 13 21Z"
+        fill="currentColor"
+      />
+      <path
+        d="M21 11C21 10.4477 20.5523 9.99999 20 9.99999C19.4477 9.99999 19 10.4477 19 11C19 11.5523 19.4477 12 20 12C20.5523 12 21 11.5523 21 11Z"
+        fill="currentColor"
+      />
+      <path
+        d="M19.9295 14.2679C20.4078 14.5441 20.5716 15.1557 20.2955 15.634C20.0193 16.1123 19.4078 16.2761 18.9295 16C18.4512 15.7238 18.2873 15.1123 18.5634 14.634C18.8396 14.1557 19.4512 13.9918 19.9295 14.2679Z"
+        fill="currentColor"
+      />
+      <path
+        d="M17.3676 19.2942C17.8459 19.0181 18.0098 18.4065 17.7336 17.9282C17.4575 17.4499 16.8459 17.286 16.3676 17.5621C15.8893 17.8383 15.7254 18.4499 16.0016 18.9282C16.2777 19.4065 16.8893 19.5703 17.3676 19.2942Z"
+        fill="currentColor"
+      />
+      <path
+        d="M18.9269 7.99998C18.4487 8.27612 17.8371 8.11225 17.5609 7.63396C17.2848 7.15566 17.4487 6.54407 17.9269 6.26793C18.4052 5.99179 19.0168 6.15566 19.293 6.63396C19.5691 7.11225 19.4052 7.72384 18.9269 7.99998Z"
+        fill="currentColor"
+      />
+    </svg>
+  )
+}
+
+function IconStop({ className, ...props }: React.ComponentProps<'svg'>) {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      viewBox="0 0 256 256"
+      fill="currentColor"
+      className={cn('size-4', className)}
+      {...props}
+    >
+      <path d="M128 24a104 104 0 1 0 104 104A104.11 104.11 0 0 0 128 24Zm0 192a88 88 0 1 1 88-88 88.1 88.1 0 0 1-88 88Zm24-120h-48a8 8 0 0 0-8 8v48a8 8 0 0 0 8 8h48a8 8 0 0 0 8-8v-48a8 8 0 0 0-8-8Zm-8 48h-32v-32h32Z" />
+    </svg>
+  )
+}
+
+function IconCopy({ className, ...props }: React.ComponentProps<'svg'>) {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      viewBox="0 0 256 256"
+      fill="currentColor"
+      className={cn('size-4', className)}
+      {...props}
+    >
+      <path d="M216 32H88a8 8 0 0 0-8 8v40H40a8 8 0 0 0-8 8v128a8 8 0 0 0 8 8h128a8 8 0 0 0 8-8v-40h40a8 8 0 0 0 8-8V40a8 8 0 0 0-8-8Zm-56 176H48V96h112Zm48-48h-32V88a8 8 0 0 0-8-8H96V48h112Z" />
+    </svg>
+  )
+}
+
+function IconCheck({ className, ...props }: React.ComponentProps<'svg'>) {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      viewBox="0 0 256 256"
+      fill="currentColor"
+      className={cn('size-4', className)}
+      {...props}
+    >
+      <path d="m229.66 77.66-128 128a8 8 0 0 1-11.32 0l-56-56a8 8 0 0 1 11.32-11.32L96 188.69 218.34 66.34a8 8 0 0 1 11.32 11.32Z" />
+    </svg>
+  )
+}
+
+function IconDownload({ className, ...props }: React.ComponentProps<'svg'>) {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      viewBox="0 0 256 256"
+      fill="currentColor"
+      className={cn('size-4', className)}
+      {...props}
+    >
+      <path d="M224 152v56a16 16 0 0 1-16 16H48a16 16 0 0 1-16-16v-56a8 8 0 0 1 16 0v56h160v-56a8 8 0 0 1 16 0Zm-101.66 5.66a8 8 0 0 0 11.32 0l40-40a8 8 0 0 0-11.32-11.32L136 132.69V40a8 8 0 0 0-16 0v92.69l-26.34-26.35a8 8 0 0 0-11.32 11.32Z" />
+    </svg>
+  )
+}
+
+function IconClose({ className, ...props }: React.ComponentProps<'svg'>) {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      viewBox="0 0 256 256"
+      fill="currentColor"
+      className={cn('size-4', className)}
+      {...props}
+    >
+      <path d="M205.66 194.34a8 8 0 0 1-11.32 11.32L128 139.31l-66.34 66.35a8 8 0 0 1-11.32-11.32L116.69 128 50.34 61.66a8 8 0 0 1 11.32-11.32L128 116.69l66.34-66.35a8 8 0 0 1 11.32 11.32L139.31 128Z" />
+    </svg>
+  )
+}
+
+function IconSparkles({ className, ...props }: React.ComponentProps<'svg'>) {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      fill="none"
+      viewBox="0 0 24 24"
+      strokeWidth={1.5}
+      stroke="currentColor"
+      className={cn('size-4', className)}
+    >
+      <path
+        strokeLinecap="round"
+        strokeLinejoin="round"
+        d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z"
+      />
+    </svg>
+  )
+}
+
+export function IconChat(props: JSX.IntrinsicElements['svg']) {
+  return (
+    <svg
+      {...props}
+      xmlns="http://www.w3.org/2000/svg"
+      width="24"
+      height="24"
+      viewBox="0 0 24 24"
+      fill="none"
+    >
+      <path
+        fillRule="evenodd"
+        clipRule="evenodd"
+        d="M2 9C2 5.68629 4.68629 3 8 3H16C19.3137 3 22 5.68629 22 9V15C22 18.3137 19.3137 21 16 21H3C2.44772 21 2 20.5523 2 20V9ZM9 9C8.44772 9 8 9.44772 8 10C8 10.5523 8.44772 11 9 11H15C15.5523 11 16 10.5523 16 10C16 9.44772 15.5523 9 15 9H9ZM9 13C8.44772 13 8 13.4477 8 14C8 14.5523 8.44772 15 9 15H12C12.5523 15 13 14.5523 13 14C13 13.4477 12.5523 13 12 13H9Z"
+        fill="currentColor"
+      />
+    </svg>
+  )
+}
+
+function IconInfo({ className, ...props }: React.ComponentProps<'svg'>) {
+  return (
+    <svg
+      {...props}
+      xmlns="http://www.w3.org/2000/svg"
+      width="24"
+      height="24"
+      viewBox="0 0 24 24"
+      fill="none"
+      className={cn('size-4', className)}
+    >
+      <path
+        fillRule="evenodd"
+        clipRule="evenodd"
+        d="M8.75869 3C7.95372 2.99999 7.28937 2.99998 6.74818 3.04419C6.18608 3.09012 5.66937 3.18868 5.18404 3.43598C4.43139 3.81947 3.81947 4.43139 3.43598 5.18404C3.18868 5.66937 3.09012 6.18608 3.04419 6.74818C2.99998 7.28937 2.99999 7.95372 3 8.75869V15.2413C2.99999 16.0463 2.99998 16.7106 3.04419 17.2518C3.09012 17.8139 3.18868 18.3306 3.43598 18.816C3.81947 19.5686 4.43139 20.1805 5.18404 20.564C5.66937 20.8113 6.18608 20.9099 6.74818 20.9558C7.28937 21 7.95372 21 8.75868 21H15.2413C16.0463 21 16.7106 21 17.2518 20.9558C17.8139 20.9099 18.3306 20.8113 18.816 20.564C19.5686 20.1805 20.1805 19.5686 20.564 18.816C20.8113 18.3306 20.9099 17.8139 20.9558 17.2518C21 16.7106 21 16.0463 21 15.2413V8.75868C21 7.95372 21 7.28936 20.9558 6.74818C20.9099 6.18608 20.8113 5.66937 20.564 5.18404C20.1805 4.43139 19.5686 3.81947 18.816 3.43598C18.3306 3.18868 17.8139 3.09012 17.2518 3.04419C16.7106 2.99998 16.0463 2.99999 15.2413 3H8.75869ZM9.5 11C9.5 10.4477 9.94772 10 10.5 10H12C12.5523 10 13 10.4477 13 11V16C13 16.5523 12.5523 17 12 17C11.4477 17 11 16.5523 11 16V12H10.5C9.94772 12 9.5 11.5523 9.5 11ZM12 7C11.4477 7 11 7.44772 11 8C11 8.55229 11.4477 9 12 9C12.5523 9 13 8.55229 13 8C13 7.44772 12.5523 7 12 7Z"
+        fill="currentColor"
+      />
+    </svg>
+  )
+}
+
+function IconFork({ className, ...props }: React.ComponentProps<'svg'>) {
+  return (
+    <svg
+      {...props}
+      xmlns="http://www.w3.org/2000/svg"
+      width="24"
+      height="24"
+      viewBox="0 0 24 24"
+      fill="none"
+      className={cn('size-4', className)}
+    >
+      <path
+        d="M3 5.5C3 3.567 4.567 2 6.5 2C8.433 2 10 3.567 10 5.5C10 7.08551 8.94574 8.42479 7.5 8.85506V11H11C11.3643 11 11.7058 11.0974 12 11.2676C12.2942 11.0974 12.6357 11 13 11H16.5V8.85506C15.0543 8.42479 14 7.08551 14 5.5C14 3.567 15.567 2 17.5 2C19.433 2 21 3.567 21 5.5C21 7.08551 19.9457 8.42479 18.5 8.85506V11C18.5 12.1046 17.6046 13 16.5 13H13V15.1449C14.4457 15.5752 15.5 16.9145 15.5 18.5C15.5 20.433 13.933 22 12 22C10.067 22 8.5 20.433 8.5 18.5C8.5 16.9145 9.55426 15.5752 11 15.1449V13H7.5C6.39543 13 5.5 12.1046 5.5 11V8.85506C4.05426 8.42479 3 7.08551 3 5.5Z"
+        fill="currentColor"
+      />
+    </svg>
+  )
+}
+
+export function IconCommand(props: JSX.IntrinsicElements['svg']) {
+  return (
+    <svg
+      {...props}
+      xmlns="http://www.w3.org/2000/svg"
+      width="24"
+      height="24"
+      viewBox="0 0 24 24"
+      fill="none"
+    >
+      <path
+        fillRule="evenodd"
+        clipRule="evenodd"
+        d="M3 6.5C3 4.567 4.567 3 6.5 3C8.433 3 10 4.567 10 6.5V8H14V6.5C14 4.567 15.567 3 17.5 3C19.433 3 21 4.567 21 6.5C21 8.433 19.433 10 17.5 10H16V14H17.5C19.433 14 21 15.567 21 17.5C21 19.433 19.433 21 17.5 21C15.567 21 14 19.433 14 17.5V16H10V17.5C10 19.433 8.433 21 6.5 21C4.567 21 3 19.433 3 17.5C3 15.567 4.567 14 6.5 14H8V10H6.5C4.567 10 3 8.433 3 6.5ZM8 8V6.5C8 5.67157 7.32843 5 6.5 5C5.67157 5 5 5.67157 5 6.5C5 7.32843 5.67157 8 6.5 8H8ZM10 10V14H14V10H10ZM8 16H6.5C5.67157 16 5 16.6716 5 17.5C5 18.3284 5.67157 19 6.5 19C7.32843 19 8 18.3284 8 17.5V16ZM16 16V17.5C16 18.3284 16.6716 19 17.5 19C18.3284 19 19 18.3284 19 17.5C19 16.6716 18.3284 16 17.5 16H16ZM16 8H17.5C18.3284 8 19 7.32843 19 6.5C19 5.67157 18.3284 5 17.5 5C16.6716 5 16 5.67157 16 6.5V8Z"
+        fill="currentColor"
+      />
+    </svg>
+  )
+}
+
+export {
+  IconArrowRight,
+  IconCheck,
+  IconClose,
+  IconCopy,
+  IconDownload,
+  IconFork,
+  IconGitHub,
+  IconInfo,
+  IconMessage,
+  IconOpenAI,
+  IconRefresh,
+  IconSparkles,
+  IconSpinner,
+  IconStop,
+  IconUser
+}
diff --git a/examples/poly-explainer-bot/components/ui/separator.tsx b/examples/poly-explainer-bot/components/ui/separator.tsx
new file mode 100755
index 00000000..7ba06b3a
--- /dev/null
+++ b/examples/poly-explainer-bot/components/ui/separator.tsx
@@ -0,0 +1,32 @@
+'use client'
+
+import * as React from 'react'
+import * as SeparatorPrimitive from '@radix-ui/react-separator'
+import cn from 'mxcn'
+
+const Separator = React.forwardRef<
+  React.ElementRef<typeof SeparatorPrimitive.Root>,
+  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
+>(
+  (
+    { className, orientation = 'horizontal', decorative = true, ...props },
+    ref
+  ) => (
+    <SeparatorPrimitive.Root
+      ref={ref}
+      decorative={decorative}
+      orientation={orientation}
+      className={cn(
+        'shrink-0 bg-border',
+        orientation === 'horizontal'
+          ? 'h-[2px] w-full border-0 border-b-[1px] border-border bg-muted'
+          : 'h-full w-[1px]',
+        className
+      )}
+      {...props}
+    />
+  )
+)
+Separator.displayName = SeparatorPrimitive.Root.displayName
+
+export { Separator }
diff --git a/examples/poly-explainer-bot/components/ui/textarea.tsx b/examples/poly-explainer-bot/components/ui/textarea.tsx
new file mode 100755
index 00000000..93b82cb8
--- /dev/null
+++ b/examples/poly-explainer-bot/components/ui/textarea.tsx
@@ -0,0 +1,23 @@
+import cn from 'mxcn'
+import * as React from 'react'
+
+export interface TextareaProps
+  extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
+
+const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
+  ({ className, ...props }, ref) => {
+    return (
+      <textarea
+        className={cn(
+          'border-input ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[80px] w-full rounded-md border bg-transparent px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
+          className
+        )}
+        ref={ref}
+        {...props}
+      />
+    )
+  }
+)
+Textarea.displayName = 'Textarea'
+
+export { Textarea }
diff --git a/examples/poly-explainer-bot/lib/hooks/use-copy-to-clipboard.tsx b/examples/poly-explainer-bot/lib/hooks/use-copy-to-clipboard.tsx
new file mode 100755
index 00000000..62f7156d
--- /dev/null
+++ b/examples/poly-explainer-bot/lib/hooks/use-copy-to-clipboard.tsx
@@ -0,0 +1,33 @@
+'use client'
+
+import * as React from 'react'
+
+export interface useCopyToClipboardProps {
+  timeout?: number
+}
+
+export function useCopyToClipboard({
+  timeout = 2000
+}: useCopyToClipboardProps) {
+  const [isCopied, setIsCopied] = React.useState<Boolean>(false)
+
+  const copyToClipboard = (value: string) => {
+    if (typeof window === 'undefined' || !navigator.clipboard?.writeText) {
+      return
+    }
+
+    if (!value) {
+      return
+    }
+
+    navigator.clipboard.writeText(value).then(() => {
+      setIsCopied(true)
+
+      setTimeout(() => {
+        setIsCopied(false)
+      }, timeout)
+    })
+  }
+
+  return { isCopied, copyToClipboard }
+}
diff --git a/examples/poly-explainer-bot/lib/hooks/use-enter-submit.tsx b/examples/poly-explainer-bot/lib/hooks/use-enter-submit.tsx
new file mode 100755
index 00000000..d66b2d32
--- /dev/null
+++ b/examples/poly-explainer-bot/lib/hooks/use-enter-submit.tsx
@@ -0,0 +1,23 @@
+import { useRef, type RefObject } from 'react'
+
+export function useEnterSubmit(): {
+  formRef: RefObject<HTMLFormElement>
+  onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void
+} {
+  const formRef = useRef<HTMLFormElement>(null)
+
+  const handleKeyDown = (
+    event: React.KeyboardEvent<HTMLTextAreaElement>
+  ): void => {
+    if (
+      event.key === 'Enter' &&
+      !event.shiftKey &&
+      !event.nativeEvent.isComposing
+    ) {
+      formRef.current?.requestSubmit()
+      event.preventDefault()
+    }
+  }
+
+  return { formRef, onKeyDown: handleKeyDown }
+}
diff --git a/examples/poly-explainer-bot/lib/types.ts b/examples/poly-explainer-bot/lib/types.ts
new file mode 100755
index 00000000..cd0a5f03
--- /dev/null
+++ b/examples/poly-explainer-bot/lib/types.ts
@@ -0,0 +1,11 @@
+import { type Message } from 'ai'
+
+export interface Chat extends Record<string, any> {
+  id: string
+  title: string
+  createdAt: Date
+  userId: string
+  path: string
+  messages: Message[]
+  sharePath?: string
+}
diff --git a/examples/poly-explainer-bot/lib/utils.ts b/examples/poly-explainer-bot/lib/utils.ts
new file mode 100755
index 00000000..a5cc2c68
--- /dev/null
+++ b/examples/poly-explainer-bot/lib/utils.ts
@@ -0,0 +1,8 @@
+export function formatDate(input: string | number | Date): string {
+  const date = new Date(input)
+  return date.toLocaleDateString('en-US', {
+    month: 'long',
+    day: 'numeric',
+    year: 'numeric'
+  })
+}
diff --git a/examples/poly-explainer-bot/next.config.js b/examples/poly-explainer-bot/next.config.js
new file mode 100755
index 00000000..9b7d60b3
--- /dev/null
+++ b/examples/poly-explainer-bot/next.config.js
@@ -0,0 +1,13 @@
+/** @type {import('next').NextConfig} */
+module.exports = {
+  images: {
+    remotePatterns: [
+      {
+        protocol: 'https',
+        hostname: 'avatars.githubusercontent.com',
+        port: '',
+        pathname: '**'
+      }
+    ]
+  }
+}
diff --git a/examples/poly-explainer-bot/package.json b/examples/poly-explainer-bot/package.json
new file mode 100755
index 00000000..b5dd771a
--- /dev/null
+++ b/examples/poly-explainer-bot/package.json
@@ -0,0 +1,49 @@
+{
+  "name": "poly-explainer-bot-example",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "dev": "next dev",
+    "build": "next build",
+    "start": "next start",
+    "lint": "next lint"
+  },
+  "dependencies": {
+    "@radix-ui/react-label": "^2.0.2",
+    "@radix-ui/react-separator": "^1.0.3",
+    "@radix-ui/react-slot": "^1.0.2",
+    "@radix-ui/react-switch": "^1.0.3",
+    "ai": "3.0.16",
+    "class-variance-authority": "^0.7.0",
+    "mxcn": "^2.0.0",
+    "next": "14.0.4",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "react-markdown": "^8.0.7",
+    "react-syntax-highlighter": "^15.5.0",
+    "react-textarea-autosize": "^8.4.1",
+    "remark-gfm": "^3.0.1",
+    "remark-math": "^5.1.1",
+    "sonner": "^1.5.0"
+  },
+  "devDependencies": {
+    "@tailwindcss/typography": "^0.5.9",
+    "@types/node": "^17.0.12",
+    "@types/react": "^18.0.22",
+    "@types/react-dom": "^18.0.7",
+    "@types/react-syntax-highlighter": "^15.5.6",
+    "@typescript-eslint/parser": "^5.59.7",
+    "autoprefixer": "^10.4.13",
+    "eslint": "^8.31.0",
+    "eslint-config-next": "13.4.19",
+    "eslint-config-prettier": "^8.3.0",
+    "eslint-plugin-tailwindcss": "^3.12.0",
+    "postcss": "^8.4.21",
+    "prettier": "^3.0.3",
+    "prettier-plugin-tailwindcss": "^0.5.4",
+    "tailwind-merge": "^1.12.0",
+    "tailwindcss": "^3.4.1",
+    "tailwindcss-animate": "^1.0.5",
+    "typescript": "^5.2.2"
+  }
+}
diff --git a/examples/poly-explainer-bot/postcss.config.js b/examples/poly-explainer-bot/postcss.config.js
new file mode 100755
index 00000000..33ad091d
--- /dev/null
+++ b/examples/poly-explainer-bot/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+  plugins: {
+    tailwindcss: {},
+    autoprefixer: {},
+  },
+}
diff --git a/examples/poly-explainer-bot/prettier.config.cjs b/examples/poly-explainer-bot/prettier.config.cjs
new file mode 100755
index 00000000..ec67ed3d
--- /dev/null
+++ b/examples/poly-explainer-bot/prettier.config.cjs
@@ -0,0 +1,34 @@
+/** @type {import('prettier').Config} */
+module.exports = {
+  endOfLine: "lf",
+  semi: false,
+  useTabs: false,
+  singleQuote: true,
+  arrowParens: "avoid",
+  tabWidth: 2,
+  trailingComma: "none",
+  importOrder: [
+    "^(react/(.*)$)|^(react$)",
+    "^(next/(.*)$)|^(next$)",
+    "<THIRD_PARTY_MODULES>",
+    "",
+    "^types$",
+    "^@/types/(.*)$",
+    "^@/config/(.*)$",
+    "^@/lib/(.*)$",
+    "^@/hooks/(.*)$",
+    "^@/components/ui/(.*)$",
+    "^@/components/(.*)$",
+    "^@/registry/(.*)$",
+    "^@/styles/(.*)$",
+    "^@/app/(.*)$",
+    "",
+    "^[./]"
+  ],
+  importOrderSeparation: false,
+  importOrderSortSpecifiers: true,
+  importOrderBuiltinModulesToTop: true,
+  importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
+  importOrderMergeDuplicateImports: true,
+  importOrderCombineTypeAndValueImports: true
+}
diff --git a/examples/poly-explainer-bot/public/chatbot.jpg b/examples/poly-explainer-bot/public/chatbot.jpg
new file mode 100644
index 00000000..577b8194
Binary files /dev/null and b/examples/poly-explainer-bot/public/chatbot.jpg differ
diff --git a/examples/poly-explainer-bot/tailwind.config.ts b/examples/poly-explainer-bot/tailwind.config.ts
new file mode 100755
index 00000000..8a0ab881
--- /dev/null
+++ b/examples/poly-explainer-bot/tailwind.config.ts
@@ -0,0 +1,177 @@
+const { fontFamily } = require('tailwindcss/defaultTheme')
+import type { Config } from 'tailwindcss'
+
+const config: Config = {
+  darkMode: 'selector',
+  content: [
+    './pages/**/*.{js,ts,jsx,tsx,mdx}',
+    './components/**/*.{js,ts,jsx,tsx,mdx}',
+    './app/**/*.{js,ts,jsx,tsx,mdx}'
+  ],
+  theme: {
+    transparent: 'transparent',
+    current: 'currentColor',
+    extend: {
+      backgroundImage: {
+        'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
+        'gradient-conic':
+          'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))'
+      },
+      colors: {
+        border: 'hsl(var(--border))',
+        input: 'hsl(var(--input))',
+        ring: 'hsl(var(--ring))',
+        background: 'hsl(var(--background))',
+        foreground: 'hsl(var(--foreground))',
+        primary: {
+          DEFAULT: 'hsl(var(--primary))',
+          foreground: 'hsl(var(--primary-foreground))'
+        },
+        secondary: {
+          DEFAULT: 'hsl(var(--secondary))',
+          foreground: 'hsl(var(--secondary-foreground))'
+        },
+        destructive: {
+          DEFAULT: 'hsl(var(--destructive))',
+          foreground: 'hsl(var(--destructive-foreground))'
+        },
+        warning: {
+          DEFAULT: 'hsl(var(--warning))',
+          foreground: 'hsl(var(--warning-foreground))'
+        },
+        muted: {
+          DEFAULT: 'hsl(var(--muted))',
+          foreground: 'hsl(var(--muted-foreground))'
+        },
+        accent: {
+          DEFAULT: 'hsl(var(--accent))',
+          foreground: 'hsl(var(--accent-foreground))'
+        },
+        popover: {
+          DEFAULT: 'hsl(var(--popover))',
+          foreground: 'hsl(var(--popover-foreground))'
+        },
+        card: {
+          DEFAULT: 'hsl(var(--card))',
+          foreground: 'hsl(var(--card-foreground))'
+        },
+        // light mode
+        tremor: {
+          brand: {
+            faint: '#eff6ff', // blue-50
+            muted: '#bfdbfe', // blue-200
+            subtle: '#60a5fa', // blue-400
+            DEFAULT: '#3b82f6', // blue-500
+            emphasis: '#1d4ed8', // blue-700
+            inverted: '#ffffff' // white
+          },
+          background: {
+            muted: '#f9fafb', // gray-50
+            subtle: '#f3f4f6', // gray-100
+            DEFAULT: '#ffffff', // white
+            emphasis: '#374151' // gray-700
+          },
+          border: {
+            DEFAULT: '#e5e7eb' // gray-200
+          },
+          ring: {
+            DEFAULT: '#e5e7eb' // gray-200
+          },
+          content: {
+            subtle: '#9ca3af', // gray-400
+            DEFAULT: '#6b7280', // gray-500
+            emphasis: '#374151', // gray-700
+            strong: '#111827', // gray-900
+            inverted: '#ffffff' // white
+          }
+        },
+        // dark mode
+        'dark-tremor': {
+          brand: {
+            faint: 'hsl(var(--background))', // custom
+            muted: 'hsl(var(--muted))', // blue-950
+            subtle: '#1e40af', // blue-800
+            DEFAULT: '#3b82f6', // blue-500
+            emphasis: '#60a5fa', // blue-400
+            inverted: 'hsl(var(--muted))' // gray-950
+          },
+          background: {
+            muted: 'hsl(var(--muted))', // custom
+            subtle: 'hsl(var(--muted))', // gray-800
+            DEFAULT: 'hsl(var(--background))', // gray-900
+            emphasis: '#d1d5db' // gray-300
+          },
+          border: {
+            DEFAULT: 'hsl(var(--border))' // gray-800
+          },
+          ring: {
+            DEFAULT: 'hsl(var(--muted))' // gray-800
+          },
+          content: {
+            subtle: '#4b5563', // gray-600
+            DEFAULT: '#6b7280', // gray-600
+            emphasis: '#e5e7eb', // gray-200
+            strong: '#f9fafb', // gray-50
+            inverted: '#000000' // black
+          }
+        }
+      },
+      boxShadow: {
+        // light
+        'tremor-input': '0 1px 2px 0 rgb(0 0 0 / 0.05)',
+        'tremor-card':
+          '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
+        'tremor-dropdown':
+          '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
+        // dark
+        'dark-tremor-input': '0 1px 2px 0 rgb(0 0 0 / 0.05)',
+        'dark-tremor-card':
+          '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
+        'dark-tremor-dropdown':
+          '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)'
+      },
+      borderRadius: {
+        lg: `var(--radius)`,
+        md: `calc(var(--radius) - 2px)`,
+        sm: 'calc(var(--radius) - 4px)',
+        'tremor-small': '0.375rem',
+        'tremor-default': '0.5rem',
+        'tremor-full': '9999px'
+      },
+      fontSize: {
+        // 'tremor-label': ['0.75rem'],
+        'tremor-label': '0.75rem',
+        'tremor-default': ['0.875rem', { lineHeight: '1.25rem' }],
+        'tremor-title': ['1.125rem', { lineHeight: '1.75rem' }],
+        'tremor-metric': ['1.875rem', { lineHeight: '2.25rem' }]
+      },
+      fontFamily: {
+        sans: ['var(--font-sans)', ...fontFamily.sans]
+      },
+      keyframes: {
+        'accordion-down': {
+          from: { height: '0' },
+          to: { height: 'var(--radix-accordion-content-height)' }
+        },
+        'accordion-up': {
+          from: { height: 'var(--radix-accordion-content-height)' },
+          to: { height: '0' }
+        },
+
+        slide: {
+          to: {
+            transform: 'translate(calc(100cqw - 100%), 0)'
+          }
+        }
+      },
+      animation: {
+        'accordion-down': 'accordion-down 0.2s ease-out',
+        'accordion-up': 'accordion-up 0.2s ease-out',
+        // spin: 'spin calc(var(--speed) * 2) infinite linear',
+        slide: 'slide var(--speed) ease-in-out infinite alternate'
+      }
+    }
+  },
+  plugins: [require('@tailwindcss/typography')]
+}
+export default config
diff --git a/examples/poly-explainer-bot/tsconfig.json b/examples/poly-explainer-bot/tsconfig.json
new file mode 100755
index 00000000..1b03bc25
--- /dev/null
+++ b/examples/poly-explainer-bot/tsconfig.json
@@ -0,0 +1,29 @@
+{
+  "compilerOptions": {
+    "lib": ["dom", "dom.iterable", "esnext"],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "strict": true,
+    "forceConsistentCasingInFileNames": true,
+    "noEmit": true,
+    "incremental": true,
+    "esModuleInterop": true,
+    "module": "esnext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "jsx": "preserve",
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./*"]
+    },
+    "plugins": [
+      {
+        "name": "next"
+      }
+    ],
+    "strictNullChecks": true
+  },
+  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+  "exclude": ["node_modules"]
+}