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 ? ( + + ) : ( + messages?.length > 0 && ( + + ) + )} +
+
+ { + 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 ( +
+ +
+ ) +} 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 ( +
+
+

+ + + Langbase + +

+
+ +
+ + + GitHub + + + + + Fork on Langbase + + +
+
+ ) +} 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 ( +
{ + e.preventDefault() + if (!input?.trim()) { + return + } + setInput('') + await onSubmit(input) + }} + ref={formRef} + > +
+
+ +
+