├── .env.example ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── Todo.md ├── app ├── (chat) │ ├── chat │ │ └── [id] │ │ │ └── page.tsx │ ├── layout.tsx │ └── page.tsx ├── actions.ts └── layout.tsx ├── components.json ├── components ├── button-scroll-to-bottom.tsx ├── chat-history.tsx ├── chat-list.tsx ├── chat-message-actions.tsx ├── chat-panel.tsx ├── chat-scroll-anchor.tsx ├── chat.tsx ├── clear-history.tsx ├── empty-screen.tsx ├── external-link.tsx ├── footer.tsx ├── header.tsx ├── hover-card-model-select.txt ├── markdown.tsx ├── model-config-slider.tsx ├── model-config.tsx ├── model-selector-item.tsx ├── model-selector.tsx ├── onboarding-screen.tsx ├── prompt-form.tsx ├── prompt-library.tsx ├── prompt-list.tsx ├── providers.tsx ├── sidebar-actions.tsx ├── sidebar-desktop.tsx ├── sidebar-footer.tsx ├── sidebar-item.tsx ├── sidebar-items.tsx ├── sidebar-list.tsx ├── sidebar-mobile.tsx ├── sidebar-toggle.tsx ├── sidebar.tsx ├── stocks │ ├── events-skeleton.tsx │ ├── events.tsx │ ├── index.tsx │ ├── message.tsx │ ├── models-backup.json │ ├── spinner.tsx │ ├── stock-purchase.tsx │ ├── stock-skeleton.tsx │ ├── stock.tsx │ ├── stocks-skeleton.tsx │ └── stocks.tsx ├── tailwind-indicator.tsx ├── theme-toggle.tsx ├── ui │ ├── alert-dialog.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── card.tsx │ ├── codeblock.tsx │ ├── collapsible.tsx │ ├── command.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── hover-card.tsx │ ├── icons.tsx │ ├── input.tsx │ ├── label.tsx │ ├── popover.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── slider.tsx │ ├── sonner.tsx │ ├── switch.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ └── tooltip.tsx └── user-menu.tsx ├── database ├── chats.json ├── functions.ts ├── models.json ├── prompts.json └── prompts.ts ├── lib ├── chat │ ├── actions-old.tsx │ └── actions.tsx ├── hooks │ ├── use-at-bottom.tsx │ ├── use-copy-to-clipboard.tsx │ ├── use-enter-submit.tsx │ ├── use-form-input.tsx │ ├── use-local-storage.ts │ ├── use-mutation-observer.ts │ ├── use-scroll-anchor.tsx │ ├── use-sidebar.tsx │ └── use-streamable-text.ts ├── types.ts └── utils.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prettier.config.cjs ├── public ├── apple-touch-icon.png ├── demo-video.mp4 ├── favicon-16x16.png ├── favicon.ico ├── gemma.png ├── next.svg ├── ollama.svg ├── screen.png ├── thirteen.svg └── vercel.svg ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── src │ └── main.rs └── tauri.conf.json ├── styles └── globals.css ├── tailwind.config.ts └── 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= 4 | 5 | # Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32` 6 | AUTH_SECRET= 7 | 8 | # Instructions to create kv database here: https://vercel.com/docs/storage/vercel-kv/quickstart and 9 | KV_URL= 10 | KV_REST_API_URL= 11 | KV_REST_API_TOKEN= 12 | KV_REST_API_READ_ONLY_TOKEN= -------------------------------------------------------------------------------- /.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 | "tailwindcss/classnames-order": "off" 13 | }, 14 | "settings": { 15 | "tailwindcss": { 16 | "callees": ["cn", "cva"], 17 | "config": "tailwind.config.js" 18 | } 19 | }, 20 | "overrides": [ 21 | { 22 | "files": ["*.ts", "*.tsx"], 23 | "parser": "@typescript-eslint/parser" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.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 | .env 36 | .vercel 37 | .vscode 38 | .env*.local 39 | -------------------------------------------------------------------------------- /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 | Screenshot 2023-10-29 at 13 39 22 2 | 3 |

InteractAI Hub

4 | 5 |

6 | InteractAI Hub provides a simple and easy-to-use interface for interacting with AI models. 7 |

8 | 9 |

10 | Demo Video · 11 | Features · 12 | Roadmap · 13 | Running locally. 14 | Acknowledgments 15 |

16 |
17 | 18 |

19 | This project is a work in progress and will be updated with new features and improvements. 20 |

