├── types ├── index.ts ├── data.ts ├── next-auth.d.ts ├── settings.ts ├── folder.ts ├── google.ts ├── error.ts ├── prompt.ts ├── user.ts ├── env.ts ├── storage.ts ├── export.ts ├── llmUsage.ts ├── chat.ts ├── chatmode.ts └── agent.ts ├── public ├── locales │ ├── ar │ │ ├── common.json │ │ ├── settings.json │ │ ├── markdown.json │ │ ├── promptbar.json │ │ ├── sidebar.json │ │ └── chat.json │ ├── bn │ │ ├── common.json │ │ ├── settings.json │ │ ├── markdown.json │ │ ├── sidebar.json │ │ └── promptbar.json │ ├── de │ │ ├── common.json │ │ ├── settings.json │ │ ├── markdown.json │ │ ├── sidebar.json │ │ └── promptbar.json │ ├── en │ │ ├── common.json │ │ └── error.json │ ├── es │ │ ├── common.json │ │ ├── settings.json │ │ ├── markdown.json │ │ ├── sidebar.json │ │ ├── error.json │ │ └── promptbar.json │ ├── fr │ │ ├── common.json │ │ ├── settings.json │ │ ├── markdown.json │ │ ├── sidebar.json │ │ ├── promptbar.json │ │ └── chat.json │ ├── he │ │ ├── common.json │ │ ├── settings.json │ │ ├── markdown.json │ │ ├── sidebar.json │ │ ├── promptbar.json │ │ └── chat.json │ ├── id │ │ ├── common.json │ │ ├── settings.json │ │ ├── markdown.json │ │ ├── sidebar.json │ │ ├── promptbar.json │ │ └── chat.json │ ├── it │ │ ├── common.json │ │ ├── settings.json │ │ ├── markdown.json │ │ ├── sidebar.json │ │ └── promptbar.json │ ├── ja │ │ ├── common.json │ │ ├── markdown.json │ │ ├── settings.json │ │ ├── sidebar.json │ │ ├── promptbar.json │ │ └── chat.json │ ├── ko │ │ ├── common.json │ │ ├── settings.json │ │ ├── markdown.json │ │ ├── sidebar.json │ │ ├── promptbar.json │ │ └── chat.json │ ├── pl │ │ ├── common.json │ │ ├── settings.json │ │ ├── markdown.json │ │ ├── sidebar.json │ │ ├── promptbar.json │ │ └── chat.json │ ├── pt │ │ ├── common.json │ │ ├── settings.json │ │ ├── markdown.json │ │ ├── sidebar.json │ │ └── promptbar.json │ ├── ro │ │ ├── common.json │ │ ├── settings.json │ │ ├── markdown.json │ │ ├── sidebar.json │ │ └── promptbar.json │ ├── ru │ │ ├── common.json │ │ ├── settings.json │ │ ├── markdown.json │ │ ├── sidebar.json │ │ ├── promptbar.json │ │ └── chat.json │ ├── si │ │ ├── common.json │ │ ├── settings.json │ │ ├── markdown.json │ │ ├── sidebar.json │ │ ├── promptbar.json │ │ └── chat.json │ ├── sv │ │ ├── common.json │ │ ├── settings.json │ │ ├── markdown.json │ │ ├── sidebar.json │ │ └── promptbar.json │ ├── te │ │ ├── common.json │ │ ├── settings.json │ │ ├── markdown.json │ │ ├── sidebar.json │ │ └── promptbar.json │ ├── vi │ │ ├── common.json │ │ ├── settings.json │ │ ├── markdown.json │ │ ├── sidebar.json │ │ └── promptbar.json │ ├── zh │ │ ├── common.json │ │ ├── markdown.json │ │ ├── settings.json │ │ ├── sidebar.json │ │ ├── promptbar.json │ │ └── chat.json │ └── zh_TW │ │ ├── common.json │ │ ├── markdown.json │ │ ├── settings.json │ │ ├── sidebar.json │ │ ├── promptbar.json │ │ └── chat.json ├── favicon.ico ├── plugins.json └── screenshots │ └── screenshot-0402023.jpg ├── .eslintrc.json ├── components ├── Folder │ └── index.ts ├── Search │ ├── index.ts │ └── Search.tsx ├── Sidebar │ ├── index.ts │ ├── SidebarButton.tsx │ └── components │ │ └── OpenCloseButton.tsx ├── Spinner │ ├── index.ts │ └── Spinner.tsx ├── Promptbar │ ├── index.ts │ ├── components │ │ ├── PromptbarSettings.tsx │ │ └── Prompts.tsx │ ├── Promptbar.state.tsx │ └── PromptBar.context.tsx ├── Buttons │ └── SidebarActionButton │ │ ├── index.ts │ │ └── SidebarActionButton.tsx ├── Chat │ ├── MemoizedChatMessage.tsx │ ├── Chat.state.tsx │ ├── Chat.context.tsx │ ├── ChatModeIcon.tsx │ ├── ErrorMessageDiv.tsx │ ├── ChatLoader.tsx │ ├── Regenerate.tsx │ ├── ChatInputTokenCount.tsx │ ├── ChatPluginList.tsx │ ├── PromptList.tsx │ ├── Temperature.tsx │ └── ChatPluginPicker.tsx ├── Chatbar │ ├── Chatbar.state.tsx │ ├── components │ │ ├── Conversations.tsx │ │ ├── ClearConversations.tsx │ │ └── ChatbarSettings.tsx │ └── Chatbar.context.tsx ├── Markdown │ └── MemoizedReactMarkdown.tsx ├── Input │ ├── Checkbox.tsx │ ├── InputText.tsx │ └── Textarea.tsx ├── Mobile │ └── Navbar.tsx ├── Home │ └── HomeMain.tsx └── Dialog │ └── Dialog.tsx ├── .dockerignore ├── pages ├── index.tsx ├── api │ ├── home │ │ ├── index.ts │ │ ├── home.context.tsx │ │ └── home.state.tsx │ ├── trpc │ │ └── [trpc].ts │ ├── plugins.ts │ ├── runplugin.ts │ ├── planning.ts │ └── planningconv.ts ├── _document.tsx ├── _app.tsx └── auth │ └── autologin.tsx ├── .editorconfig ├── docs ├── screenshot_2023-04-18.png ├── screenshot_2023-05-08.png └── google_search.md ├── postcss.config.js ├── vercel.json ├── utils ├── app │ ├── conversation.ts │ ├── api.ts │ ├── const.ts │ ├── codeblock.ts │ ├── homeUpdater.ts │ └── clientstream.ts ├── server │ ├── error.ts │ ├── http.ts │ ├── tiktoken.ts │ ├── similarity.ts │ ├── openai.ts │ ├── auth.ts │ ├── message.test.ts │ ├── webpage.test.ts │ ├── message.ts │ └── llmUsage.ts ├── data │ └── throttle.ts └── trpc.ts ├── vitest.config.ts ├── agent ├── agentUtil.test.ts ├── plugins │ ├── index.ts │ ├── wikipedia.ts │ ├── python.ts │ └── executor.ts ├── prompts │ └── agent.ts ├── agent.test.ts └── agentUtil.ts ├── next.config.js ├── tailwind.config.js ├── Makefile ├── .github ├── workflows │ └── run-test-suite.yml └── dependabot.yml ├── hooks ├── useChain.ts ├── usePlugins.ts ├── useImporter.ts ├── useCreateReducer.ts ├── useExporter.ts ├── chatmode │ └── useChatModeRunner.ts ├── useMessageSender.ts └── usePublicPrompts.ts ├── next-i18next.config.js ├── middleware.ts ├── server ├── routers │ ├── _app.ts │ ├── settings.ts │ ├── publicFolders.ts │ ├── prompts.ts │ ├── folders.ts │ ├── conversations.ts │ └── publicPrompts.ts ├── trpc.ts └── context.ts ├── .gitignore ├── docker-compose.yml ├── tsconfig.json ├── styles └── globals.css ├── docker-compose.dev.yml ├── Dockerfile ├── prettier.config.js ├── scripts └── create-mongodb-backup.sh ├── license ├── k8s └── chatbot-ui.yaml ├── services └── useApiError.ts ├── init-mongo.js ├── CONTRIBUTING.md ├── .env.local.example ├── .vscode └── launch.json └── package.json /types/index.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /public/locales/ar/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/locales/bn/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/locales/de/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/locales/en/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/locales/es/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/locales/fr/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/locales/he/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/locales/id/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/locales/it/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/locales/ja/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/locales/ko/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/locales/pl/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/locales/pt/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/locales/ro/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/locales/ru/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/locales/si/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/locales/sv/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/locales/te/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/locales/vi/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/locales/zh/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /public/locales/zh_TW/common.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /components/Folder/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Folder'; 2 | -------------------------------------------------------------------------------- /components/Search/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Search'; 2 | -------------------------------------------------------------------------------- /components/Sidebar/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Sidebar'; 2 | -------------------------------------------------------------------------------- /components/Spinner/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Spinner'; 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | .env.local 3 | node_modules 4 | test-results 5 | -------------------------------------------------------------------------------- /components/Promptbar/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Promptbar'; 2 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | export { default, getServerSideProps } from './api/home'; 2 | -------------------------------------------------------------------------------- /pages/api/home/index.ts: -------------------------------------------------------------------------------- 1 | export { default, getServerSideProps } from './home'; 2 | -------------------------------------------------------------------------------- /types/data.ts: -------------------------------------------------------------------------------- 1 | export interface KeyValuePair { 2 | key: string; 3 | value: any; 4 | } 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotneet/smart-chatbot-ui/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.ts] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /components/Buttons/SidebarActionButton/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './SidebarActionButton'; 2 | -------------------------------------------------------------------------------- /public/locales/ko/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Dark mode": "다크 모드", 3 | "Light mode": "라이트 모드" 4 | } 5 | -------------------------------------------------------------------------------- /public/plugins.json: -------------------------------------------------------------------------------- 1 | { 2 | "urls": ["https://www.klarna.com/.well-known/ai-plugin.json"] 3 | } 4 | -------------------------------------------------------------------------------- /public/locales/bn/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Dark mode": "ডার্ক মোড", 3 | "Light mode": "লাইট মোড" 4 | } 5 | -------------------------------------------------------------------------------- /public/locales/de/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Dark mode": "Dark Mode", 3 | "Light mode": "Light Mode" 4 | } 5 | -------------------------------------------------------------------------------- /public/locales/he/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Dark mode": "מצב כהה", 3 | "Light mode": "מצב בהיר" 4 | } 5 | -------------------------------------------------------------------------------- /public/locales/ar/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Dark mode": "الوضع الداكن", 3 | "Light mode": "الوضع الفاتح" 4 | } 5 | -------------------------------------------------------------------------------- /public/locales/es/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Dark mode": "Modo oscuro", 3 | "Light mode": "Modo claro" 4 | } 5 | -------------------------------------------------------------------------------- /public/locales/fr/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Dark mode": "Mode sombre", 3 | "Light mode": "Mode clair" 4 | } 5 | -------------------------------------------------------------------------------- /public/locales/id/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Dark mode": "Mode gelap", 3 | "Light mode": "Mode terang" 4 | } 5 | -------------------------------------------------------------------------------- /public/locales/pl/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Dark mode": "Tryb ciemny", 3 | "Light mode": "Tryb jasny" 4 | } 5 | -------------------------------------------------------------------------------- /public/locales/pt/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Dark mode": "Modo escuro", 3 | "Light mode": "Modo claro" 4 | } 5 | -------------------------------------------------------------------------------- /public/locales/sv/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Dark mode": "Mörkt läge", 3 | "Light mode": "Ljust läge" 4 | } 5 | -------------------------------------------------------------------------------- /public/locales/te/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Dark mode": "డార్క్ మోడ్", 3 | "Light mode": "లైట్ మోడ్" 4 | } 5 | -------------------------------------------------------------------------------- /public/locales/vi/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Dark mode": "Chế độ tối", 3 | "Light mode": "Chế độ sáng" 4 | } 5 | -------------------------------------------------------------------------------- /public/locales/it/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Dark mode": "Modalità scura", 3 | "Light mode": "Modalità chiara" 4 | } 5 | -------------------------------------------------------------------------------- /public/locales/ru/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Dark mode": "Темный режим", 3 | "Light mode": "Светлый режим" 4 | } 5 | -------------------------------------------------------------------------------- /public/locales/si/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Dark mode": "අඳුරු මාදිලිය", 3 | "Light mode": "ආලෝක මාදිලිය" 4 | } 5 | -------------------------------------------------------------------------------- /docs/screenshot_2023-04-18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotneet/smart-chatbot-ui/HEAD/docs/screenshot_2023-04-18.png -------------------------------------------------------------------------------- /docs/screenshot_2023-05-08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotneet/smart-chatbot-ui/HEAD/docs/screenshot_2023-05-08.png -------------------------------------------------------------------------------- /public/locales/ro/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Dark mode": "Modul întunecat", 3 | "Light mode": "Modul de golire" 4 | } 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/locales/zh/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Copy code": "复制代码", 3 | "Copied!": "已复制!", 4 | "Enter file name": "输入文件名" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/zh_TW/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Copy code": "複製程式碼", 3 | "Copied!": "已複製!", 4 | "Enter file name": "輸入檔名" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/ja/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Copy code": "コードをコピー", 3 | "Copied!": "コピーしました!", 4 | "Enter file name": "ファイル名を入力" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/ko/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Copy code": "코드 복사", 3 | "Copied!": "복사 완료!", 4 | "Enter file name": "파일 이름을 입력하세요" 5 | } 6 | -------------------------------------------------------------------------------- /public/screenshots/screenshot-0402023.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dotneet/smart-chatbot-ui/HEAD/public/screenshots/screenshot-0402023.jpg -------------------------------------------------------------------------------- /public/locales/ar/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Copy code": "نسخ الكود", 3 | "Copied!": "تم النسخ!", 4 | "Enter file name": "أدخل اسم الملف" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/he/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Copy code": "העתק קוד", 3 | "Copied!": "נשמר בזכרון", 4 | "Enter file name": "הקלד שם לקובץ" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/sv/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Copy code": "Kopiera kod", 3 | "Copied!": "Kopierad!", 4 | "Enter file name": "Ange filnamn" 5 | } 6 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "pages/api/*.ts": { 4 | "includeFiles": "./public/tiktoken_bg.wasm" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /public/locales/vi/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Copy code": "Sao chép mã", 3 | "Copied!": "Đã sao chép!", 4 | "Enter file name": "Nhập tên file" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/bn/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Copy code": "কোড কপি করুন", 3 | "Copied!": "কপি করা হয়েছে!", 4 | "Enter file name": "ফাইল নাম লিখুন" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/de/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Copy code": "Code kopieren", 3 | "Copied!": "Kopiert!", 4 | "Enter file name": "Dateinamen eingeben" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/id/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Copy code": "Salin kode", 3 | "Copied!": "Kode disalin!", 4 | "Enter file name": "Masukkan nama file" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/pl/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Copy code": "Skopiuj kod", 3 | "Copied!": "Skopiowany", 4 | "Enter file name": "Wprowadź nazwę pliku" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/fr/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Copy code": "Copier le code", 3 | "Copied!": "Copié !", 4 | "Enter file name": "Entrez le nom du fichier" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/it/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Copy code": "Copia codice", 3 | "Copied!": "Copiato!", 4 | "Enter file name": "Inserisci il nome del file" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/pt/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Copy code": "Copiar código", 3 | "Copied!": "Copiado!", 4 | "Enter file name": "Insira o nome do arquivo" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/zh/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Settings": "设置", 3 | "Theme": "主题", 4 | "Dark mode": "深色模式", 5 | "Light mode": "浅色模式", 6 | "Save": "保存" 7 | } 8 | -------------------------------------------------------------------------------- /public/locales/es/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Copy code": "Copiar código", 3 | "Copied!": "¡Copiado!", 4 | "Enter file name": "Ingrese el nombre del archivo" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/ja/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Settings": "設定", 3 | "Theme": "テーマ", 4 | "Save": "保存", 5 | "Dark mode": "ダークモード", 6 | "Light mode": "ライトモード" 7 | } 8 | -------------------------------------------------------------------------------- /public/locales/ro/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Copy code": "Copiați codul", 3 | "Copied!": "Copiat!", 4 | "Enter file name": "Introduceți numele fișierului" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/ru/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Copy code": "Скопировать", 3 | "Copied!": "Скопировано!", 4 | "Enter file name": "Введите имя файла для загрузки" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/si/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Copy code": "කේතය පිටපත් කරන්න", 3 | "Copied!": "පිටපත් කළා!", 4 | "Enter file name": "ගොනු නාමය ඇතුළත් කරන්න" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/zh_TW/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Settings": "設定", 3 | "Theme": "主題", 4 | "Dark mode": "深色模式", 5 | "Light mode": "淺色模式", 6 | "Save": "儲存" 7 | } 8 | -------------------------------------------------------------------------------- /public/locales/te/markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "Copy code": "కోడ్‌ను కాపీ చేయండి", 3 | "Copied!": "కాపీ చేయబడింది!", 4 | "Enter file name": "ఫైల్ పేరు నమోదు చేయండి" 5 | } 6 | -------------------------------------------------------------------------------- /utils/app/conversation.ts: -------------------------------------------------------------------------------- 1 | export function createConversationNameFromMessage(content: string): string { 2 | return content.length > 30 ? content.substring(0, 30) + '...' : content; 3 | } 4 | -------------------------------------------------------------------------------- /components/Promptbar/components/PromptbarSettings.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | interface Props {} 4 | 5 | export const PromptbarSettings: FC = () => { 6 | return
; 7 | }; 8 | -------------------------------------------------------------------------------- /types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { DefaultSession } from "next-auth"; 2 | import { User } from './user' 3 | declare module "next-auth" { 4 | interface Session extends DefaultSession { 5 | user?: User 6 | } 7 | } -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | resolve: { 6 | alias: { 7 | '@': path.resolve(__dirname, './'), 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /types/settings.ts: -------------------------------------------------------------------------------- 1 | import * as z from 'zod'; 2 | 3 | export const SettingsSchema = z.object({ 4 | userId: z.string(), 5 | theme: z.enum(['light', 'dark']), 6 | defaultTemperature: z.number(), 7 | }); 8 | 9 | export type Settings = z.infer; 10 | -------------------------------------------------------------------------------- /components/Chat/MemoizedChatMessage.tsx: -------------------------------------------------------------------------------- 1 | import { FC, memo } from "react"; 2 | import { ChatMessage, Props } from "./ChatMessage"; 3 | 4 | export const MemoizedChatMessage: FC = memo( 5 | ChatMessage, 6 | (prevProps, nextProps) => ( 7 | prevProps.message.content === nextProps.message.content 8 | ) 9 | ); 10 | -------------------------------------------------------------------------------- /components/Chatbar/Chatbar.state.tsx: -------------------------------------------------------------------------------- 1 | import { Conversation } from '@/types/chat'; 2 | 3 | export interface ChatbarInitialState { 4 | searchTerm: string; 5 | filteredConversations: Conversation[]; 6 | } 7 | 8 | export const initialState: ChatbarInitialState = { 9 | searchTerm: '', 10 | filteredConversations: [], 11 | }; 12 | -------------------------------------------------------------------------------- /components/Markdown/MemoizedReactMarkdown.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 | ) 9 | ); 10 | -------------------------------------------------------------------------------- /public/locales/ko/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New folder": "새 폴더", 3 | "New chat": "새 채팅", 4 | "No conversations.": "대화가 없습니다.", 5 | "Search conversations...": "대화 검색...", 6 | "OpenAI API Key": "OpenAI API 키", 7 | "Import data": "대화 가져오기", 8 | "Are you sure?": "확실합니까?", 9 | "Clear conversations": "대화 지우기", 10 | "Export data": "대화 내보내기" 11 | } 12 | -------------------------------------------------------------------------------- /utils/server/error.ts: -------------------------------------------------------------------------------- 1 | import { ApiError, ApiErrorBody, } from "@/types/error"; 2 | 3 | export function getErrorResponseBody(error: any): ApiErrorBody { 4 | if (error instanceof ApiError) { 5 | return (error as ApiError).getApiError(); 6 | } else { 7 | return { error: error instanceof Error ? error.message : 'Error' }; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /components/Chat/Chat.state.tsx: -------------------------------------------------------------------------------- 1 | import { Plugin } from '@/types/agent'; 2 | import { ChatMode, ChatModes } from '@/types/chatmode'; 3 | 4 | export interface ChatInitialState { 5 | chatMode: ChatMode; 6 | selectedPlugins: Plugin[]; 7 | } 8 | 9 | export const initialState: ChatInitialState = { 10 | chatMode: ChatModes.direct, 11 | selectedPlugins: [], 12 | }; 13 | -------------------------------------------------------------------------------- /components/Promptbar/Promptbar.state.tsx: -------------------------------------------------------------------------------- 1 | import { Prompt } from '@/types/prompt'; 2 | 3 | export interface PromptbarInitialState { 4 | searchTerm: string; 5 | filteredPrompts: Prompt[]; 6 | filteredPublicPrompts: Prompt[]; 7 | } 8 | 9 | export const initialState: PromptbarInitialState = { 10 | searchTerm: '', 11 | filteredPrompts: [], 12 | filteredPublicPrompts: [], 13 | }; 14 | -------------------------------------------------------------------------------- /public/locales/he/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New folder": "תיקיה חדשה", 3 | "New chat": "שיחה חדשה", 4 | "No conversations.": "אין שיחות חדשות", 5 | "Search conversations...": "חיפוש שיחות...", 6 | "OpenAI API Key": "מפתח אישי ל openAI", 7 | "Import data": "ייבוא שיחות", 8 | "Are you sure?": "אתה בטוח?", 9 | "Clear conversations": "ניקוי שיחות", 10 | "Export data": "ייצוא שיחות" 11 | } 12 | -------------------------------------------------------------------------------- /types/folder.ts: -------------------------------------------------------------------------------- 1 | import * as z from 'zod'; 2 | 3 | export const FolderSchema = z.object({ 4 | id: z.string(), 5 | name: z.string(), 6 | type: z.union([z.literal('chat'), z.literal('prompt')]), 7 | }); 8 | 9 | export const FolderSchemaArray = z.array(FolderSchema); 10 | export type FolderType = z.infer['type']; 11 | export type FolderInterface = z.infer; 12 | -------------------------------------------------------------------------------- /agent/agentUtil.test.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from '@/types/agent'; 2 | 3 | import { stripQuotes } from './agentUtil'; 4 | 5 | import { describe, expect, it } from 'vitest'; 6 | 7 | describe('stripQuotes', () => { 8 | it('should strip quotes', () => { 9 | expect(stripQuotes('"some text"')).toEqual('some text'); 10 | expect(stripQuotes('"some\' \'text"')).toEqual("some' 'text"); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /public/locales/ru/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New folder": "Новая папка", 3 | "New chat": "Новый чат", 4 | "No conversations.": "Нет чатов.", 5 | "Search conversations...": "Поиск чатов...", 6 | "OpenAI API Key": "API-ключ OpenAI", 7 | "Import data": "Импортировать чаты", 8 | "Are you sure?": "Вы уверены?", 9 | "Clear conversations": "Удалить чаты", 10 | "Export data": "Экспортировать чаты" 11 | } 12 | -------------------------------------------------------------------------------- /public/locales/si/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New folder": "නව ෆෝල්ඩරය", 3 | "New chat": "නව සංවාදයක්", 4 | "No conversations.": "සංවාද නැත.", 5 | "Search conversations...": "සංවාද සොයන්න...", 6 | "OpenAI API Key": "OpenAI API යතුර", 7 | "Import data": "සංවාද ආයාත කරන්න", 8 | "Are you sure?": "ඔබට විශ්වාස ද?", 9 | "Clear conversations": "සංවාද මකන්න", 10 | "Export data": "සංවාද නිර්යාත කරන්න" 11 | } 12 | -------------------------------------------------------------------------------- /public/locales/pl/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New folder": "Nowy folder", 3 | "New chat": "Nowy chat", 4 | "No conversations.": "Brak rozmów.", 5 | "Search conversations...": "Szukaj rozmów...", 6 | "OpenAI API Key": "Klucz do OpenAI API", 7 | "Import data": "Import rozmów", 8 | "Are you sure?": "Czy jesteś pewien?", 9 | "Clear conversations": "Usuń rozmowy", 10 | "Export data": "Eksport rozmów" 11 | } 12 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const { i18n } = require('./next-i18next.config'); 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | i18n, 6 | reactStrictMode: true, 7 | 8 | webpack(config, { isServer, dev }) { 9 | config.experiments = { 10 | asyncWebAssembly: true, 11 | layers: true, 12 | }; 13 | 14 | return config; 15 | }, 16 | }; 17 | 18 | module.exports = nextConfig; 19 | -------------------------------------------------------------------------------- /public/locales/pt/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New folder": "Nova pasta", 3 | "New chat": "Novo chat", 4 | "No conversations.": "Não há conversas.", 5 | "Search conversations...": "Buscar conversas...", 6 | "OpenAI API Key": "API Key da OpenAI", 7 | "Import data": "Importar conversas", 8 | "Are you sure?": "Tem certeza?", 9 | "Clear conversations": "Apagar conversas", 10 | "Export data": "Exportar conversas" 11 | } 12 | -------------------------------------------------------------------------------- /public/locales/zh/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New folder": "新建文件夹", 3 | "New chat": "新建聊天", 4 | "New Conversation": "新的聊天", 5 | "No conversations.": "无对话", 6 | "Search conversations...": "搜索对话...", 7 | "OpenAI API Key": "OpenAI API 密钥", 8 | "Import data": "导入对话", 9 | "Are you sure?": "确定吗?", 10 | "Clear conversations": "清空对话", 11 | "Settings": "设置", 12 | "Export data": "导出对话", 13 | "Plugin Keys": "插件密钥" 14 | } 15 | -------------------------------------------------------------------------------- /public/locales/id/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New folder": "Folder baru", 3 | "New chat": "Percakapan baru", 4 | "No conversations.": "Tidak ada percakapan.", 5 | "Search conversations...": "Cari percakapan...", 6 | "OpenAI API Key": "Kunci API OpenAI", 7 | "Import data": "Impor percakapan", 8 | "Are you sure?": "Apakah Anda yakin?", 9 | "Clear conversations": "Hapus percakapan", 10 | "Export data": "Ekspor percakapan" 11 | } 12 | -------------------------------------------------------------------------------- /public/locales/zh_TW/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New folder": "新資料夾", 3 | "New chat": "新的聊天", 4 | "New Conversation": "開新對話", 5 | "No conversations.": "沒有對話。", 6 | "Search conversations...": "搜尋對話…", 7 | "OpenAI API Key": "OpenAI API 金鑰", 8 | "Import data": "匯入資料", 9 | "Are you sure?": "您確定嗎?", 10 | "Clear conversations": "清除所有對話", 11 | "Settings": "設定選項", 12 | "Export data": "匯出資料", 13 | "Plugin Keys": "外掛金鑰" 14 | } 15 | -------------------------------------------------------------------------------- /types/google.ts: -------------------------------------------------------------------------------- 1 | import { ChatBody, Message } from './chat'; 2 | 3 | export interface GoogleBody extends ChatBody { 4 | googleAPIKey: string; 5 | googleCSEId: string; 6 | } 7 | 8 | export interface GoogleResponse { 9 | message: Message; 10 | } 11 | 12 | export interface GoogleSource { 13 | title: string; 14 | link: string; 15 | displayLink: string; 16 | snippet: string; 17 | image: string; 18 | text: string; 19 | } 20 | -------------------------------------------------------------------------------- /public/locales/bn/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New folder": "নতুন ফোল্ডার", 3 | "New chat": "নতুন আড্ডা", 4 | "No conversations.": "কোনো আলাপচারিতা নেই।", 5 | "Search conversations...": "আলাপচারিতা খুঁজুন...", 6 | "OpenAI API Key": "OpenAI API Key", 7 | "Import data": "আলাপচারিতা ইমপোর্ট", 8 | "Are you sure?": "আপনি কি নিশ্চিত?", 9 | "Clear conversations": "কথোপকথন পরিষ্কার করুন", 10 | "Export data": "আলাপচারিতা এক্সপোর্ট" 11 | } 12 | -------------------------------------------------------------------------------- /public/locales/it/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New folder": "Nuova cartella", 3 | "New chat": "Nuova conversazione", 4 | "No conversations.": "Nessuna conversazione.", 5 | "Search conversations...": "Cerca conversazioni...", 6 | "OpenAI API Key": "Chiave API OpenAI", 7 | "Import data": "Importa dati", 8 | "Are you sure?": "Sei sicuro?", 9 | "Clear conversations": "Elimina conversazioni", 10 | "Export data": "Esporta dati" 11 | } 12 | -------------------------------------------------------------------------------- /public/locales/ro/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New folder": "Folder nou", 3 | "New chat": "Conversație nouă", 4 | "No conversations.": "Nicio conversație.", 5 | "Search conversations...": "Căutați conversații...", 6 | "OpenAI API Key": "Cheia API OpenAI", 7 | "Import data": "Importați conversații", 8 | "Are you sure?": "Esti sigur?", 9 | "Clear conversations": "Ștergeți conversațiile", 10 | "Export data": "Exportați conversații" 11 | } 12 | -------------------------------------------------------------------------------- /public/locales/sv/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New folder": "Ny mapp", 3 | "New chat": "Ny chatt", 4 | "No conversations.": "Inga konversationer.", 5 | "Search conversations...": "Sök konversationer...", 6 | "OpenAI API Key": "OpenAI API-nyckel", 7 | "Import data": "Importera konversationer", 8 | "Are you sure?": "Är du säker?", 9 | "Clear conversations": "Radera konversationer", 10 | "Export data": "Exportera konversationer" 11 | } 12 | -------------------------------------------------------------------------------- /public/locales/te/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New folder": "కొత్త ఫోల్డర్", 3 | "New chat": "కొత్త చాట్", 4 | "No conversations.": "సంభాషణలు లేవు.", 5 | "Search conversations...": "సంభాషణలు వెతకండి...", 6 | "OpenAI API Key": "ఒపెన్ ఎయి ఐ API కీ ", 7 | "Import data": "సంభాషణలు దిగుమతి చేయండి", 8 | "Are you sure?": "మీరు ఖచ్చితంగా ఉన్నారా?", 9 | "Clear conversations": "సంభాషణలు తొలగించు", 10 | "Export data": "సంభాషణలు ఎగుమతి చేయండి" 11 | } 12 | -------------------------------------------------------------------------------- /components/Chat/Chat.context.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, createContext } from 'react'; 2 | 3 | import { ActionType } from '@/hooks/useCreateReducer'; 4 | 5 | import { ChatInitialState } from './Chat.state'; 6 | 7 | export interface ChatContextProps { 8 | state: ChatInitialState; 9 | dispatch: Dispatch>; 10 | } 11 | 12 | const ChatContext = createContext(undefined!); 13 | 14 | export default ChatContext; 15 | -------------------------------------------------------------------------------- /public/locales/ja/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New folder": "新規フォルダ", 3 | "New chat": "新規チャット", 4 | "No conversations.": "会話履歴はありません。", 5 | "Search conversations...": "会話を検索...", 6 | "OpenAI API Key": "OpenAI APIキー", 7 | "Import data": "会話履歴をインポート", 8 | "Are you sure?": "よろしいですか?", 9 | "Clear conversations": " 会話をクリア", 10 | "Export data": "会話履歴をエクスポート", 11 | "Dark mode": "ダークモード", 12 | "Light mode": "ライトモード", 13 | "Settings": "設定" 14 | } 15 | -------------------------------------------------------------------------------- /types/error.ts: -------------------------------------------------------------------------------- 1 | export enum ErrorResponseCode { 2 | OPENAI_RATE_LIMIT_REACHED = "openAIRateLimitReached", 3 | OPENAI_SERVICE_OVERLOADED = "openAIServiceOverloaded", 4 | ERROR_DEFAULT = "errorDefault" 5 | } 6 | export interface ApiErrorBody { 7 | error: { 8 | code: ErrorResponseCode; 9 | message?: string; 10 | } | string; 11 | } 12 | 13 | export abstract class ApiError extends Error { 14 | abstract getApiError(): ApiErrorBody; 15 | } 16 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './app/**/*.{js,ts,jsx,tsx}', 5 | './pages/**/*.{js,ts,jsx,tsx}', 6 | './components/**/*.{js,ts,jsx,tsx}', 7 | ], 8 | darkMode: 'class', 9 | theme: { 10 | extend: {}, 11 | }, 12 | variants: { 13 | extend: { 14 | visibility: ['group-hover'], 15 | }, 16 | }, 17 | plugins: [require('@tailwindcss/typography')], 18 | }; 19 | -------------------------------------------------------------------------------- /public/locales/de/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New folder": "Neuer Ordner", 3 | "New chat": "Neue Konversation", 4 | "No conversations.": "Keine Konversationen.", 5 | "Search conversations...": "Konversationen suchen...", 6 | "OpenAI API Key": "OpenAI API-Schlüssel", 7 | "Import data": "Konversationen importieren", 8 | "Are you sure?": "Bist du sicher?", 9 | "Clear conversations": "Konversationen löschen", 10 | "Export data": "Konversationen exportieren" 11 | } 12 | -------------------------------------------------------------------------------- /public/locales/es/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New folder": "Nueva carpeta", 3 | "New chat": "Nueva conversación", 4 | "No conversations.": "No hay conversaciones.", 5 | "Search conversations...": "Buscar conversaciones...", 6 | "OpenAI API Key": "Llave de API de OpenAI", 7 | "Import data": "Importar conversaciones", 8 | "Are you sure?": "¿Estás seguro?", 9 | "Clear conversations": "Borrar conversaciones", 10 | "Export data": "Exportar conversaciones" 11 | } 12 | -------------------------------------------------------------------------------- /public/locales/vi/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New folder": "Thư mục mới", 3 | "New chat": "Tạo hội thoại mới", 4 | "No conversations.": "Không có hội thoại nào.", 5 | "Search conversations...": "Tìm kiếm các cuộc hội thoại...", 6 | "OpenAI API Key": "OpenAI API Key", 7 | "Import data": "Nhập dữ liệu hội thoại", 8 | "Are you sure?": "Bạn chắc chắn chứ?", 9 | "Clear conversations": "Xoá các đoạn hội thoại", 10 | "Export data": "Xuất dữ liệu hội thoại" 11 | } 12 | -------------------------------------------------------------------------------- /components/Input/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | export const Checkbox = ( 2 | props: React.InputHTMLAttributes, 3 | ) => { 4 | return ( 5 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /public/locales/fr/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New folder": "Nouveau dossier", 3 | "New chat": "Nouvelle discussion", 4 | "No conversations.": "Aucune conversation.", 5 | "Search conversations...": "Rechercher des conversations...", 6 | "OpenAI API Key": "Clé API OpenAI", 7 | "Import data": "Importer des conversations", 8 | "Are you sure?": "Êtes-vous sûr ?", 9 | "Clear conversations": "Effacer les conversations", 10 | "Export data": "Exporter les conversations" 11 | } 12 | -------------------------------------------------------------------------------- /public/locales/ja/promptbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New prompt": "新しいプロンプト", 3 | "New folder": "新しいフォルダ", 4 | "No prompts.": "プロンプトはありません", 5 | "Search prompts...": "プロンプトの検索...", 6 | "Name": "名前", 7 | "Description": "説明", 8 | "A description for your prompt.": "プロンプトの説明", 9 | "Prompt": "プロンプト", 10 | "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "プロンプトの内容。変数を表すには {{}} を使ってください。 例: {{name}} is a {{adjective}} {{noun}}", 11 | "Save": "保存" 12 | } 13 | -------------------------------------------------------------------------------- /types/prompt.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIModelSchema } from './openai'; 2 | 3 | import * as z from 'zod'; 4 | 5 | export const PromptSchema = z.object({ 6 | id: z.string(), 7 | name: z.string(), 8 | description: z.string(), 9 | content: z.string(), 10 | model: OpenAIModelSchema, 11 | folderId: z.string().nullable(), 12 | userId: z.string().optional(), 13 | }); 14 | 15 | export const PromptSchemaArray = z.array(PromptSchema); 16 | 17 | export type Prompt = z.infer; 18 | -------------------------------------------------------------------------------- /types/user.ts: -------------------------------------------------------------------------------- 1 | import * as z from 'zod'; 2 | 3 | export enum UserRole { 4 | ADMIN = "admin", 5 | USER = "user" 6 | } 7 | 8 | export const UserSchema = z.object({ 9 | _id: z.string().optional(), 10 | email: z.string(), 11 | name: z.string().optional(), 12 | role: z.nativeEnum(UserRole).optional(), 13 | monthlyUSDConsumptionLimit: z.number().optional() 14 | }); 15 | 16 | export const UserSchemaArray = z.array(UserSchema); 17 | 18 | export type User = z.infer; 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include .env 2 | 3 | .PHONY: all 4 | 5 | build: 6 | docker build -t chatbot-ui . 7 | 8 | run: 9 | export $(cat .env | xargs) 10 | docker stop chatbot-ui || true && docker rm chatbot-ui || true 11 | docker run --name chatbot-ui --rm -e OPENAI_API_KEY=${OPENAI_API_KEY} -p 3000:3000 chatbot-ui 12 | 13 | logs: 14 | docker logs -f chatbot-ui 15 | 16 | push: 17 | docker tag chatbot-ui:latest ${DOCKER_USER}/chatbot-ui:${DOCKER_TAG} 18 | docker push ${DOCKER_USER}/chatbot-ui:${DOCKER_TAG} -------------------------------------------------------------------------------- /public/locales/zh/promptbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New prompt": "新建提示", 3 | "New folder": "新建文件夹", 4 | "No prompts.": "无提示词", 5 | "Search prompts...": "搜索提示...", 6 | "Name": "名称", 7 | "A name for your prompt.": "提示词名称", 8 | "Description": "描述", 9 | "A description for your prompt.": "提示词描述", 10 | "Prompt": "提示词", 11 | "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "提示内容。使用 {{}} 表示一个变量。例如:{{name}} 是一个 {{adjective}} {{noun}}", 12 | "Save": "保存" 13 | } 14 | -------------------------------------------------------------------------------- /public/locales/zh_TW/promptbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New prompt": "新增提示", 3 | "New folder": "新資料夾", 4 | "No prompts.": "無提示", 5 | "Search prompts...": "搜尋提示...", 6 | "Name": "名稱", 7 | "A name for your prompt.": "給您的提示設定名稱", 8 | "Description": "描述", 9 | "A description for your prompt.": "給您的提示加上說明", 10 | "Prompt": "提示", 11 | "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "提示內容。使用 {{}} 來標註變數。例如:{{name}} 是一個 {{adjective}} {{noun}}", 12 | "Save": "儲存" 13 | } 14 | -------------------------------------------------------------------------------- /components/Chat/ChatModeIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IconBolt, IconBrandGoogle, IconRobot } from '@tabler/icons-react'; 2 | 3 | import { ChatMode, ChatModeID } from '@/types/chatmode'; 4 | 5 | export const ChatModeIcon = ({ chatMode }: { chatMode: ChatMode }) => { 6 | switch (chatMode.id) { 7 | case ChatModeID.AGENT: 8 | return ; 9 | case ChatModeID.GOOGLE_SEARCH: 10 | return ; 11 | default: 12 | return ; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /public/locales/ar/promptbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New prompt": "مطلب جديد", 3 | "New folder": "مجلد جديد", 4 | "No prompts.": "لا يوجد مطالبات.", 5 | "Search prompts...": "...البحث عن مطالبات", 6 | "Name": "الاسم", 7 | "Description": "الوصف", 8 | "A description for your prompt.": "وصف لمطلبك", 9 | "Prompt": "مطلب", 10 | "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "محتوى المطلب. استخدم {{}} للإشارة إلى متغير. مثال: {{الاسم}} هي {{صفة}} {{اسم}}", 11 | "Save": "حفظ" 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/run-test-suite.yml: -------------------------------------------------------------------------------- 1 | name: Run Unit Tests 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 5 14 | container: 15 | image: node:16 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v2 20 | 21 | - name: Install dependencies 22 | run: npm ci 23 | 24 | - name: Run Vitest Suite 25 | run: npm test 26 | -------------------------------------------------------------------------------- /public/locales/ar/sidebar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New folder": "مجلد جديد", 3 | "New chat": "محادثة جديدة", 4 | "No conversations.": "لا يوجد محادثات", 5 | "Search conversations...": "...البحث عن المحادثات", 6 | "OpenAI API Key": " (أوبن أيه أي) OpenAI API Key (مفتاح واجهة برمجة تطبيقات)", 7 | "Import data": "استيراد المحادثات", 8 | "Are you sure?": "هل أنت متأكد؟", 9 | "Clear conversations": "مسح المحادثات", 10 | "Export data": "تصدير المحادثات", 11 | "Dark mode": "الوضع الداكن", 12 | "Light mode": "الوضع الفاتح" 13 | } 14 | -------------------------------------------------------------------------------- /components/Buttons/SidebarActionButton/SidebarActionButton.tsx: -------------------------------------------------------------------------------- 1 | import { MouseEventHandler, ReactElement } from 'react'; 2 | 3 | interface Props { 4 | handleClick: MouseEventHandler; 5 | children: ReactElement; 6 | } 7 | 8 | const SidebarActionButton = ({ handleClick, children }: Props) => ( 9 | 15 | ); 16 | 17 | export default SidebarActionButton; 18 | -------------------------------------------------------------------------------- /hooks/useChain.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, MutableRefObject } from 'react'; 2 | 3 | import { ActionType } from '@/hooks/useCreateReducer'; 4 | 5 | import useApiService from '@/services/useApiService'; 6 | 7 | import { Conversation, Message } from '@/types/chat'; 8 | 9 | import { HomeInitialState } from '@/pages/api/home/home.state'; 10 | 11 | export const useChain = ( 12 | conversation: Conversation, 13 | homeDispatch: Dispatch>, 14 | ) => { 15 | const apiService = useApiService(); 16 | }; 17 | -------------------------------------------------------------------------------- /pages/api/trpc/[trpc].ts: -------------------------------------------------------------------------------- 1 | import { appRouter } from '../../../server/routers/_app'; 2 | 3 | import { createContext } from '@/server/context'; 4 | import * as trpcNext from '@trpc/server/adapters/next'; 5 | 6 | // export API handler 7 | // @see https://trpc.io/docs/api-handler 8 | export default trpcNext.createNextApiHandler({ 9 | router: appRouter, 10 | createContext, 11 | }); 12 | 13 | export const config = { 14 | api: { 15 | bodyParser: { 16 | sizeLimit: '5mb', 17 | }, 18 | responseLimit: '5mb', 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /public/locales/he/promptbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New prompt": "פקודת מכונה חדשה", 3 | "New folder": "תיקיה חדשה", 4 | "No prompts.": "לא נמצאו פקודות מכונות", 5 | "Search prompts...": "חיפוש פקודות...", 6 | "Name": "שם", 7 | "Description": "תיאור", 8 | "A description for your prompt.": "תיאור שורת הפקודה למכונה", 9 | "Prompt": "פקודה", 10 | "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "תיאור הפקודה. השתמש {{}} להגדרת משתנים. לדוגמא {{שם משתנה}} הוא {{תואר}} {{שם עצם}}", 11 | "Save": "Save" 12 | } 13 | -------------------------------------------------------------------------------- /types/env.ts: -------------------------------------------------------------------------------- 1 | export interface ProcessEnv { 2 | OPENAI_API_KEY: string; 3 | OPENAI_API_HOST?: string; 4 | OPENAI_API_TYPE?: 'openai' | 'azure'; 5 | OPENAI_API_VERSION?: string; 6 | OPENAI_ORGANIZATION?: string; 7 | NEXT_PUBLIC_DEFAULT_SYSTEM_PROMPT: string; 8 | MONGODB_URI: string; 9 | MONGODB_DB: string; 10 | GITHUB_CLIENT_ID?: string; 11 | GITHUB_CLIENT_SECRET?: string; 12 | GOOGLE_CLIENT_ID?: string; 13 | GOOGLE_CLIENT_SECRET?: string; 14 | NEXTAUTH_ENABLED: 'true' | 'false'; 15 | NEXTAUTH_EMAIL_PATTERN?: string; 16 | } 17 | -------------------------------------------------------------------------------- /public/locales/de/promptbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New prompt": "New prompt", 3 | "New folder": "New folder", 4 | "No prompts.": "No prompts.", 5 | "Search prompts...": "Search prompts...", 6 | "Name": "Name", 7 | "Description": "Description", 8 | "A description for your prompt.": "A description for your prompt.", 9 | "Prompt": "Prompt", 10 | "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}", 11 | "Save": "Save" 12 | } 13 | -------------------------------------------------------------------------------- /public/locales/fr/promptbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New prompt": "New prompt", 3 | "New folder": "New folder", 4 | "No prompts.": "No prompts.", 5 | "Search prompts...": "Search prompts...", 6 | "Name": "Name", 7 | "Description": "Description", 8 | "A description for your prompt.": "A description for your prompt.", 9 | "Prompt": "Prompt", 10 | "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}", 11 | "Save": "Save" 12 | } 13 | -------------------------------------------------------------------------------- /public/locales/id/promptbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New prompt": "New prompt", 3 | "New folder": "New folder", 4 | "No prompts.": "No prompts.", 5 | "Search prompts...": "Search prompts...", 6 | "Name": "Name", 7 | "Description": "Description", 8 | "A description for your prompt.": "A description for your prompt.", 9 | "Prompt": "Prompt", 10 | "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}", 11 | "Save": "Save" 12 | } 13 | -------------------------------------------------------------------------------- /public/locales/ko/promptbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New prompt": "New prompt", 3 | "New folder": "New folder", 4 | "No prompts.": "No prompts.", 5 | "Search prompts...": "Search prompts...", 6 | "Name": "Name", 7 | "Description": "Description", 8 | "A description for your prompt.": "A description for your prompt.", 9 | "Prompt": "Prompt", 10 | "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}", 11 | "Save": "Save" 12 | } 13 | -------------------------------------------------------------------------------- /public/locales/pt/promptbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New prompt": "New prompt", 3 | "New folder": "New folder", 4 | "No prompts.": "No prompts.", 5 | "Search prompts...": "Search prompts...", 6 | "Name": "Name", 7 | "Description": "Description", 8 | "A description for your prompt.": "A description for your prompt.", 9 | "Prompt": "Prompt", 10 | "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}", 11 | "Save": "Save" 12 | } 13 | -------------------------------------------------------------------------------- /public/locales/ru/promptbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New prompt": "New prompt", 3 | "New folder": "New folder", 4 | "No prompts.": "No prompts.", 5 | "Search prompts...": "Search prompts...", 6 | "Name": "Name", 7 | "Description": "Description", 8 | "A description for your prompt.": "A description for your prompt.", 9 | "Prompt": "Prompt", 10 | "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}", 11 | "Save": "Save" 12 | } 13 | -------------------------------------------------------------------------------- /public/locales/si/promptbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New prompt": "New prompt", 3 | "New folder": "New folder", 4 | "No prompts.": "No prompts.", 5 | "Search prompts...": "Search prompts...", 6 | "Name": "Name", 7 | "Description": "Description", 8 | "A description for your prompt.": "A description for your prompt.", 9 | "Prompt": "Prompt", 10 | "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}", 11 | "Save": "Save" 12 | } 13 | -------------------------------------------------------------------------------- /public/locales/te/promptbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New prompt": "New prompt", 3 | "New folder": "New folder", 4 | "No prompts.": "No prompts.", 5 | "Search prompts...": "Search prompts...", 6 | "Name": "Name", 7 | "Description": "Description", 8 | "A description for your prompt.": "A description for your prompt.", 9 | "Prompt": "Prompt", 10 | "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}", 11 | "Save": "Save" 12 | } 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /public/locales/pl/promptbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New prompt": "Nowy prompt", 3 | "New folder": "Nowy folder", 4 | "No prompts.": "Brak promptów.", 5 | "Search prompts...": "Szukaj promptów...", 6 | "Name": "Nazwa", 7 | "Description": "Opis", 8 | "A description for your prompt.": "Opis Twojego prompta.", 9 | "Prompt": "Prompt", 10 | "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "Zawartość prompta. Użyj {{}} żeby oznaczyć zmienną. Np.: {{nazwa}} jest {{przymiotnik}} {{rzeczownik}}", 11 | "Save": "Zapisz" 12 | } 13 | -------------------------------------------------------------------------------- /public/locales/sv/promptbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New prompt": "Ny prompt", 3 | "New folder": "Ny mapp", 4 | "No prompts.": "Inga prompts.", 5 | "Search prompts...": "Sök prompts...", 6 | "Name": "Namn", 7 | "Description": "Beskrivning", 8 | "A description for your prompt.": "En beskrivning för din prompt.", 9 | "Prompt": "Prompt", 10 | "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "Prompt-innehåll. Använd {{}} för att beteckna en variabel. Ex: {{namn}} är ett {{adjektiv}} {{substantiv}}", 11 | "Save": "Spara" 12 | } 13 | -------------------------------------------------------------------------------- /public/locales/vi/promptbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New prompt": "Prompt mới", 3 | "New folder": "Thư mục mới", 4 | "No prompts.": "Không có Prompt nào.", 5 | "Search prompts...": "Tìm kiếm các Prompt...", 6 | "Name": "Tên", 7 | "Description": "Mô tả", 8 | "A description for your prompt.": "Một mô tả cho Prompt của bạn.", 9 | "Prompt": "Prompt", 10 | "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "Nội dung Prompt. Sử dụng {{}} để biểu thị một biến. Ví dụ: {{name}} là một {{adjective}} {{noun}}", 11 | "Save": "Lưu" 12 | } 13 | -------------------------------------------------------------------------------- /public/locales/bn/promptbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New prompt": "নতুন prompt", 3 | "New folder": "নতুন ফোল্ডার", 4 | "No prompts.": "কোনো prompts নেই।", 5 | "Search prompts...": "prompts অনুসন্ধান হচ্ছে...", 6 | "Name": "নাম", 7 | "Description": "বর্ণনা", 8 | "A description for your prompt.": "আপনার Prompt জন্য একটি বিবরণ লিখুন.", 9 | "Prompt": "Prompt", 10 | "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}", 11 | "Save": "সংরক্ষণ করুন" 12 | } 13 | -------------------------------------------------------------------------------- /public/locales/en/error.json: -------------------------------------------------------------------------------- 1 | { 2 | "openAIRateLimitReached": "Whoops! High demand is causing our service to be temporarily unresponsive. Please be patient and retry in a moment. If the problem persists, please contact {{supportEmail}}.", 3 | "openAIServiceOverloaded": "Whoops! High demand is causing our service to be temporarily unresponsive. Please be patient and retry in a moment. If the problem persists, please contact {{supportEmail}}.", 4 | "errorDefault": "An unexpected error occurred. Please try again in a few moments. If the problem persists, please contact {{supportEmail}}." 5 | } -------------------------------------------------------------------------------- /agent/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from '@/types/agent'; 2 | 3 | import { TaskExecutionContext } from './executor'; 4 | import { 5 | RequestsGetTool, 6 | RequestsGetWebpageTool, 7 | RequestsPostTool, 8 | RequestsPostWebpageTool, 9 | } from './requests'; 10 | 11 | export const createApiTools = (context: TaskExecutionContext): Plugin[] => { 12 | return [new RequestsGetTool(), new RequestsPostTool()]; 13 | }; 14 | 15 | export const createWebpageTools = (context: TaskExecutionContext): Plugin[] => { 16 | return [new RequestsGetWebpageTool(), new RequestsPostWebpageTool()]; 17 | }; 18 | -------------------------------------------------------------------------------- /public/locales/it/promptbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New prompt": "Nuovo prompt", 3 | "New folder": "Nuova cartella", 4 | "No prompts.": "Nessun prompt.", 5 | "Search prompts...": "Cerca prompts...", 6 | "Name": "Nome", 7 | "Description": "Descrizione", 8 | "A description for your prompt.": "Descrizione del tuo prompt.", 9 | "Prompt": "Prompt", 10 | "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "Contenuto del prompt. Utilizza {{}} per indicare una variabile. Per esempio: {{nome}} è un {{aggettivo}} {{sostantivo}}", 11 | "Save": "Salva" 12 | } 13 | -------------------------------------------------------------------------------- /components/Sidebar/SidebarButton.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | interface Props { 4 | text: string; 5 | icon: JSX.Element; 6 | onClick: () => void; 7 | } 8 | 9 | export const SidebarButton: FC = ({ text, icon, onClick }) => { 10 | return ( 11 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /public/locales/ro/promptbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New prompt": "Noua solicitare", 3 | "New folder": "Dosar nou", 4 | "No prompts.": "Fără solicitări.", 5 | "Search prompts...": "Cereri de căutare...", 6 | "Name": "Nume", 7 | "Description": "Descriere", 8 | "A description for your prompt.": "Descrierea solicitării prompt.", 9 | "Prompt": "Îndemnuri", 10 | "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "Conținut prompt. Utilizați {{}} pentru a indica o variabilă. De exemplu: {{nume}} este un {{adjectiv}} {{substantiv}}", 11 | "Save": "Salvați" 12 | } 13 | -------------------------------------------------------------------------------- /next-i18next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | i18n: { 3 | defaultLocale: 'en', 4 | locales: [ 5 | "bn", 6 | "de", 7 | "en", 8 | "es", 9 | "fr", 10 | "he", 11 | "id", 12 | "it", 13 | "ja", 14 | "ko", 15 | "pl", 16 | "pt", 17 | "ru", 18 | "ro", 19 | "sv", 20 | "te", 21 | "vi", 22 | "zh", 23 | "zh_TW", 24 | "ar", 25 | ], 26 | }, 27 | localePath: 28 | typeof window === 'undefined' 29 | ? require('path').resolve('./public/locales') 30 | : '/public/locales', 31 | }; 32 | -------------------------------------------------------------------------------- /components/Chatbar/components/Conversations.tsx: -------------------------------------------------------------------------------- 1 | import { Conversation } from '@/types/chat'; 2 | 3 | import { ConversationComponent } from './Conversation'; 4 | 5 | interface Props { 6 | conversations: Conversation[]; 7 | } 8 | 9 | export const Conversations = ({ conversations }: Props) => { 10 | return ( 11 |
12 | {conversations 13 | .filter((conversation) => !conversation.folderId) 14 | .map((conversation, index) => ( 15 | 16 | ))} 17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /utils/data/throttle.ts: -------------------------------------------------------------------------------- 1 | export function throttle any>( 2 | func: T, 3 | limit: number, 4 | ): T { 5 | let lastFunc: ReturnType; 6 | let lastRan: number; 7 | 8 | return ((...args) => { 9 | if (!lastRan) { 10 | func(...args); 11 | lastRan = Date.now(); 12 | } else { 13 | clearTimeout(lastFunc); 14 | lastFunc = setTimeout(() => { 15 | if (Date.now() - lastRan >= limit) { 16 | func(...args); 17 | lastRan = Date.now(); 18 | } 19 | }, limit - (Date.now() - lastRan)); 20 | } 21 | }) as T; 22 | } 23 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { withAuth } from 'next-auth/middleware'; 2 | 3 | export default withAuth({ 4 | callbacks: { 5 | async authorized({ token }) { 6 | if (process.env.NEXTAUTH_ENABLED === 'false' && token?.email) { 7 | return true; 8 | } 9 | if (!token?.email) { 10 | return false; 11 | } else { 12 | const pattern = process.env.NEXTAUTH_EMAIL_PATTERN || ''; 13 | if (!pattern || token?.email?.match('^' + pattern + '$')) { 14 | return true; 15 | } 16 | return false; 17 | } 18 | }, 19 | }, 20 | }); 21 | 22 | export const config = { matcher: ['/'] }; 23 | -------------------------------------------------------------------------------- /server/routers/_app.ts: -------------------------------------------------------------------------------- 1 | import { router } from '../trpc'; 2 | import { conversations } from './conversations'; 3 | import { folders } from './folders'; 4 | import { publicFolders } from './publicFolders'; 5 | import { models } from './models'; 6 | import { prompts } from './prompts'; 7 | import { publicPrompts } from './publicPrompts'; 8 | import { settings } from './settings'; 9 | 10 | export const appRouter = router({ 11 | models, 12 | settings, 13 | prompts, 14 | publicPrompts, 15 | folders, 16 | publicFolders, 17 | conversations, 18 | }); 19 | 20 | // export type definition of API 21 | export type AppRouter = typeof appRouter; 22 | -------------------------------------------------------------------------------- /hooks/usePlugins.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from 'react-query'; 2 | 3 | import useApiService from '@/services/useApiService'; 4 | 5 | import { Plugin } from '@/types/agent'; 6 | 7 | export interface UsePluginResult { 8 | plugins: Plugin[] | undefined; 9 | error: any; 10 | } 11 | 12 | export const usePlugins = (): UsePluginResult => { 13 | const apiSerivce = useApiService(); 14 | const result = useQuery('plugins', () => apiSerivce.getPlugins(), { 15 | enabled: true, 16 | refetchOnMount: false, 17 | refetchOnWindowFocus: false, 18 | }); 19 | return { 20 | plugins: result.data, 21 | error: result.error, 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /public/locales/es/error.json: -------------------------------------------------------------------------------- 1 | { 2 | "openAIRateLimitReached": "¡Ups! Debido a la alta demanda, nuestro servicio esté temporalmente no disponible. Por favor, ten paciencia y vuelve a intentarlo en un momento. Si el problema persiste, por favor contacta a {{supportEmail}}.", 3 | "openAIServiceOverloaded": "¡Ups! Debido a la alta demanda, nuestro servicio esté temporalmente no disponible. Por favor, ten paciencia y vuelve a intentarlo en un momento. Si el problema persiste, por favor contacta a {{supportEmail}}.", 4 | "errorDefault": "Ocurrió un error inesperado. Por favor, inténtalo de nuevo en un momento. Si el problema persiste, por favor contacta a {{supportEmail}}." 5 | } -------------------------------------------------------------------------------- /.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 | /test-results 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | /dist 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | .pnpm-debug.log* 29 | 30 | # local env files 31 | .env 32 | .env*.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | .idea 41 | pnpm-lock.yaml 42 | 43 | # docker 44 | /docker-volumes 45 | 46 | -------------------------------------------------------------------------------- /pages/api/home/home.context.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, createContext } from 'react'; 2 | 3 | import { ActionType } from '@/hooks/useCreateReducer'; 4 | 5 | import { Conversation } from '@/types/chat'; 6 | import { KeyValuePair } from '@/types/data'; 7 | import { FolderType } from '@/types/folder'; 8 | 9 | import { HomeInitialState } from './home.state'; 10 | 11 | export interface HomeContextProps { 12 | state: HomeInitialState; 13 | dispatch: Dispatch>; 14 | handleSelectConversation: (conversation: Conversation) => void; 15 | } 16 | 17 | const HomeContext = createContext(undefined!); 18 | 19 | export default HomeContext; 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | 3 | services: 4 | chatui: 5 | build: . 6 | ports: 7 | - 3000:3000 8 | env_file: 9 | - .env.local 10 | mongo: 11 | image: mongo:5.0 12 | restart: always 13 | volumes: 14 | - mongodb-configdb:/data/configdb 15 | - mongodb-data:/data/db 16 | - ./init-mongo.js:/docker-entrypoint-initdb.d/init-mongo.js 17 | env_file: 18 | # Specify these envs through .env.local 19 | # MONGO_INITDB_ROOT_USERNAME: root 20 | # MONGO_INITDB_ROOT_PASSWORD: example 21 | - .env.local 22 | 23 | volumes: 24 | mongodb-data: 25 | name: chatui-mongodb-data 26 | mongodb-configdb: 27 | name: chatui-mongodb-configdb 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "commonjs", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "types": ["vitest/globals"], 18 | "paths": { 19 | "@/*": ["./*"] 20 | } 21 | }, 22 | "include": ["next-env.d.ts", "middleware.ts", "**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /types/storage.ts: -------------------------------------------------------------------------------- 1 | import { Conversation } from './chat'; 2 | import { ChatModeKey } from './chatmode'; 3 | import { FolderInterface } from './folder'; 4 | import { Prompt } from './prompt'; 5 | 6 | // keep track of local storage schema 7 | export interface LocalStorage { 8 | apiKey: string; 9 | conversationHistory: Conversation[]; 10 | selectedConversation: Conversation; 11 | theme: 'light' | 'dark'; 12 | // added folders (3/23/23) 13 | folders: FolderInterface[]; 14 | // added prompts (3/26/23) 15 | prompts: Prompt[]; 16 | // added showChatbar and showPromptbar (3/26/23) 17 | showChatbar: boolean; 18 | showPromptbar: boolean; 19 | // added plugin keys (4/3/23) 20 | pluginKeys: ChatModeKey[]; 21 | } 22 | -------------------------------------------------------------------------------- /utils/server/http.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest } from 'next'; 2 | 3 | import { Headers } from '@/agent/plugins/requests'; 4 | 5 | export const extractHeaders = (request: Request | NextApiRequest): Headers => { 6 | let result: Record = {}; 7 | if (request instanceof Request) { 8 | let ite = request.headers.entries(); 9 | let entry = ite.next(); 10 | while (!entry.done) { 11 | result[entry.value[0]] = entry.value[1]; 12 | entry = ite.next(); 13 | } 14 | return result; 15 | } else { 16 | const headers = (request as NextApiRequest).headers; 17 | for (const key in headers) { 18 | result[key] = headers[key]?.toString() || ''; 19 | } 20 | } 21 | return result; 22 | }; 23 | -------------------------------------------------------------------------------- /components/Chat/ErrorMessageDiv.tsx: -------------------------------------------------------------------------------- 1 | import { IconCircleX } from '@tabler/icons-react'; 2 | import { FC } from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | interface Props { 6 | error: Error; 7 | } 8 | 9 | export const ErrorMessageDiv: FC = ({ error }) => { 10 | const { t } = useTranslation('chat'); 11 | const title = t('Error fetching models.'); 12 | return ( 13 |
14 |
15 | 16 |
17 |
{title}
18 |
{error.message}
19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { DocumentProps, Head, Html, Main, NextScript } from 'next/document'; 2 | 3 | import i18nextConfig from '../next-i18next.config'; 4 | 5 | type Props = DocumentProps & { 6 | // add custom document props 7 | }; 8 | 9 | export default function Document(props: Props) { 10 | const currentLocale = 11 | props.__NEXT_DATA__.locale ?? i18nextConfig.i18n.defaultLocale; 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /public/locales/es/promptbar.json: -------------------------------------------------------------------------------- 1 | { 2 | "New prompt": "Nuevo prompt", 3 | "New folder": "Nueva carpeta", 4 | "No prompts.": "No hay prompts.", 5 | "Search prompts...": "Buscar prompts...", 6 | "Name": "Nombre", 7 | "Description": "Descripción", 8 | "A description for your prompt.": "Descripción de su prompt.", 9 | "Prompt": "Prompt", 10 | "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "Contenido del prompt. Utilice {{}} para indicar una variable. Ej: {{nombre}} es un {{adjetivo}} {{nombre}}", 11 | "Public prompts": "Prompts públicas", 12 | "My prompts": "Mis prompts", 13 | "Save": "Guardar", 14 | "Select public folder": "Selecciona una carpeta pública", 15 | "Publish": "Publicar" 16 | } -------------------------------------------------------------------------------- /pages/api/plugins.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | import { ensureHasValidSession } from '@/utils/server/auth'; 4 | 5 | import { listTools } from '@/agent/plugins/list'; 6 | import { getErrorResponseBody } from '@/utils/server/error'; 7 | 8 | const handler = async (req: NextApiRequest, res: NextApiResponse) => { 9 | if (!(await ensureHasValidSession(req, res))) { 10 | return res.status(401).json({ error: 'Unauthorized' }); 11 | } 12 | 13 | try { 14 | const tools = await listTools(); 15 | res.status(200).json(tools); 16 | } catch (error) { 17 | console.error(error); 18 | const errorRes = getErrorResponseBody(error); 19 | res.status(500).json(errorRes); 20 | } 21 | }; 22 | 23 | export default handler; 24 | -------------------------------------------------------------------------------- /server/routers/settings.ts: -------------------------------------------------------------------------------- 1 | import { UserDb } from '@/utils/server/storage'; 2 | 3 | import { SettingsSchema } from '@/types/settings'; 4 | 5 | import { procedure, router } from '../trpc'; 6 | 7 | export const settings = router({ 8 | get: procedure.query(async ({ ctx }) => { 9 | try { 10 | const userDb = await UserDb.fromUserHash(ctx.userHash); 11 | return await userDb.getSettings(); 12 | } catch (e) { 13 | console.error(e); 14 | throw e; 15 | } 16 | }), 17 | settingsUpdate: procedure 18 | .input(SettingsSchema) 19 | .mutation(async ({ ctx, input }) => { 20 | const userDb = await UserDb.fromUserHash(ctx.userHash); 21 | await userDb.saveSettings(input); 22 | return { success: true }; 23 | }), 24 | }); 25 | -------------------------------------------------------------------------------- /components/Chat/ChatLoader.tsx: -------------------------------------------------------------------------------- 1 | import { IconRobot } from '@tabler/icons-react'; 2 | import { IconDots } from '@tabler/icons-react'; 3 | import { FC } from 'react'; 4 | 5 | interface Props { } 6 | 7 | export const ChatLoader: FC = () => { 8 | return ( 9 |
13 |
14 |
15 | 16 |
17 | 18 |
19 |
20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | ::-webkit-scrollbar-track { 6 | background-color: transparent; 7 | } 8 | 9 | ::-webkit-scrollbar-thumb { 10 | background-color: #ccc; 11 | border-radius: 10px; 12 | } 13 | 14 | ::-webkit-scrollbar-thumb:hover { 15 | background-color: #aaa; 16 | } 17 | 18 | ::-webkit-scrollbar-track:hover { 19 | background-color: #f2f2f2; 20 | } 21 | 22 | ::-webkit-scrollbar-corner { 23 | background-color: transparent; 24 | } 25 | 26 | ::-webkit-scrollbar { 27 | width: 6px; 28 | height: 6px; 29 | } 30 | 31 | html { 32 | background: #202123; 33 | } 34 | 35 | @media (max-width: 720px) { 36 | pre { 37 | width: calc(100vw - 110px); 38 | } 39 | } 40 | 41 | pre:has(div.codeblock) { 42 | padding: 0; 43 | } 44 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | 3 | services: 4 | mongo: 5 | image: mongo 6 | restart: always 7 | ports: 8 | - 27017:27017 9 | environment: 10 | MONGO_INITDB_ROOT_USERNAME: root 11 | MONGO_INITDB_ROOT_PASSWORD: example 12 | MONGO_INITDB_DATABASE: chatui 13 | env_file: 14 | - .env.local 15 | volumes: 16 | - mongo-data:/data/db 17 | - ./init-mongo.js:/docker-entrypoint-initdb.d/init-mongo.js 18 | 19 | mongo-express: 20 | image: mongo-express 21 | restart: always 22 | ports: 23 | - 8081:8081 24 | environment: 25 | ME_CONFIG_MONGODB_ADMINUSERNAME: root 26 | ME_CONFIG_MONGODB_ADMINPASSWORD: example 27 | ME_CONFIG_MONGODB_URL: mongodb://root:example@mongo:27017/ 28 | 29 | volumes: 30 | mongo-data: 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # ---- Base Node ---- 2 | FROM node:19-alpine AS base 3 | WORKDIR /app 4 | COPY package*.json ./ 5 | 6 | # ---- Dependencies ---- 7 | FROM base AS dependencies 8 | RUN npm ci 9 | 10 | # ---- Build ---- 11 | FROM dependencies AS build 12 | COPY . . 13 | RUN npm run build 14 | 15 | # ---- Production ---- 16 | FROM node:19-alpine AS production 17 | WORKDIR /app 18 | COPY --from=dependencies /app/node_modules ./node_modules 19 | COPY --from=build /app/.next ./.next 20 | COPY --from=build /app/public ./public 21 | COPY --from=build /app/package*.json ./ 22 | COPY --from=build /app/next.config.js ./next.config.js 23 | COPY --from=build /app/next-i18next.config.js ./next-i18next.config.js 24 | 25 | # Expose the port the app will run on 26 | EXPOSE 3000 27 | 28 | # Start the application 29 | CMD ["npm", "start"] 30 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: 'all', 3 | singleQuote: true, 4 | plugins: [ 5 | 'prettier-plugin-tailwindcss', 6 | '@trivago/prettier-plugin-sort-imports', 7 | ], 8 | importOrder: [ 9 | 'react', // React 10 | '^react-.*$', // React-related imports 11 | '^next', // Next-related imports 12 | '^next-.*$', // Next-related imports 13 | '^next/.*$', // Next-related imports 14 | '^.*/hooks/.*$', // Hooks 15 | '^.*/services/.*$', // Services 16 | '^.*/utils/.*$', // Utils 17 | '^.*/types/.*$', // Types 18 | '^.*/pages/.*$', // Components 19 | '^.*/components/.*$', // Components 20 | '^[./]', // Other imports 21 | '.*', // Any uncaught imports 22 | ], 23 | importOrderSeparation: true, 24 | importOrderSortSpecifiers: true, 25 | }; 26 | -------------------------------------------------------------------------------- /docs/google_search.md: -------------------------------------------------------------------------------- 1 | # Google Search Tool 2 | 3 | Use the Google Search API to search the web in Chatbot UI. 4 | 5 | ## How To Enable 6 | 7 | 1. Create a new project at https://console.developers.google.com/apis/dashboard 8 | 9 | 2. Create a new API key at https://console.developers.google.com/apis/credentials 10 | 11 | 3. Enable the Custom Search API at https://console.developers.google.com/apis/library/customsearch.googleapis.com 12 | 13 | 4. Create a new Custom Search Engine at https://cse.google.com/cse/all 14 | 15 | 5. Add your API Key and your Custom Search Engine ID to your .env.local file 16 | 17 | 6. You can now select the Google Search Tool in the search tools dropdown 18 | 19 | ## Usage Limits 20 | 21 | Google gives you 100 free searches per day. You can increase this limit by creating a billing account. 22 | -------------------------------------------------------------------------------- /server/trpc.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from './context'; 2 | 3 | import { TRPCError, initTRPC } from '@trpc/server'; 4 | 5 | // Avoid exporting the entire t-object 6 | // since it's not very descriptive. 7 | // For instance, the use of a t variable 8 | // is common in i18n libraries. 9 | const t = initTRPC.context().create(); 10 | 11 | export const middleware = t.middleware; 12 | 13 | const secure = middleware(async ({ ctx, next }) => { 14 | if (!ctx.userHash) { 15 | throw new TRPCError({ code: 'UNAUTHORIZED' }); 16 | } 17 | return next({ 18 | ctx: { 19 | userHash: ctx.userHash, 20 | }, 21 | }); 22 | }); 23 | 24 | // Base router and procedure helpers 25 | export const router = t.router; 26 | export const publicProcedure = t.procedure; 27 | export const procedure = t.procedure.use(secure); 28 | -------------------------------------------------------------------------------- /components/Mobile/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { IconPlus } from '@tabler/icons-react'; 2 | import { FC } from 'react'; 3 | 4 | import { Conversation } from '@/types/chat'; 5 | 6 | interface Props { 7 | selectedConversation: Conversation; 8 | onNewConversation: () => void; 9 | } 10 | 11 | export const Navbar: FC = ({ 12 | selectedConversation, 13 | onNewConversation, 14 | }) => { 15 | return ( 16 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /utils/server/tiktoken.ts: -------------------------------------------------------------------------------- 1 | import cl100k from 'tiktoken/encoders/cl100k_base.json'; 2 | import p50k from 'tiktoken/encoders/p50k_base.json'; 3 | import { Tiktoken } from 'tiktoken/lite'; 4 | const { encoding_for_model } = require('tiktoken'); 5 | 6 | export const getTiktokenEncoding = async (model: string): Promise => { 7 | // Azure fix 8 | const modelId = model.replace('gpt-35', 'gpt-3.5') 9 | if (modelId.indexOf('text-davinci-') !== -1) { 10 | return new Tiktoken(p50k.bpe_ranks, p50k.special_tokens, p50k.pat_str); 11 | } 12 | if (modelId.indexOf('gpt-3.5') !== -1 || modelId.indexOf('gpt-4') !== -1) { 13 | return encoding_for_model(modelId, { 14 | '<|im_start|>': 100264, 15 | '<|im_end|>': 100265, 16 | '<|im_sep|>': 100266, 17 | }); 18 | } 19 | return new Tiktoken(cl100k.bpe_ranks, cl100k.special_tokens, cl100k.pat_str); 20 | }; 21 | -------------------------------------------------------------------------------- /components/Promptbar/components/Prompts.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | import { Prompt } from '@/types/prompt'; 4 | 5 | import { PromptComponent } from './Prompt'; 6 | 7 | interface Props { 8 | prompts: Prompt[]; 9 | handleUpdatePrompt(prompt: Prompt): void; 10 | handleDeletePrompt(prompt: Prompt): void 11 | handleCreatePublicPrompt(prompt: Prompt): void 12 | isShareable?: boolean 13 | } 14 | 15 | export const Prompts: FC = ({ prompts, handleUpdatePrompt, handleDeletePrompt, handleCreatePublicPrompt, isShareable = false }) => { 16 | return ( 17 |
18 | {prompts.map((prompt, index) => ( 19 | 25 | ))} 26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /utils/app/api.ts: -------------------------------------------------------------------------------- 1 | import { ChatMode, ChatModeID } from '@/types/chatmode'; 2 | 3 | export const getEndpoint = (plugin: ChatMode | null) => { 4 | if (!plugin) { 5 | return 'api/chat'; 6 | } 7 | 8 | if (plugin.id === ChatModeID.GOOGLE_SEARCH) { 9 | return 'api/google'; 10 | } 11 | 12 | return 'api/chat'; 13 | }; 14 | 15 | export const watchRefToAbort = async ( 16 | ref: React.MutableRefObject, 17 | fn: (controller: AbortController) => Promise, 18 | ): Promise => { 19 | const controller = new AbortController(); 20 | let interval: any | null = null; 21 | try { 22 | interval = setInterval(() => { 23 | if (ref.current === true) { 24 | ref.current = false; 25 | controller.abort(); 26 | if (interval) { 27 | clearInterval(interval); 28 | interval = null; 29 | } 30 | } 31 | }, 200); 32 | return await fn(controller); 33 | } finally { 34 | if (interval) { 35 | clearInterval(interval); 36 | } 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /hooks/useImporter.ts: -------------------------------------------------------------------------------- 1 | import { cleanData } from '@/utils/app/importExport'; 2 | import { trpc } from '@/utils/trpc'; 3 | 4 | import { SupportedExportFormats } from '@/types/export'; 5 | import { Settings } from '@/types/settings'; 6 | 7 | export const useImporter = () => { 8 | const conversationsMutation = trpc.conversations.updateAll.useMutation(); 9 | const foldersMutation = trpc.folders.updateAll.useMutation(); 10 | const promptsMutation = trpc.prompts.updateAll.useMutation(); 11 | 12 | return { 13 | importData: async (settings: Settings, data: SupportedExportFormats) => { 14 | const cleanedData = cleanData(data, { 15 | temperature: settings.defaultTemperature, 16 | }); 17 | const { history, folders, prompts } = cleanedData; 18 | const conversations = history; 19 | await conversationsMutation.mutateAsync(conversations); 20 | await foldersMutation.mutateAsync(folders); 21 | await promptsMutation.mutateAsync(prompts); 22 | return cleanedData; 23 | }, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { SessionProvider } from 'next-auth/react'; 2 | import { Toaster } from 'react-hot-toast'; 3 | import { QueryClient, QueryClientProvider } from 'react-query'; 4 | 5 | import { Session } from 'next-auth'; 6 | import { appWithTranslation } from 'next-i18next'; 7 | import type { AppProps } from 'next/app'; 8 | import { Inter } from 'next/font/google'; 9 | 10 | import '@/styles/globals.css'; 11 | import { trpc } from '../utils/trpc'; 12 | 13 | const inter = Inter({ subsets: ['latin'] }); 14 | 15 | function App({ Component, pageProps }: AppProps<{ session: Session }>) { 16 | const queryClient = new QueryClient(); 17 | 18 | return ( 19 | 20 |
21 | 22 | 23 | 24 | 25 |
26 |
27 | ); 28 | } 29 | 30 | export default trpc.withTRPC(appWithTranslation(App)); 31 | -------------------------------------------------------------------------------- /scripts/create-mongodb-backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | # Create a dump from the MongoDB container running in local docker environment. 6 | 7 | mongo_user=$(cat .env.local|grep MONGO_INITDB_ROOT_USERNAME|cut -d '=' -f 2) 8 | mongo_pass=$(cat .env.local|grep MONGO_INITDB_ROOT_PASSWORD|cut -d '=' -f 2) 9 | mongo_db=$(cat .env.local|grep MONGODB_DB|cut -d '=' -f 2) 10 | mongo_host=localhost 11 | mongo_port=27017 12 | output="./${mongo_db}.gz" 13 | while getopts "h:p:d:o:" opt; do 14 | case $opt in 15 | h) mongo_host=$OPTARG;; 16 | p) mongo_port=$OPTARG;; 17 | d) mongo_db=$OPTARG;; 18 | o) output=$OPTARG;; 19 | *) echo "Invalid option: -$OPTARG" >&2;; 20 | esac 21 | done 22 | 23 | docker-compose exec mongo mongodump --authenticationDatabase admin \ 24 | -u "$mongo_user" -p "$mongo_pass" \ 25 | --host="${mongo_host}" --port="${mongo_port}" \ 26 | --db="${mongo_db}" --gzip --archive=/tmp/chatui.tar.gz 27 | docker-compose cp mongo:/tmp/chatui.tar.gz "${output}" 28 | 29 | -------------------------------------------------------------------------------- /components/Chat/Regenerate.tsx: -------------------------------------------------------------------------------- 1 | import { IconRefresh } from '@tabler/icons-react'; 2 | import { FC } from 'react'; 3 | 4 | import { useTranslation } from 'next-i18next'; 5 | 6 | interface Props { 7 | onRegenerate: () => void; 8 | } 9 | 10 | export const Regenerate: FC = ({ onRegenerate }) => { 11 | const { t } = useTranslation('chat'); 12 | return ( 13 |
14 |
15 | {t('Sorry, there was an error.')} 16 |
17 | 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /utils/server/similarity.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIApi } from 'openai'; 2 | import { saveLlmUsage } from './llmUsage'; 3 | import { OpenAIModelID, OpenAIModels } from '@/types/openai'; 4 | export const createEmbedding = async ( 5 | text: string, 6 | openai: OpenAIApi, 7 | userId: string, 8 | ): Promise => { 9 | const modelId = OpenAIModels[OpenAIModelID.TEXT_EMBEDDING_ADA_002].id; 10 | const result = await openai.createEmbedding({ 11 | model: modelId, 12 | input: text, 13 | }); 14 | await saveLlmUsage(userId, modelId, "embedding", { 15 | prompt: result.data.usage?.prompt_tokens, 16 | completion: 0, 17 | total: result.data.usage?.total_tokens 18 | }) 19 | return result.data.data[0].embedding; 20 | }; 21 | 22 | export function calcCosineSimilarity(a: number[], b: number[]) { 23 | const dot = a.reduce((acc, v, i) => acc + v * b[i], 0); 24 | const normA = Math.sqrt(a.reduce((acc, v) => acc + v * v, 0)); 25 | const normB = Math.sqrt(b.reduce((acc, v) => acc + v * v, 0)); 26 | return dot / (normA * normB); 27 | } 28 | -------------------------------------------------------------------------------- /components/Chatbar/Chatbar.context.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, createContext } from 'react'; 2 | 3 | import { ActionType } from '@/hooks/useCreateReducer'; 4 | 5 | import { Conversation } from '@/types/chat'; 6 | import { ChatModeKey } from '@/types/chatmode'; 7 | import { SupportedExportFormats } from '@/types/export'; 8 | 9 | import { ChatbarInitialState } from './Chatbar.state'; 10 | 11 | export interface ChatbarContextProps { 12 | state: ChatbarInitialState; 13 | dispatch: Dispatch>; 14 | handleDeleteConversation: (conversation: Conversation) => void; 15 | handleClearConversations: () => void; 16 | handleExportData: () => Promise; 17 | handleImportConversations: (data: SupportedExportFormats) => Promise; 18 | handlePluginKeyChange: (pluginKey: ChatModeKey) => void; 19 | handleClearPluginKey: (pluginKey: ChatModeKey) => void; 20 | handleApiKeyChange: (apiKey: string) => void; 21 | } 22 | 23 | const ChatbarContext = createContext(undefined!); 24 | 25 | export default ChatbarContext; 26 | -------------------------------------------------------------------------------- /utils/server/openai.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, OpenAIApi } from "openai"; 2 | import { OPENAI_API_TYPE, OPENAI_API_HOST, OPENAI_API_VERSION, AZURE_DEPLOYMENT_ID_EMBEDDINGS } from "../app/const"; 3 | 4 | export const getOpenAIApi = (deploymentId?: string): OpenAIApi => { 5 | const apiKey = process.env.OPENAI_API_KEY; 6 | 7 | let openaiConfig; 8 | if (OPENAI_API_TYPE == "azure") { 9 | openaiConfig = new Configuration({ 10 | basePath: new URL(OPENAI_API_HOST + "/openai/deployments/" + deploymentId).toString(), 11 | baseOptions: { 12 | headers: { 'api-key': apiKey }, 13 | params: { 14 | 'api-version': OPENAI_API_VERSION 15 | } 16 | } 17 | }); 18 | } else { 19 | openaiConfig = new Configuration({ 20 | apiKey 21 | }); 22 | } 23 | return new OpenAIApi(openaiConfig); 24 | } 25 | 26 | export const getOpenAIApiEmbeddings = (): OpenAIApi => { 27 | return getOpenAIApi(AZURE_DEPLOYMENT_ID_EMBEDDINGS) 28 | } -------------------------------------------------------------------------------- /components/Spinner/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | 3 | interface Props { 4 | size?: string; 5 | className?: string; 6 | } 7 | 8 | const Spinner = ({ size = '1em', className = '' }: Props) => { 9 | return ( 10 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default Spinner; 35 | -------------------------------------------------------------------------------- /hooks/useCreateReducer.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useReducer } from 'react'; 2 | 3 | // Extracts property names from initial state of reducer to allow typesafe dispatch objects 4 | export type FieldNames = { 5 | [K in keyof T]: T[K] extends string ? K : K; 6 | }[keyof T]; 7 | 8 | // Returns the Action Type for the dispatch object to be used for typing in things like context 9 | export type ActionType = 10 | | { type: 'reset' } 11 | | { type: 'replace_all'; value: T } 12 | | { type?: 'change'; field: FieldNames; value: any }; 13 | 14 | // Returns a typed dispatch and state 15 | export const useCreateReducer = ({ initialState }: { initialState: T }) => { 16 | const reducer = (state: T, action: ActionType) => { 17 | if (!action.type) return { ...state, [action.field]: action.value }; 18 | 19 | if (action.type === 'replace_all') return action.value; 20 | if (action.type === 'reset') return initialState; 21 | 22 | throw new Error(); 23 | }; 24 | 25 | const [state, dispatch] = useReducer(reducer, initialState); 26 | 27 | return useMemo(() => ({ state, dispatch }), [state, dispatch]); 28 | }; 29 | -------------------------------------------------------------------------------- /server/routers/publicFolders.ts: -------------------------------------------------------------------------------- 1 | import { PublicPromptsDb, getDb } from '@/utils/server/storage'; 2 | 3 | import { FolderSchema } from '@/types/folder'; 4 | 5 | import { procedure, router } from '../trpc'; 6 | 7 | import { z } from 'zod'; 8 | import { validateAdminAccess } from '../context'; 9 | 10 | export const publicFolders = router({ 11 | list: procedure.query(async ({ ctx }) => { 12 | const publicPromptsDb = new PublicPromptsDb(await getDb()); 13 | return await publicPromptsDb.getFolders(); 14 | }), 15 | remove: procedure 16 | .input(z.object({ id: z.string() })) 17 | .mutation(async ({ ctx, input }) => { 18 | await validateAdminAccess(ctx); 19 | const publicPromptsDb = new PublicPromptsDb(await getDb()); 20 | await publicPromptsDb.removeFolder(input.id); 21 | return { success: true }; 22 | }), 23 | update: procedure.input(FolderSchema).mutation(async ({ ctx, input }) => { 24 | await validateAdminAccess(ctx); 25 | const publicPromptsDb = new PublicPromptsDb(await getDb()); 26 | await publicPromptsDb.saveFolder(input); 27 | return { success: true }; 28 | }) 29 | }); 30 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mckay Wrigley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /utils/app/const.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_SYSTEM_PROMPT = 2 | process.env.NEXT_PUBLIC_DEFAULT_SYSTEM_PROMPT || 3 | "You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown."; 4 | 5 | export const OPENAI_API_HOST = 6 | process.env.OPENAI_API_HOST || 'https://api.openai.com'; 7 | 8 | export const OPENAI_API_TYPE = process.env.OPENAI_API_TYPE || 'openai'; 9 | 10 | export const OPENAI_API_VERSION = 11 | process.env.OPENAI_API_VERSION || '2023-03-15-preview'; 12 | 13 | export const OPENAI_ORGANIZATION = process.env.OPENAI_ORGANIZATION || ''; 14 | 15 | export const AZURE_DEPLOYMENT_ID_EMBEDDINGS = process.env.AZURE_DEPLOYMENT_ID_EMBEDDINGS || ''; 16 | 17 | export const MONGODB_DB = process.env.MONGODB_DB || ''; 18 | 19 | export const SUPPORT_EMAIL = process.env.SUPPORT_EMAIL || ''; 20 | 21 | export const PROMPT_SHARING_ENABLED: boolean = process.env.PROMPT_SHARING_ENABLED === "true" || false; 22 | 23 | export const DEFAULT_USER_LIMIT_USD_MONTHLY: number = process.env.DEFAULT_USER_LIMIT_USD_MONTHLY != undefined ? Number.parseFloat(process.env.DEFAULT_USER_LIMIT_USD_MONTHLY) : -1; 24 | -------------------------------------------------------------------------------- /utils/app/codeblock.ts: -------------------------------------------------------------------------------- 1 | interface languageMap { 2 | [key: string]: string | undefined; 3 | } 4 | 5 | export const programmingLanguages: languageMap = { 6 | javascript: '.js', 7 | python: '.py', 8 | java: '.java', 9 | c: '.c', 10 | cpp: '.cpp', 11 | 'c++': '.cpp', 12 | 'c#': '.cs', 13 | ruby: '.rb', 14 | php: '.php', 15 | swift: '.swift', 16 | 'objective-c': '.m', 17 | kotlin: '.kt', 18 | typescript: '.ts', 19 | go: '.go', 20 | perl: '.pl', 21 | rust: '.rs', 22 | scala: '.scala', 23 | haskell: '.hs', 24 | lua: '.lua', 25 | shell: '.sh', 26 | sql: '.sql', 27 | html: '.html', 28 | css: '.css', 29 | // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component 30 | }; 31 | 32 | export const generateRandomString = (length: number, lowercase = false) => { 33 | const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789'; // excluding similar looking characters like Z, 2, I, 1, O, 0 34 | let result = ''; 35 | for (let i = 0; i < length; i++) { 36 | result += chars.charAt(Math.floor(Math.random() * chars.length)); 37 | } 38 | return lowercase ? result.toLowerCase() : result; 39 | }; 40 | -------------------------------------------------------------------------------- /utils/server/auth.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | import { getServerSession } from 'next-auth'; 3 | 4 | import { authOptions } from '@/pages/api/auth/[...nextauth]'; 5 | 6 | import crypto from 'crypto'; 7 | 8 | export const ensureHasValidSession = async ( 9 | req: NextApiRequest, 10 | res: NextApiResponse, 11 | ): Promise => { 12 | const session = await getServerSession(req, res, authOptions); 13 | return session !== null; 14 | }; 15 | 16 | export const getUserHash = async ( 17 | req: NextApiRequest, 18 | res: NextApiResponse, 19 | ): Promise => { 20 | // TODO: support no auth environment. 21 | 22 | const session = await getServerSession(req, res, authOptions); 23 | if (!session) { 24 | throw new Error('Unauthorized'); 25 | } 26 | const email = session.user?.email; 27 | if (!email) { 28 | throw new Error('Unauthorized. No email found in session'); 29 | } 30 | return getUserHashFromMail(email); 31 | }; 32 | 33 | export const getUserHashFromMail = (email: string): string => { 34 | const hash = crypto.createHash('sha256').update(email).digest('hex'); 35 | return hash; 36 | }; 37 | -------------------------------------------------------------------------------- /server/context.ts: -------------------------------------------------------------------------------- 1 | import { getServerSession } from 'next-auth'; 2 | 3 | import { getUserHash } from '@/utils/server/auth'; 4 | 5 | import { authOptions } from '@/pages/api/auth/[...nextauth]'; 6 | 7 | import { TRPCError, inferAsyncReturnType } from '@trpc/server'; 8 | import { CreateNextContextOptions } from '@trpc/server/adapters/next'; 9 | import { UserRole } from '@/types/user'; 10 | 11 | export async function createContext(opts: CreateNextContextOptions) { 12 | const session = await getServerSession(opts.req, opts.res, authOptions); 13 | let userHash: string | undefined; 14 | if (session) { 15 | userHash = await getUserHash(opts.req, opts.res); 16 | } 17 | return { 18 | req: opts.req, 19 | res: opts.res, 20 | session, 21 | userHash, 22 | }; 23 | } 24 | export type Context = inferAsyncReturnType; 25 | 26 | export async function validateAdminAccess(ctx: Context) { 27 | if (!isAdminUser(ctx)) authError(); 28 | } 29 | 30 | export function isAdminUser(ctx: Context): boolean { 31 | return ctx.session?.user?.role === UserRole.ADMIN; 32 | } 33 | 34 | export async function authError() { 35 | throw new TRPCError({ code: 'UNAUTHORIZED' }); 36 | } -------------------------------------------------------------------------------- /server/routers/prompts.ts: -------------------------------------------------------------------------------- 1 | import { UserDb } from '@/utils/server/storage'; 2 | 3 | import { PromptSchema, PromptSchemaArray } from '@/types/prompt'; 4 | 5 | import { procedure, router } from '../trpc'; 6 | 7 | import { z } from 'zod'; 8 | 9 | export const prompts = router({ 10 | list: procedure.query(async ({ ctx }) => { 11 | const userDb = await UserDb.fromUserHash(ctx.userHash); 12 | return await userDb.getPrompts(); 13 | }), 14 | remove: procedure 15 | .input(z.object({ id: z.string() })) 16 | .mutation(async ({ ctx, input }) => { 17 | const userDb = await UserDb.fromUserHash(ctx.userHash); 18 | await userDb.removePrompt(input.id); 19 | return { success: true }; 20 | }), 21 | update: procedure.input(PromptSchema).mutation(async ({ ctx, input }) => { 22 | const userDb = await UserDb.fromUserHash(ctx.userHash); 23 | await userDb.savePrompt(input); 24 | return { success: true }; 25 | }), 26 | updateAll: procedure 27 | .input(PromptSchemaArray) 28 | .mutation(async ({ ctx, input }) => { 29 | const userDb = await UserDb.fromUserHash(ctx.userHash); 30 | await userDb.savePrompts(input); 31 | return { success: true }; 32 | }), 33 | }); 34 | -------------------------------------------------------------------------------- /components/Promptbar/PromptBar.context.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, createContext } from 'react'; 2 | 3 | import { ActionType } from '@/hooks/useCreateReducer'; 4 | 5 | import { Prompt } from '@/types/prompt'; 6 | 7 | import { PromptbarInitialState } from './Promptbar.state'; 8 | import { FolderInterface } from '@/types/folder'; 9 | 10 | export interface PromptbarContextProps { 11 | state: PromptbarInitialState; 12 | dispatch: Dispatch>; 13 | handleCreatePrompt: () => void; 14 | handleDeletePrompt: (prompt: Prompt) => void; 15 | handleUpdatePrompt: (prompt: Prompt) => void; 16 | handleCreatePublicPrompt: (prompt: Prompt) => void; 17 | handleDeletePublicPrompt: (prompt: Prompt) => void; 18 | handleUpdatePublicPrompt: (prompt: Prompt) => void; 19 | handleCreateFolder: () => void; 20 | handleEditFolder: (folder: FolderInterface) => void; 21 | handleDeleteFolder: (folder: FolderInterface) => void; 22 | handleCreatePublicFolder: () => void; 23 | handleEditPublicFolder: (folder: FolderInterface) => void; 24 | handleDeletePublicFolder: (folder: FolderInterface) => void; 25 | } 26 | 27 | const PromptbarContext = createContext(undefined!); 28 | 29 | export default PromptbarContext; 30 | -------------------------------------------------------------------------------- /utils/server/message.test.ts: -------------------------------------------------------------------------------- 1 | import {describe, expect, it} from 'vitest' 2 | import {Message} from '@/types/chat'; 3 | 4 | import {createMessagesToSend} from './message'; 5 | import {getTiktokenEncoding} from './tiktoken'; 6 | import {OpenAIModel, OpenAIModelID, OpenAIModelType} from "@/types/openai"; 7 | 8 | describe('createMessagesToSend', () => { 9 | it('should create messages to send and return max token', async () => { 10 | const encoding = await getTiktokenEncoding('gpt-3.5-turbo'); 11 | const systemPrompt = 'Hello'; 12 | const model: OpenAIModel = { 13 | id: OpenAIModelID.GPT_3_5, 14 | name: 'gpt-3.5-turbo', 15 | tokenLimit: 1100, 16 | maxLength: 4000, 17 | type: OpenAIModelType.CHAT 18 | } 19 | const messages: Message[] = [ 20 | {role: 'user', content: 'World'}, 21 | {role: 'assistant', content: 'How are you?'}, 22 | {role: 'user', content: 'Fine, thank you.'}, 23 | ]; 24 | 25 | const result = createMessagesToSend( 26 | encoding, 27 | model, 28 | systemPrompt, 29 | 100, 30 | messages, 31 | ); 32 | 33 | expect(result.messages[0]).toEqual({role: 'user', content: 'World'}); 34 | expect(result.maxToken).toEqual(1066); 35 | }); 36 | }) -------------------------------------------------------------------------------- /agent/prompts/agent.ts: -------------------------------------------------------------------------------- 1 | export const systemPrefix = `Answer the following questions as best you can. 2 | Use the language {locale} for your thought and final answer. 3 | You have access to the following tools: 4 | 5 | {tool_descriptions}`; 6 | 7 | export const systemPrompt = `ALWAYS use the following format in your response:: 8 | 9 | Question: the input question you must answer 10 | Thought: you should always think about what to do 11 | Action: the action to take, should be one of [{tool_names}]. 12 | Action Input: the input to the action 13 | Observation: the result of the action. you have no need to output this item. 14 | ... (this Thought/Action/Action Input/Observation can repeat N times.) 15 | Thought: I now know the final answer 16 | Final Answer: the final answer to the original input question 17 | Positivity: the positivity of the final answer. the range is 0 - 10 18 | `; 19 | 20 | export const systemSuffix = `Begin! Reminder to always use the exact characters \`Final Answer\` when responding.`; 21 | 22 | export const userPrompt = ` 23 | Question: {input} 24 | {agent_scratchpad}`; 25 | 26 | // eslint-disable-next-line import/no-anonymous-default-export 27 | export default { 28 | systemPrefix, 29 | systemPrompt, 30 | systemSuffix, 31 | userPrompt, 32 | }; 33 | -------------------------------------------------------------------------------- /types/export.ts: -------------------------------------------------------------------------------- 1 | import { Conversation, Message } from './chat'; 2 | import { FolderInterface } from './folder'; 3 | import { Prompt } from './prompt'; 4 | 5 | export type SupportedExportFormats = 6 | | ExportFormatV1 7 | | ExportFormatV2 8 | | ExportFormatV3 9 | | ExportFormatV4; 10 | export type LatestExportFormat = ExportFormatV4; 11 | 12 | //////////////////////////////////////////////////////////////////////////////////////////// 13 | interface ConversationV1 { 14 | id: number; 15 | name: string; 16 | messages: Message[]; 17 | } 18 | 19 | export type ExportFormatV1 = ConversationV1[]; 20 | 21 | //////////////////////////////////////////////////////////////////////////////////////////// 22 | interface ChatFolder { 23 | id: number; 24 | name: string; 25 | } 26 | 27 | export interface ExportFormatV2 { 28 | history: Conversation[] | null; 29 | folders: ChatFolder[] | null; 30 | } 31 | 32 | //////////////////////////////////////////////////////////////////////////////////////////// 33 | export interface ExportFormatV3 { 34 | version: 3; 35 | history: Conversation[]; 36 | folders: FolderInterface[]; 37 | } 38 | 39 | export interface ExportFormatV4 { 40 | version: 4; 41 | history: Conversation[]; 42 | folders: FolderInterface[]; 43 | prompts: Prompt[]; 44 | } 45 | -------------------------------------------------------------------------------- /types/llmUsage.ts: -------------------------------------------------------------------------------- 1 | import * as z from 'zod'; 2 | import { OpenAIModelID } from './openai'; 3 | 4 | export const OpenAIModelIdEnumSchema = z.nativeEnum(OpenAIModelID); 5 | 6 | export const LlmPriceRate = z.object({ 7 | modelId: OpenAIModelIdEnumSchema, 8 | promptPriceUSDPer1000: z.number(), 9 | completionPriceUSDPer1000: z.number() 10 | }); 11 | 12 | export type LlmPriceRate = z.infer; 13 | 14 | export const TokenUsageCountSchema = z.object({ 15 | prompt: z.number(), 16 | completion: z.number(), 17 | total: z.number() 18 | }); 19 | export type TokenUsageCount = z.infer; 20 | 21 | export const LlmUsageModeEnum = z.enum(["chat", "agent", "agentConv", "google", "agentPlugin", "embedding"]); 22 | export type LlmUsageMode = z.infer; 23 | 24 | export const UserLlmUsageSchema = z.object({ 25 | _id: z.string().optional(), 26 | date: z.coerce.date(), 27 | tokens: TokenUsageCountSchema, 28 | totalPriceUSD: z.number().optional(), 29 | modelId: z.string(), 30 | mode: LlmUsageModeEnum, 31 | userId: z.string() 32 | }); 33 | 34 | export const NewUserLlmUsageSchema = UserLlmUsageSchema.omit({ userId: true, _id: true }) 35 | export type NewUserLlmUsage = z.infer; 36 | export type UserLlmUsage = z.infer; -------------------------------------------------------------------------------- /k8s/chatbot-ui.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: chatbot-ui 5 | --- 6 | apiVersion: v1 7 | kind: Secret 8 | metadata: 9 | namespace: chatbot-ui 10 | name: chatbot-ui 11 | type: Opaque 12 | data: 13 | OPENAI_API_KEY: 14 | --- 15 | apiVersion: apps/v1 16 | kind: Deployment 17 | metadata: 18 | namespace: chatbot-ui 19 | name: chatbot-ui 20 | labels: 21 | app: chatbot-ui 22 | spec: 23 | replicas: 1 24 | selector: 25 | matchLabels: 26 | app: chatbot-ui 27 | template: 28 | metadata: 29 | labels: 30 | app: chatbot-ui 31 | spec: 32 | containers: 33 | - name: chatbot-ui 34 | image: /chatbot-ui:latest 35 | resources: {} 36 | ports: 37 | - containerPort: 3000 38 | env: 39 | - name: OPENAI_API_KEY 40 | valueFrom: 41 | secretKeyRef: 42 | name: chatbot-ui 43 | key: OPENAI_API_KEY 44 | --- 45 | kind: Service 46 | apiVersion: v1 47 | metadata: 48 | namespace: chatbot-ui 49 | name: chatbot-ui 50 | labels: 51 | app: chatbot-ui 52 | spec: 53 | ports: 54 | - name: http 55 | protocol: TCP 56 | port: 80 57 | targetPort: 3000 58 | selector: 59 | app: chatbot-ui 60 | type: ClusterIP 61 | -------------------------------------------------------------------------------- /components/Search/Search.tsx: -------------------------------------------------------------------------------- 1 | import { IconX } from '@tabler/icons-react'; 2 | import { FC } from 'react'; 3 | 4 | import { useTranslation } from 'next-i18next'; 5 | 6 | import { InputText } from '../Input/InputText'; 7 | 8 | interface Props { 9 | placeholder: string; 10 | searchTerm: string; 11 | onSearch: (searchTerm: string) => void; 12 | } 13 | const Search: FC = ({ placeholder, searchTerm, onSearch }) => { 14 | const { t } = useTranslation('sidebar'); 15 | 16 | const handleSearchChange = (e: React.ChangeEvent) => { 17 | onSearch(e.target.value); 18 | }; 19 | 20 | const clearSearch = () => { 21 | onSearch(''); 22 | }; 23 | 24 | return ( 25 |
26 | 33 | 34 | {searchTerm && ( 35 | 40 | )} 41 |
42 | ); 43 | }; 44 | 45 | export default Search; 46 | -------------------------------------------------------------------------------- /hooks/useExporter.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | 3 | import { exportData } from '@/utils/app/importExport'; 4 | import { trpc } from '@/utils/trpc'; 5 | 6 | import HomeContext from '@/pages/api/home/home.context'; 7 | 8 | export const useExporter = () => { 9 | const conversationsListQuery = trpc.conversations.list.useQuery(undefined, { 10 | enabled: false, 11 | }); 12 | const foldersListQuery = trpc.folders.list.useQuery(undefined, { 13 | enabled: false, 14 | }); 15 | const promptsListQuery = trpc.prompts.list.useQuery(undefined, { 16 | enabled: false, 17 | }); 18 | const { 19 | state: { prompts }, 20 | } = useContext(HomeContext); 21 | return { 22 | exportData: async () => { 23 | const conversationsResult = await conversationsListQuery.refetch(); 24 | if (conversationsResult.isError) { 25 | throw conversationsResult.error; 26 | } 27 | const foldersResult = await foldersListQuery.refetch(); 28 | if (foldersResult.isError) { 29 | throw foldersResult.error; 30 | } 31 | const promptsResult = await promptsListQuery.refetch(); 32 | if (promptsResult.isError) { 33 | throw promptsResult.error; 34 | } 35 | await exportData( 36 | conversationsResult.data!, 37 | foldersResult.data!, 38 | promptsResult.data!, 39 | ); 40 | }, 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /components/Chat/ChatInputTokenCount.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | 4 | import HomeContext from '@/pages/api/home/home.context'; 5 | 6 | import cl100k_base from 'tiktoken/encoders/cl100k_base.json'; 7 | import { Tiktoken } from 'tiktoken/lite'; 8 | 9 | export function ChatInputTokenCount(props: { content: string | undefined }) { 10 | const { t } = useTranslation('chat'); 11 | const { 12 | state: { selectedConversation }, 13 | } = useContext(HomeContext); 14 | 15 | const [tokenizer, setTokenizer] = useState(null); 16 | 17 | useEffect(() => { 18 | let model: Tiktoken | null = new Tiktoken( 19 | cl100k_base.bpe_ranks, 20 | { 21 | ...cl100k_base.special_tokens, 22 | '<|im_start|>': 100264, 23 | '<|im_end|>': 100265, 24 | '<|im_sep|>': 100266, 25 | }, 26 | cl100k_base.pat_str, 27 | ); 28 | 29 | setTokenizer(model); 30 | return () => model?.free(); 31 | }, []); 32 | 33 | const serialized = `${props.content || ''}`; 34 | const count = tokenizer?.encode(serialized, 'all').length; 35 | if (count == null) return null; 36 | return ( 37 |
38 | {t('{{count}} tokens', { 39 | count, 40 | })} 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /components/Home/HomeMain.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useRef } from 'react'; 2 | 3 | import useConversations from '@/hooks/useConversations'; 4 | 5 | import { Conversation } from '@/types/chat'; 6 | 7 | import HomeContext from '@/pages/api/home/home.context'; 8 | 9 | import { Chat } from '../Chat/Chat'; 10 | import { Chatbar } from '../Chatbar/Chatbar'; 11 | import { Navbar } from '../Mobile/Navbar'; 12 | import Promptbar from '../Promptbar'; 13 | 14 | type HomeMainProps = { 15 | selectedConversation: Conversation; 16 | }; 17 | 18 | export const HomeMain = ({ selectedConversation }: HomeMainProps) => { 19 | const { 20 | state: { settings }, 21 | } = useContext(HomeContext); 22 | 23 | const [_, conversationsAction] = useConversations(); 24 | return ( 25 |
28 |
29 | conversationsAction.add()} 32 | /> 33 |
34 | 35 |
36 | 37 | 38 |
39 | 40 |
41 | 42 | 43 |
44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /services/useApiError.ts: -------------------------------------------------------------------------------- 1 | import HomeContext from "@/pages/api/home/home.context"; 2 | import { ErrorResponseCode } from "@/types/error"; 3 | import { useContext } from "react"; 4 | import { useTranslation } from "react-i18next"; 5 | 6 | interface TranslationContext { 7 | supportEmail?: string | null 8 | } 9 | 10 | const useApiError = () => { 11 | const { 12 | state: { 13 | supportEmail 14 | }, 15 | } = useContext(HomeContext); 16 | const { t } = useTranslation("error"); 17 | const translationContext: TranslationContext = { supportEmail }; 18 | const defaultMessage = t("errorDefault", translationContext) || "Error"; 19 | 20 | const resolveResponseMessage = async (error: any): Promise => { 21 | if (error instanceof Response) { 22 | const json = await error.json(); 23 | if (json.error?.code) { 24 | if (json.error.code == ErrorResponseCode.ERROR_DEFAULT || !supportEmail) { 25 | return json.error?.message || defaultMessage; 26 | } 27 | return t(json.error.code.toString(), translationContext) || defaultMessage; 28 | } else { 29 | return t(json.error || json.message) || defaultMessage 30 | } 31 | } else { 32 | return typeof error == "string" ? t(error).toString() : defaultMessage; 33 | } 34 | } 35 | 36 | return { 37 | resolveResponseMessage, 38 | }; 39 | } 40 | 41 | export default useApiError; -------------------------------------------------------------------------------- /utils/trpc.ts: -------------------------------------------------------------------------------- 1 | import { httpBatchLink } from '@trpc/client'; 2 | import { createTRPCNext } from '@trpc/next'; 3 | import type { AppRouter } from '../server/routers/_app'; 4 | 5 | function getBaseUrl() { 6 | if (typeof window !== 'undefined') 7 | // browser should use relative path 8 | return ''; 9 | 10 | if (process.env.VERCEL_URL) 11 | // reference for vercel.com 12 | return `https://${process.env.VERCEL_URL}`; 13 | 14 | if (process.env.RENDER_INTERNAL_HOSTNAME) 15 | // reference for render.com 16 | return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`; 17 | 18 | // assume localhost 19 | return `http://localhost:${process.env.PORT ?? 3000}`; 20 | } 21 | 22 | export const trpc = createTRPCNext({ 23 | config({ ctx }) { 24 | return { 25 | links: [ 26 | httpBatchLink({ 27 | /** 28 | * If you want to use SSR, you need to use the server's full URL 29 | * @link https://trpc.io/docs/ssr 30 | **/ 31 | url: `${getBaseUrl()}/api/trpc`, 32 | 33 | // You can pass any HTTP headers you wish here 34 | async headers() { 35 | return { 36 | // authorization: getAuthCookie(), 37 | }; 38 | }, 39 | }), 40 | ], 41 | }; 42 | }, 43 | /** 44 | * @link https://trpc.io/docs/ssr 45 | **/ 46 | ssr: false, 47 | }); 48 | -------------------------------------------------------------------------------- /utils/app/homeUpdater.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch } from 'react'; 2 | 3 | import { ActionType } from '@/hooks/useCreateReducer'; 4 | 5 | import { Conversation, Message } from '@/types/chat'; 6 | 7 | import { HomeInitialState } from '@/pages/api/home/home.state'; 8 | 9 | export class HomeUpdater { 10 | constructor( 11 | private readonly dispatch: Dispatch>, 12 | ) {} 13 | 14 | addMessage(conversation: Conversation, message: Message): Conversation { 15 | const updatedMessages: Message[] = [...conversation.messages, message]; 16 | conversation = { 17 | ...conversation, 18 | messages: updatedMessages, 19 | }; 20 | this.dispatch({ 21 | field: 'selectedConversation', 22 | value: conversation, 23 | }); 24 | return conversation; 25 | } 26 | 27 | appendChunkToLastMessage( 28 | conversation: Conversation, 29 | chunk: string, 30 | ): Conversation { 31 | const lastIndex = conversation.messages.length - 1; 32 | const lastMessage = conversation.messages[lastIndex]; 33 | const messages = [ 34 | ...conversation.messages.slice(0, lastIndex - 1), 35 | lastMessage, 36 | ]; 37 | conversation = { 38 | ...conversation, 39 | messages: messages, 40 | }; 41 | this.dispatch({ 42 | field: 'selectedConversation', 43 | value: conversation, 44 | }); 45 | return conversation; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /hooks/chatmode/useChatModeRunner.ts: -------------------------------------------------------------------------------- 1 | import { MutableRefObject, useContext } from 'react'; 2 | 3 | import { useAgentMode } from '@/hooks/chatmode/useAgentMode'; 4 | import { useDirectMode } from '@/hooks/chatmode/useDirectMode'; 5 | import { useGoogleMode } from '@/hooks/chatmode/useGoogleMode'; 6 | 7 | import { ChatModeRunner, Conversation } from '@/types/chat'; 8 | import { ChatMode, ChatModeID } from '@/types/chatmode'; 9 | 10 | import HomeContext from '@/pages/api/home/home.context'; 11 | 12 | export const useChatModeRunner = (conversations: Conversation[]) => { 13 | const { 14 | state: { stopConversationRef }, 15 | } = useContext(HomeContext); 16 | const directMode = useDirectMode(conversations, stopConversationRef); 17 | const googleMode = useGoogleMode(conversations); 18 | const conversationalAgentMode = useAgentMode( 19 | conversations, 20 | stopConversationRef, 21 | true, 22 | ); 23 | const agentMode = useAgentMode(conversations, stopConversationRef, false); 24 | return (plugin: ChatMode | null): ChatModeRunner => { 25 | if (!plugin) { 26 | return directMode; 27 | } 28 | switch (plugin.id) { 29 | case ChatModeID.GOOGLE_SEARCH: 30 | return googleMode; 31 | case ChatModeID.AGENT: 32 | return agentMode; 33 | case ChatModeID.CONVERSATIONAL_AGENT: 34 | return conversationalAgentMode; 35 | default: 36 | return directMode; 37 | } 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /init-mongo.js: -------------------------------------------------------------------------------- 1 | db.createCollection("llmPriceRate"); 2 | parseAndUpdateApiRates(); 3 | 4 | function parseAndUpdateApiRates() { 5 | // modelId from /types/openai OpenAIModelID 6 | Object.entries(process.env) 7 | .filter(([key, value]) => key.startsWith("MODEL_PRICING_1000_")) 8 | .forEach(([key, value]) => { 9 | let modelId, promptPricing, completionPricing; 10 | if (key.startsWith("MODEL_PRICING_1000_PROMPT_")) { 11 | modelId = key.replace("MODEL_PRICING_1000_PROMPT_", ""); 12 | promptPricing = parseFloat(value); 13 | } else if (key.startsWith("MODEL_PRICING_1000_COMPLETION_")) { 14 | modelId = key.replace("MODEL_PRICING_1000_COMPLETION_", ""); 15 | completionPricing = parseFloat(value); 16 | } 17 | console.log("Setting " + (promptPricing ? "prompt" : "completion") + " price rate for model " + modelId + 18 | " to " + (promptPricing || completionPricing)); 19 | db.llmPriceRate.updateOne( 20 | { modelId }, 21 | { 22 | $set: { 23 | modelId, 24 | ...(promptPricing ? { promptPriceUSDPer1000: promptPricing } : {}), 25 | ...(completionPricing ? { completionPriceUSDPer1000: completionPricing } : {}), 26 | } 27 | }, 28 | { upsert: true } 29 | ) 30 | }) 31 | } -------------------------------------------------------------------------------- /server/routers/folders.ts: -------------------------------------------------------------------------------- 1 | import { UserDb } from '@/utils/server/storage'; 2 | 3 | import { FolderSchema, FolderSchemaArray } from '@/types/folder'; 4 | 5 | import { procedure, router } from '../trpc'; 6 | 7 | import { z } from 'zod'; 8 | 9 | export const folders = router({ 10 | list: procedure.query(async ({ ctx }) => { 11 | const userDb = await UserDb.fromUserHash(ctx.userHash); 12 | return await userDb.getFolders(); 13 | }), 14 | remove: procedure 15 | .input(z.object({ id: z.string() })) 16 | .mutation(async ({ ctx, input }) => { 17 | const userDb = await UserDb.fromUserHash(ctx.userHash); 18 | await userDb.removeFolder(input.id); 19 | return { success: true }; 20 | }), 21 | removeAll: procedure 22 | .input(z.object({ type: z.string() })) 23 | .mutation(async ({ ctx, input }) => { 24 | const userDb = await UserDb.fromUserHash(ctx.userHash); 25 | await userDb.removeAllFolders(input.type); 26 | return { success: true }; 27 | }), 28 | update: procedure.input(FolderSchema).mutation(async ({ ctx, input }) => { 29 | const userDb = await UserDb.fromUserHash(ctx.userHash); 30 | await userDb.saveFolder(input); 31 | return { success: true }; 32 | }), 33 | updateAll: procedure 34 | .input(FolderSchemaArray) 35 | .mutation(async ({ ctx, input }) => { 36 | const userDb = await UserDb.fromUserHash(ctx.userHash); 37 | await userDb.saveFolders(input); 38 | return { success: true }; 39 | }), 40 | }); 41 | -------------------------------------------------------------------------------- /server/routers/conversations.ts: -------------------------------------------------------------------------------- 1 | import { UserDb } from '@/utils/server/storage'; 2 | 3 | import { ConversationSchema, ConversationSchemaArray } from '@/types/chat'; 4 | 5 | import { procedure, router } from '../trpc'; 6 | 7 | import { z } from 'zod'; 8 | 9 | export const conversations = router({ 10 | list: procedure.query(async ({ ctx }) => { 11 | const userDb = await UserDb.fromUserHash(ctx.userHash); 12 | return await userDb.getConversations(); 13 | }), 14 | remove: procedure 15 | .input(z.object({ id: z.string() })) 16 | .mutation(async ({ ctx, input }) => { 17 | const userDb = await UserDb.fromUserHash(ctx.userHash); 18 | await userDb.removeConversation(input.id); 19 | return { success: true }; 20 | }), 21 | removeAll: procedure.mutation(async ({ ctx }) => { 22 | const userDb = await UserDb.fromUserHash(ctx.userHash); 23 | await userDb.removeAllConversations(); 24 | return { success: true }; 25 | }), 26 | update: procedure 27 | .input(ConversationSchema) 28 | .mutation(async ({ ctx, input }) => { 29 | const userDb = await UserDb.fromUserHash(ctx.userHash); 30 | await userDb.saveConversation(input); 31 | return { success: true }; 32 | }), 33 | updateAll: procedure 34 | .input(ConversationSchemaArray) 35 | .mutation(async ({ ctx, input }) => { 36 | const userDb = await UserDb.fromUserHash(ctx.userHash); 37 | await userDb.saveConversations(input); 38 | return { success: true }; 39 | }), 40 | }); 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | **Welcome to Chatbot UI!** 4 | 5 | We appreciate your interest in contributing to our project. 6 | 7 | Before you get started, please read our guidelines for contributing. 8 | 9 | ## Types of Contributions 10 | 11 | We welcome the following types of contributions: 12 | 13 | - Bug fixes 14 | - New features 15 | - Documentation improvements 16 | - Code optimizations 17 | - Translations 18 | - Tests 19 | 20 | ## Getting Started 21 | 22 | To get started, fork the project on GitHub and clone it locally on your machine. Then, create a new branch to work on your changes. 23 | 24 | ``` 25 | git clone https://github.com/mckaywrigley/chatbot-ui.git 26 | cd chatbot-ui 27 | git checkout -b my-branch-name 28 | 29 | ``` 30 | 31 | Before submitting your pull request, please make sure your changes pass our automated tests and adhere to our code style guidelines. 32 | 33 | ## Pull Request Process 34 | 35 | 1. Fork the project on GitHub. 36 | 2. Clone your forked repository locally on your machine. 37 | 3. Create a new branch from the main branch. 38 | 4. Make your changes on the new branch. 39 | 5. Ensure that your changes adhere to our code style guidelines and pass our automated tests. 40 | 6. Commit your changes and push them to your forked repository. 41 | 7. Submit a pull request to the main branch of the main repository. 42 | 43 | ## Contact 44 | 45 | If you have any questions or need help getting started, feel free to reach out to me on [Twitter](https://twitter.com/mckaywrigley). 46 | -------------------------------------------------------------------------------- /components/Sidebar/components/OpenCloseButton.tsx: -------------------------------------------------------------------------------- 1 | import { IconArrowBarLeft, IconArrowBarRight } from '@tabler/icons-react'; 2 | 3 | interface Props { 4 | onClick: any; 5 | side: 'left' | 'right'; 6 | } 7 | 8 | export const CloseSidebarButton = ({ onClick, side }: Props) => { 9 | return ( 10 | <> 11 | 21 |
25 | 26 | ); 27 | }; 28 | 29 | export const OpenSidebarButton = ({ onClick, side }: Props) => { 30 | return ( 31 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /types/chat.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from './agent'; 2 | import { OpenAIModelSchema } from './openai'; 3 | 4 | import * as z from 'zod'; 5 | 6 | export const RoleSchema = z.union([z.literal('system'), z.literal('assistant'), z.literal('user')]); 7 | 8 | export type Role = z.infer; 9 | 10 | export const MessageSchema = z.object({ 11 | role: RoleSchema, 12 | content: z.string(), 13 | }); 14 | 15 | export type Message = z.infer; 16 | 17 | export const ChatBodySchema = z.object({ 18 | model: OpenAIModelSchema, 19 | messages: z.array(MessageSchema), 20 | key: z.string(), 21 | prompt: z.string(), 22 | temperature: z.number(), 23 | googleAPIKey: z.string().optional(), 24 | googleCSEId: z.string().optional(), 25 | }); 26 | 27 | export type ChatBody = z.infer; 28 | 29 | export interface ChatModeRunner { 30 | run: (params: ChatModeRunnerParams) => void; 31 | } 32 | 33 | export interface ChatModeRunnerParams { 34 | body: ChatBody; 35 | message: Message; 36 | conversation: Conversation; 37 | selectedConversation: Conversation; 38 | plugins: Plugin[]; 39 | } 40 | 41 | export const ConversationSchema = z.object({ 42 | id: z.string(), 43 | name: z.string(), 44 | messages: z.array(MessageSchema), 45 | model: OpenAIModelSchema, 46 | prompt: z.string(), 47 | temperature: z.number(), 48 | folderId: z.string().nullable(), 49 | }); 50 | 51 | export const ConversationSchemaArray = z.array(ConversationSchema); 52 | 53 | export type Conversation = z.infer; 54 | -------------------------------------------------------------------------------- /types/chatmode.ts: -------------------------------------------------------------------------------- 1 | import { KeyValuePair } from './data'; 2 | 3 | export interface ChatMode { 4 | id: ChatModeID; 5 | name: ChatModeName; 6 | requiredKeys: KeyValuePair[]; 7 | } 8 | 9 | export interface ChatModeKey { 10 | chatModeId: ChatModeID; 11 | requiredKeys: KeyValuePair[]; 12 | } 13 | 14 | export enum ChatModeID { 15 | DIRECT = 'direct', 16 | AGENT = 'agent', 17 | CONVERSATIONAL_AGENT = 'conversational-agent', 18 | GOOGLE_SEARCH = 'google-search', 19 | } 20 | 21 | export enum ChatModeName { 22 | DIRECT = 'Chat', 23 | AGENT = 'Agent', 24 | CONVERSATIONAL_AGENT = 'Conversational Agent', 25 | GOOGLE_SEARCH = 'Google Search', 26 | } 27 | 28 | export const ChatModes: Record = { 29 | [ChatModeID.DIRECT]: { 30 | id: ChatModeID.DIRECT, 31 | name: ChatModeName.DIRECT, 32 | requiredKeys: [], 33 | }, 34 | [ChatModeID.AGENT]: { 35 | id: ChatModeID.AGENT, 36 | name: ChatModeName.AGENT, 37 | requiredKeys: [], 38 | }, 39 | [ChatModeID.CONVERSATIONAL_AGENT]: { 40 | id: ChatModeID.CONVERSATIONAL_AGENT, 41 | name: ChatModeName.CONVERSATIONAL_AGENT, 42 | requiredKeys: [], 43 | }, 44 | [ChatModeID.GOOGLE_SEARCH]: { 45 | id: ChatModeID.GOOGLE_SEARCH, 46 | name: ChatModeName.GOOGLE_SEARCH, 47 | requiredKeys: [ 48 | { 49 | key: 'GOOGLE_API_KEY', 50 | value: '', 51 | }, 52 | { 53 | key: 'GOOGLE_CSE_ID', 54 | value: '', 55 | }, 56 | ], 57 | }, 58 | }; 59 | 60 | export const ChatModeList = Object.values(ChatModes); 61 | -------------------------------------------------------------------------------- /utils/server/webpage.test.ts: -------------------------------------------------------------------------------- 1 | import { extractUrl } from './webpage'; 2 | 3 | import { describe, expect, it } from 'vitest'; 4 | 5 | describe('extractUrl', () => { 6 | it('returns null when passed an invalid URL', () => { 7 | expect(extractUrl('not a url')).toBeNull(); 8 | }); 9 | 10 | it('returns the given URL when passed a valid URL', () => { 11 | expect(extractUrl('https://google.com')).toBe('https://google.com'); 12 | }); 13 | 14 | it('returns the given URL when passed a valid URL with query', () => { 15 | expect(extractUrl('https://google.com/?q=hoge&page=8')).toBe( 16 | 'https://google.com/?q=hoge&page=8', 17 | ); 18 | }); 19 | 20 | it('ignores garbage before and after the URL', () => { 21 | expect(extractUrl('blah blah https://google.com/?q=test blah')).toBe( 22 | 'https://google.com/?q=test', 23 | ); 24 | }); 25 | 26 | it('matches URLs with different protocols (http and https)', () => { 27 | expect(extractUrl('http://test.com')).toBe('http://test.com'); 28 | expect(extractUrl('https://test.com')).toBe('https://test.com'); 29 | }); 30 | 31 | it('matches URLs with different subdomains', () => { 32 | expect(extractUrl('https://www.google.com')).toBe('https://www.google.com'); 33 | expect(extractUrl('https://drive.google.com')).toBe( 34 | 'https://drive.google.com', 35 | ); 36 | }); 37 | 38 | it('matches URLs with ports and paths', () => { 39 | expect(extractUrl('https://test.com:8000/path/to/resource')).toBe( 40 | 'https://test.com:8000/path/to/resource', 41 | ); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /.env.local.example: -------------------------------------------------------------------------------- 1 | # Chatbot UI 2 | DEFAULT_MODEL=gpt-3.5-turbo 3 | OPENAI_API_KEY=YOUR_KEY 4 | NEXT_PUBLIC_DEFAULT_SYSTEM_PROMPT="You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown." 5 | 6 | # Specify url to a json file that list available plugins. 7 | # When specifying multiple URLs, separate them with commas. 8 | # Example json file: `/public/plugin.js` 9 | PLUGINS_JSON_URLS=http://localhost:3000/plugins.json 10 | PLUGINS_INTERNAL=wikipedia_search,google_search 11 | # To enable python interpreter, specify codeapi endpoint to `PYTHON_INTERPRETER_BACKEND` and add 'python_interpreter' to PLUGINS_INTERNAL 12 | # codeapi server is here: 13 | # https://github.com/dotneet/codeapi 14 | PYTHON_INTERPRETER_BACKEND= 15 | 16 | # for Google Plugin 17 | GOOGLE_API_KEY= 18 | GOOGLE_CSE_ID= 19 | 20 | # MongoDB URI for Local 21 | MONGODB_URI=mongodb://root:example@127.0.0.1:27017/ 22 | MONGODB_DB=chatui 23 | # MongoDB URI for docker-compose 24 | # MONGODB_URI=mongodb://root:example@mongo:27017/ 25 | # MONGO_INITDB_ROOT_USERNAME=root 26 | # MONGO_INITDB_ROOT_PASSWORD=example 27 | 28 | # NextAuth 29 | NEXTAUTH_ENABLED=false 30 | NEXTAUTH_EMAIL_PATTERN=.+@example.com 31 | NEXTAUTH_URL=http://localhost:3000 32 | # must replace if you use NextAuth 33 | NEXTAUTH_SECRET=dummy 34 | NEXTAUTH_SESSION_MAX_AGE=86400 35 | GITHUB_CLIENT_ID= 36 | GITHUB_CLIENT_SECRET= 37 | GOOGLE_CLIENT_ID= 38 | GOOGLE_CLIENT_SECRET= 39 | COGNITO_CLIENT_ID= 40 | COGNITO_CLIENT_SECRET= 41 | COGNITO_ISSUER= 42 | 43 | # Audit Log 44 | AUDIT_LOG_ENABLED=false 45 | 46 | # For Debugging 47 | DEBUG_AGENT_LLM_LOGGING=true 48 | -------------------------------------------------------------------------------- /agent/plugins/wikipedia.ts: -------------------------------------------------------------------------------- 1 | import { Action, Plugin } from '@/types/agent'; 2 | import { GoogleSource } from '@/types/google'; 3 | 4 | import { TaskExecutionContext } from './executor'; 5 | 6 | const ALLOWED_LOCALES = [ 7 | 'en', 8 | 'ja', 9 | 'fr', 10 | 'de', 11 | 'es', 12 | 'ru', 13 | 'pt', 14 | 'zh', 15 | 'it', 16 | 'ar', 17 | 'pl', 18 | 'uk', 19 | ]; 20 | 21 | export default { 22 | nameForModel: 'wikipedia_search', 23 | nameForHuman: 'Wikipedia Search', 24 | descriptionForHuman: 'useful for when you need to ask wikipedia.', 25 | descriptionForModel: 'useful for when you need to ask wikipedia.', 26 | displayForUser: true, 27 | execute: async ( 28 | context: TaskExecutionContext, 29 | action: Action, 30 | ): Promise => { 31 | const query = action.pluginInput; 32 | const locale = context.locale; 33 | if (ALLOWED_LOCALES.indexOf(locale) === -1) { 34 | throw new Error('Unsupported locale: ' + locale); 35 | } 36 | const encodedQuery = encodeURIComponent(query); 37 | const uri = `http://${locale}.wikipedia.org/w/api.php?format=json&action=query&list=search&prop=revisions&rvprop=content&srsearch=${encodedQuery}`; 38 | const response = await fetch(uri); 39 | const result = await response.json(); 40 | if (result.query.search.length === 0) { 41 | return 'No Result'; 42 | } 43 | const texts = []; 44 | for (let key in result.query.search) { 45 | if (result.query.search[key].missing !== undefined) { 46 | continue; 47 | } 48 | const text = result.query.search[key].snippet; 49 | texts.push(text); 50 | } 51 | return texts.join('\n').slice(2000) || 'No Result'; 52 | }, 53 | } as Plugin; 54 | -------------------------------------------------------------------------------- /components/Input/InputText.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | 3 | interface Props { 4 | inputRef?: React.RefObject; 5 | isEditable?: boolean 6 | } 7 | export const InputText = ({ 8 | inputRef, 9 | onChange, 10 | onKeyDown, 11 | isEditable = true, 12 | ...restProps 13 | }: Props & React.InputHTMLAttributes) => { 14 | const [isTyping, setIsTyping] = useState(false); 15 | const [lastDownKey, setLastDownKey] = useState(''); 16 | const [endComposing, setEndComposing] = useState(false); 17 | 18 | const handleChange = useCallback( 19 | (e: React.ChangeEvent) => { 20 | onChange?.call(e, e); 21 | }, 22 | [onChange], 23 | ); 24 | 25 | const handleKeyDown = useCallback( 26 | (e: React.KeyboardEvent) => { 27 | // safari support 28 | const composing = endComposing; 29 | setLastDownKey(e.key); 30 | setEndComposing(false); 31 | if (e.key === 'Enter' && composing) { 32 | return; 33 | } 34 | if (isTyping || e.key !== 'Enter') { 35 | return; 36 | } 37 | onKeyDown?.call(e, e); 38 | }, 39 | [endComposing, isTyping, onKeyDown], 40 | ); 41 | 42 | return ( 43 | { 47 | setIsTyping(true); 48 | setEndComposing(true); 49 | }} 50 | onCompositionEnd={() => { 51 | setIsTyping(false); 52 | if (lastDownKey !== 'Enter') { 53 | setEndComposing(true); 54 | } 55 | }} 56 | readOnly={!isEditable} 57 | onChange={handleChange} 58 | onKeyDown={handleKeyDown} 59 | {...restProps} 60 | /> 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /pages/api/runplugin.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | import { ensureHasValidSession, getUserHash } from '@/utils/server/auth'; 4 | 5 | import { PluginResult, RunPluginRequest } from '@/types/agent'; 6 | 7 | import { createContext, executeTool } from '@/agent/plugins/executor'; 8 | import path from 'node:path'; 9 | import { getErrorResponseBody } from '@/utils/server/error'; 10 | import { verifyUserLlmUsage } from '@/utils/server/llmUsage'; 11 | 12 | const handler = async (req: NextApiRequest, res: NextApiResponse) => { 13 | // Vercel Hack 14 | // https://github.com/orgs/vercel/discussions/1278 15 | // eslint-disable-next-line no-unused-vars 16 | const vercelFunctionHack = path.resolve('./public', ''); 17 | 18 | if (!(await ensureHasValidSession(req, res))) { 19 | return res.status(401).json({ error: 'Unauthorized' }); 20 | } 21 | const userId = await getUserHash(req, res); 22 | 23 | try { 24 | const { 25 | taskId, 26 | model, 27 | action: toolAction, 28 | } = (await req.body) as RunPluginRequest; 29 | try { 30 | await verifyUserLlmUsage(userId, model.id); 31 | } catch (e: any) { 32 | return res.status(429).json({ error: e.message }); 33 | } 34 | 35 | const verbose = process.env.DEBUG_AGENT_LLM_LOGGING === 'true'; 36 | const context = await createContext(taskId, req, res, model, verbose); 37 | const toolResult = await executeTool(context, toolAction); 38 | const result: PluginResult = { 39 | action: toolAction, 40 | result: toolResult, 41 | }; 42 | res.status(200).json(result); 43 | } catch (error) { 44 | console.error(error); 45 | const errorRes = getErrorResponseBody(error); 46 | res.status(500).json(errorRes); 47 | } 48 | }; 49 | 50 | export default handler; 51 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Run Program", 8 | "skipFiles": ["/**"], 9 | "program": "${file}" 10 | }, 11 | { 12 | "name": "Next.js: debug server-side", 13 | "type": "node-terminal", 14 | "request": "launch", 15 | "command": "npm run dev" 16 | }, 17 | { 18 | "name": "Next.js: debug client-side", 19 | "type": "chrome", 20 | "request": "launch", 21 | "url": "http://localhost:3000" 22 | }, 23 | { 24 | "name": "Next.js: debug full stack", 25 | "type": "node-terminal", 26 | "request": "launch", 27 | "command": "npm run dev", 28 | "serverReadyAction": { 29 | "pattern": "started server on .+, url: (https?://.+)", 30 | "uriFormat": "%s", 31 | "action": "debugWithChrome" 32 | } 33 | }, 34 | { 35 | "name": "Run TypeScript file", 36 | "type": "node", 37 | "request": "launch", 38 | "args": ["${relativeFile}"], 39 | "runtimeArgs": ["--require", "ts-node/register"], 40 | "cwd": "${workspaceFolder}", 41 | "protocol": "inspector", 42 | "console": "integratedTerminal", 43 | "internalConsoleOptions": "neverOpen", 44 | "sourceMaps": true 45 | }, 46 | { 47 | "type": "node", 48 | "request": "launch", 49 | "name": "Debug Current Test File", 50 | "autoAttachChildProcesses": true, 51 | "skipFiles": ["/**", "**/node_modules/**"], 52 | "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", 53 | "args": ["run", "${relativeFile}"], 54 | "smartStep": true, 55 | "console": "integratedTerminal" 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /server/routers/publicPrompts.ts: -------------------------------------------------------------------------------- 1 | import { PublicPromptsDb, UserDb, getDb } from '@/utils/server/storage'; 2 | 3 | import { PromptSchema } from '@/types/prompt'; 4 | 5 | import { procedure, router } from '../trpc'; 6 | 7 | import { z } from 'zod'; 8 | import { TRPCError } from '@trpc/server'; 9 | import { Context, isAdminUser } from '../context'; 10 | 11 | export const publicPrompts = router({ 12 | list: procedure.query(async ({ ctx }) => { 13 | const publicPromptsDb = new PublicPromptsDb(await getDb()); 14 | return await publicPromptsDb.getPrompts(); 15 | }), 16 | add: procedure.input(PromptSchema).mutation(async ({ ctx, input }) => { 17 | const userDb = await UserDb.fromUserHash(ctx.userHash); 18 | await userDb.publishPrompt(input); 19 | return { success: true }; 20 | }), 21 | remove: procedure 22 | .input(z.object({ id: z.string() })) 23 | .mutation(async ({ ctx, input }) => { 24 | const publicPromptsDb = new PublicPromptsDb(await getDb()); 25 | await validateOwnerOrAdminAccess(input.id, ctx); 26 | await publicPromptsDb.removePrompt(input.id); 27 | return { success: true }; 28 | }), 29 | update: procedure.input(PromptSchema).mutation(async ({ ctx, input }) => { 30 | const publicPromptsDb = new PublicPromptsDb(await getDb()); 31 | await validateOwnerOrAdminAccess(input.id, ctx); 32 | await publicPromptsDb.savePrompt(input); 33 | return { success: true }; 34 | }) 35 | }); 36 | 37 | async function validateOwnerOrAdminAccess(promptId: string, ctx: Context) { 38 | if (!isAdminUser(ctx)) { 39 | const publicPromptsDb = new PublicPromptsDb(await getDb()); 40 | const prompt = await publicPromptsDb.getPrompt(promptId); 41 | if (!prompt || prompt.userId != ctx.userHash!) { 42 | throw new TRPCError({ code: 'UNAUTHORIZED' }); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /components/Chatbar/components/ClearConversations.tsx: -------------------------------------------------------------------------------- 1 | import { IconCheck, IconTrash, IconX } from '@tabler/icons-react'; 2 | import { FC, useState } from 'react'; 3 | 4 | import { useTranslation } from 'next-i18next'; 5 | 6 | import { SidebarButton } from '@/components/Sidebar/SidebarButton'; 7 | 8 | interface Props { 9 | onClearConversations: () => void; 10 | } 11 | 12 | export const ClearConversations: FC = ({ onClearConversations }) => { 13 | const [isConfirming, setIsConfirming] = useState(false); 14 | 15 | const { t } = useTranslation('sidebar'); 16 | 17 | const handleClearConversations = () => { 18 | onClearConversations(); 19 | setIsConfirming(false); 20 | }; 21 | 22 | return isConfirming ? ( 23 |
24 | 25 | 26 |
27 | {t('Are you sure?')} 28 |
29 | 30 |
31 | { 35 | e.stopPropagation(); 36 | handleClearConversations(); 37 | }} 38 | /> 39 | 40 | { 44 | e.stopPropagation(); 45 | setIsConfirming(false); 46 | }} 47 | /> 48 |
49 |
50 | ) : ( 51 | } 54 | onClick={() => setIsConfirming(true)} 55 | /> 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /components/Input/Textarea.tsx: -------------------------------------------------------------------------------- 1 | import React, { DetailedHTMLProps, useCallback, useState } from 'react'; 2 | 3 | interface Props { 4 | textareaRef?: React.RefObject; 5 | rows?: number; 6 | isEditable?: boolean; 7 | } 8 | 9 | export const Textarea = ({ 10 | textareaRef, 11 | rows, 12 | onChange, 13 | onKeyDown, 14 | isEditable = true, 15 | ...restProps 16 | }: Props & 17 | DetailedHTMLProps< 18 | React.InputHTMLAttributes, 19 | HTMLTextAreaElement 20 | >) => { 21 | const [isTyping, setIsTyping] = useState(false); 22 | const [lastDownKey, setLastDownKey] = useState(''); 23 | const [endComposing, setEndComposing] = useState(false); 24 | 25 | const handleChange = useCallback( 26 | (e: React.ChangeEvent) => { 27 | onChange?.call(e, e); 28 | }, 29 | [onChange], 30 | ); 31 | 32 | const handleKeyDown = useCallback( 33 | (e: React.KeyboardEvent) => { 34 | // safari support 35 | const composing = endComposing; 36 | setLastDownKey(e.key); 37 | setEndComposing(false); 38 | if (e.key === 'Enter' && composing) { 39 | return; 40 | } 41 | if (isTyping) { 42 | return; 43 | } 44 | onKeyDown?.call(e, e); 45 | }, 46 | [endComposing, isTyping, onKeyDown], 47 | ); 48 | 49 | return ( 50 |