├── .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 |
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 | 
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 | 
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 | [](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 |
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 |
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 |
76 | )
77 | }
78 |
--------------------------------------------------------------------------------
/components/providers.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { ThemeProvider as NextThemesProvider } from 'next-themes'
5 | import { ThemeProviderProps } from 'next-themes/dist/types'
6 |
7 | import { TooltipProvider } from '@/components/ui/tooltip'
8 |
9 | export function Providers({ children, ...props }: ThemeProviderProps) {
10 | return (
11 |
12 | {children}
13 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/components/theme-toggle.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { useTheme } from 'next-themes'
5 |
6 | import { Button } from '@/components/ui/button'
7 | import { IconMoon, IconSun } from '@/components/ui/icons'
8 |
9 | export function ThemeToggle() {
10 | const { setTheme, theme } = useTheme()
11 | const [_, startTransition] = React.useTransition()
12 | const [mounted, setMounted] = React.useState(false)
13 |
14 | // useEffect only runs on the client, so now we can safely show the UI
15 | React.useEffect(() => {
16 | setMounted(true)
17 | }, [])
18 |
19 | if (!mounted) {
20 | return null
21 | }
22 |
23 | return (
24 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/components/toaster.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | export { Toaster } from 'react-hot-toast'
4 |
--------------------------------------------------------------------------------
/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
5 |
6 | import { cn } from '@/lib/utils'
7 | import { buttonVariants } from '@/components/ui/button'
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12 |
13 | const AlertDialogPortal = ({
14 | className,
15 | children,
16 | ...props
17 | }: AlertDialogPrimitive.AlertDialogPortalProps) => (
18 |
19 |
20 | {children}
21 |
22 |
23 | )
24 | AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName
25 |
26 | const AlertDialogOverlay = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, children, ...props }, ref) => (
30 |
38 | ))
39 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
40 |
41 | const AlertDialogContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, ...props }, ref) => (
45 |
46 |
47 |
55 |
56 | ))
57 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
58 |
59 | const AlertDialogHeader = ({
60 | className,
61 | ...props
62 | }: React.HTMLAttributes) => (
63 |
70 | )
71 | AlertDialogHeader.displayName = 'AlertDialogHeader'
72 |
73 | const AlertDialogFooter = ({
74 | className,
75 | ...props
76 | }: React.HTMLAttributes) => (
77 |
84 | )
85 | AlertDialogFooter.displayName = 'AlertDialogFooter'
86 |
87 | const AlertDialogTitle = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => (
91 |
96 | ))
97 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
98 |
99 | const AlertDialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | AlertDialogDescription.displayName =
110 | AlertDialogPrimitive.Description.displayName
111 |
112 | const AlertDialogAction = React.forwardRef<
113 | React.ElementRef,
114 | React.ComponentPropsWithoutRef
115 | >(({ className, ...props }, ref) => (
116 |
121 | ))
122 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
123 |
124 | const AlertDialogCancel = React.forwardRef<
125 | React.ElementRef,
126 | React.ComponentPropsWithoutRef
127 | >(({ className, ...props }, ref) => (
128 |
137 | ))
138 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
139 |
140 | export {
141 | AlertDialog,
142 | AlertDialogTrigger,
143 | AlertDialogContent,
144 | AlertDialogHeader,
145 | AlertDialogFooter,
146 | AlertDialogTitle,
147 | AlertDialogDescription,
148 | AlertDialogAction,
149 | AlertDialogCancel
150 | }
151 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { cva, type VariantProps } from 'class-variance-authority'
3 |
4 | import { cn } from '@/lib/utils'
5 |
6 | const badgeVariants = cva(
7 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
13 | secondary:
14 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
15 | destructive:
16 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
17 | outline: 'text-foreground'
18 | }
19 | },
20 | defaultVariants: {
21 | variant: 'default'
22 | }
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Slot } from '@radix-ui/react-slot'
3 | import { cva, type VariantProps } from 'class-variance-authority'
4 |
5 | import { cn } from '@/lib/utils'
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center rounded-md text-sm font-medium shadow ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | 'bg-primary text-primary-foreground shadow-md hover:bg-primary/90',
14 | destructive:
15 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
16 | outline:
17 | 'border border-input hover:bg-accent hover:text-accent-foreground',
18 | secondary:
19 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
20 | ghost: 'shadow-none hover:bg-accent hover:text-accent-foreground',
21 | link: 'text-primary underline-offset-4 shadow-none hover:underline'
22 | },
23 | size: {
24 | default: 'h-8 px-4 py-2',
25 | sm: 'h-8 rounded-md px-3',
26 | lg: 'h-11 rounded-md px-8',
27 | icon: 'h-8 w-8 p-0'
28 | }
29 | },
30 | defaultVariants: {
31 | variant: 'default',
32 | size: 'default'
33 | }
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : 'button'
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = 'Button'
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/components/ui/codeblock.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/Markdown/CodeBlock.tsx
3 |
4 | 'use client'
5 |
6 | import { FC, memo } from 'react'
7 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
8 | import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
9 |
10 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
11 | import { IconCheck, IconCopy, IconDownload } from '@/components/ui/icons'
12 | import { Button } from '@/components/ui/button'
13 |
14 | interface Props {
15 | language: string
16 | value: string
17 | }
18 |
19 | interface languageMap {
20 | [key: string]: string | undefined
21 | }
22 |
23 | export const programmingLanguages: languageMap = {
24 | javascript: '.js',
25 | python: '.py',
26 | java: '.java',
27 | c: '.c',
28 | cpp: '.cpp',
29 | 'c++': '.cpp',
30 | 'c#': '.cs',
31 | ruby: '.rb',
32 | php: '.php',
33 | swift: '.swift',
34 | 'objective-c': '.m',
35 | kotlin: '.kt',
36 | typescript: '.ts',
37 | go: '.go',
38 | perl: '.pl',
39 | rust: '.rs',
40 | scala: '.scala',
41 | haskell: '.hs',
42 | lua: '.lua',
43 | shell: '.sh',
44 | sql: '.sql',
45 | html: '.html',
46 | css: '.css'
47 | // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
48 | }
49 |
50 | export const generateRandomString = (length: number, lowercase = false) => {
51 | const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789' // excluding similar looking characters like Z, 2, I, 1, O, 0
52 | let result = ''
53 | for (let i = 0; i < length; i++) {
54 | result += chars.charAt(Math.floor(Math.random() * chars.length))
55 | }
56 | return lowercase ? result.toLowerCase() : result
57 | }
58 |
59 | const CodeBlock: FC = memo(({ language, value }) => {
60 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
61 |
62 | const downloadAsFile = () => {
63 | if (typeof window === 'undefined') {
64 | return
65 | }
66 | const fileExtension = programmingLanguages[language] || '.file'
67 | const suggestedFileName = `file-${generateRandomString(
68 | 3,
69 | true
70 | )}${fileExtension}`
71 | const fileName = window.prompt('Enter file name' || '', suggestedFileName)
72 |
73 | if (!fileName) {
74 | // User pressed cancel on prompt.
75 | return
76 | }
77 |
78 | const blob = new Blob([value], { type: 'text/plain' })
79 | const url = URL.createObjectURL(blob)
80 | const link = document.createElement('a')
81 | link.download = fileName
82 | link.href = url
83 | link.style.display = 'none'
84 | document.body.appendChild(link)
85 | link.click()
86 | document.body.removeChild(link)
87 | URL.revokeObjectURL(url)
88 | }
89 |
90 | const onCopy = () => {
91 | if (isCopied) return
92 | copyToClipboard(value)
93 | }
94 |
95 | return (
96 |
97 |
98 |
{language}
99 |
100 |
109 |
118 |
119 |
120 |
138 | {value}
139 |
140 |
141 | )
142 | })
143 | CodeBlock.displayName = 'CodeBlock'
144 |
145 | export { CodeBlock }
146 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as DialogPrimitive from '@radix-ui/react-dialog'
5 |
6 | import { cn } from '@/lib/utils'
7 | import { IconClose } from '@/components/ui/icons'
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = ({
14 | className,
15 | children,
16 | ...props
17 | }: DialogPrimitive.DialogPortalProps) => (
18 |
19 |
20 | {children}
21 |
22 |
23 | )
24 | DialogPortal.displayName = DialogPrimitive.Portal.displayName
25 |
26 | const DialogOverlay = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, ...props }, ref) => (
30 |
38 | ))
39 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
40 |
41 | const DialogContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, children, ...props }, ref) => (
45 |
46 |
47 |
55 | {children}
56 |
57 |
58 | Close
59 |
60 |
61 |
62 | ))
63 | DialogContent.displayName = DialogPrimitive.Content.displayName
64 |
65 | const DialogHeader = ({
66 | className,
67 | ...props
68 | }: React.HTMLAttributes) => (
69 |
76 | )
77 | DialogHeader.displayName = 'DialogHeader'
78 |
79 | const DialogFooter = ({
80 | className,
81 | ...props
82 | }: React.HTMLAttributes) => (
83 |
90 | )
91 | DialogFooter.displayName = 'DialogFooter'
92 |
93 | const DialogTitle = React.forwardRef<
94 | React.ElementRef,
95 | React.ComponentPropsWithoutRef
96 | >(({ className, ...props }, ref) => (
97 |
105 | ))
106 | DialogTitle.displayName = DialogPrimitive.Title.displayName
107 |
108 | const DialogDescription = React.forwardRef<
109 | React.ElementRef,
110 | React.ComponentPropsWithoutRef
111 | >(({ className, ...props }, ref) => (
112 |
117 | ))
118 | DialogDescription.displayName = DialogPrimitive.Description.displayName
119 |
120 | export {
121 | Dialog,
122 | DialogTrigger,
123 | DialogContent,
124 | DialogHeader,
125 | DialogFooter,
126 | DialogTitle,
127 | DialogDescription
128 | }
129 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const DropdownMenu = DropdownMenuPrimitive.Root
9 |
10 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
11 |
12 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
13 |
14 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
15 |
16 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
17 |
18 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
19 |
20 | const DropdownMenuSubContent = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, ...props }, ref) => (
24 |
32 | ))
33 | DropdownMenuSubContent.displayName =
34 | DropdownMenuPrimitive.SubContent.displayName
35 |
36 | const DropdownMenuContent = React.forwardRef<
37 | React.ElementRef,
38 | React.ComponentPropsWithoutRef
39 | >(({ className, sideOffset = 4, ...props }, ref) => (
40 |
41 |
50 |
51 | ))
52 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
53 |
54 | const DropdownMenuItem = React.forwardRef<
55 | React.ElementRef,
56 | React.ComponentPropsWithoutRef & {
57 | inset?: boolean
58 | }
59 | >(({ className, inset, ...props }, ref) => (
60 |
69 | ))
70 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
71 |
72 | const DropdownMenuLabel = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef & {
75 | inset?: boolean
76 | }
77 | >(({ className, inset, ...props }, ref) => (
78 |
87 | ))
88 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
89 |
90 | const DropdownMenuSeparator = React.forwardRef<
91 | React.ElementRef,
92 | React.ComponentPropsWithoutRef
93 | >(({ className, ...props }, ref) => (
94 |
99 | ))
100 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
101 |
102 | const DropdownMenuShortcut = ({
103 | className,
104 | ...props
105 | }: React.HTMLAttributes) => {
106 | return (
107 |
111 | )
112 | }
113 | DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
114 |
115 | export {
116 | DropdownMenu,
117 | DropdownMenuTrigger,
118 | DropdownMenuContent,
119 | DropdownMenuItem,
120 | DropdownMenuLabel,
121 | DropdownMenuSeparator,
122 | DropdownMenuShortcut,
123 | DropdownMenuGroup,
124 | DropdownMenuPortal,
125 | DropdownMenuSub,
126 | DropdownMenuSubContent,
127 | DropdownMenuRadioGroup
128 | }
129 |
--------------------------------------------------------------------------------
/components/ui/icons.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | import { cn } from '@/lib/utils'
6 |
7 | function IconNextChat({
8 | className,
9 | inverted,
10 | ...props
11 | }: React.ComponentProps<'svg'> & { inverted?: boolean }) {
12 | const id = React.useId()
13 |
14 | return (
15 |
88 | )
89 | }
90 |
91 | function IconOpenAI({ className, ...props }: React.ComponentProps<'svg'>) {
92 | return (
93 |
104 | )
105 | }
106 |
107 | function IconVercel({ className, ...props }: React.ComponentProps<'svg'>) {
108 | return (
109 |
121 | )
122 | }
123 |
124 | function IconGitHub({ className, ...props }: React.ComponentProps<'svg'>) {
125 | return (
126 |
137 | )
138 | }
139 |
140 | function IconSeparator({ className, ...props }: React.ComponentProps<'svg'>) {
141 | return (
142 |
156 | )
157 | }
158 |
159 | function IconArrowDown({ className, ...props }: React.ComponentProps<'svg'>) {
160 | return (
161 |
170 | )
171 | }
172 |
173 | function IconArrowRight({ className, ...props }: React.ComponentProps<'svg'>) {
174 | return (
175 |
184 | )
185 | }
186 |
187 | function IconUser({ className, ...props }: React.ComponentProps<'svg'>) {
188 | return (
189 |
198 | )
199 | }
200 |
201 | function IconPlus({ className, ...props }: React.ComponentProps<'svg'>) {
202 | return (
203 |
212 | )
213 | }
214 |
215 | function IconArrowElbow({ className, ...props }: React.ComponentProps<'svg'>) {
216 | return (
217 |
226 | )
227 | }
228 |
229 | function IconSpinner({ className, ...props }: React.ComponentProps<'svg'>) {
230 | return (
231 |
240 | )
241 | }
242 |
243 | function IconMessage({ className, ...props }: React.ComponentProps<'svg'>) {
244 | return (
245 |
254 | )
255 | }
256 |
257 | function IconTrash({ className, ...props }: React.ComponentProps<'svg'>) {
258 | return (
259 |
268 | )
269 | }
270 |
271 | function IconRefresh({ className, ...props }: React.ComponentProps<'svg'>) {
272 | return (
273 |
282 | )
283 | }
284 |
285 | function IconStop({ className, ...props }: React.ComponentProps<'svg'>) {
286 | return (
287 |
296 | )
297 | }
298 |
299 | function IconSidebar({ className, ...props }: React.ComponentProps<'svg'>) {
300 | return (
301 |
310 | )
311 | }
312 |
313 | function IconMoon({ className, ...props }: React.ComponentProps<'svg'>) {
314 | return (
315 |
324 | )
325 | }
326 |
327 | function IconSun({ className, ...props }: React.ComponentProps<'svg'>) {
328 | return (
329 |
338 | )
339 | }
340 |
341 | function IconCopy({ className, ...props }: React.ComponentProps<'svg'>) {
342 | return (
343 |
352 | )
353 | }
354 |
355 | function IconCheck({ className, ...props }: React.ComponentProps<'svg'>) {
356 | return (
357 |
366 | )
367 | }
368 |
369 | function IconDownload({ className, ...props }: React.ComponentProps<'svg'>) {
370 | return (
371 |
380 | )
381 | }
382 |
383 | function IconClose({ className, ...props }: React.ComponentProps<'svg'>) {
384 | return (
385 |
394 | )
395 | }
396 |
397 | function IconEdit({ className, ...props }: React.ComponentProps<'svg'>) {
398 | return (
399 |
414 | )
415 | }
416 |
417 | function IconShare({ className, ...props }: React.ComponentProps<'svg'>) {
418 | return (
419 |
428 | )
429 | }
430 |
431 | function IconUsers({ className, ...props }: React.ComponentProps<'svg'>) {
432 | return (
433 |
442 | )
443 | }
444 |
445 | function IconExternalLink({
446 | className,
447 | ...props
448 | }: React.ComponentProps<'svg'>) {
449 | return (
450 |
459 | )
460 | }
461 |
462 | function IconChevronUpDown({
463 | className,
464 | ...props
465 | }: React.ComponentProps<'svg'>) {
466 | return (
467 |
476 | )
477 | }
478 |
479 | export {
480 | IconEdit,
481 | IconNextChat,
482 | IconOpenAI,
483 | IconVercel,
484 | IconGitHub,
485 | IconSeparator,
486 | IconArrowDown,
487 | IconArrowRight,
488 | IconUser,
489 | IconPlus,
490 | IconArrowElbow,
491 | IconSpinner,
492 | IconMessage,
493 | IconTrash,
494 | IconRefresh,
495 | IconStop,
496 | IconSidebar,
497 | IconMoon,
498 | IconSun,
499 | IconCopy,
500 | IconCheck,
501 | IconDownload,
502 | IconClose,
503 | IconShare,
504 | IconUsers,
505 | IconExternalLink,
506 | IconChevronUpDown
507 | }
508 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils'
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = 'Input'
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as SelectPrimitive from '@radix-ui/react-select'
5 |
6 | import { cn } from '@/lib/utils'
7 | import {
8 | IconArrowDown,
9 | IconCheck,
10 | IconChevronUpDown
11 | } from '@/components/ui/icons'
12 |
13 | const Select = SelectPrimitive.Root
14 |
15 | const SelectGroup = SelectPrimitive.Group
16 |
17 | const SelectValue = SelectPrimitive.Value
18 |
19 | const SelectTrigger = React.forwardRef<
20 | React.ElementRef,
21 | React.ComponentPropsWithoutRef
22 | >(({ className, children, ...props }, ref) => (
23 |
31 | {children}
32 |
33 |
34 |
35 |
36 | ))
37 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
38 |
39 | const SelectContent = React.forwardRef<
40 | React.ElementRef,
41 | React.ComponentPropsWithoutRef
42 | >(({ className, children, position = 'popper', ...props }, ref) => (
43 |
44 |
54 |
61 | {children}
62 |
63 |
64 |
65 | ))
66 | SelectContent.displayName = SelectPrimitive.Content.displayName
67 |
68 | const SelectLabel = React.forwardRef<
69 | React.ElementRef,
70 | React.ComponentPropsWithoutRef
71 | >(({ className, ...props }, ref) => (
72 |
77 | ))
78 | SelectLabel.displayName = SelectPrimitive.Label.displayName
79 |
80 | const SelectItem = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef
83 | >(({ className, children, ...props }, ref) => (
84 |
92 |
93 |
94 |
95 |
96 |
97 | {children}
98 |
99 | ))
100 | SelectItem.displayName = SelectPrimitive.Item.displayName
101 |
102 | const SelectSeparator = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
113 |
114 | export {
115 | Select,
116 | SelectGroup,
117 | SelectValue,
118 | SelectTrigger,
119 | SelectContent,
120 | SelectLabel,
121 | SelectItem,
122 | SelectSeparator
123 | }
124 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as SeparatorPrimitive from '@radix-ui/react-separator'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = 'horizontal', decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as SheetPrimitive from '@radix-ui/react-dialog'
5 |
6 | import { cn } from '@/lib/utils'
7 | import { IconClose } from '@/components/ui/icons'
8 |
9 | const Sheet = SheetPrimitive.Root
10 |
11 | const SheetTrigger = SheetPrimitive.Trigger
12 |
13 | const SheetClose = SheetPrimitive.Close
14 |
15 | const SheetPortal = ({
16 | className,
17 | children,
18 | ...props
19 | }: SheetPrimitive.DialogPortalProps) => (
20 |
24 | {children}
25 |
26 | )
27 | SheetPortal.displayName = SheetPrimitive.Portal.displayName
28 |
29 | const SheetOverlay = React.forwardRef<
30 | React.ElementRef,
31 | React.ComponentPropsWithoutRef
32 | >(({ className, children, ...props }, ref) => (
33 |
41 | ))
42 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
43 |
44 | const SheetContent = React.forwardRef<
45 | React.ElementRef,
46 | React.ComponentPropsWithoutRef
47 | >(({ className, children, ...props }, ref) => (
48 |
49 |
57 | {children}
58 |
59 |
60 | Close
61 |
62 |
63 |
64 | ))
65 | SheetContent.displayName = SheetPrimitive.Content.displayName
66 |
67 | const SheetHeader = ({
68 | className,
69 | ...props
70 | }: React.HTMLAttributes) => (
71 |
72 | )
73 | SheetHeader.displayName = 'SheetHeader'
74 |
75 | const SheetFooter = ({
76 | className,
77 | ...props
78 | }: React.HTMLAttributes) => (
79 |
86 | )
87 | SheetFooter.displayName = 'SheetFooter'
88 |
89 | const SheetTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ))
99 | SheetTitle.displayName = SheetPrimitive.Title.displayName
100 |
101 | const SheetDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | SheetDescription.displayName = SheetPrimitive.Description.displayName
112 |
113 | export {
114 | Sheet,
115 | SheetTrigger,
116 | SheetClose,
117 | SheetContent,
118 | SheetHeader,
119 | SheetFooter,
120 | SheetTitle,
121 | SheetDescription
122 | }
123 |
--------------------------------------------------------------------------------
/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils'
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = 'Textarea'
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as TooltipPrimitive from '@radix-ui/react-tooltip'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/lib/analytics.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest } from 'next'
2 | import type { NextFetchEvent, NextRequest } from 'next/server'
3 |
4 | export const initAnalytics = ({
5 | request,
6 | event
7 | }: {
8 | request: NextRequest | NextApiRequest | Request
9 | event?: NextFetchEvent
10 | }) => {
11 | const endpoint = process.env.VERCEL_URL
12 |
13 | return {
14 | track: async (eventName: string, data?: any) => {
15 | try {
16 | if (!endpoint && process.env.NODE_ENV === 'development') {
17 | console.log(
18 | `[Vercel Web Analytics] Track "${eventName}"` +
19 | (data ? ` with data ${JSON.stringify(data || {})}` : '')
20 | )
21 | return
22 | }
23 |
24 | const headers: { [key: string]: string } = {}
25 | Object.entries(request.headers).map(([key, value]) => {
26 | headers[key] = value
27 | })
28 |
29 | const body = {
30 | o: headers.referer,
31 | ts: new Date().getTime(),
32 | r: '',
33 | en: eventName,
34 | ed: data
35 | }
36 |
37 | const promise = fetch(
38 | `https://${process.env.VERCEL_URL}/_vercel/insights/event`,
39 | {
40 | headers: {
41 | 'content-type': 'application/json',
42 | 'user-agent': headers['user-agent'] as string,
43 | 'x-forwarded-for': headers['x-forwarded-for'] as string,
44 | 'x-va-server': '1'
45 | },
46 | body: JSON.stringify(body),
47 | method: 'POST'
48 | }
49 | )
50 |
51 | if (event) {
52 | event.waitUntil(promise)
53 | }
54 | {
55 | await promise
56 | }
57 | } catch (err) {
58 | console.error(err)
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/lib/fonts.ts:
--------------------------------------------------------------------------------
1 | import { JetBrains_Mono as FontMono, Inter as FontSans } from 'next/font/google'
2 |
3 | export const fontSans = FontSans({
4 | subsets: ['latin'],
5 | variable: '--font-sans'
6 | })
7 |
8 | export const fontMono = FontMono({
9 | subsets: ['latin'],
10 | variable: '--font-mono'
11 | })
12 |
--------------------------------------------------------------------------------
/lib/hooks/use-at-bottom.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export function useAtBottom(offset = 0) {
4 | const [isAtBottom, setIsAtBottom] = React.useState(false)
5 |
6 | React.useEffect(() => {
7 | const handleScroll = () => {
8 | setIsAtBottom(
9 | window.innerHeight + window.scrollY >=
10 | document.body.offsetHeight - offset
11 | )
12 | }
13 |
14 | window.addEventListener('scroll', handleScroll, { passive: true })
15 | handleScroll()
16 |
17 | return () => {
18 | window.removeEventListener('scroll', handleScroll)
19 | }
20 | }, [offset])
21 |
22 | return isAtBottom
23 | }
24 |
--------------------------------------------------------------------------------
/lib/hooks/use-copy-to-clipboard.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | export interface useCopyToClipboardProps {
6 | timeout?: number
7 | }
8 |
9 | export function useCopyToClipboard({
10 | timeout = 2000
11 | }: useCopyToClipboardProps) {
12 | const [isCopied, setIsCopied] = React.useState(false)
13 |
14 | const copyToClipboard = (value: string) => {
15 | if (typeof window === 'undefined' || !navigator.clipboard?.writeText) {
16 | return
17 | }
18 |
19 | if (!value) {
20 | return
21 | }
22 |
23 | navigator.clipboard.writeText(value).then(() => {
24 | setIsCopied(true)
25 |
26 | setTimeout(() => {
27 | setIsCopied(false)
28 | }, timeout)
29 | })
30 | }
31 |
32 | return { isCopied, copyToClipboard }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/hooks/use-enter-submit.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, type RefObject } from 'react'
2 |
3 | export function useEnterSubmit(): {
4 | formRef: RefObject
5 | onKeyDown: (event: React.KeyboardEvent) => void
6 | } {
7 | const formRef = useRef(null)
8 |
9 | const handleKeyDown = (
10 | event: React.KeyboardEvent
11 | ): void => {
12 | if (
13 | event.key === 'Enter' &&
14 | !event.shiftKey &&
15 | !event.nativeEvent.isComposing
16 | ) {
17 | formRef.current?.requestSubmit()
18 | event.preventDefault()
19 | }
20 | }
21 |
22 | return { formRef, onKeyDown: handleKeyDown }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/hooks/use-local-storage.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | export const useLocalStorage = (
4 | key: string,
5 | initialValue: T
6 | ): [T, (value: T) => void] => {
7 | const [storedValue, setStoredValue] = useState(initialValue)
8 |
9 | useEffect(() => {
10 | // Retrieve from localStorage
11 | const item = window.localStorage.getItem(key)
12 | if (item) {
13 | setStoredValue(JSON.parse(item))
14 | }
15 | }, [key])
16 |
17 | const setValue = (value: T) => {
18 | // Save state
19 | setStoredValue(value)
20 | // Save to localStorage
21 | window.localStorage.setItem(key, JSON.stringify(value))
22 | }
23 | return [storedValue, setValue]
24 | }
25 |
--------------------------------------------------------------------------------
/lib/humanloop-stream.ts:
--------------------------------------------------------------------------------
1 | import { PromptCallStreamResponse } from 'humanloop/api'
2 | import { Stream } from 'humanloop/core/streaming-fetcher/Stream'
3 |
4 | /**
5 | * Converts the Humanloop stream to a stream of just text
6 | *
7 | * (This is only necessary to make this work nicely with the ai package's useChat hook)
8 | *
9 | * @param input a stream of Humanloop responses { output: string, id: string }
10 | * @returns a stream of just text
11 | */
12 | export function HumanloopStream(
13 | input: Stream
14 | ): ReadableStream {
15 | const encoder = new TextEncoder()
16 |
17 | return new ReadableStream({
18 | async pull(controller) {
19 | for await (const value of input) {
20 | if (typeof value.output === 'string') {
21 | const encodedValue = encoder.encode(value.output)
22 | controller.enqueue(encodedValue)
23 | }
24 | }
25 | controller.close()
26 | }
27 | })
28 | }
29 |
--------------------------------------------------------------------------------
/lib/types.ts:
--------------------------------------------------------------------------------
1 | import { type Message } from 'ai'
2 |
3 | export interface Chat extends Record {
4 | id: string
5 | title: string
6 | createdAt: Date
7 | userId: string
8 | path: string
9 | messages: Message[]
10 | sharePath?: string
11 | }
12 |
13 | export type ServerActionResult = Promise<
14 | | Result
15 | | {
16 | error: string
17 | }
18 | >
19 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from 'clsx'
2 | import { customAlphabet } from 'nanoid'
3 | import { twMerge } from 'tailwind-merge'
4 |
5 | export function cn(...inputs: ClassValue[]) {
6 | return twMerge(clsx(inputs))
7 | }
8 |
9 | export const nanoid = customAlphabet(
10 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
11 | 7
12 | ) // 7-character random string
13 |
14 | export async function fetcher(
15 | input: RequestInfo,
16 | init?: RequestInit
17 | ): Promise {
18 | const res = await fetch(input, init)
19 |
20 | if (!res.ok) {
21 | const json = await res.json()
22 | if (json.error) {
23 | const error = new Error(json.error) as Error & {
24 | status: number
25 | }
26 | error.status = res.status
27 | throw error
28 | } else {
29 | throw new Error('An unexpected error occurred')
30 | }
31 | }
32 |
33 | return res.json()
34 | }
35 |
36 | export function formatDate(input: string | number | Date): string {
37 | const date = new Date(input)
38 | return date.toLocaleDateString('en-US', {
39 | month: 'long',
40 | day: 'numeric',
41 | year: 'numeric'
42 | })
43 | }
44 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | module.exports = {
3 | reactStrictMode: true,
4 | experimental: {
5 | serverActions: true,
6 | },
7 | images: {
8 | remotePatterns: [
9 | {
10 | protocol: 'https',
11 | hostname: 'avatars.githubusercontent.com',
12 | port: '',
13 | pathname: '**',
14 | },
15 | ],
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chatbot-starter",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "lint:fix": "next lint --fix",
11 | "preview": "next build && next start",
12 | "type-check": "tsc --noEmit",
13 | "format:write": "prettier --write \"{app,lib,components}/**/*.{ts,tsx,mdx}\" --cache",
14 | "format:check": "prettier --check \"{app,lib,components}**/*.{ts,tsx,mdx}\" --cache"
15 | },
16 | "dependencies": {
17 | "@radix-ui/react-alert-dialog": "^1.0.4",
18 | "@radix-ui/react-dialog": "^1.0.4",
19 | "@radix-ui/react-dropdown-menu": "^2.0.5",
20 | "@radix-ui/react-label": "^2.0.2",
21 | "@radix-ui/react-select": "^1.2.2",
22 | "@radix-ui/react-separator": "^1.0.3",
23 | "@radix-ui/react-slot": "^1.0.2",
24 | "@radix-ui/react-switch": "^1.0.3",
25 | "@radix-ui/react-tooltip": "^1.0.6",
26 | "@vercel/analytics": "^1.0.0",
27 | "@vercel/og": "^0.5.7",
28 | "ai": "^2.2.12",
29 | "class-variance-authority": "^0.7.0",
30 | "clsx": "^1.2.1",
31 | "focus-trap-react": "^10.1.1",
32 | "humanloop": "^0.8.0-beta12",
33 | "nanoid": "^4.0.2",
34 | "next": "13.4.19",
35 | "next-themes": "^0.2.1",
36 | "openai": "^4.56.0",
37 | "react": "^18.2.0",
38 | "react-dom": "^18.2.0",
39 | "react-hot-toast": "^2.4.1",
40 | "react-intersection-observer": "^9.4.4",
41 | "react-markdown": "^8.0.7",
42 | "react-syntax-highlighter": "^15.5.0",
43 | "react-textarea-autosize": "^8.4.1",
44 | "remark-gfm": "^3.0.1",
45 | "remark-math": "^5.1.1"
46 | },
47 | "devDependencies": {
48 | "@tailwindcss/typography": "^0.5.9",
49 | "@types/node": "^17.0.12",
50 | "@types/react": "^18.0.22",
51 | "@types/react-dom": "^18.0.7",
52 | "@types/react-syntax-highlighter": "^15.5.6",
53 | "@typescript-eslint/parser": "^5.59.7",
54 | "autoprefixer": "^10.4.13",
55 | "eslint": "^8.31.0",
56 | "eslint-config-next": "13.4.19",
57 | "eslint-config-prettier": "^8.3.0",
58 | "eslint-plugin-tailwindcss": "^3.12.0",
59 | "postcss": "^8.4.21",
60 | "prettier": "^3.0.3",
61 | "prettier-plugin-tailwindcss": "^0.5.4",
62 | "tailwind-merge": "^1.12.0",
63 | "tailwindcss": "^3.3.1",
64 | "tailwindcss-animate": "^1.0.5",
65 | "typescript": "^5.2.2"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/prettier.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('prettier').Config} */
2 | module.exports = {
3 | endOfLine: "lf",
4 | semi: false,
5 | useTabs: false,
6 | singleQuote: true,
7 | arrowParens: "avoid",
8 | tabWidth: 2,
9 | trailingComma: "none",
10 | importOrder: [
11 | "^(react/(.*)$)|^(react$)",
12 | "^(next/(.*)$)|^(next$)",
13 | "",
14 | "",
15 | "^types$",
16 | "^@/types/(.*)$",
17 | "^@/config/(.*)$",
18 | "^@/lib/(.*)$",
19 | "^@/hooks/(.*)$",
20 | "^@/components/ui/(.*)$",
21 | "^@/components/(.*)$",
22 | "^@/registry/(.*)$",
23 | "^@/styles/(.*)$",
24 | "^@/app/(.*)$",
25 | "",
26 | "^[./]"
27 | ],
28 | importOrderSeparation: false,
29 | importOrderSortSpecifiers: true,
30 | importOrderBuiltinModulesToTop: true,
31 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
32 | importOrderMergeDuplicateImports: true,
33 | importOrderCombineTypeAndValueImports: true
34 | }
35 |
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/humanloop/chatbot-starter/7577e9ac88289fdd4f8cc93d6462ab15be8ceeac/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/humanloop/chatbot-starter/7577e9ac88289fdd4f8cc93d6462ab15be8ceeac/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/humanloop/chatbot-starter/7577e9ac88289fdd4f8cc93d6462ab15be8ceeac/public/favicon.ico
--------------------------------------------------------------------------------
/public/humanloop-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/humanloop/chatbot-starter/7577e9ac88289fdd4f8cc93d6462ab15be8ceeac/public/humanloop-example.png
--------------------------------------------------------------------------------
/public/interface-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/humanloop/chatbot-starter/7577e9ac88289fdd4f8cc93d6462ab15be8ceeac/public/interface-example.png
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/thirteen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const { fontFamily } = require('tailwindcss/defaultTheme')
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | darkMode: ['class'],
6 | content: ['app/**/*.{ts,tsx}', 'components/**/*.{ts,tsx}'],
7 | theme: {
8 | container: {
9 | center: true,
10 | padding: '2rem',
11 | screens: {
12 | '2xl': '1400px'
13 | }
14 | },
15 | extend: {
16 | fontFamily: {
17 | sans: ['var(--font-sans)', ...fontFamily.sans]
18 | },
19 | colors: {
20 | border: 'hsl(var(--border))',
21 | input: 'hsl(var(--input))',
22 | ring: 'hsl(var(--ring))',
23 | background: 'hsl(var(--background))',
24 | foreground: 'hsl(var(--foreground))',
25 | primary: {
26 | DEFAULT: 'hsl(var(--primary))',
27 | foreground: 'hsl(var(--primary-foreground))'
28 | },
29 | secondary: {
30 | DEFAULT: 'hsl(var(--secondary))',
31 | foreground: 'hsl(var(--secondary-foreground))'
32 | },
33 | destructive: {
34 | DEFAULT: 'hsl(var(--destructive))',
35 | foreground: 'hsl(var(--destructive-foreground))'
36 | },
37 | muted: {
38 | DEFAULT: 'hsl(var(--muted))',
39 | foreground: 'hsl(var(--muted-foreground))'
40 | },
41 | accent: {
42 | DEFAULT: 'hsl(var(--accent))',
43 | foreground: 'hsl(var(--accent-foreground))'
44 | },
45 | popover: {
46 | DEFAULT: 'hsl(var(--popover))',
47 | foreground: 'hsl(var(--popover-foreground))'
48 | },
49 | card: {
50 | DEFAULT: 'hsl(var(--card))',
51 | foreground: 'hsl(var(--card-foreground))'
52 | }
53 | },
54 | borderRadius: {
55 | lg: `var(--radius)`,
56 | md: `calc(var(--radius) - 2px)`,
57 | sm: 'calc(var(--radius) - 4px)'
58 | },
59 | keyframes: {
60 | 'accordion-down': {
61 | from: { height: 0 },
62 | to: { height: 'var(--radix-accordion-content-height)' }
63 | },
64 | 'accordion-up': {
65 | from: { height: 'var(--radix-accordion-content-height)' },
66 | to: { height: 0 }
67 | },
68 | 'slide-from-left': {
69 | '0%': {
70 | transform: 'translateX(-100%)'
71 | },
72 | '100%': {
73 | transform: 'translateX(0)'
74 | }
75 | },
76 | 'slide-to-left': {
77 | '0%': {
78 | transform: 'translateX(0)'
79 | },
80 | '100%': {
81 | transform: 'translateX(-100%)'
82 | }
83 | }
84 | },
85 | animation: {
86 | 'slide-from-left':
87 | 'slide-from-left 0.3s cubic-bezier(0.82, 0.085, 0.395, 0.895)',
88 | 'slide-to-left':
89 | 'slide-to-left 0.25s cubic-bezier(0.82, 0.085, 0.395, 0.895)',
90 | 'accordion-down': 'accordion-down 0.2s ease-out',
91 | 'accordion-up': 'accordion-up 0.2s ease-out'
92 | }
93 | }
94 | },
95 | plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')]
96 | }
97 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "noEmit": true,
9 | "incremental": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "baseUrl": ".",
17 | "paths": {
18 | "@/*": ["./*"]
19 | },
20 | "plugins": [
21 | {
22 | "name": "next"
23 | }
24 | ],
25 | "strictNullChecks": true
26 | },
27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
28 | "exclude": ["node_modules"]
29 | }
30 |
--------------------------------------------------------------------------------