├── .env.example ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── app ├── api │ └── chat │ │ └── route.ts ├── globals.css ├── layout.tsx ├── opengraph-image.png ├── page.tsx └── twitter-image.png ├── assets └── fonts │ ├── Inter-Bold.woff │ └── Inter-Regular.woff ├── components ├── button-scroll-to-bottom.tsx ├── chat-list.tsx ├── chat-message-actions.tsx ├── chat-message.tsx ├── chat-panel.tsx ├── chat-scroll-anchor.tsx ├── chat.tsx ├── empty-screen.tsx ├── external-link.tsx ├── footer.tsx ├── header.tsx ├── markdown.tsx ├── new-chat-button.tsx ├── prompt-form.tsx ├── providers.tsx ├── theme-toggle.tsx ├── toaster.tsx └── ui │ ├── alert-dialog.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── codeblock.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── icons.tsx │ ├── input.tsx │ ├── label.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── switch.tsx │ ├── textarea.tsx │ └── tooltip.tsx ├── lib ├── analytics.ts ├── fonts.ts ├── hooks │ ├── use-at-bottom.tsx │ ├── use-copy-to-clipboard.tsx │ ├── use-enter-submit.tsx │ └── use-local-storage.ts ├── humanloop-stream.ts ├── types.ts └── utils.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── prettier.config.cjs ├── public ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon.ico ├── humanloop-example.png ├── interface-example.png ├── next.svg ├── thirteen.svg └── vercel.svg ├── tailwind.config.js └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # You must first activate a Billing Account here: https://platform.openai.com/account/billing/overview 2 | # Then get your OpenAI API Key here: https://platform.openai.com/account/api-keys 3 | OPENAI_API_KEY=XXXXXXXX 4 | 5 | # Generate a Humanloop API Key here: https://app.humanloop.com/account/api-keys 6 | HUMANLOOP_API_KEY=XXXXXXXX 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/eslintrc", 3 | "root": true, 4 | "extends": [ 5 | "next/core-web-vitals", 6 | "prettier", 7 | "plugin:tailwindcss/recommended" 8 | ], 9 | "plugins": ["tailwindcss"], 10 | "rules": { 11 | "tailwindcss/no-custom-classname": "off" 12 | }, 13 | "settings": { 14 | "tailwindcss": { 15 | "callees": ["cn", "cva"], 16 | "config": "tailwind.config.js" 17 | } 18 | }, 19 | "overrides": [ 20 | { 21 | "files": ["*.ts", "*.tsx"], 22 | "parser": "@typescript-eslint/parser" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .pnpm-debug.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | 35 | .contentlayer 36 | .env 37 | .vercel 38 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Vercel, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Next.js 13 and app template Router-ready AI chatbot. 3 |

Humanloop AI Chatbot

4 |
5 | 6 |

7 | An open-source AI chatbot app template built with Next.js, the Vercel AI SDK, OpenAI, and Humanloop. 8 |

9 | 10 |

11 | Features · 12 | Model Providers · 13 | Deploy Your Own · 14 | Running locally · 15 | Authors 16 |