21 | 22 | ## Features 23 | 24 | - [Next.js](https://nextjs.org) App Router 25 | - React Server Components (RSCs), Suspense, and Server Actions 26 | - [Vercel AI SDK](https://sdk.vercel.ai/docs) for streaming chat UI 27 | - [shadcn/ui](https://ui.shadcn.com) 28 | - Styling with [Tailwind CSS](https://tailwindcss.com) 29 | - [Radix UI](https://radix-ui.com) for headless component primitives 30 | - Icons from [Phosphor Icons](https://phosphoricons.com) 31 | 32 | ## Roadmap 33 | 34 | - Convert to desktop app using [Tauri](https://tauri.app/) 35 | 36 | ## Running locally 37 | 38 | You will need to use the environment variables [defined in `.env.example`](.env.example) 39 | 40 | > Note: You should not commit your `.env` file or it will expose secrets that will allow others to control access to your various OpenAI and authentication provider accounts. 41 | 42 | ```bash 43 | pnpm install 44 | pnpm dev 45 | ``` 46 | 47 | Should now be running on [localhost:3000](http://localhost:3000/). 48 | 49 | ## Acknowledgments 50 | 51 | This won't be possible without the following resources: 52 | 53 | - [Vercel AI SDK](https://sdk.vercel.ai/docs) 54 | - [Vercel Templates](https://vercel.com/templates) 55 | - [Shadcn UI](https://ui.shadcn.com) 56 | - [Ollama](https://ollama.com) 57 | -------------------------------------------------------------------------------- /Todo.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | 4 | 5 | 6 | 7 | - [] remove Generative UI support for now 8 | - [] add more models support 9 | - [] add ollama docs support 10 | - [] add perplexity like using generative ui 11 | - [] convert to a desktop app using turi 12 | -------------------------------------------------------------------------------- /app/(chat)/chat/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from 'next' 2 | import { notFound, redirect } from 'next/navigation' 3 | 4 | import { 5 | getChat, 6 | getChats, 7 | getModelsList, 8 | getOllamaModels, 9 | isOllamaAvailable, 10 | syncModels 11 | } from '@/app/actions' 12 | import { Chat } from '@/components/chat' 13 | import { AI } from '@/lib/chat/actions' 14 | import { Model } from 'openai/resources' 15 | 16 | export interface ChatPageProps { 17 | params: { 18 | id: string 19 | } 20 | } 21 | 22 | // export async function generateMetadata({ 23 | // params 24 | // }: ChatPageProps): Promise { 25 | // const chat = await getChat(params.id) 26 | // return { 27 | // title: chat?.title.toString().slice(0, 50) ?? 'Chat' 28 | // } 29 | // } 30 | 31 | export default async function ChatPage({ params }: ChatPageProps) { 32 | const chat = await getChat(params.id) 33 | const models = await getModelsList() 34 | 35 | if (!chat) { 36 | redirect('/') 37 | } 38 | 39 | return ( 40 | 47 | 48 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /app/(chat)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { SidebarDesktop } from '@/components/sidebar-desktop' 2 | 3 | import { PromptLibrary } from '@/components/prompt-library' 4 | import { ChatHistory } from '@/components/chat-history' 5 | 6 | interface ChatLayoutProps { 7 | children: React.ReactNode 8 | } 9 | 10 | export default async function ChatLayout({ children }: ChatLayoutProps) { 11 | return ( 12 | // h-[calc(100vh_-_theme(spacing.16))] 13 |
14 | 15 | 16 | 17 | {children} 18 | 19 | 20 | 21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /app/(chat)/page.tsx: -------------------------------------------------------------------------------- 1 | import { nanoid } from '@/lib/utils' 2 | import { Chat } from '@/components/chat' 3 | import { AI } from '@/lib/chat/actions' 4 | import { 5 | getModelsList, 6 | getOllamaModels, 7 | isOllamaAvailable, 8 | syncModels 9 | } from '../actions' 10 | import { Models } from '@/lib/types' 11 | import { OnBoardingScreen } from '@/components/onboarding-screen' 12 | 13 | interface IndexPageProps { 14 | searchParams: { 15 | model: string 16 | } 17 | } 18 | 19 | export default async function IndexPage({ searchParams }: IndexPageProps) { 20 | const id = nanoid() 21 | 22 | const status = await isOllamaAvailable() 23 | if (!status) return 24 | 25 | const ollama_models = await getOllamaModels() 26 | if (ollama_models.length === 0) return 27 | 28 | await syncModels(ollama_models) 29 | const models: Models = (await getModelsList()) ?? {} 30 | const installed = Object.values(models).filter(model => model.installed) 31 | const defaultModel = searchParams.model 32 | ? installed.find(model => model.name === searchParams.model) 33 | : installed[0] 34 | 35 | return ( 36 | 37 | 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /app/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | // GLOBAL: Auth: userId: "123" 4 | 5 | import { revalidatePath } from 'next/cache' 6 | import { redirect } from 'next/navigation' 7 | import { 8 | readDatabase, 9 | writeDatabase, 10 | readDatabaseModels, 11 | writeDatabaseModels 12 | } from '@/database/functions' 13 | 14 | import { type Chat } from '@/lib/types' 15 | 16 | export async function getChats(): Promise { 17 | try { 18 | const allChats = await readDatabase() 19 | const chats = Object.values(allChats) 20 | return chats as Chat[] 21 | } catch (error) { 22 | console.error('Error getting chats:', error) 23 | return [] 24 | } 25 | } 26 | 27 | export async function getChat(id: string): Promise { 28 | try { 29 | const chats = await readDatabase() 30 | const chatKey = `chat:${id}` // Construct the correct key 31 | const chat = chats[chatKey] // Use the constructed key to access the chat 32 | 33 | return chat 34 | } catch (error) { 35 | console.error('Error getting chat:', error) 36 | return null 37 | } 38 | } 39 | 40 | export async function removeChat({ id, path }: { id: string; path: string }) { 41 | try { 42 | const chats = await readDatabase() 43 | const chatKey = `chat:${id}` // Construct the correct key 44 | const chat = chats[chatKey] // Use the constructed key to access the chat 45 | 46 | delete chats[chatKey] // Delete using the correct key 47 | await writeDatabase(chats) 48 | revalidatePath('/') 49 | return revalidatePath(path) 50 | } catch (error) { 51 | console.error('Error removing chat:', error) 52 | return { 53 | error: 'Something went wrong' 54 | } 55 | } 56 | } 57 | 58 | export async function clearChats() { 59 | try { 60 | const chats = await readDatabase() 61 | const userChats = Object.values(chats) 62 | 63 | if (userChats.length === 0) { 64 | return redirect('/') 65 | } 66 | 67 | // Remove user's chats from the database 68 | for (const chat of userChats) { 69 | delete chats[chat.id] 70 | } 71 | 72 | await writeDatabase(chats) 73 | 74 | revalidatePath('/') 75 | return redirect('/') 76 | } catch (error) { 77 | console.error('Error clearing chats:', error) 78 | return { 79 | error: 'Something went wrong' 80 | } 81 | } 82 | } 83 | 84 | export async function saveChat(chat: Chat) { 85 | const chats = await readDatabase() 86 | chats[`chat:${chat.id}`] = chat 87 | await writeDatabase(chats) 88 | } 89 | 90 | // ********************** OLLAMA ********************** 91 | 92 | const OLLAMA_URL = 'http://localhost:11434' 93 | 94 | export async function isOllamaAvailable() { 95 | try { 96 | const data = await fetch(OLLAMA_URL) 97 | return data.ok 98 | } catch (error) { 99 | console.error('Error checking OLLAMA:', error) 100 | return false 101 | } 102 | } 103 | 104 | // create a sync function to sync installed models with supported models by getting installed models and then updating the supported models installed field to true. 105 | 106 | export async function getOllamaModels() { 107 | const res = await fetch('http://localhost:11434/api/tags', { 108 | cache: 'no-store' 109 | }) 110 | const data = await res.json() 111 | 112 | console.log('data', data) 113 | 114 | return data 115 | } 116 | 117 | export async function getModelsList() { 118 | const models = await readDatabaseModels() 119 | return models 120 | } 121 | 122 | // TODO: Handle unsupported models 123 | export async function syncModels(ollama_models: any) { 124 | let models = await getModelsList() 125 | // @ts-ignore 126 | const installedModelNames = ollama_models.models.map(model => model.name) 127 | 128 | for (let modelName in models) { 129 | if (installedModelNames.includes(modelName)) { 130 | models[modelName].installed = true 131 | } 132 | } 133 | 134 | await writeDatabaseModels(models) 135 | return 'Models synced successfully' 136 | } 137 | 138 | // get installed models 139 | export async function getInstalled() { 140 | const models = await readDatabaseModels() 141 | // filter out models with installed field set to true 142 | const installed = Object.values(models).filter(model => model.installed) 143 | return installed 144 | } 145 | 146 | // get uninstalled models 147 | export async function getUninstalled() { 148 | const models = await readDatabaseModels() 149 | // filter out models with installed field set to false 150 | const uninstalled = Object.values(models).filter(model => !model.installed) 151 | return uninstalled 152 | } 153 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { GeistSans } from 'geist/font/sans' 2 | import { GeistMono } from 'geist/font/mono' 3 | 4 | import '@/styles/globals.css' 5 | import { cn } from '@/lib/utils' 6 | // import { TailwindIndicator } from '@/components/tailwind-indicator' 7 | import { Providers } from '@/components/providers' 8 | import { Toaster } from '@/components/ui/sonner' 9 | import { Header } from '@/components/header' 10 | 11 | export const metadata = { 12 | metadataBase: new URL(`https://${process.env.VERCEL_URL}`), 13 | title: { 14 | default: 'General AI Chatbot', 15 | template: `%s - Next.js AI Chatbot` 16 | }, 17 | description: 'An AI-powered chatbot template built with Next.js and Vercel.', 18 | icons: { 19 | icon: '/favicon.ico', 20 | shortcut: '/favicon-16x16.png', 21 | apple: '/apple-touch-icon.png' 22 | } 23 | } 24 | 25 | export const viewport = { 26 | themeColor: [ 27 | { media: '(prefers-color-scheme: light)', color: 'white' }, 28 | { media: '(prefers-color-scheme: dark)', color: 'black' } 29 | ] 30 | } 31 | 32 | interface RootLayoutProps { 33 | children: React.ReactNode 34 | } 35 | 36 | export default function RootLayout({ children }: RootLayoutProps) { 37 | return ( 38 | 39 | 46 | 47 | 53 | {/*
54 |
55 |
{children}
56 |
*/} 57 |
58 | {/*
*/} 59 |
60 | {children} 61 |
62 |
63 |
64 | 65 | 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /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 { Button, type ButtonProps } from '@/components/ui/button' 7 | import { IconArrowDown } from '@/components/ui/icons' 8 | 9 | interface ButtonScrollToBottomProps extends ButtonProps { 10 | isAtBottom: boolean 11 | scrollToBottom: () => void 12 | } 13 | 14 | export function ButtonScrollToBottom({ 15 | className, 16 | isAtBottom, 17 | scrollToBottom, 18 | ...props 19 | }: ButtonScrollToBottomProps) { 20 | return ( 21 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /components/chat-history.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import Link from 'next/link' 4 | 5 | import { cn } from '@/lib/utils' 6 | import { SidebarList } from '@/components/sidebar-list' 7 | import { buttonVariants } from '@/components/ui/button' 8 | import { IconPlus } from '@/components/ui/icons' 9 | import { Input } from '@/components/ui/input' 10 | 11 | interface ChatHistoryProps {} 12 | 13 | export function ChatHistory({}: ChatHistoryProps) { 14 | return ( 15 |
16 |
17 | 24 | 25 | New Chat 26 | 27 |
28 | 31 | {Array.from({ length: 10 }).map((_, i) => ( 32 |
36 | ))} 37 |
38 | } 39 | > 40 | 41 |
42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /components/chat-list.tsx: -------------------------------------------------------------------------------- 1 | import { UIState } from '@/lib/chat/actions' 2 | 3 | export interface ChatList { 4 | messages: UIState 5 | } 6 | 7 | export function ChatList({ messages }: ChatList) { 8 | if (!messages.length) { 9 | return null 10 | } 11 | 12 | return ( 13 |
14 | {messages.map((message, index) => ( 15 |
19 |
20 | {message.display} 21 |
22 |
23 | ))} 24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /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-panel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { PromptForm } from '@/components/prompt-form' 4 | import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom' 5 | import { FooterText } from '@/components/footer' 6 | import { useAIState, useActions, useUIState } from 'ai/rsc' 7 | import type { AI } from '@/lib/chat/actions' 8 | import { cn } from '@/lib/utils' 9 | import { useSidebar } from '@/lib/hooks/use-sidebar' 10 | 11 | export interface ChatPanelProps { 12 | id?: string 13 | title?: string 14 | isAtBottom: boolean 15 | scrollToBottom: () => void 16 | } 17 | 18 | export function ChatPanel({ 19 | id, 20 | title, 21 | isAtBottom, 22 | scrollToBottom 23 | }: ChatPanelProps) { 24 | const { isLeftSidebarOpen, isRightSidebarOpen } = useSidebar() 25 | 26 | const exampleMessages = [ 27 | { 28 | heading: 'What are the', 29 | subheading: 'trending memecoins today?', 30 | message: `What are the trending memecoins today?` 31 | }, 32 | { 33 | heading: 'What is the price of', 34 | subheading: '$DOGE right now?', 35 | message: 'What is the price of $DOGE right now?' 36 | }, 37 | { 38 | heading: 'I would like to buy', 39 | subheading: '42 $DOGE', 40 | message: `I would like to buy 42 $DOGE` 41 | }, 42 | { 43 | heading: 'What are some', 44 | subheading: `recent events about $DOGE?`, 45 | message: `What are some recent events about $DOGE?` 46 | } 47 | ] 48 | 49 | return ( 50 | //
51 |
59 | 63 | 64 |
65 | {/*
66 | {messages.length === 0 && 67 | exampleMessages.map((example, index) => ( 68 |
1 && 'hidden md:block' 72 | }`} 73 | onClick={async () => { 74 | setMessages(currentMessages => [ 75 | ...currentMessages, 76 | { 77 | id: nanoid(), 78 | display: {example.message} 79 | } 80 | ]) 81 | 82 | const responseMessage = await submitUserMessage( 83 | example.message 84 | ) 85 | 86 | setMessages(currentMessages => [ 87 | ...currentMessages, 88 | responseMessage 89 | ]) 90 | }} 91 | > 92 |
{example.heading}
93 |
94 | {example.subheading} 95 |
96 |
97 | ))} 98 |
*/} 99 | 100 | 101 | {/* */} 102 |
103 |
104 | ) 105 | } 106 | -------------------------------------------------------------------------------- /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 { cn } from '@/lib/utils' 4 | import { ChatList } from '@/components/chat-list' 5 | import { ChatPanel } from '@/components/chat-panel' 6 | import { EmptyScreen } from '@/components/empty-screen' 7 | import { useLocalStorage } from '@/lib/hooks/use-local-storage' 8 | import { useEffect, useState } from 'react' 9 | import { useUIState, useAIState } from 'ai/rsc' 10 | import { usePathname, useRouter } from 'next/navigation' 11 | import { Message } from '@/lib/chat/actions' 12 | import { useScrollAnchor } from '@/lib/hooks/use-scroll-anchor' 13 | import { Header } from './header' 14 | import { Models } from '@/lib/types' 15 | import { useSidebar } from '@/lib/hooks/use-sidebar' 16 | // import { toast } from 'sonner' 17 | 18 | import { useInput } from '@/lib/hooks/use-form-input' 19 | 20 | export interface ChatProps extends React.ComponentProps<'div'> { 21 | initialMessages?: Message[] 22 | id?: string 23 | models: Models 24 | } 25 | 26 | export function Chat({ id, className, models }: ChatProps) { 27 | const router = useRouter() 28 | const path = usePathname() 29 | // const [input, setInput] = useState('') 30 | const { inputValue, setInputValue } = useInput() 31 | const [messages] = useUIState() 32 | const [aiState] = useAIState() 33 | 34 | const [_, setNewChatId] = useLocalStorage('newChatId', id) 35 | 36 | useEffect(() => { 37 | if (!path.includes('chat') && messages.length === 1) { 38 | window.history.replaceState({}, '', `/chat/${id}`) 39 | } 40 | }, [id, path, messages]) 41 | 42 | useEffect(() => { 43 | const messagesLength = aiState.messages?.length 44 | if (messagesLength === 2) { 45 | router.refresh() 46 | } 47 | }, [aiState.messages, router]) 48 | 49 | useEffect(() => { 50 | setNewChatId(id) 51 | setInputValue('') 52 | // eslint-disable-next-line react-hooks/exhaustive-deps 53 | }, []) 54 | 55 | const { messagesRef, scrollRef, visibilityRef, isAtBottom, scrollToBottom } = 56 | useScrollAnchor() 57 | 58 | const { isLeftSidebarOpen, isRightSidebarOpen } = useSidebar() 59 | 60 | return ( 61 |
70 |
71 |
72 | {messages.length ? : } 73 |
74 |
75 | 80 |
81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /components/clear-history.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { useRouter } from 'next/navigation' 5 | import { toast } from 'sonner' 6 | 7 | import { ServerActionResult } from '@/lib/types' 8 | import { Button } from '@/components/ui/button' 9 | import { 10 | AlertDialog, 11 | AlertDialogAction, 12 | AlertDialogCancel, 13 | AlertDialogContent, 14 | AlertDialogDescription, 15 | AlertDialogFooter, 16 | AlertDialogHeader, 17 | AlertDialogTitle, 18 | AlertDialogTrigger 19 | } from '@/components/ui/alert-dialog' 20 | import { IconSpinner } from '@/components/ui/icons' 21 | 22 | interface ClearHistoryProps { 23 | isEnabled: boolean 24 | clearChats: () => ServerActionResult 25 | } 26 | 27 | export function ClearHistory({ 28 | isEnabled = false, 29 | clearChats 30 | }: ClearHistoryProps) { 31 | const [open, setOpen] = React.useState(false) 32 | const [isPending, startTransition] = React.useTransition() 33 | const router = useRouter() 34 | 35 | return ( 36 | 37 | 38 | 42 | 43 | 44 | 45 | Are you absolutely sure? 46 | 47 | This will permanently delete your chat history and remove your data 48 | from our servers. 49 | 50 | 51 | 52 | Cancel 53 | { 56 | event.preventDefault() 57 | startTransition(async () => { 58 | const result = await clearChats() 59 | if (result && 'error' in result) { 60 | toast.error(result.error) 61 | return 62 | } 63 | 64 | setOpen(false) 65 | }) 66 | }} 67 | > 68 | {isPending && } 69 | Delete 70 | 71 | 72 | 73 | 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /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 { 6 | IconArrowRight, 7 | IconGitHub, 8 | IconNextChat, 9 | getModelIcon 10 | } from '@/components/ui/icons' 11 | import { Card, CardContent } from '@/components/ui/card' 12 | import { useAIState } from 'ai/rsc' 13 | 14 | import { prompts } from '@/database/prompts' 15 | 16 | import { PromptCardHome } from './prompt-list' 17 | import Link from 'next/link' 18 | 19 | const exampleMessages = [ 20 | { 21 | heading: 'Explain technical concepts', 22 | message: `What is a "serverless function"?` 23 | }, 24 | { 25 | heading: 'Summarize an article', 26 | message: 'Summarize the following article for a 2nd grader: \n' 27 | }, 28 | { 29 | heading: 'Draft an email', 30 | message: `Draft an email to my boss about the following: \n` 31 | } 32 | ] 33 | 34 | // export function EmptyScreen({ setInput }: Pick) { 35 | export function EmptyScreen() { 36 | const [aiState] = useAIState() 37 | const selected = aiState.model 38 | 39 | const icon = getModelIcon(selected.created_by) 40 | 41 | // get the first 5 prompts 42 | const examplePrompts = prompts.slice(87, 91) 43 | 44 | return ( 45 |
46 | 47 |
48 |
49 | {icon} 50 | 51 |
52 | Model 53 | / 54 | 55 | {selected.label} 56 | 57 |
58 |
59 |
60 | {selected.description} 61 |
62 |
63 | 64 | {/*
65 |
Context
66 |
67 |
68 |
69 |
70 |
Input Pricing
71 |
72 |
73 |
74 |
75 |
Output Pricing
76 |
77 |
78 |
*/} 79 |
80 | You can start a conversation here or try the following examples: 81 |
82 | 83 |
84 | {examplePrompts.map((prompt: any, index: any) => ( 85 | 86 | ))} 87 |
88 |
89 |
90 |
91 |
92 | 96 | 97 | Github 98 | 99 | 100 |
101 | 105 | 106 | Twitter 107 | 108 | 109 |
110 |
111 |
112 |
113 | ) 114 | } 115 | -------------------------------------------------------------------------------- /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 | 32 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /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 ( 8 |

15 | Open source AI chatbot built with{' '} 16 | Next.js and{' '} 17 | 18 | Vercel AI SDK 19 | 20 | . 21 |

22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /components/header.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | import { Button, buttonVariants } from '@/components/ui/button' 6 | import { IconSettings, IconDownload } from '@/components/ui/icons' 7 | import { SidebarMobile } from './sidebar-mobile' 8 | import { SidebarToggle } from './sidebar-toggle' 9 | import { ChatHistory } from './chat-history' 10 | import { ModelSelector } from './model-selector' 11 | import { ModelConfig } from './model-config' 12 | import { Models } from '@/lib/types' 13 | 14 | import { PromptLibrary } from './prompt-library' 15 | 16 | interface HeaderProps { 17 | models: Models 18 | } 19 | 20 | export function Header({ models }: HeaderProps) { 21 | return ( 22 |
23 |
24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 32 |
33 |
34 | 35 | {/* space-x-2 */} 36 |
37 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /components/hover-card-model-select.txt: -------------------------------------------------------------------------------- 1 | 2 | 10 |
11 |
12 |
13 |
14 | {/* */} 15 |
16 | Anthropic 17 | / 18 | 19 | claude-instant-1.2 20 | 21 |
22 |
23 |
24 | A faster, cheaper yet still very capable version of 25 | Claude, which can handle a range of tasks including casual 26 | dialogue, text analysis, summarization, and document 27 | comprehension. 28 |
29 |
30 |
31 |
32 |
Context
33 |
34 | 100,000 tokens 35 |
36 |
37 |
38 |
39 | Input Pricing 40 |
41 |
42 | $1.63 / million tokens 43 |
44 |
45 |
46 |
47 | Output Pricing 48 |
49 |
50 | $5.51 / million tokens 51 |
52 |
53 |
54 |
Uptime
55 |
56 |
57 |
58 |
12 hrs ago
59 |
60 |
61 | 100.00% Uptime 62 |
63 |
64 |
Now
65 |
66 |
67 |
68 |
69 |
70 |
71 | {/*

{peekedModel.name}

72 |
73 | {peekedModel.description} 74 |
75 | {peekedModel.strengths ? ( 76 |
77 |
78 | Strengths 79 |
80 |
    81 | {peekedModel.strengths} 82 |
83 |
84 | ) : null} */} 85 |
86 |
-------------------------------------------------------------------------------- /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/model-config-slider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | import { SliderProps } from '@radix-ui/react-slider' 6 | 7 | import { 8 | HoverCard, 9 | HoverCardContent, 10 | HoverCardTrigger 11 | } from '@/components//ui/hover-card' 12 | import { Slider } from '@/components//ui/slider' 13 | import { Label } from '@/components/ui/label' 14 | import { InfoCircledIcon } from '@radix-ui/react-icons' 15 | 16 | interface ConfigSliderProps { 17 | defaultValue: SliderProps['defaultValue'] 18 | label: string 19 | step?: number 20 | min_value: number 21 | max_value: number 22 | information: string 23 | } 24 | 25 | export function ConfigSlider({ 26 | defaultValue, 27 | label, 28 | step = 1, 29 | min_value, 30 | max_value, 31 | information 32 | }: ConfigSliderProps) { 33 | const [value, setValue] = React.useState(defaultValue) 34 | 35 | return ( 36 |
37 | 38 |
39 |
40 |
41 | 44 | 45 | 46 | 47 |
48 | 49 | 50 | {value} 51 | 52 |
53 | 63 |
64 | 68 | {information} 69 | 70 |
71 |
72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /components/model-config.tsx: -------------------------------------------------------------------------------- 1 | interface ModelConfigProps {} 2 | import { Button } from '@/components/ui/button' 3 | 4 | import { 5 | DropdownMenu, 6 | DropdownMenuContent, 7 | DropdownMenuTrigger 8 | } from '@/components/ui/dropdown-menu' 9 | import { MixerHorizontalIcon } from '@radix-ui/react-icons' 10 | 11 | export function ModelConfig({}: ModelConfigProps) { 12 | return ( 13 | 14 | 15 | 18 | 19 | 20 | 21 | model config is in development :) 22 | 23 | 24 | ) 25 | } 26 | 27 | // TODO: DELETE THIS 28 | import { 29 | Card, 30 | CardContent, 31 | CardDescription, 32 | CardFooter 33 | } from '@/components/ui/card' 34 | import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' 35 | 36 | export function TabsDemo() { 37 | return ( 38 | 39 | 40 | Only This Chat 41 | All llama2 Chats 42 | {/* All Chats */} 43 | 44 | 45 | 46 | This Configuration will only affect this chat. 47 | 48 | 49 | {/* 50 | Conversation 51 | 52 | This Configuration will only affect this chat. 53 | 54 | */} 55 | 56 | 57 | {/*
58 | 59 | 60 |
61 |
62 | 63 | 64 |
*/} 65 | {/* 66 | 67 | 68 | */} 69 |
70 | 71 | 72 | 73 | 74 |
75 |
76 | 77 | {/* 78 | 79 | Password 80 | 81 | Change your password here. After saving, you'll be logged out. 82 | 83 | 84 | 85 |
86 | 87 | 88 |
89 |
90 | 91 | 92 |
93 |
94 | 95 | 96 | 97 |
*/} 98 |
99 |
100 | ) 101 | } 102 | -------------------------------------------------------------------------------- /components/model-selector-item.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { useMutationObserver } from '@/lib/hooks/use-mutation-observer' 5 | import { CommandItem } from '@/components/ui/command' 6 | import { getModelIcon } from '@/components/ui/icons' 7 | import { CheckIcon } from '@radix-ui/react-icons' 8 | import { cn } from '@/lib/utils' 9 | 10 | interface ModelItemProps { 11 | model: any 12 | isSelected: boolean 13 | onSelect: () => void 14 | onPeek: (model: any) => void 15 | } 16 | 17 | export function ModelItem({ 18 | model, 19 | isSelected, 20 | onSelect, 21 | onPeek 22 | }: ModelItemProps) { 23 | const ref = React.useRef(null) 24 | const icon = getModelIcon(model.created_by) 25 | 26 | useMutationObserver(ref, mutations => { 27 | for (const mutation of mutations) { 28 | if (mutation.type === 'attributes') { 29 | if (mutation.attributeName === 'aria-selected') { 30 | onPeek(model) 31 | } 32 | } 33 | } 34 | }) 35 | 36 | return ( 37 | 43 | {icon} 44 | 45 | {model.name} 46 | 47 | 53 | 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /components/model-selector.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { CaretSortIcon } from '@radix-ui/react-icons' 3 | import { useRouter } from 'next/navigation' 4 | import { ModelItem } from './model-selector-item' 5 | import { useAIState } from 'ai/rsc' 6 | import { getModelIcon } from '@/components/ui/icons' 7 | import { Models } from '@/lib/types' 8 | import { usePathname } from 'next/navigation' 9 | 10 | import { Button } from '@/components/ui/button' 11 | import { 12 | Command, 13 | CommandEmpty, 14 | CommandGroup, 15 | CommandInput, 16 | CommandList 17 | } from '@/components/ui/command' 18 | 19 | import { 20 | Popover, 21 | PopoverContent, 22 | PopoverTrigger 23 | } from '@/components/ui/popover' 24 | 25 | interface ModelSelectorProps { 26 | models: Models 27 | } 28 | 29 | export function ModelSelector({ models }: ModelSelectorProps) { 30 | // url and check if include something after chat then don't allow to change model 31 | const pathname = usePathname() 32 | const router = useRouter() 33 | // const router = useRouter() 34 | const [aiState, setAIState] = useAIState() 35 | 36 | const [open, setOpen] = React.useState(false) 37 | const [peekedModel, setPeekedModel] = React.useState(models[0]) 38 | const installed = Object.values(models).filter(model => model.installed) 39 | 40 | const [selectedModel, setSelectedModel] = React.useState( 41 | aiState.model ?? installed[0] 42 | ) 43 | 44 | const icon = getModelIcon(selectedModel.created_by) 45 | 46 | return ( 47 | 48 | 49 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | No Models found. 72 | 73 | {installed.map(model => ( 74 | setPeekedModel(model)} 79 | onSelect={() => { 80 | if (pathname.includes('/chat/')) { 81 | router.push(`/?model=${model.name}`) 82 | } else { 83 | setSelectedModel(model) 84 | setAIState({ ...aiState, model: model }) 85 | setOpen(false) 86 | } 87 | }} 88 | /> 89 | ))} 90 | 91 | 92 | 93 | 94 | 95 | ) 96 | } 97 | -------------------------------------------------------------------------------- /components/onboarding-screen.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { Button } from '@/components/ui/button' 4 | import { 5 | Card, 6 | CardContent, 7 | CardDescription, 8 | CardFooter, 9 | CardHeader, 10 | CardTitle 11 | } from '@/components/ui/card' 12 | import { Input } from '@/components/ui/input' 13 | import { Label } from '@/components/ui/label' 14 | import { 15 | Select, 16 | SelectContent, 17 | SelectItem, 18 | SelectTrigger, 19 | SelectValue 20 | } from '@/components/ui/select' 21 | import { IconCopy, IconOllama } from './ui/icons' 22 | import { DownloadIcon } from '@radix-ui/react-icons' 23 | 24 | // Get up and running with large language models locally. 25 | 26 | // macOS 27 | // Download 28 | 29 | // Windows preview 30 | // Download 31 | 32 | // Linux 33 | // curl -fsSL https://ollama.com/install.sh | sh 34 | // Manual install instructions 35 | 36 | // Docker 37 | // The official Ollama Docker image ollama/ollama is available on Docker Hub. 38 | 39 | // Libraries 40 | // ollama-python 41 | // ollama-js 42 | // Quickstart 43 | // To run and chat with Llama 2: 44 | 45 | // ollama run llama2 46 | 47 | export function OnBoardingScreen() { 48 | return ( 49 |
50 | 51 | 52 | Get started with Ollama 53 | 54 | Oops! It seems like you don't have Ollama running or installed, 55 | or perhaps you haven't added any models yet. 56 | 57 | 58 | 59 | {/* download mac, linux, windows */} 60 |
61 |
62 | Install Ollama for your operating system: 63 |
64 |
65 | 66 | 67 | 68 |
69 |
70 | 71 |
72 |
73 | Install Ollama manually using the following command: 74 |
75 | 76 |
77 | 78 |
79 |
80 | Install one of supported models: 81 |
82 | 83 | Once you have Ollama installed and running and have added a model 84 | make sure ollama is running on port 11434 85 | 86 |
87 | 88 | 89 |
90 |
91 |
92 |
93 |
94 | ) 95 | } 96 | 97 | function ClickToCopyCommand({ command }: { command: string }) { 98 | // const [isCopied, setIsCopied] = React.useState(false) 99 | 100 | return ( 101 |
102 | 107 | 110 |
111 | ) 112 | } 113 | -------------------------------------------------------------------------------- /components/prompt-form.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import Textarea from 'react-textarea-autosize' 5 | import { useActions, useUIState } from 'ai/rsc' 6 | import { UserMessage } from './stocks/message' 7 | import { type AI } from '@/lib/chat/actions' 8 | import { Button } from '@/components/ui/button' 9 | import { IconArrowElbow, IconPaperPlane, IconPlus } from '@/components/ui/icons' 10 | import { useEnterSubmit } from '@/lib/hooks/use-enter-submit' 11 | import { nanoid } from 'nanoid' 12 | import { useRouter } from 'next/navigation' 13 | import { LightningBoltIcon } from '@radix-ui/react-icons' 14 | 15 | import { 16 | Tooltip, 17 | TooltipContent, 18 | TooltipTrigger 19 | } from '@/components/ui/tooltip' 20 | import { useInput } from '@/lib/hooks/use-form-input' 21 | 22 | export function PromptForm() { 23 | const router = useRouter() 24 | const { formRef, onKeyDown } = useEnterSubmit() 25 | const inputRef = React.useRef(null) 26 | const { submitUserMessage } = useActions() 27 | const [_, setMessages] = useUIState() 28 | 29 | const { inputValue, setInputValue } = useInput() 30 | 31 | React.useEffect(() => { 32 | if (inputRef.current) { 33 | inputRef.current.focus() 34 | } 35 | }, []) 36 | 37 | return ( 38 |
{ 41 | e.preventDefault() 42 | 43 | // Blur focus on mobile 44 | if (window.innerWidth < 600) { 45 | e.target['message']?.blur() 46 | } 47 | 48 | const value = inputValue.trim() 49 | setInputValue('') 50 | if (!value) return 51 | 52 | // Optimistically add user message UI 53 | setMessages(currentMessages => [ 54 | ...currentMessages, 55 | { 56 | id: nanoid(), 57 | display: {value} 58 | } 59 | ]) 60 | 61 | // Submit and get response message 62 | const responseMessage = await submitUserMessage(value) 63 | setMessages(currentMessages => [...currentMessages, responseMessage]) 64 | }} 65 | > 66 |
67 | 68 | 69 | 80 | 81 | Tools 82 | 83 |