diff --git a/examples/proverb-pro/.env.example b/examples/proverb-pro/.env.example
new file mode 100644
index 00000000..85d27328
--- /dev/null
+++ b/examples/proverb-pro/.env.example
@@ -0,0 +1 @@
+NEXT_LB_PIPE_API_KEY=""
\ No newline at end of file
diff --git a/examples/proverb-pro/.gitignore b/examples/proverb-pro/.gitignore
new file mode 100644
index 00000000..54a02eee
--- /dev/null
+++ b/examples/proverb-pro/.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/proverb-pro/README.md b/examples/proverb-pro/README.md
new file mode 100755
index 00000000..1da9f408
--- /dev/null
+++ b/examples/proverb-pro/README.md
@@ -0,0 +1,79 @@
+![Proverb Pro Chatbot by ⌘ Langbase][cover]
+
+![License: MIT][mit] [![Fork to ⌘ Langbase][fork]][pipe]
+
+## Build a Proverb Pro 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
+
+- 💬 [Proverb Pro 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 [Proverb Pro 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 [Proverb Pro 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]
+**_Built by ⌘ [Langbase.com][lb] — Ship hyper-personalized AI assistants with memory!_**
+
+
+[demo]: https://proverb-pro.langbase.dev
+[lb]: https://langbase.com
+[pipe]: https://beta.langbase.com/examples/proverb-pro
+[gh]: https://github.com/LangbaseInc/langbase-examples/tree/main/examples/proverb-pro
+[cover]:https://raw.githubusercontent.com/LangbaseInc/docs-images/main/examples/proverb-pro/proverb-pro.png
+[download]:https://download-directory.github.io/?url=https://github.com/LangbaseInc/langbase-examples/tree/main/examples/proverb-pro
+[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/proverb-pro/app/api/chat/route.ts b/examples/proverb-pro/app/api/chat/route.ts
new file mode 100755
index 00000000..95af6ed7
--- /dev/null
+++ b/examples/proverb-pro/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/proverb-pro/app/favicon.ico b/examples/proverb-pro/app/favicon.ico
new file mode 100644
index 00000000..7c6d4a8a
Binary files /dev/null and b/examples/proverb-pro/app/favicon.ico differ
diff --git a/examples/proverb-pro/app/globals.css b/examples/proverb-pro/app/globals.css
new file mode 100755
index 00000000..36084b74
--- /dev/null
+++ b/examples/proverb-pro/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/proverb-pro/app/layout.tsx b/examples/proverb-pro/app/layout.tsx
new file mode 100755
index 00000000..13b48a53
--- /dev/null
+++ b/examples/proverb-pro/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: 'Proverb Pro Chatbot - Langbase',
+ description: 'Build a Proverb Pro Chatbot with ⌘ Langbase using any LLM model.',
+ keywords: ['Proverb Pro', 'Chatbot', 'Langbase']
+}
+
+export default function RootLayout({
+ children
+}: Readonly<{
+ children: React.ReactNode
+}>) {
+ return (
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+ )
+}
diff --git a/examples/proverb-pro/app/page.tsx b/examples/proverb-pro/app/page.tsx
new file mode 100755
index 00000000..f278d230
--- /dev/null
+++ b/examples/proverb-pro/app/page.tsx
@@ -0,0 +1,7 @@
+import { Chatbot } from '@/components/chatbot-page'
+
+export const runtime = 'edge'
+
+export default function ChatPage() {
+ return
+}
diff --git a/examples/proverb-pro/components/chat-input.tsx b/examples/proverb-pro/components/chat-input.tsx
new file mode 100755
index 00000000..569fbc6f
--- /dev/null
+++ b/examples/proverb-pro/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 (
+
+
+
+ {isLoading ? (
+ stop()}
+ className="bg-background"
+ size={'sm'}
+ >
+
+ Stop generating
+
+ ) : (
+ messages?.length > 0 && (
+ reload()}
+ className="bg-background"
+ size={'sm'}
+ >
+
+ Regenerate response
+
+ )
+ )}
+
+
+
{
+ await append({
+ content: value,
+ role: 'user'
+ })
+ }}
+ input={input}
+ setInput={setInput}
+ isLoading={isLoading}
+ />
+
+
+
+ )
+}
diff --git a/examples/proverb-pro/components/chat-list.tsx b/examples/proverb-pro/components/chat-list.tsx
new file mode 100755
index 00000000..32074928
--- /dev/null
+++ b/examples/proverb-pro/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 (
+
+ {messages.map((message, index) => (
+
+
+ {index < messages.length - 1 && (
+
+ )}
+
+ ))}
+
+ )
+}
diff --git a/examples/proverb-pro/components/chat-message-actions.tsx b/examples/proverb-pro/components/chat-message-actions.tsx
new file mode 100755
index 00000000..37f68b2a
--- /dev/null
+++ b/examples/proverb-pro/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 (
+
+
+ {isCopied ? : }
+ Copy message
+
+
+ )
+}
diff --git a/examples/proverb-pro/components/chat-message.tsx b/examples/proverb-pro/components/chat-message.tsx
new file mode 100755
index 00000000..c359618d
--- /dev/null
+++ b/examples/proverb-pro/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 (
+
+
+ {message.role === 'user' ? : }
+
+
+ {children}
+ },
+ code({ node, inline, className, children, ...props }) {
+ if (children.length) {
+ if (children[0] == '▍') {
+ return (
+ ▍
+ )
+ }
+
+ children[0] = (children[0] as string).replace('`▍`', '▍')
+ }
+
+ const match = /language-(\w+)/.exec(className || '')
+
+ if (inline) {
+ return (
+
+ {children}
+
+ )
+ }
+
+ return (
+
+ )
+ }
+ }}
+ >
+ {message.content}
+
+
+
+
+ )
+}
diff --git a/examples/proverb-pro/components/chatbot-page.tsx b/examples/proverb-pro/components/chatbot-page.tsx
new file mode 100755
index 00000000..80203378
--- /dev/null
+++ b/examples/proverb-pro/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)
+ 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 (
+
+
+ {messages.length ? (
+ <>
+
+ >
+ ) : (
+
+ )}
+
+
+
+ )
+}
diff --git a/examples/proverb-pro/components/header.tsx b/examples/proverb-pro/components/header.tsx
new file mode 100755
index 00000000..16953c40
--- /dev/null
+++ b/examples/proverb-pro/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 (
+
+ )
+}
diff --git a/examples/proverb-pro/components/markdown.tsx b/examples/proverb-pro/components/markdown.tsx
new file mode 100755
index 00000000..d4491467
--- /dev/null
+++ b/examples/proverb-pro/components/markdown.tsx
@@ -0,0 +1,9 @@
+import { FC, memo } from 'react'
+import ReactMarkdown, { Options } from 'react-markdown'
+
+export const MemoizedReactMarkdown: FC = memo(
+ ReactMarkdown,
+ (prevProps, nextProps) =>
+ prevProps.children === nextProps.children &&
+ prevProps.className === nextProps.className
+)
diff --git a/examples/proverb-pro/components/opening.tsx b/examples/proverb-pro/components/opening.tsx
new file mode 100755
index 00000000..11ad082d
--- /dev/null
+++ b/examples/proverb-pro/components/opening.tsx
@@ -0,0 +1,81 @@
+import Link from 'next/link'
+
+export function Opening() {
+ return (
+
+
+
+
+
+ Chatbot Example
+
+
+
+
+
+
+ Proverb Pro Chatbot by a
+
+ pipe on ⌘ Langbase
+
+
+
+ Ship hyper-personalized AI assistants with memory.
+
+
+
+
+
Learn more by checking out:
+
+
+ 1.
+ Fork this Proverb Pro Chatbot Pipe on ⌘ Langbase
+
+
+ 2.
+ Use LangUI.dev's open source code components
+
+
+
+ 3.
+ Go through Documentaion: Pipe Quickstart
+
+
+ 4.
+
+ Learn more about Pipes & Memory features on ⌘ Langbase
+
+
+
+
+
+
+
+ )
+}
+
+// Description Link
+function Dlink({
+ href,
+ children,
+ ...props
+}: {
+ href: string
+ children: React.ReactNode
+ [key: string]: any
+}) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/examples/proverb-pro/components/prompt-form.tsx b/examples/proverb-pro/components/prompt-form.tsx
new file mode 100755
index 00000000..49b6cb77
--- /dev/null
+++ b/examples/proverb-pro/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 {
+ onSubmit: (value: string) => Promise
+ isLoading: boolean
+}
+
+export function PromptForm({
+ onSubmit,
+ input,
+ setInput,
+ isLoading
+}: PromptProps) {
+ const { formRef, onKeyDown } = useEnterSubmit()
+ const inputRef = React.useRef(null)
+
+ React.useEffect(() => {
+ if (inputRef.current) {
+ inputRef.current.focus()
+ }
+ }, [])
+
+ return (
+
+ )
+}
diff --git a/examples/proverb-pro/components/ui/button.tsx b/examples/proverb-pro/components/ui/button.tsx
new file mode 100755
index 00000000..73643111
--- /dev/null
+++ b/examples/proverb-pro/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,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : 'button'
+ return (
+
+ )
+ }
+)
+Button.displayName = 'Button'
+
+export { Button, buttonVariants }
diff --git a/examples/proverb-pro/components/ui/codeblock.tsx b/examples/proverb-pro/components/ui/codeblock.tsx
new file mode 100755
index 00000000..a2e266a6
--- /dev/null
+++ b/examples/proverb-pro/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 = 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 (
+
+
+
{language}
+
+
+
+ Download
+
+
+ {isCopied ? : }
+ Copy code
+
+
+
+
+ {value}
+
+
+ )
+})
+CodeBlock.displayName = 'CodeBlock'
+
+export { CodeBlock }
diff --git a/examples/proverb-pro/components/ui/icons.tsx b/examples/proverb-pro/components/ui/icons.tsx
new file mode 100755
index 00000000..e07c2254
--- /dev/null
+++ b/examples/proverb-pro/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 (
+
+ OpenAI icon
+
+
+ )
+}
+
+function IconGitHub({ className, ...props }: React.ComponentProps<'svg'>) {
+ return (
+
+ GitHub
+
+
+ )
+}
+
+function IconArrowRight({ className, ...props }: React.ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+function IconUser(props: JSX.IntrinsicElements['svg']) {
+ return (
+
+
+
+ )
+}
+
+function IconSpinner({ className, ...props }: React.ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+function IconMessage({ className, ...props }: React.ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+function IconRefresh({ className, ...props }: React.ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function IconRegenerate(props: JSX.IntrinsicElements['svg']) {
+ return (
+
+
+
+
+
+
+
+
+ )
+}
+
+function IconStop({ className, ...props }: React.ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+function IconCopy({ className, ...props }: React.ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+function IconCheck({ className, ...props }: React.ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+function IconDownload({ className, ...props }: React.ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+function IconClose({ className, ...props }: React.ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+function IconSparkles({ className, ...props }: React.ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function IconChat(props: JSX.IntrinsicElements['svg']) {
+ return (
+
+
+
+ )
+}
+
+function IconInfo({ className, ...props }: React.ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+function IconFork({ className, ...props }: React.ComponentProps<'svg'>) {
+ return (
+
+
+
+ )
+}
+
+export function IconCommand(props: JSX.IntrinsicElements['svg']) {
+ return (
+
+
+
+ )
+}
+
+export {
+ IconArrowRight,
+ IconCheck,
+ IconClose,
+ IconCopy,
+ IconDownload,
+ IconFork,
+ IconGitHub,
+ IconInfo,
+ IconMessage,
+ IconOpenAI,
+ IconRefresh,
+ IconSparkles,
+ IconSpinner,
+ IconStop,
+ IconUser
+}
diff --git a/examples/proverb-pro/components/ui/separator.tsx b/examples/proverb-pro/components/ui/separator.tsx
new file mode 100755
index 00000000..7ba06b3a
--- /dev/null
+++ b/examples/proverb-pro/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,
+ React.ComponentPropsWithoutRef
+>(
+ (
+ { className, orientation = 'horizontal', decorative = true, ...props },
+ ref
+ ) => (
+
+ )
+)
+Separator.displayName = SeparatorPrimitive.Root.displayName
+
+export { Separator }
diff --git a/examples/proverb-pro/components/ui/textarea.tsx b/examples/proverb-pro/components/ui/textarea.tsx
new file mode 100755
index 00000000..93b82cb8
--- /dev/null
+++ b/examples/proverb-pro/components/ui/textarea.tsx
@@ -0,0 +1,23 @@
+import cn from 'mxcn'
+import * as React from 'react'
+
+export interface TextareaProps
+ extends React.TextareaHTMLAttributes {}
+
+const Textarea = React.forwardRef(
+ ({ className, ...props }, ref) => {
+ return (
+
+ )
+ }
+)
+Textarea.displayName = 'Textarea'
+
+export { Textarea }
diff --git a/examples/proverb-pro/lib/hooks/use-copy-to-clipboard.tsx b/examples/proverb-pro/lib/hooks/use-copy-to-clipboard.tsx
new file mode 100755
index 00000000..62f7156d
--- /dev/null
+++ b/examples/proverb-pro/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(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/proverb-pro/lib/hooks/use-enter-submit.tsx b/examples/proverb-pro/lib/hooks/use-enter-submit.tsx
new file mode 100755
index 00000000..d66b2d32
--- /dev/null
+++ b/examples/proverb-pro/lib/hooks/use-enter-submit.tsx
@@ -0,0 +1,23 @@
+import { useRef, type RefObject } from 'react'
+
+export function useEnterSubmit(): {
+ formRef: RefObject
+ onKeyDown: (event: React.KeyboardEvent) => void
+} {
+ const formRef = useRef(null)
+
+ const handleKeyDown = (
+ event: React.KeyboardEvent
+ ): void => {
+ if (
+ event.key === 'Enter' &&
+ !event.shiftKey &&
+ !event.nativeEvent.isComposing
+ ) {
+ formRef.current?.requestSubmit()
+ event.preventDefault()
+ }
+ }
+
+ return { formRef, onKeyDown: handleKeyDown }
+}
diff --git a/examples/proverb-pro/lib/types.ts b/examples/proverb-pro/lib/types.ts
new file mode 100755
index 00000000..cd0a5f03
--- /dev/null
+++ b/examples/proverb-pro/lib/types.ts
@@ -0,0 +1,11 @@
+import { type Message } from 'ai'
+
+export interface Chat extends Record {
+ id: string
+ title: string
+ createdAt: Date
+ userId: string
+ path: string
+ messages: Message[]
+ sharePath?: string
+}
diff --git a/examples/proverb-pro/lib/utils.ts b/examples/proverb-pro/lib/utils.ts
new file mode 100755
index 00000000..a5cc2c68
--- /dev/null
+++ b/examples/proverb-pro/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/proverb-pro/next.config.js b/examples/proverb-pro/next.config.js
new file mode 100755
index 00000000..9b7d60b3
--- /dev/null
+++ b/examples/proverb-pro/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/proverb-pro/package.json b/examples/proverb-pro/package.json
new file mode 100755
index 00000000..dfaba6de
--- /dev/null
+++ b/examples/proverb-pro/package.json
@@ -0,0 +1,49 @@
+{
+ "name": "proverb-pro-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/proverb-pro/postcss.config.js b/examples/proverb-pro/postcss.config.js
new file mode 100755
index 00000000..33ad091d
--- /dev/null
+++ b/examples/proverb-pro/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/examples/proverb-pro/prettier.config.cjs b/examples/proverb-pro/prettier.config.cjs
new file mode 100755
index 00000000..ec67ed3d
--- /dev/null
+++ b/examples/proverb-pro/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$)",
+ "",
+ "",
+ "^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/proverb-pro/public/chatbot.jpg b/examples/proverb-pro/public/chatbot.jpg
new file mode 100644
index 00000000..577b8194
Binary files /dev/null and b/examples/proverb-pro/public/chatbot.jpg differ
diff --git a/examples/proverb-pro/tailwind.config.ts b/examples/proverb-pro/tailwind.config.ts
new file mode 100755
index 00000000..8a0ab881
--- /dev/null
+++ b/examples/proverb-pro/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/proverb-pro/tsconfig.json b/examples/proverb-pro/tsconfig.json
new file mode 100755
index 00000000..1b03bc25
--- /dev/null
+++ b/examples/proverb-pro/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"]
+}