17 |
18 | 19 | ## Features 20 | 21 | - [Next.js](https://nextjs.org) App Router 22 | - React Server Components (RSCs), Suspense, and Server Actions 23 | - [Vercel AI SDK](https://sdk.vercel.ai/docs) for streaming chat UI 24 | - [Humanloop](https://humanloop.com) integration for observability, logging and monitoring 25 | - Edge runtime-ready 26 | - [shadcn/ui](https://ui.shadcn.com) 27 | - Styling with [Tailwind CSS](https://tailwindcss.com) 28 | - [Radix UI](https://radix-ui.com) for headless component primitives 29 | - Icons from [Phosphor Icons](https://phosphoricons.com) 30 | 31 | ## Running locally 32 | 33 | You will need to populate the two evironment variables shown in `.env.example`. Copy this file to a file called `.env`. You can retrieve you OpenAI API key [here](https://platform.openai.com/account/api-keys) and your Humanloop API key [here](https://app.humanloop.com/account/api-keys). 34 | 35 | To run the application locally, simply run: 36 | 37 | ```bash 38 | npm install 39 | npm run dev 40 | ``` 41 | 42 | Your app template should now be running on [localhost:3000](http://localhost:3000/). 43 | 44 | ## Preview 45 | 46 | The chat interface powered by Next.js looks something like this: 47 | 48 | ![Screenshot of the chat interface](./public/interface-example.png) 49 | 50 | Meanwhile, in your Humanloop project, you can explore the generated logs from the app, iterate on the prompt, evaluate it and redeploy. 51 | 52 | ![The Humanloop project linked to the chatbot app](./public/humanloop-example.png) 53 | 54 | ## Humanloop 55 | 56 | In `app/api/chat/route.ts`, LLM chat calls are made via Humanloop's TypeScript SDK. Note that this file is an example of a Next.js [Route Handler](https://nextjs.org/docs/app/building-your-application/routing/route-handlers). This means it will run server-side, and therefore does not expose your OpenAI and Humanloop API keys to the client. This backend Route Handler is called from `components/chat.tsx` with the Vercel SDK's `useChat` hook. 57 | 58 | After sending and receiving some chat messages in your app, visit the [Humanloop app](https://app.humnaloop.com), and you will see a project called `sdk-example`. Visit the `Logs` tab and you will find all the chat histories from your running application. 59 | 60 | ## Deploy Your Own 61 | 62 | You can deploy your own version of the Next.js AI Chatbot to Vercel with one click: 63 | 64 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?demo-title=Next.js+Chat&demo-description=A+full-featured%2C+hackable+Next.js+AI+chatbot+built+by+Vercel+Labs&demo-url=https%3A%2F%2Fchat.vercel.ai%2F&demo-image=%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F4aVPvWuTmBvzM5cEdRdqeW%2F4234f9baf160f68ffb385a43c3527645%2FCleanShot_2023-06-16_at_17.09.21.png&project-name=Next.js+Chat&repository-name=nextjs-chat&repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fai-chatbot&from=templates&skippable-integrations=1&env=OPENAI_API_KEY%2CAUTH_GITHUB_ID%2CAUTH_GITHUB_SECRET%2CAUTH_SECRET&envDescription=How+to+get+these+env+vars&envLink=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fai-chatbot%2Fblob%2Fmain%2F.env.example&teamCreateStatus=hidden&stores=[{"type":"kv"}]) 65 | 66 | ## Authors 67 | 68 | Credit for the foundational design, app, and implementation is due to [Vercel](https://vercel.com) and their [AI Chatbot Template](https://github.com/vercel-labs/ai-chatbot) with contributions from: 69 | 70 | - Jared Palmer ([@jaredpalmer](https://twitter.com/jaredpalmer)) - [Vercel](https://vercel.com) 71 | - Shu Ding ([@shuding\_](https://twitter.com/shuding_)) - [Vercel](https://vercel.com) 72 | - shadcn ([@shadcn](https://twitter.com/shadcn)) - [Contractor](https://shadcn.com) 73 | 74 | We've mostly just ripped out the good (but more complicated) auth and KV store, and powered the AI chatbot with [Humanloop](https://humanloop.com) to make it easier to customise, evaluate and improve the underlying AI. 75 | -------------------------------------------------------------------------------- /app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { HumanloopStream } from '@/lib/humanloop-stream' 2 | import { StreamingTextResponse } from 'ai' 3 | import { HumanloopClient } from 'humanloop' 4 | 5 | export const runtime = 'edge' 6 | 7 | const HUMANLOOP_API_KEY = process.env.HUMANLOOP_API_KEY 8 | 9 | const client = new HumanloopClient({ 10 | apiKey: HUMANLOOP_API_KEY || '' 11 | }) 12 | 13 | export async function POST(req: Request) { 14 | if (!HUMANLOOP_API_KEY) { 15 | throw new Error('HUMANLOOP_API_KEY is not set') 16 | } 17 | 18 | const { messages } = await req.json() 19 | 20 | const chatResponse = await client.prompts.callStream({ 21 | path: 'sdk-example', 22 | messages 23 | }) 24 | 25 | return new StreamingTextResponse(HumanloopStream(chatResponse)) 26 | } 27 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | 10 | --muted: 240 4.8% 95.9%; 11 | --muted-foreground: 240 3.8% 46.1%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 240 10% 3.9%; 15 | 16 | --card: 0 0% 100%; 17 | --card-foreground: 240 10% 3.9%; 18 | 19 | --border: 240 5.9% 90%; 20 | --input: 240 5.9% 90%; 21 | 22 | --primary: 240 5.9% 10%; 23 | --primary-foreground: 0 0% 98%; 24 | 25 | --secondary: 240 4.8% 95.9%; 26 | --secondary-foreground: 240 5.9% 10%; 27 | 28 | --accent: 240 4.8% 95.9%; 29 | --accent-foreground: ; 30 | 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 0 0% 98%; 33 | 34 | --ring: 240 5% 64.9%; 35 | 36 | --radius: 0.5rem; 37 | } 38 | 39 | .dark { 40 | --background: 240 10% 3.9%; 41 | --foreground: 0 0% 98%; 42 | 43 | --muted: 240 3.7% 15.9%; 44 | --muted-foreground: 240 5% 64.9%; 45 | 46 | --popover: 240 10% 3.9%; 47 | --popover-foreground: 0 0% 98%; 48 | 49 | --card: 240 10% 3.9%; 50 | --card-foreground: 0 0% 98%; 51 | 52 | --border: 240 3.7% 15.9%; 53 | --input: 240 3.7% 15.9%; 54 | 55 | --primary: 0 0% 98%; 56 | --primary-foreground: 240 5.9% 10%; 57 | 58 | --secondary: 240 3.7% 15.9%; 59 | --secondary-foreground: 0 0% 98%; 60 | 61 | --accent: 240 3.7% 15.9%; 62 | --accent-foreground: ; 63 | 64 | --destructive: 0 62.8% 30.6%; 65 | --destructive-foreground: 0 85.7% 97.3%; 66 | 67 | --ring: 240 3.7% 15.9%; 68 | } 69 | } 70 | 71 | @layer base { 72 | * { 73 | @apply border-border; 74 | } 75 | body { 76 | @apply bg-background text-foreground; 77 | } 78 | } 79 | 80 | .debug { 81 | @apply border-red-500 border; 82 | } 83 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next' 2 | 3 | import { Toaster } from 'react-hot-toast' 4 | 5 | import '@/app/globals.css' 6 | import { fontMono, fontSans } from '@/lib/fonts' 7 | import { cn } from '@/lib/utils' 8 | import { Providers } from '@/components/providers' 9 | import { Header } from '@/components/header' 10 | 11 | export const metadata: Metadata = { 12 | title: { 13 | default: 'Humanloop Chatbot Stater', 14 | template: `%s - Humanloop Chatbot Starter` 15 | }, 16 | description: 17 | 'An AI-powered chatbot template built with Humanloop and Next.js.', 18 | themeColor: [ 19 | { media: '(prefers-color-scheme: light)', color: 'white' }, 20 | { media: '(prefers-color-scheme: dark)', color: 'black' } 21 | ], 22 | icons: { 23 | icon: '/favicon.ico', 24 | shortcut: '/favicon-16x16.png', 25 | apple: '/apple-touch-icon.png' 26 | } 27 | } 28 | 29 | interface RootLayoutProps { 30 | children: React.ReactNode 31 | } 32 | 33 | export default function RootLayout({ children }: RootLayoutProps) { 34 | return ( 35 | 36 | 37 | 44 | 45 | 46 |
47 | {/* @ts-ignore */} 48 |
49 |
{children}
50 |
51 |
52 | 53 | 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humanloop/chatbot-starter/7577e9ac88289fdd4f8cc93d6462ab15be8ceeac/app/opengraph-image.png -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { nanoid } from '@/lib/utils' 2 | import { Chat } from '@/components/chat' 3 | 4 | export const runtime = 'edge' 5 | 6 | export default function IndexPage() { 7 | const id = nanoid() 8 | 9 | return 10 | } 11 | -------------------------------------------------------------------------------- /app/twitter-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humanloop/chatbot-starter/7577e9ac88289fdd4f8cc93d6462ab15be8ceeac/app/twitter-image.png -------------------------------------------------------------------------------- /assets/fonts/Inter-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humanloop/chatbot-starter/7577e9ac88289fdd4f8cc93d6462ab15be8ceeac/assets/fonts/Inter-Bold.woff -------------------------------------------------------------------------------- /assets/fonts/Inter-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humanloop/chatbot-starter/7577e9ac88289fdd4f8cc93d6462ab15be8ceeac/assets/fonts/Inter-Regular.woff -------------------------------------------------------------------------------- /components/button-scroll-to-bottom.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | import { cn } from '@/lib/utils' 6 | import { useAtBottom } from '@/lib/hooks/use-at-bottom' 7 | import { Button, type ButtonProps } from '@/components/ui/button' 8 | import { IconArrowDown } from '@/components/ui/icons' 9 | 10 | export function ButtonScrollToBottom({ className, ...props }: ButtonProps) { 11 | const isAtBottom = useAtBottom() 12 | 13 | return ( 14 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /components/chat-list.tsx: -------------------------------------------------------------------------------- 1 | import { type Message } from 'ai' 2 | 3 | import { Separator } from '@/components/ui/separator' 4 | import { ChatMessage } from '@/components/chat-message' 5 | 6 | export interface ChatList { 7 | messages: Message[] 8 | } 9 | 10 | export function ChatList({ messages }: ChatList) { 11 | if (!messages.length) { 12 | return null 13 | } 14 | 15 | return ( 16 |
17 | {messages.map((message, index) => ( 18 |
19 | 20 | {index < messages.length - 1 && ( 21 | 22 | )} 23 |
24 | ))} 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /components/chat-message-actions.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { type Message } from 'ai' 4 | 5 | import { Button } from '@/components/ui/button' 6 | import { IconCheck, IconCopy } from '@/components/ui/icons' 7 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard' 8 | import { cn } from '@/lib/utils' 9 | 10 | interface ChatMessageActionsProps extends React.ComponentProps<'div'> { 11 | message: Message 12 | } 13 | 14 | export function ChatMessageActions({ 15 | message, 16 | className, 17 | ...props 18 | }: ChatMessageActionsProps) { 19 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }) 20 | 21 | const onCopy = () => { 22 | if (isCopied) return 23 | copyToClipboard(message.content) 24 | } 25 | 26 | return ( 27 |
34 | 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /components/chat-message.tsx: -------------------------------------------------------------------------------- 1 | // Inspired by Chatbot-UI and modified to fit the needs of this project 2 | // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatMessage.tsx 3 | 4 | import { Message } from 'ai' 5 | import remarkGfm from 'remark-gfm' 6 | import remarkMath from 'remark-math' 7 | 8 | import { cn } from '@/lib/utils' 9 | import { CodeBlock } from '@/components/ui/codeblock' 10 | import { MemoizedReactMarkdown } from '@/components/markdown' 11 | import { IconOpenAI, IconUser } from '@/components/ui/icons' 12 | import { ChatMessageActions } from '@/components/chat-message-actions' 13 | 14 | export interface ChatMessageProps { 15 | message: Message 16 | } 17 | 18 | export function ChatMessage({ message, ...props }: ChatMessageProps) { 19 | return ( 20 |
24 |
32 | {message.role === 'user' ? : } 33 |
34 |
35 | {children}

41 | }, 42 | code({ node, inline, className, children, ...props }) { 43 | if (children.length) { 44 | if (children[0] == '▍') { 45 | return ( 46 | 47 | ) 48 | } 49 | 50 | children[0] = (children[0] as string).replace('`▍`', '▍') 51 | } 52 | 53 | const match = /language-(\w+)/.exec(className || '') 54 | 55 | if (inline) { 56 | return ( 57 | 58 | {children} 59 | 60 | ) 61 | } 62 | 63 | return ( 64 | 70 | ) 71 | } 72 | }} 73 | > 74 | {message.content} 75 |
76 | 77 |
78 |
79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /components/chat-panel.tsx: -------------------------------------------------------------------------------- 1 | import { type UseChatHelpers } from 'ai/react' 2 | 3 | import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom' 4 | import { PromptForm } from '@/components/prompt-form' 5 | import { Button } from '@/components/ui/button' 6 | import { IconRefresh, IconStop } from '@/components/ui/icons' 7 | 8 | export interface ChatPanelProps 9 | extends Pick< 10 | UseChatHelpers, 11 | | 'append' 12 | | 'isLoading' 13 | | 'reload' 14 | | 'messages' 15 | | 'stop' 16 | | 'input' 17 | | 'setInput' 18 | > { 19 | id?: string 20 | } 21 | 22 | export function ChatPanel({ 23 | id, 24 | isLoading, 25 | stop, 26 | append, 27 | reload, 28 | input, 29 | setInput, 30 | messages 31 | }: ChatPanelProps) { 32 | return ( 33 |
34 | 35 |
36 |
37 | {isLoading ? ( 38 | 46 | ) : ( 47 | messages?.length > 0 && ( 48 | 56 | ) 57 | )} 58 |
59 |
60 | { 62 | await append({ 63 | id, 64 | content: value, 65 | role: 'user' 66 | }) 67 | }} 68 | input={input} 69 | setInput={setInput} 70 | isLoading={isLoading} 71 | /> 72 |
73 |
74 |
75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /components/chat-scroll-anchor.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { useInView } from 'react-intersection-observer' 5 | 6 | import { useAtBottom } from '@/lib/hooks/use-at-bottom' 7 | 8 | interface ChatScrollAnchorProps { 9 | trackVisibility?: boolean 10 | } 11 | 12 | export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) { 13 | const isAtBottom = useAtBottom() 14 | const { ref, entry, inView } = useInView({ 15 | trackVisibility, 16 | delay: 100, 17 | rootMargin: '0px 0px -150px 0px' 18 | }) 19 | 20 | React.useEffect(() => { 21 | if (isAtBottom && trackVisibility && !inView) { 22 | entry?.target.scrollIntoView({ 23 | block: 'start' 24 | }) 25 | } 26 | }, [inView, entry, isAtBottom, trackVisibility]) 27 | 28 | return
29 | } 30 | -------------------------------------------------------------------------------- /components/chat.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useChat, type Message } from 'ai/react' 4 | 5 | import { ChatList } from '@/components/chat-list' 6 | import { ChatPanel } from '@/components/chat-panel' 7 | import { ChatScrollAnchor } from '@/components/chat-scroll-anchor' 8 | import { EmptyScreen } from '@/components/empty-screen' 9 | import { cn } from '@/lib/utils' 10 | import { toast } from 'react-hot-toast' 11 | 12 | export interface ChatProps extends React.ComponentProps<'div'> { 13 | initialMessages?: Message[] 14 | id?: string 15 | } 16 | 17 | export function Chat({ id, initialMessages, className }: ChatProps) { 18 | const { messages, append, reload, stop, isLoading, input, setInput } = 19 | useChat({ 20 | api: '/api/chat', 21 | initialMessages, 22 | id, 23 | body: { id }, 24 | onResponse(response) { 25 | if (response.status !== 200) { 26 | toast.error(response.statusText) 27 | } 28 | } 29 | }) 30 | return ( 31 | <> 32 |
33 | {messages.length ? ( 34 | <> 35 | 36 | 37 | 38 | ) : ( 39 | 40 | )} 41 |
42 | 52 | 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /components/empty-screen.tsx: -------------------------------------------------------------------------------- 1 | import { UseChatHelpers } from 'ai/react' 2 | 3 | import { Button } from '@/components/ui/button' 4 | import { ExternalLink } from '@/components/external-link' 5 | import { IconArrowRight } from '@/components/ui/icons' 6 | 7 | const exampleMessages = [ 8 | { 9 | heading: 'Explain technical concepts', 10 | message: `What is a "serverless function"?` 11 | }, 12 | { 13 | heading: 'Summarize an article', 14 | message: 'Summarize the following article for a 2nd grader: \n' 15 | }, 16 | { 17 | heading: 'Draft an email', 18 | message: `Draft an email to my boss about the following: \n` 19 | } 20 | ] 21 | 22 | export function EmptyScreen({ setInput }: Pick) { 23 | return ( 24 |
25 |
26 |

27 | Welcome to Chatbot Starter. 28 |

29 |

30 | This is an open source AI chatbot app template built with{' '} 31 | Next.js and{' '} 32 | Humanloop. 33 |

34 |

35 | You can start a conversation here or try the following examples: 36 |

37 |
38 | {exampleMessages.map((message, index) => ( 39 | 48 | ))} 49 |
50 |
51 |
52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /components/external-link.tsx: -------------------------------------------------------------------------------- 1 | export function ExternalLink({ 2 | href, 3 | children 4 | }: { 5 | href: string 6 | children: React.ReactNode 7 | }) { 8 | return ( 9 | 14 | {children} 15 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /components/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | import { ExternalLink } from '@/components/external-link' 5 | 6 | export function FooterText({ className, ...props }: React.ComponentProps<'p'>) { 7 | return null 8 | return ( 9 |

16 | Open source AI chatbot built with{' '} 17 | Next.js and{' '} 18 | Humanloop. 19 |

20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /components/header.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | 3 | import { buttonVariants } from '@/components/ui/button' 4 | import { IconGitHub } from '@/components/ui/icons' 5 | import { cn } from '@/lib/utils' 6 | import { NewChatButton } from './new-chat-button' 7 | import { ThemeToggle } from './theme-toggle' 8 | 9 | export const HumanloopLogomark = ( 10 | props: React.ComponentProps<'svg'> 11 | ): JSX.Element => ( 12 | 13 | 18 | 19 | ) 20 | 21 | export async function Header() { 22 | return ( 23 |
24 |
25 | 26 | 27 | 28 |
29 | 30 | 31 | 43 |
44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /components/markdown.tsx: -------------------------------------------------------------------------------- 1 | import { FC, memo } from 'react' 2 | import ReactMarkdown, { Options } from 'react-markdown' 3 | 4 | export const MemoizedReactMarkdown: FC = memo( 5 | ReactMarkdown, 6 | (prevProps, nextProps) => 7 | prevProps.children === nextProps.children && 8 | prevProps.className === nextProps.className 9 | ) 10 | -------------------------------------------------------------------------------- /components/new-chat-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { cn } from '@/lib/utils' 4 | import { useRouter } from 'next/navigation' 5 | import { buttonVariants } from './ui/button' 6 | 7 | export function NewChatButton() { 8 | const router = useRouter() 9 | 10 | return ( 11 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /components/prompt-form.tsx: -------------------------------------------------------------------------------- 1 | import { UseChatHelpers } from 'ai/react' 2 | import * as React from 'react' 3 | import Textarea from 'react-textarea-autosize' 4 | 5 | import { Button } from '@/components/ui/button' 6 | import { IconArrowElbow } from '@/components/ui/icons' 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipTrigger 11 | } from '@/components/ui/tooltip' 12 | import { useEnterSubmit } from '@/lib/hooks/use-enter-submit' 13 | 14 | export interface PromptProps 15 | extends Pick { 16 | onSubmit: (value: string) => Promise 17 | isLoading: boolean 18 | } 19 | 20 | export function PromptForm({ 21 | onSubmit, 22 | input, 23 | setInput, 24 | isLoading 25 | }: PromptProps) { 26 | const { formRef, onKeyDown } = useEnterSubmit() 27 | const inputRef = React.useRef(null) 28 | 29 | React.useEffect(() => { 30 | if (inputRef.current) { 31 | inputRef.current.focus() 32 | } 33 | }, []) 34 | 35 | return ( 36 |
{ 38 | e.preventDefault() 39 | if (!input?.trim()) { 40 | return 41 | } 42 | setInput('') 43 | await onSubmit(input) 44 | }} 45 | ref={formRef} 46 | > 47 |
48 |