├── .env.global ├── .eslintrc.json ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── app ├── api │ ├── assistant │ │ ├── delete-file │ │ │ └── route.ts │ │ ├── delete │ │ │ └── route.ts │ │ ├── retrieve │ │ │ └── route.ts │ │ ├── update │ │ │ └── route.ts │ │ └── upload │ │ │ └── route.ts │ ├── auth │ │ └── [...nextauth] │ │ │ └── route.ts │ ├── chat │ │ └── route.ts │ ├── embed │ │ ├── conversation │ │ │ └── route.ts │ │ └── message │ │ │ └── route.ts │ ├── memory │ │ ├── append │ │ │ ├── nosql │ │ │ │ └── route.ts │ │ │ └── vector │ │ │ │ └── route.ts │ │ ├── augment │ │ │ ├── nosql │ │ │ │ └── route.ts │ │ │ └── vector │ │ │ │ └── route.ts │ │ ├── retrieve │ │ │ └── route.ts │ │ ├── update-metadata │ │ │ └── vector │ │ │ │ └── route.ts │ │ └── update │ │ │ └── route.ts │ ├── parse │ │ └── route.ts │ ├── rag │ │ ├── delete-file │ │ │ └── route.ts │ │ ├── retrieve │ │ │ └── route.ts │ │ ├── update-file-status │ │ │ └── route.ts │ │ ├── update │ │ │ └── route.ts │ │ ├── upload │ │ │ └── route.ts │ │ └── vector-db │ │ │ ├── delete-file │ │ │ └── route.ts │ │ │ ├── query │ │ │ └── route.ts │ │ │ └── upsert │ │ │ └── route.ts │ ├── speech │ │ ├── retrieve │ │ │ └── route.ts │ │ ├── stt │ │ │ └── route.ts │ │ ├── tts │ │ │ └── route.ts │ │ └── update │ │ │ └── route.ts │ └── vision │ │ ├── add-url │ │ └── route.ts │ │ ├── delete-url │ │ └── route.ts │ │ ├── retrieve │ │ └── route.ts │ │ └── update │ │ └── route.ts ├── components │ ├── AppBar │ │ ├── index.module.css │ │ └── index.tsx │ ├── Assistant │ │ ├── AssistantFileList.tsx │ │ ├── AssistantForm.tsx │ │ ├── ConfirmationDialog.tsx │ │ └── index.tsx │ ├── Chat │ │ ├── index.module.css │ │ └── index.tsx │ ├── CustomizedInputBase │ │ └── index.tsx │ ├── FileList │ │ └── index.tsx │ ├── Loader │ │ └── index.tsx │ ├── LongTermMemory │ │ ├── LongTermMemoryForm.tsx │ │ └── index.tsx │ ├── MessagesField │ │ ├── index.module.css │ │ └── index.tsx │ ├── Rag │ │ ├── RagFileList.tsx │ │ ├── RagForm.tsx │ │ └── index.tsx │ ├── Speech │ │ ├── stt │ │ │ └── index.tsx │ │ └── tts │ │ │ ├── SpeechForm.tsx │ │ │ └── index.tsx │ └── Vision │ │ ├── AddUrlDialog.tsx │ │ ├── VisionFileList.tsx │ │ └── index.tsx ├── favicon.ico ├── globals.css ├── hooks │ ├── index.ts │ ├── useChatForm.ts │ ├── useCustomInput.ts │ ├── useMessageProcessing.ts │ └── useRecordVoice.ts ├── layout.tsx ├── lib │ ├── client │ │ ├── mongodb.ts │ │ └── pinecone.ts │ └── utils │ │ ├── db.ts │ │ ├── response.ts │ │ └── vectorDb.ts ├── models │ ├── Conversation.ts │ ├── File.ts │ ├── Message.ts │ └── User.ts ├── page.module.css ├── page.tsx ├── providers.jsx ├── services │ ├── SpeechtoTextService.ts │ ├── assistantService.ts │ ├── chatService.ts │ ├── commonService.ts │ ├── embeddingService.ts │ ├── longTermMemoryService.ts │ ├── ragService.ts │ ├── textToSpeechService.ts │ ├── unstructuredService.ts │ ├── vectorDbService.ts │ └── visionService.ts └── utils │ └── persistentMemoryUtils.ts ├── next.config.js ├── package-lock.json ├── package.json ├── public ├── next.svg └── vercel.svg └── tsconfig.json /.env.global: -------------------------------------------------------------------------------- 1 | # openai 2 | OPENAI_API_KEY= 3 | OPENAI_API_MODEL= 4 | 5 | # next-auth 6 | GITHUB_ID= 7 | GITHUB_SECRET= 8 | GOOGLE_ID= 9 | GOOGLE_SECRET= 10 | NEXTAUTH_URL= 11 | NEXTAUTH_SECRET= 12 | 13 | # mongodb 14 | NODE_ENV= 15 | MONGODB_URI= 16 | 17 | # pinecone 18 | PINECONE_API= 19 | PINECONE_INDEX= 20 | PINECONE_DISABLE_RUNTIME_VALIDATIONS= 21 | 22 | # unstructured 23 | UNSTRUCTURED_API= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - develop 8 | pull_request: 9 | branches: 10 | - main 11 | - develop 12 | 13 | jobs: 14 | ci: 15 | runs-on: ${{ matrix.os }} 16 | 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest] 20 | node: [18] 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@master 25 | 26 | - name: Setup node env 27 | uses: actions/setup-node@v3 28 | with: 29 | node-version: ${{ matrix.node }} 30 | 31 | - name: Cache node_modules 32 | uses: actions/cache@v2 33 | with: 34 | path: ~/.npm 35 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 36 | restore-keys: | 37 | ${{ runner.os }}-node- 38 | 39 | - name: Install dependencies 40 | run: npm i 41 | 42 | - name: Run linting 43 | run: npm run lint 44 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # env files 29 | .env 30 | .env.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # history 40 | .history -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 | Athos Georgiou 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is Titanium? 2 | 3 | Titanium is a modern web application built with Next.js, leveraging the latest OpenAI APIs to offer an advanced Generative and Conversational AI experience. It's still pretty much a prototype, but I think it's a good start. Here's a list of some of the features: 4 | 5 | - Multi-user Authentication using next-auth, including a custom CredentialProvider for guest accounts.✅ 6 | - Customizable, Multipurpose Assistants with File Upload support. Also supports complete deletion of all Assistant related data.✅ 7 | - Vision via 'gpt-4-vision-preview'. Currently supports Image Analysis for multiple urls. File uploads may come later, but not a priority.✅ 8 | - Speech 9 | - Text to Speech (TTS), Supporting tts-1, tts-1-hd and all available voice models.✅ 10 | - Speech to Text (STT), available via button toggle in the input chat box.✅ 11 | - Retrieval Augmented Generation (RAG), Using advanced document parsing by Unstructured.io API and Pinecone Serverless for fast and efficient indexing & retrieval.✅ 12 | - Persistent multi-user memory.✅ 13 | - NoSQL Based.✅ 14 | - Vector Based.✅ 15 | 16 | # Libraries 17 | 18 | - Next.js is a React framework that allows you to build server-rendered applications. It is a complete full-stack solution that includes a variety of features, such as server-side rendering, static site generation, and API routes. 19 | - Next-auth + mongodb adapter for authentication 20 | - OpenAI API to leverage the latest Generative and Conversational capabilities of OpenAI's GPT-4. [OpenAI's API](https://platform.openai.com/docs/api-reference). 21 | - Material UI for the UI components. 22 | - MongoDB Atlas for user data and state management. 23 | - Unstructured.io API for advanced document parsing. 24 | - Pinecone Serverless for advanced Semantic Search. 25 | - Vercel for deployment. 26 | 27 | # Setting Up Your Development Environment 28 | 29 | ## OpenAI 30 | 31 | 1. Go to [OpenAI](https://beta.openai.com/signup/) and create a new account or sign in to your existing account. 32 | 2. Navigate to the API section. 33 | 3. You'll see a key listed under "API Keys". This is your OpenAI API key. 34 | 4. Add this key to your `.env.local` file: 35 | 36 | ```env 37 | OPENAI_API_KEY= 38 | OPENAI_API_MODEL="gpt-4-turbo-preview" // This model is required for using the latest beta features, such as assistants. 39 | ``` 40 | 41 | ## MongoDB Atlas 42 | 43 | 1. Go to [MongoDB Atlas](https://www.mongodb.com/cloud/atlas) and create a free account or sign in to your existing account. 44 | 2. Click on "Create a New Cluster". Select the free tier and choose the options that best suit your needs. 45 | 3. Wait for the cluster to be created. This can take a few minutes. 46 | 4. Once the cluster is created, click on "CONNECT". 47 | 5. Add a new database user. Remember the username and password, you will need them to connect to the database. 48 | 6. Add your IP address to the IP Whitelist. If you're not sure what your IP address is, you can select "Allow Access from Anywhere", but be aware that this is less secure. 49 | 7. Choose "Connect your application". Select "Node.js" as your driver and copy the connection string. 50 | 8. Replace `` in the connection string with the password of the database user you created earlier. Also replace `` with the name of the database you want to connect to. 51 | 9. In your `.env.local` file, set `MONGODB_URI` to the connection string you just created: 52 | 53 | ## GitHub and Google Credentials for NextAuth 54 | 55 | NextAuth requires credentials for the authentication providers you want to use. You can find instructions on how to set up credentials for each provider below. If you wish to skip this step you can use the Custom Credential Provider to login with guest account, but keep in mind that upon logout, you will lose access to your assistant and all related data. 56 | 57 | ### GitHub 58 | 59 | 1. Go to [GitHub Developer Settings](https://github.com/settings/developers) and click on "New OAuth App". 60 | 2. Fill in the "Application name", "Homepage URL" and "Application description" as you see fit. 61 | 3. For the "Authorization callback URL", enter `http://localhost:3000/api/auth/callback/github` (replace `http://localhost:3000` with your deployment URL if you're deploying your app). 62 | 4. Click on "Register application". 63 | 5. You'll now see a "Client ID" and a "Client Secret". Add these to your `.env.local` file: 64 | 65 | ```env 66 | GITHUB_ID= 67 | GITHUB_SECRET= 68 | ``` 69 | 70 | ### Google 71 | 72 | 1. Go to [Google Cloud Console](https://console.cloud.google.com/) and create a new project. 73 | 2. Search for "OAuth consent screen" and fill in the required fields. 74 | 3. Go to "Credentials", click on "Create Credentials" and choose "OAuth client ID". 75 | 4. Choose "Web application", enter a name for your credentials and under "Authorized redirect URIs" enter `http://localhost:3000/api/auth/callback/google` (replace `http://localhost:3000` with your deployment URL if you're deploying your app). 76 | 5. Click on "Create". 77 | 6. You'll now see a "Client ID" and a "Client Secret". Add these to your `.env.local` file: 78 | 79 | ```env 80 | GOOGLE_ID= 81 | GOOGLE_SECRET= 82 | ``` 83 | 84 | ## Unstructured.io 85 | 86 | Go to `https://unstructured.io/api-key-free` and sign up for a free account. You will receive an API key in your email. Add this key to your `.env.local` file: 87 | 88 | ```env 89 | UNSTRUCTURED_API= 90 | ``` 91 | 92 | ## Pinecone Serverless 93 | 94 | Go to `https://www.pinecone.io/` and sign up for a free account. You will receive an API key in your email. When creating your index, make sure to select the Serverless Option and the "ada-002" template. Add this key to your `.env.local` file: 95 | 96 | ```env 97 | PINECONE_API='your-pinecone-api-key' 98 | PINECONE_INDEX='your-pinecone-index-name' 99 | PINECONE_DISABLE_RUNTIME_VALIDATIONS='false' 100 | ``` 101 | 102 | ## Other Credentials 103 | 104 | ```env 105 | NEXTAUTH_URL=http://localhost:3000 106 | NEXTAUTH_SECRET= // Generate a secret using `openssl rand -base64 32` 107 | NODE_ENV='development' 108 | ``` 109 | 110 | # Running the App locally 111 | 112 | Start the development server with the following command: 113 | 114 | ```bash 115 | npm run dev 116 | # or 117 | yarn dev 118 | # or 119 | pnpm dev 120 | # or 121 | bun dev 122 | ``` 123 | 124 | Then, open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 125 | ![image](https://github.com/athrael-soju/Titanium/assets/25455658/7ec9bd34-3aec-4a48-8584-aeee0fd34a15) 126 | 127 | # Logging in 128 | 129 | If you set up credentials for GitHub and/or Google, you can use those to log in. If you didn't set up credentials, you can use the Custom Credential Provider to log in with a guest account. This can be done by clicking on the avatar icon on the top right corner of the screen. 130 | 131 | Keep in mind that upon logout from a guest account, you will lose access to your assistant and all related data. 132 | ![image-1](https://github.com/athrael-soju/Titanium/assets/25455658/1dedc1e1-6250-4302-b9f3-aca27e88a206) 133 | 134 | # Features 135 | 136 | To access the features, you can click the hamburger icon at the left of the inpupt box. A menu will pop up, allowing you to make a selection. Available features are: 137 | 138 | - R.A.G. (Retrieval Augmented Generation) 139 | - Assistant 140 | - Vision 141 | - Speech 142 | 143 | ## Streaming Chat 144 | 145 | ![image](https://github.com/athrael-soju/Titanium/assets/25455658/541e228a-d1ec-4a85-b55c-202f55f24a80) 146 | 147 | Streaming Chat is the default mode of chat. It's a simple chat interface that allows you to chat with the AI in a seamless manner. All you have to do is type your message and press enter. The AI will respond with a message of its own. 148 | 149 | ## R.A.G. (Retrieval Augmented Generation) 150 | 151 | ![image](https://github.com/athrael-soju/Titanium/assets/25455658/2337ea2c-083b-4d1d-85dd-e1edca25f9aa) 152 | 153 | R.A.G, or Retrieval Augmented Generation, is an advanced feature that allows you to query the AI with a document. The AI will then use the information in the document to generate a response. To use R.A.G, you can click the "R.A.G" button at the left of the input box. A menu will pop up showing: 154 | 155 | - Top K: The number of documents to return using semantic search. More documents will give the AI more information to work with, but it will also take longer to process. 156 | - Batch Size: The number of documents to process at once. Due to limitations on how much data can be upserted to the Vector Index at once, a large number of documents maybe rejected, so a default of 250 can be used as a default. 157 | - Parsing Strategy: The strategy to use for parsing the document. Each of the options has its own strengths and weaknesses, so you may need to experiment to find the best one for your use case. If you care more for the quality of the parsed data, you can use the "Hi Res" option. If you care more for the speed of the parsing, you can use the "Fast" option. Otherwise, "auto" will work fine. 158 | 159 | ## Assistant 160 | 161 | ![image](https://github.com/athrael-soju/Titanium/assets/25455658/6909c5a9-115b-436e-85ea-8434d35a92e8) 162 | 163 | The Assistant is an OpenAI feature that allows you to create and manage your own AI assistant. 164 | You can: 165 | 166 | - Specify a Name, 167 | - Set a description, which the Assistant will abide to. 168 | - Upload files, which the AI will use to generate responses. 169 | - Delete the assistant and all associated files by pressing "DELETE" and confirming in a followup dialog. This is non reversible. 170 | 171 | The Assistant is a handy tool, which comes with R.A.G. and Long term memory, as well as capabilities to interpret and generate code. However, it does not yet support streaming chat and costs more to use. 172 | 173 | ## Vision 174 | 175 | ![image](https://github.com/athrael-soju/Titanium/assets/25455658/e295197b-53d8-4225-9145-5b94b7079fb4) 176 | 177 | Vision will allow a user to add a URL to an image and the AI will analyze the image and provide a response. The response will include a description of the image, as well as any other relevant information, including numerical data. At this point, the feature does not support file uploads, but this may be added in the future. 178 | 179 | ## Speech 180 | 181 | ![image](https://github.com/athrael-soju/Titanium/assets/25455658/c67a8fa4-f0d4-42ef-8707-773f24028d06) 182 | 183 | Speech comes in two parts: Text to Speech (TTS) and Speech to Text (STT). TTS will allow the AI to generate speech from text, while STT will allow the AI to transcribe speech to text. To use TTS, you can select a model and voice from the dropdown menu. To use STT, you can click the microphone icon at the very left of the input box to record a message. Once done, you can click the microphone icon again to stop recording. The AI will then transcribe the message and you can send it to the AI by pressing enter. 184 | 185 | # Feature Combinations 186 | 187 | Some features can play really well together. For example: 188 | 189 | - All features, except the Assistant are used in conjunction with the Streaming Chat feature. This means that you can use R.A.G., Vision, and Speech in the chat, allowing you to query the AI with documents, images, and speech, respectively and receive a response in real time. 190 | - Speech can be used in conjunction with any other feature, allowing you to speak to the AI instead of typing. This can be especially useful if you're on the go or if you have a disability that makes typing difficult. 191 | - Vision, combined with Text to Speech, as well as Speech to Text, can be a great tool when accessibility is a concern. For example, you can use Vision to analyze an image, verbally ask the AI a question about the image, and then have the AI respond with a verbal answer, as well. 192 | 193 | # Feature Limitations 194 | 195 | Since some features have contradicting functionalities, when one is enabled, others will be disabled. So, enabling either the Assistant, R.A.G., or Vision will disable the others. This is because each of these features involve uploading files, which they use to draw information from. 196 | 197 | # General Notes 198 | 199 | - Disabling all features will revert chat to the default streaming chat, without any context or memory. 200 | - When making changes to the features, you'll need to click "UPDATE" to save any changes. These changes will persist even after you log out, for your respective account. 201 | - When deleting files with R.A.G. they will also be deleted from the Pinecone Index. This is to support data privacy and security. The same applies to the Assistant, where all files will be deleted from the OpenAI server. 202 | 203 | # Deploy on Vercel 204 | 205 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 206 | 207 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 208 | 209 | # That's all folks! 210 | 211 | Do you want to collaborate, or have suggestions for improvement? You can reach me on: 212 | 213 | - [LinkedIn](https://www.linkedin.com/in/athosg/) 214 | - [Professional Website](https://athosgeorgiou.com/) 215 | - [Tech Blog](https://athrael.net/) 216 | 217 | ### Happy coding! 218 | -------------------------------------------------------------------------------- /app/api/assistant/delete-file/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import OpenAI from 'openai'; 3 | import { sendErrorResponse } from '@/app/lib/utils/response'; 4 | 5 | const openai = new OpenAI(); 6 | 7 | interface DeleteFileRequest { 8 | file: { 9 | id: string; 10 | assistantId: string; 11 | }; 12 | } 13 | 14 | export async function POST(req: NextRequest): Promise { 15 | const requestBody = await req.json(); 16 | const { file } = requestBody as DeleteFileRequest; 17 | 18 | try { 19 | const assistantFileDeletionResponse = 20 | await openai.beta.assistants.files.del(file.assistantId, file.id); 21 | const openaiFileDeletionResponse = await openai.files.del(file.id); 22 | 23 | return NextResponse.json({ 24 | message: 'File deleted successfully', 25 | assistantFileDeletionResponse, 26 | openaiFileDeletionResponse, 27 | status: 200, 28 | }); 29 | } catch (error: any) { 30 | console.error('Assistant file deletion unsuccessful: ', error); 31 | return sendErrorResponse('Assistant file deletion unsuccessful', 500); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/api/assistant/delete/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getDb, getUserByEmail } from '@/app/lib/utils/db'; 3 | import { sendErrorResponse } from '@/app/lib/utils/response'; 4 | import OpenAI from 'openai'; 5 | import { Collection } from 'mongodb'; 6 | 7 | const openai = new OpenAI(); 8 | export async function POST(req: NextRequest): Promise { 9 | const { userEmail } = await req.json(); 10 | 11 | try { 12 | const db = await getDb(); 13 | const usersCollection = db.collection('users'); 14 | const user = await getUserByEmail(usersCollection, userEmail); 15 | 16 | if (!user) { 17 | return sendErrorResponse('User not found', 404); 18 | } 19 | 20 | if (user.assistantId) { 21 | await deleteUserAssistant(user.assistantId, userEmail, usersCollection); 22 | return NextResponse.json({ 23 | message: 'Assistant deleted (With all associated files)', 24 | status: 200, 25 | }); 26 | } 27 | 28 | return sendErrorResponse('No assistant found for the user', 404); 29 | } catch (error: any) { 30 | return sendErrorResponse('Assistant deletion unsuccessful', 400); 31 | } 32 | } 33 | 34 | async function deleteUserAssistant( 35 | assistantId: string, 36 | userEmail: string, 37 | usersCollection: Collection 38 | ): Promise { 39 | const assistantFiles = await openai.beta.assistants.files.list(assistantId); 40 | for (const file of assistantFiles.data) { 41 | await openai.beta.assistants.files.del(assistantId, file.id); 42 | await openai.files.del(file.id); 43 | } 44 | await openai.beta.assistants.del(assistantId); 45 | await usersCollection.updateOne( 46 | { email: userEmail }, 47 | { $set: { assistantId: null, threadId: null, isAssistantEnabled: false } } 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /app/api/assistant/retrieve/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getDatabaseAndUser, getDb } from '@/app/lib/utils/db'; 3 | import { sendErrorResponse } from '@/app/lib/utils/response'; 4 | import OpenAI from 'openai'; 5 | 6 | const openai = new OpenAI(); 7 | 8 | export const dynamic = 'force-dynamic' 9 | 10 | export async function GET(req: NextRequest): Promise { 11 | try { 12 | const db = await getDb(); 13 | const userEmail = req.headers.get('userEmail') as string; 14 | const serviceName = req.headers.get('serviceName'); 15 | const { user } = await getDatabaseAndUser(db, userEmail); 16 | 17 | if (serviceName === 'assistant' && user.assistantId) { 18 | const [assistant, thread, fileList] = await Promise.all([ 19 | openai.beta.assistants.retrieve(user.assistantId), 20 | openai.beta.threads.retrieve(user.threadId as string), 21 | openai.beta.assistants.files.list(user.assistantId), 22 | ]); 23 | 24 | const filesWithNames = await Promise.all( 25 | fileList.data.map(async (fileObject) => { 26 | const file = await openai.files.retrieve(fileObject.id); 27 | return { 28 | id: fileObject.id, 29 | name: file.filename, 30 | assistantId: user.assistantId, 31 | }; 32 | }) 33 | ); 34 | 35 | return NextResponse.json({ 36 | message: 'Assistant retrieved', 37 | assistant, 38 | threadId: thread?.id, 39 | fileList: filesWithNames, 40 | isAssistantEnabled: user.isAssistantEnabled, 41 | status: 200, 42 | }); 43 | } 44 | 45 | return NextResponse.json({ 46 | message: 'Assistant cannot be retrieved, as it has not been created', 47 | status: 200, 48 | }); 49 | } catch (error: any) { 50 | console.error('Assistant retrieval unsuccessful', error); 51 | return sendErrorResponse('Assistant retrieval unsuccessful', 400); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/api/assistant/update/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import clientPromise from '../../../lib/client/mongodb'; 3 | import OpenAI from 'openai'; 4 | import { Collection } from 'mongodb'; 5 | 6 | const openai = new OpenAI(); 7 | 8 | import { sendErrorResponse } from '@/app/lib/utils/response'; 9 | 10 | interface AssistantUpdateRequest { 11 | userEmail: string; 12 | name: string; 13 | description: string; 14 | isAssistantEnabled: boolean; 15 | files: { name: string; id: string; assistantId: string }[]; 16 | } 17 | 18 | export async function POST(req: NextRequest): Promise { 19 | try { 20 | const client = await clientPromise; 21 | const db = client.db(); 22 | const requestBody = await req.json(); 23 | const { userEmail, name, description, isAssistantEnabled, files } = 24 | requestBody as AssistantUpdateRequest; 25 | 26 | if ( 27 | !userEmail || 28 | !name || 29 | !description || 30 | isAssistantEnabled === undefined 31 | ) { 32 | return sendErrorResponse('Missing required parameters', 400); 33 | } 34 | 35 | const usersCollection = db.collection('users'); 36 | const user = await usersCollection.findOne({ email: userEmail }); 37 | 38 | if (!user) { 39 | return sendErrorResponse('User not found', 404); 40 | } 41 | 42 | const { assistant, thread } = await createOrUpdateAssistant( 43 | user, 44 | name, 45 | description, 46 | isAssistantEnabled, 47 | usersCollection, 48 | files 49 | ); 50 | 51 | return NextResponse.json({ 52 | message: 'Assistant updated', 53 | assistantId: assistant.id, 54 | threadId: thread.id, 55 | isAssistantEnabled, 56 | status: 200, 57 | }); 58 | } catch (error: any) { 59 | console.error('Error in assistant update: ', error); 60 | return sendErrorResponse('Error in assistant update', 400); 61 | } 62 | } 63 | 64 | async function createOrUpdateAssistant( 65 | user: IUser, 66 | name: string, 67 | description: string, 68 | isAssistantEnabled: boolean, 69 | usersCollection: Collection, 70 | files: { name: string; id: string; assistantId: string }[] 71 | ): Promise<{ assistant: any; thread: any }> { 72 | let assistant, thread; 73 | const isVisionEnabled = isAssistantEnabled ? false : user.isVisionEnabled; 74 | const isRagEnabled = isAssistantEnabled ? false : user.isRagEnabled; 75 | 76 | if (!user.assistantId) { 77 | // Create a new assistant and thread 78 | assistant = await openai.beta.assistants.create({ 79 | instructions: description, 80 | name: name, 81 | tools: [{ type: 'retrieval' }, { type: 'code_interpreter' }], 82 | model: process.env.OPENAI_API_MODEL as string, 83 | file_ids: files.map((file) => file.id), 84 | }); 85 | thread = await openai.beta.threads.create(); 86 | } else { 87 | // Update an existing assistant 88 | assistant = await openai.beta.assistants.update(user.assistantId, { 89 | instructions: description, 90 | name: name, 91 | tools: [{ type: 'retrieval' }, { type: 'code_interpreter' }], 92 | model: process.env.OPENAI_API_MODEL as string, 93 | file_ids: files.map((file) => file.id), 94 | }); 95 | thread = await openai.beta.threads.retrieve(user.threadId as string); 96 | } 97 | 98 | await usersCollection.updateOne( 99 | { email: user.email }, 100 | { 101 | $set: { 102 | assistantId: assistant.id, 103 | threadId: thread.id, 104 | isAssistantEnabled: isAssistantEnabled, 105 | isVisionEnabled: isVisionEnabled, 106 | isRagEnabled: isRagEnabled, 107 | }, 108 | } 109 | ); 110 | 111 | return { assistant, thread }; 112 | } 113 | -------------------------------------------------------------------------------- /app/api/assistant/upload/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getDb, getUserByEmail } from '@/app/lib/utils/db'; 3 | import { sendErrorResponse } from '@/app/lib/utils/response'; 4 | import fs from 'fs/promises'; 5 | import OpenAI from 'openai'; 6 | import { createReadStream } from 'fs'; 7 | import { tmpdir } from 'os'; 8 | import { join } from 'path'; 9 | 10 | const openai = new OpenAI(); 11 | 12 | interface FileUploadResponse { 13 | id: string; 14 | object: string; 15 | } 16 | 17 | export async function POST(request: NextRequest): Promise { 18 | const data = await request.formData(); 19 | const file = data.get('file') as unknown as File; 20 | const userEmail = data.get('userEmail') as string; 21 | 22 | try { 23 | const db = await getDb(); 24 | const usersCollection = db.collection('users'); 25 | const user = await getUserByEmail(usersCollection, userEmail); 26 | 27 | if (!user) { 28 | return sendErrorResponse('User not found', 404); 29 | } 30 | 31 | if (user.assistantId) { 32 | const fileResponse = await addFileToAssistant(user, file); 33 | return NextResponse.json({ 34 | message: 'File uploaded', 35 | file: fileResponse, 36 | status: 200, 37 | }); 38 | } else { 39 | return sendErrorResponse( 40 | 'Assistant must be created before files can be uploaded', 41 | 400 42 | ); 43 | } 44 | } catch (error: any) { 45 | console.error('Error processing file: ', error); 46 | return sendErrorResponse('Error processing file', 500); 47 | } 48 | } 49 | 50 | async function addFileToAssistant( 51 | user: IUser, 52 | file: File 53 | ): Promise { 54 | const tempFilePath = join(tmpdir(), file.name); 55 | const arrayBuffer = await file.arrayBuffer(); 56 | const buffer = Buffer.from(arrayBuffer); 57 | await fs.writeFile(tempFilePath, buffer); 58 | 59 | try { 60 | const fileStream = createReadStream(tempFilePath); 61 | const fileResponse = await openai.files.create({ 62 | file: fileStream, 63 | purpose: 'assistants', 64 | }); 65 | 66 | return await openai.beta.assistants.files.create( 67 | user.assistantId as string, 68 | { 69 | file_id: fileResponse.id, 70 | } 71 | ); 72 | } catch (error: any) { 73 | console.error('Error in file upload to assistant: ', error); 74 | throw new Error('File upload to assistant failed'); 75 | } finally { 76 | await fs.unlink(tempFilePath); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { 2 | Account, 3 | Profile, 4 | SessionStrategy, 5 | Session, 6 | User, 7 | } from 'next-auth'; 8 | import type { NextAuthOptions } from 'next-auth'; 9 | import { MongoDBAdapter } from '@auth/mongodb-adapter'; 10 | import { JWT } from 'next-auth/jwt'; 11 | import { AdapterUser } from 'next-auth/adapters'; 12 | import GitHubProvider from 'next-auth/providers/github'; 13 | import GoogleProvider from 'next-auth/providers/google'; 14 | import clientPromise from '../../../lib/client/mongodb'; 15 | import { 16 | uniqueNamesGenerator, 17 | Config, 18 | adjectives, 19 | colors, 20 | starWars, 21 | } from 'unique-names-generator'; 22 | import CredentialsProvider from 'next-auth/providers/credentials'; 23 | import { randomUUID } from 'crypto'; 24 | import Debug from 'debug'; 25 | const debug = Debug('nextjs:api:auth'); 26 | 27 | interface CustomUser extends User { 28 | provider?: string; 29 | } 30 | 31 | interface CustomSession extends Session { 32 | token_provider?: string; 33 | } 34 | 35 | const createAnonymousUser = (): User => { 36 | const customConfig: Config = { 37 | dictionaries: [adjectives, colors, starWars], 38 | separator: '-', 39 | length: 3, 40 | style: 'capital', 41 | }; 42 | const unique_handle: string = uniqueNamesGenerator(customConfig).replaceAll( 43 | ' ', 44 | '' 45 | ); 46 | const unique_realname: string = unique_handle.split('-').slice(1).join(' '); 47 | const unique_uuid: string = randomUUID(); 48 | return { 49 | id: unique_uuid, 50 | name: unique_realname, 51 | email: `${unique_handle.toLowerCase()}@titanium-guest.com`, 52 | image: '', 53 | }; 54 | }; 55 | 56 | const providers = [ 57 | GitHubProvider({ 58 | clientId: process.env.GITHUB_ID as string, 59 | clientSecret: process.env.GITHUB_SECRET as string, 60 | }), 61 | GoogleProvider({ 62 | clientId: process.env.GOOGLE_ID as string, 63 | clientSecret: process.env.GOOGLE_SECRET as string, 64 | }), 65 | CredentialsProvider({ 66 | name: 'a Guest Account', 67 | credentials: {}, 68 | async authorize(credentials, req) { 69 | const user = createAnonymousUser(); 70 | 71 | // Get the MongoDB client and database 72 | const client = await clientPromise; 73 | const db = client.db(); 74 | 75 | // Check if user already exists 76 | const existingUser = await db 77 | .collection('users') 78 | .findOne({ email: user.email }); 79 | if (!existingUser) { 80 | // Save the new user if not exists 81 | await db.collection('users').insertOne(user); 82 | } 83 | 84 | return user; 85 | }, 86 | }), 87 | ]; 88 | 89 | const options: NextAuthOptions = { 90 | providers, 91 | adapter: MongoDBAdapter(clientPromise), 92 | callbacks: { 93 | async jwt({ 94 | token, 95 | account, 96 | profile, 97 | }: { 98 | token: JWT; 99 | account: Account | null; 100 | profile?: Profile; 101 | }): Promise { 102 | if (account?.expires_at && account?.type === 'oauth') { 103 | token.access_token = account.access_token; 104 | token.expires_at = account.expires_at; 105 | token.refresh_token = account.refresh_token; 106 | token.refresh_token_expires_in = account.refresh_token_expires_in; 107 | token.provider = 'github'; 108 | } 109 | if (!token.provider) token.provider = 'Titanium'; 110 | return token; 111 | }, 112 | async session({ 113 | session, 114 | token, 115 | user, 116 | }: { 117 | session: CustomSession; 118 | token: JWT; 119 | user: AdapterUser; 120 | }): Promise { 121 | if (token.provider) { 122 | session.token_provider = token.provider as string; 123 | } 124 | return session; 125 | }, 126 | }, 127 | events: { 128 | async signIn({ 129 | user, 130 | account, 131 | profile, 132 | }: { 133 | user: CustomUser; 134 | account: Account | null; 135 | profile?: Profile; 136 | }): Promise { 137 | debug( 138 | `signIn of ${user.name} from ${user?.provider ?? account?.provider}` 139 | ); 140 | }, 141 | async signOut({ 142 | session, 143 | token, 144 | }: { 145 | session: Session; 146 | token: JWT; 147 | }): Promise { 148 | debug(`signOut of ${token.name} from ${token.provider}`); 149 | }, 150 | }, 151 | session: { 152 | strategy: 'jwt' as SessionStrategy, 153 | }, 154 | }; 155 | 156 | const handler = NextAuth(options); 157 | export { handler as GET, handler as POST }; 158 | -------------------------------------------------------------------------------- /app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import OpenAI, { ClientOptions } from 'openai'; 3 | import clientPromise from '../../lib/client/mongodb'; 4 | 5 | if (!process.env.OPENAI_API_KEY) { 6 | throw new Error('OPENAI_API_KEY is not set'); 7 | } 8 | 9 | const options: ClientOptions = { apiKey: process.env.OPENAI_API_KEY }; 10 | const openai = new OpenAI(options); 11 | 12 | import { sendErrorResponse } from '@/app/lib/utils/response'; 13 | 14 | export async function POST(req: NextRequest) { 15 | return await handlePostRequest(req); 16 | } 17 | 18 | async function fetchAssistantMessage( 19 | threadId: string, 20 | assistantId: string, 21 | userMessage: string 22 | ) { 23 | await openai.beta.threads.messages.create(threadId, { 24 | role: 'user', 25 | content: userMessage, 26 | }); 27 | 28 | let run = await openai.beta.threads.runs.create(threadId, { 29 | assistant_id: assistantId, 30 | }); 31 | while (run.status !== 'completed') { 32 | await new Promise((resolve) => setTimeout(resolve, 1000)); 33 | run = await openai.beta.threads.runs.retrieve(threadId, run.id); 34 | } 35 | 36 | const messages = await openai.beta.threads.messages.list(threadId); 37 | return messages.data.find((message) => message.role === 'assistant'); 38 | } 39 | 40 | async function handlePostRequest(req: NextRequest) { 41 | try { 42 | const { userMessage, userEmail } = await req.json(); 43 | const client = await clientPromise; 44 | const db = client.db(); 45 | const usersCollection = db.collection('users'); 46 | const user = await usersCollection.findOne({ email: userEmail }); 47 | 48 | if (!user) { 49 | return NextResponse.json({ message: 'User not found' }, { status: 404 }); 50 | } 51 | 52 | if (user.isAssistantEnabled && user.assistantId && user.threadId) { 53 | const assistantMessage = await fetchAssistantMessage( 54 | user.threadId, 55 | user.assistantId, 56 | userMessage 57 | ); 58 | 59 | if (!assistantMessage) { 60 | return NextResponse.json( 61 | { error: 'No assistant message found' }, 62 | { status: 404 } 63 | ); 64 | } 65 | 66 | const assistantMessageContent = assistantMessage.content.at(0); 67 | if (!assistantMessageContent || !('text' in assistantMessageContent)) { 68 | return NextResponse.json( 69 | { error: 'No valid assistant message content found' }, 70 | { status: 404 } 71 | ); 72 | } 73 | 74 | return new Response(assistantMessageContent.text.value); 75 | } 76 | let model = process.env.OPENAI_API_MODEL as string, 77 | content = userMessage; 78 | if (user.isVisionEnabled && user.visionId) { 79 | model = 'gpt-4-vision-preview'; 80 | content = [{ type: 'text', text: userMessage }] as any[]; 81 | const fileCollection = db.collection('files'); 82 | const visionFileList = await fileCollection 83 | .find({ visionId: user.visionId }) 84 | .toArray(); 85 | 86 | if (visionFileList) { 87 | visionFileList.forEach((file: { url: any }) => { 88 | content.push({ 89 | type: 'image_url', 90 | image_url: { 91 | url: file.url, 92 | }, 93 | }); 94 | }); 95 | } 96 | } 97 | const response = await openai.chat.completions.create({ 98 | model: model, 99 | messages: [{ role: 'user', content: content }], 100 | stream: true, 101 | max_tokens: 1024, 102 | }); 103 | return new Response(response.toReadableStream()); 104 | } catch (error: any) { 105 | console.error('Error processing request: ', error); 106 | return sendErrorResponse('Error processing request', 400); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /app/api/embed/conversation/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getDatabaseAndUser, getDb } from '@/app/lib/utils/db'; 3 | import { sendErrorResponse } from '@/app/lib/utils/response'; 4 | import OpenAI, { ClientOptions } from 'openai'; 5 | 6 | if (!process.env.OPENAI_API_KEY) { 7 | throw new Error('OPENAI_API_KEY is not set'); 8 | } 9 | 10 | const options: ClientOptions = { apiKey: process.env.OPENAI_API_KEY }; 11 | const openai = new OpenAI(options); 12 | 13 | function delay(ms: number): Promise { 14 | return new Promise((resolve) => setTimeout(resolve, ms)); 15 | } 16 | 17 | export async function POST(req: NextRequest): Promise { 18 | try { 19 | const db = await getDb(); 20 | const requestBody = await req.json(); 21 | const { data, userEmail } = requestBody; 22 | 23 | if (!userEmail || !data) { 24 | throw new Error( 25 | 'Incomplete request headers. Please provide userEmail and data.' 26 | ); 27 | } 28 | 29 | const { user } = await getDatabaseAndUser(db, userEmail); 30 | const fileCollection = db.collection('files'); 31 | const chunkIdList: string[] = []; 32 | const ragId = user.ragId as string; 33 | 34 | const embeddings = await Promise.all( 35 | data.map(async (item: any) => { 36 | delay(13); // Temporary fix for rate limiting 5000 RPM 37 | const response = await openai.embeddings.create({ 38 | model: 'text-embedding-3-large', 39 | input: item.text, 40 | encoding_format: 'float', 41 | }); 42 | const transformedMetadata = transformObjectValues(item.metadata); 43 | const newId = crypto.randomUUID(); 44 | chunkIdList.push(newId); 45 | const embeddingValues = response.data[0].embedding; 46 | return { 47 | id: newId, 48 | values: embeddingValues, 49 | metadata: { 50 | ...transformedMetadata, 51 | text: item.text, 52 | rag_id: ragId, 53 | user_email: user.email, 54 | }, 55 | }; 56 | }) 57 | ); 58 | 59 | await fileCollection.updateOne( 60 | { ragId: ragId }, 61 | { 62 | $set: { 63 | chunks: chunkIdList, 64 | }, 65 | } 66 | ); 67 | 68 | return NextResponse.json({ 69 | message: 'Embeddings generated successfully', 70 | chunks: chunkIdList, 71 | embeddings: embeddings, 72 | status: 200, 73 | }); 74 | } catch (error: any) { 75 | console.error('Error generating embeddings: ', error); 76 | return sendErrorResponse('Error generating embeddings', 400); 77 | } 78 | } 79 | 80 | const transformObjectValues = ( 81 | obj: Record 82 | ): Record => { 83 | return Object.entries(obj).reduce((acc, [key, value]) => { 84 | if (typeof value === 'object' && value !== null) { 85 | acc[key] = Object.entries(value).map( 86 | ([k, v]) => `${k}:${JSON.stringify(v)}` 87 | ); 88 | } else { 89 | acc[key] = value; 90 | } 91 | return acc; 92 | }, {} as Record); 93 | }; 94 | -------------------------------------------------------------------------------- /app/api/embed/message/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { sendErrorResponse } from '@/app/lib/utils/response'; 3 | import OpenAI, { ClientOptions } from 'openai'; 4 | 5 | if (!process.env.OPENAI_API_KEY) { 6 | throw new Error('OPENAI_API_KEY is not set'); 7 | } 8 | 9 | const options: ClientOptions = { apiKey: process.env.OPENAI_API_KEY }; 10 | const openai = new OpenAI(options); 11 | 12 | export async function POST(req: NextRequest): Promise { 13 | try { 14 | const requestBody = await req.json(); 15 | const { message } = requestBody; 16 | 17 | if (!message) { 18 | throw new Error('Incomplete request headers. Please mesage.'); 19 | } 20 | const messageToEmbed = `Date: ${message.createdAt}. User: ${message.conversationId}. Message: ${message.text}. Metadata: ${message.metadata}`; 21 | const response = await openai.embeddings.create({ 22 | model: 'text-embedding-3-large', 23 | input: messageToEmbed, 24 | encoding_format: 'float', 25 | }); 26 | 27 | const embeddingValues = response.data[0].embedding; 28 | 29 | return NextResponse.json({ 30 | message: 'Message embeddings generated successfully', 31 | values: embeddingValues, 32 | status: 200, 33 | }); 34 | } catch (error: any) { 35 | console.error('Error generating message embeddings: ', error); 36 | return sendErrorResponse('Error generating message embeddings', 400); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/api/memory/append/nosql/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { 3 | getDb, 4 | getConversation, 5 | createConversation, 6 | updateConversationSettings, 7 | } from '@/app/lib/utils/db'; 8 | import { sendErrorResponse } from '@/app/lib/utils/response'; 9 | 10 | export async function POST(req: NextRequest): Promise { 11 | const db = await getDb(); 12 | const { userEmail, message } = await req.json(); 13 | try { 14 | const conversationCollection = 15 | db.collection('conversations'); 16 | let { conversation } = await getConversation(db, userEmail); 17 | 18 | if (conversation) { 19 | await updateConversationSettings( 20 | conversation, 21 | conversationCollection, 22 | message 23 | ); 24 | } else { 25 | conversation = { 26 | id: userEmail, 27 | messages: [message], 28 | } as IConversation; 29 | console.log('No existing conversation found. Creating...'); 30 | await createConversation(conversation, conversationCollection); 31 | } 32 | return NextResponse.json({ 33 | message: `Conversation message appended via NoSQL database.`, 34 | userEmail, 35 | newMessage: message, 36 | status: 200, 37 | }); 38 | } catch (error: any) { 39 | console.error( 40 | `Error appending message to NoSQL database: ${error}` 41 | ); 42 | return sendErrorResponse( 43 | `Error appending message to NoSQL database`, 44 | 400 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/api/memory/append/vector/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { sendErrorResponse } from '@/app/lib/utils/response'; 3 | import { pinecone } from '@/app/lib/client/pinecone'; 4 | 5 | export async function POST(req: NextRequest): Promise { 6 | const { userEmail, vectorMessage } = await req.json(); 7 | try { 8 | if (userEmail) { 9 | const nameSpace = `${userEmail}_history`; 10 | const response = await pinecone.upsertOne([vectorMessage], nameSpace); 11 | if (response.success === false) { 12 | return sendErrorResponse( 13 | 'Error appending message to Vector database', 14 | 400 15 | ); 16 | } 17 | return NextResponse.json({ 18 | message: `Conversation message appended via Vector database.`, 19 | id: userEmail, 20 | response, 21 | status: 200, 22 | }); 23 | } else { 24 | return sendErrorResponse( 25 | 'Append cannot proceed without a valid user userEmail', 26 | 400 27 | ); 28 | } 29 | } catch (error: any) { 30 | console.error(`Error appending message to Vector database: ${error}`); 31 | return sendErrorResponse(`Error appending message to Vector database`, 400); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/api/memory/augment/nosql/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { 3 | getDb, 4 | getConversation, 5 | getFormattedConversationHistory, 6 | } from '@/app/lib/utils/db'; 7 | import { sendErrorResponse } from '@/app/lib/utils/response'; 8 | 9 | export async function POST(req: NextRequest): Promise { 10 | const db = await getDb(); 11 | const { userEmail, message, historyLength } = await req.json(); 12 | let formattedConversationHistory = message; 13 | try { 14 | const { conversation } = await getConversation(db, userEmail); 15 | if (conversation?.messages?.length > 0) { 16 | formattedConversationHistory = await getFormattedConversationHistory( 17 | historyLength, 18 | conversation 19 | ); 20 | } 21 | if (!formattedConversationHistory) { 22 | return sendErrorResponse( 23 | `Error Augmenting message with NoSQL database`, 24 | 404 25 | ); 26 | } 27 | 28 | return NextResponse.json({ 29 | message: `NoSQL Message augmentation successful`, 30 | userEmail: userEmail, 31 | formattedConversationHistory: formattedConversationHistory, 32 | status: 200, 33 | }); 34 | } catch (error: any) { 35 | console.error(`Error Augmenting message with NoSQL database: ${error}`); 36 | return sendErrorResponse( 37 | `Error Augmenting message with NoSQL database`, 38 | 400 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/api/memory/augment/vector/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { sendErrorResponse } from '@/app/lib/utils/response'; 3 | 4 | import { pinecone } from '@/app/lib/client/pinecone'; 5 | import { getFormattedConversationHistory } from '@/app/lib/utils/vectorDb'; 6 | 7 | export async function POST(req: NextRequest): Promise { 8 | const { userEmail, historyLength, embeddedMessage } = await req.json(); 9 | try { 10 | if (userEmail) { 11 | const namespace = `${userEmail}_history`; 12 | const conversationHistoryResults = await pinecone.queryByNamespace( 13 | namespace, 14 | historyLength, 15 | embeddedMessage 16 | ); 17 | 18 | const formattedConversationHistory = 19 | await getFormattedConversationHistory( 20 | conversationHistoryResults.matches 21 | ); 22 | 23 | return NextResponse.json({ 24 | message: 'Pinecone message augmentation successful', 25 | namespace, 26 | formattedConversationHistory, 27 | status: 200, 28 | }); 29 | } else { 30 | return sendErrorResponse( 31 | 'Pinecone message augmentation cannot proceed without a valid user id', 32 | 400 33 | ); 34 | } 35 | } catch (error: any) { 36 | console.error(`Error Augmenting message with Vector database: ${error}`); 37 | return sendErrorResponse( 38 | `Error Augmenting message with Vector database`, 39 | 400 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/api/memory/retrieve/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getDatabaseAndUser, getDb } from '@/app/lib/utils/db'; 3 | import { 4 | sendErrorResponse, 5 | sendInformationResponse, 6 | } from '@/app/lib/utils/response'; 7 | 8 | export const dynamic = 'force-dynamic'; 9 | 10 | export async function GET(req: NextRequest): Promise { 11 | try { 12 | const db = await getDb(); 13 | const userEmail = req.headers.get('userEmail') as string; 14 | const serviceName = req.headers.get('serviceName'); 15 | const { user } = await getDatabaseAndUser(db, userEmail); 16 | 17 | if (serviceName === 'memory') { 18 | const { isLongTermMemoryEnabled, memoryType, historyLength } = user; 19 | 20 | return NextResponse.json({ 21 | message: 'Long term memory retrieved', 22 | isLongTermMemoryEnabled, 23 | memoryType, 24 | historyLength, 25 | status: 200, 26 | }); 27 | } 28 | return sendInformationResponse( 29 | 'Long term memory not configured for the user', 30 | 202 31 | ); 32 | } catch (error: any) { 33 | console.error('Long term memory retrieval unsuccessful:', error); 34 | return sendErrorResponse('Long term memory retrieval unsuccessful', 400); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/api/memory/update-metadata/vector/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { sendErrorResponse } from '@/app/lib/utils/response'; 3 | import { pinecone } from '@/app/lib/client/pinecone'; 4 | 5 | export async function POST(req: NextRequest): Promise { 6 | const { id, metadata, userEmail } = await req.json(); 7 | try { 8 | if (id) { 9 | const nameSpace = `${userEmail}_history`; 10 | const response = await pinecone.updateMetadata(id, metadata, nameSpace); 11 | if (response.success === false) { 12 | return sendErrorResponse( 13 | 'Error updating metadata in Vector database', 14 | 400 15 | ); 16 | } 17 | return NextResponse.json({ 18 | message: `Metadata updated in Vector database.`, 19 | id: userEmail, 20 | response, 21 | status: 200, 22 | }); 23 | } else { 24 | return sendErrorResponse( 25 | 'Metadata update cannot proceed without a valid id', 26 | 400 27 | ); 28 | } 29 | } catch (error: any) { 30 | console.error(`Error updating metadata in Vector database: ${error}`); 31 | return sendErrorResponse(`Error updating metadata in Vector database`, 400); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/api/memory/update/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { 3 | getDb, 4 | updateMemorySettings, 5 | getUserByEmail, 6 | } from '@/app/lib/utils/db'; 7 | import { sendErrorResponse } from '@/app/lib/utils/response'; 8 | 9 | export async function POST(req: NextRequest): Promise { 10 | try { 11 | const db = await getDb(); 12 | const { isLongTermMemoryEnabled, userEmail, memoryType, historyLength } = 13 | (await req.json()) as { 14 | isLongTermMemoryEnabled: boolean; 15 | userEmail: string; 16 | memoryType: string; 17 | historyLength: string; 18 | }; 19 | const usersCollection = db.collection('users'); 20 | const user = await getUserByEmail(usersCollection, userEmail); 21 | 22 | if (!user) { 23 | return sendErrorResponse('User not found', 404); 24 | } 25 | 26 | await updateMemorySettings( 27 | user, 28 | usersCollection, 29 | isLongTermMemoryEnabled, 30 | memoryType, 31 | historyLength 32 | ); 33 | 34 | return NextResponse.json({ 35 | message: 'Long term memory updated', 36 | isLongTermMemoryEnabled: isLongTermMemoryEnabled, 37 | memoryType: memoryType, 38 | status: 200, 39 | }); 40 | } catch (error: any) { 41 | console.error('Error updating long term memory: ', error); 42 | return sendErrorResponse('Error updating long term memory', 400); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/api/parse/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { UnstructuredClient } from 'unstructured-client'; 3 | import * as fs from 'fs'; 4 | 5 | import { sendErrorResponse } from '@/app/lib/utils/response'; 6 | 7 | const apiKey = process.env.UNSTRUCTURED_API as string; 8 | 9 | const client = new UnstructuredClient({ 10 | security: { 11 | apiKeyAuth: apiKey, 12 | }, 13 | }); 14 | 15 | export async function POST(req: NextRequest): Promise { 16 | try { 17 | const { file, chunkSize, parsingStrategy } = await req.json(); 18 | const fileData = fs.readFileSync(file.path); 19 | let parsedDataResponse = await client.general.partition({ 20 | files: { 21 | content: fileData, 22 | fileName: file.name, 23 | }, 24 | strategy: parsingStrategy, 25 | combineUnderNChars: parseInt(chunkSize), 26 | }); 27 | return NextResponse.json({ 28 | message: 'Unstructured partition parsed successfully', 29 | file: parsedDataResponse?.elements, 30 | status: 200, 31 | }); 32 | } catch (error: any) { 33 | console.error('Unstructured partition failed to parse: ', error); 34 | return sendErrorResponse('Unstructured partition failed to parse', 400); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/api/rag/delete-file/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getDatabaseAndUser, getDb } from '@/app/lib/utils/db'; 3 | import { sendErrorResponse } from '@/app/lib/utils/response'; 4 | import fs from 'fs/promises'; 5 | 6 | interface DeleteFileRequest { 7 | file: RagFile; 8 | userEmail: string; 9 | } 10 | 11 | export async function POST(req: NextRequest): Promise { 12 | const requestBody = await req.json(); 13 | 14 | try { 15 | const db = await getDb(); 16 | const { file, userEmail } = requestBody as DeleteFileRequest; 17 | const { user } = await getDatabaseAndUser(db, userEmail); 18 | 19 | if (user.ragId !== file.ragId) { 20 | return sendErrorResponse('User ragId not found', 404); 21 | } 22 | 23 | const fileCollection = db.collection('files'); 24 | 25 | const fileDeletedFromDB = await fileCollection.deleteOne({ 26 | id: file.id, 27 | }); 28 | const fileDeletedFromDisk = await fs.unlink(file.path); 29 | return NextResponse.json({ 30 | fileDeletedFromDisk: fileDeletedFromDisk, 31 | fileDeletedFromDB: fileDeletedFromDB, 32 | status: 200, 33 | }); 34 | } catch (error: any) { 35 | console.error('R.A.G. file deletion unsuccessful: ', error); 36 | return sendErrorResponse('R.A.G. file deletion unsuccessful', 500); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/api/rag/retrieve/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getDatabaseAndUser, getDb } from '@/app/lib/utils/db'; 3 | import { 4 | sendErrorResponse, 5 | sendInformationResponse, 6 | } from '@/app/lib/utils/response'; 7 | 8 | export const dynamic = 'force-dynamic' 9 | 10 | export async function GET(req: NextRequest): Promise { 11 | try { 12 | const db = await getDb(); 13 | const userEmail = req.headers.get('userEmail') as string; 14 | const serviceName = req.headers.get('serviceName'); 15 | const { user } = await getDatabaseAndUser(db, userEmail); 16 | 17 | if (serviceName === 'rag' && user.ragId) { 18 | const fileCollection = db.collection('files'); 19 | const ragFileList = await fileCollection 20 | .find({ ragId: user.ragId }) 21 | .toArray(); 22 | return NextResponse.json({ 23 | message: 'R.A.G. retrieved', 24 | ragId: user.ragId, 25 | topK: user.topK, 26 | chunkSize: user.chunkSize, 27 | chunkBatch: user.chunkBatch, 28 | parsingStrategy: user.parsingStrategy, 29 | ragFileList, 30 | isRagEnabled: user.isRagEnabled, 31 | status: 200, 32 | }); 33 | } else { 34 | return sendInformationResponse('R.A.G. not configured for the user', 202); 35 | } 36 | } catch (error: any) { 37 | console.error('R.A.G. retrieval unsuccessful', error); 38 | return sendErrorResponse('R.A.G. Retrieval unsuccessful', 400); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/api/rag/update-file-status/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getDatabaseAndUser, getDb } from '@/app/lib/utils/db'; 3 | import { sendErrorResponse } from '@/app/lib/utils/response'; 4 | 5 | interface UpdateFileStatusRequest { 6 | file: RagFile; 7 | userEmail: string; 8 | } 9 | 10 | export async function POST(req: NextRequest): Promise { 11 | const requestBody = await req.json(); 12 | 13 | try { 14 | const db = await getDb(); 15 | const { file, userEmail } = requestBody as UpdateFileStatusRequest; 16 | const { user } = await getDatabaseAndUser(db, userEmail); 17 | 18 | if (user.ragId !== file.ragId) { 19 | return sendErrorResponse('User ragId not found', 404); 20 | } 21 | 22 | const fileCollection = db.collection('files'); 23 | 24 | await fileCollection.updateOne( 25 | { 26 | id: file.id, 27 | }, 28 | { 29 | $set: { 30 | processed: true, 31 | }, 32 | } 33 | ); 34 | file.processed = true; 35 | return NextResponse.json({ 36 | message: 'R.A.G. file status updated successfully', 37 | file: file, 38 | status: 200, 39 | }); 40 | } catch (error: any) { 41 | console.error('R.A.G. file status update failed: ', error); 42 | return sendErrorResponse('R.A.G. file status update failed', 400); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/api/rag/update/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getDb, getUserByEmail } from '@/app/lib/utils/db'; 3 | import { sendErrorResponse } from '@/app/lib/utils/response'; 4 | import { Collection } from 'mongodb'; 5 | 6 | export async function POST(req: NextRequest): Promise { 7 | try { 8 | const db = await getDb(); 9 | const { 10 | isRagEnabled, 11 | userEmail, 12 | topK, 13 | chunkSize, 14 | chunkBatch, 15 | parsingStrategy, 16 | } = await req.json(); 17 | 18 | const usersCollection = db.collection('users'); 19 | const fileCollection = db.collection('files'); 20 | const user = await getUserByEmail(usersCollection, userEmail); 21 | 22 | if (!user) { 23 | return sendErrorResponse('User not found', 404); 24 | } 25 | await updateRag( 26 | user, 27 | usersCollection, 28 | isRagEnabled, 29 | topK, 30 | chunkSize, 31 | chunkBatch, 32 | parsingStrategy 33 | ); 34 | 35 | const ragId = user.ragId as string; 36 | const ragFile = await fileCollection.findOne({ ragId: ragId }); 37 | 38 | return NextResponse.json({ 39 | message: 'R.A.G. updated', 40 | ragId: user.ragId, 41 | isRagEnabled: isRagEnabled, 42 | ragFile: ragFile, 43 | status: 200, 44 | }); 45 | } catch (error: any) { 46 | console.error('Error in R.A.G. update: ', error); 47 | return sendErrorResponse('Error in R.A.G. update', 400); 48 | } 49 | } 50 | 51 | async function updateRag( 52 | user: IUser, 53 | usersCollection: Collection, 54 | isRagEnabled: boolean, 55 | topK: string, 56 | chunkSize: string, 57 | chunkBatch: string, 58 | parsingStrategy: string 59 | ): Promise { 60 | let disableOtherServices = isRagEnabled ? false : user.isAssistantEnabled; 61 | let ragId = user.ragId; 62 | if (!ragId) { 63 | console.log('No ragId found. Creating a new one'); 64 | ragId = crypto.randomUUID(); 65 | } 66 | 67 | await usersCollection.updateOne( 68 | { email: user.email }, 69 | { 70 | $set: { 71 | isRagEnabled: isRagEnabled, 72 | isAssistantEnabled: disableOtherServices, 73 | isVisionEnabled: disableOtherServices, 74 | ragId: ragId, 75 | topK: topK, 76 | chunkSize: chunkSize, 77 | chunkBatch: chunkBatch, 78 | parsingStrategy: parsingStrategy, 79 | }, 80 | } 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /app/api/rag/upload/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getDb, getUserByEmail } from '@/app/lib/utils/db'; 3 | import { sendErrorResponse } from '@/app/lib/utils/response'; 4 | import fs from 'fs/promises'; 5 | import { tmpdir } from 'os'; 6 | import { join } from 'path'; 7 | 8 | interface FileUploadResponse { 9 | fileWrittenToDisk: boolean; 10 | uploadPath: string; 11 | } 12 | 13 | export async function POST(request: NextRequest): Promise { 14 | try { 15 | const data = await request.formData(); 16 | const file = data.get('file') as unknown as File; 17 | const userEmail = data.get('userEmail') as string; 18 | const db = await getDb(); 19 | const usersCollection = db.collection('users'); 20 | const user = await getUserByEmail(usersCollection, userEmail); 21 | let ragId; 22 | if (!user) { 23 | return sendErrorResponse('User not found', 404); 24 | } 25 | 26 | if (!user.ragId) { 27 | console.log('No ragId found. Creating a new one'); 28 | ragId = crypto.randomUUID(); 29 | await usersCollection.updateOne( 30 | { email: user.email }, 31 | { $set: { ragId: ragId } } 32 | ); 33 | } else { 34 | ragId = user.ragId; 35 | } 36 | 37 | const fileWriteResponse = await writeFile(file); 38 | 39 | const dbFile = { 40 | id: crypto.randomUUID(), 41 | name: file.name, 42 | path: fileWriteResponse.uploadPath, 43 | ragId: ragId, 44 | purpose: 'R.A.G.', 45 | processed: false, 46 | chunks: [], 47 | }; 48 | 49 | const fileCollection = db.collection('files'); 50 | const insertFileToDBResponse = await fileCollection.insertOne(dbFile); 51 | 52 | return NextResponse.json({ 53 | message: 'File upload successful', 54 | file: dbFile, 55 | fileWrittenToDisk: fileWriteResponse.fileWrittenToDisk, 56 | fileWrittenToDb: insertFileToDBResponse.insertedId, 57 | status: 200, 58 | }); 59 | } catch (error: any) { 60 | console.error('Error processing file: ', error); 61 | return sendErrorResponse('Error processing file', 500); 62 | } 63 | } 64 | 65 | async function writeFile(file: File): Promise { 66 | try { 67 | const uploadPath = join(tmpdir(), file.name); 68 | const arrayBuffer = await file.arrayBuffer(); 69 | const buffer = Buffer.from(arrayBuffer); 70 | await fs.writeFile(uploadPath, buffer); 71 | 72 | const response = { 73 | fileWrittenToDisk: true, 74 | uploadPath: uploadPath, 75 | }; 76 | 77 | return response; 78 | } catch (error: any) { 79 | console.error('Error in file upload to R.A.G.: ', error); 80 | throw new Error('File upload to R.A.G. failed'); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/api/rag/vector-db/delete-file/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getDatabaseAndUser, getDb } from '@/app/lib/utils/db'; 3 | import { sendErrorResponse } from '@/app/lib/utils/response'; 4 | 5 | import { pinecone } from '@/app/lib/client/pinecone'; 6 | 7 | interface DeleteFileRequest { 8 | file: RagFile; 9 | userEmail: string; 10 | chunkBatch: string; 11 | } 12 | 13 | export async function POST(req: NextRequest): Promise { 14 | const requestBody = await req.json(); 15 | 16 | try { 17 | const db = await getDb(); 18 | const { file, userEmail, chunkBatch } = requestBody as DeleteFileRequest; 19 | const { user } = await getDatabaseAndUser(db, userEmail); 20 | 21 | if (user.ragId !== file.ragId) { 22 | return sendErrorResponse('User ragId not found', 404); 23 | } 24 | const fileDeletedFromVectorDB = await pinecone.deleteMany( 25 | file.chunks, 26 | user, 27 | chunkBatch 28 | ); 29 | if (fileDeletedFromVectorDB.success === false) { 30 | return sendErrorResponse('Vector DB file deletion unsuccessful', 500); 31 | } 32 | return NextResponse.json({ 33 | message: 'Vector DB file deletion successful', 34 | fileDeletedFromVectorDB: fileDeletedFromVectorDB, 35 | status: 200, 36 | }); 37 | } catch (error: any) { 38 | console.error('Vector DB file deletion unsuccessful: ', error); 39 | return sendErrorResponse('Vector DB file deletion unsuccessful', 500); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/api/rag/vector-db/query/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getDatabaseAndUser, getDb } from '@/app/lib/utils/db'; 3 | import { sendErrorResponse } from '@/app/lib/utils/response'; 4 | import { pinecone } from '@/app/lib/client/pinecone'; 5 | 6 | export async function POST(req: NextRequest): Promise { 7 | try { 8 | const db = await getDb(); 9 | const requestBody = await req.json(); 10 | const { embeddedMessage, userEmail, topK } = requestBody; 11 | const { user } = await getDatabaseAndUser(db, userEmail); 12 | 13 | const namespace = user.ragId; 14 | if (namespace) { 15 | const response = await pinecone.queryByNamespace( 16 | namespace, 17 | topK, 18 | embeddedMessage[0] 19 | ); 20 | 21 | const context = response.matches 22 | .map((item: any) => { 23 | return ( 24 | `Filename: "${item.metadata.filename}"\n` + 25 | `Filetype: "${item.metadata.filetype}"\n` + 26 | `Languages: [ ${item.metadata.languages.join(', ')} ]\n` + 27 | `Page Number: ${item.metadata.page_number}\n` + 28 | `Parent ID: "${item.metadata.parent_id}"\n` + 29 | `RAG ID: "${item.metadata.rag_id}"\n` + 30 | `Text: "${item.metadata.text}"\n` + 31 | `User Email: "${item.metadata.user_email}"` 32 | ); 33 | }) 34 | .join('\n\n'); 35 | 36 | return NextResponse.json({ 37 | message: 'Pinecone query successful', 38 | ragId: namespace, 39 | context, 40 | isRagEnabled: user.isRagEnabled, 41 | status: 200, 42 | }); 43 | } else { 44 | return sendErrorResponse( 45 | 'Query cannot proceed without a valid user ragId', 46 | 400 47 | ); 48 | } 49 | } catch (error: any) { 50 | console.error('Error querying Pinecone:', error); 51 | return sendErrorResponse('Error querying Pinecone: ', 400); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/api/rag/vector-db/upsert/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getDatabaseAndUser, getDb } from '@/app/lib/utils/db'; 3 | import { sendErrorResponse } from '@/app/lib/utils/response'; 4 | 5 | import { pinecone } from '@/app/lib/client/pinecone'; 6 | 7 | export async function POST(req: NextRequest): Promise { 8 | try { 9 | const db = await getDb(); 10 | const requestBody = await req.json(); 11 | const { data, userEmail, chunkBatch } = requestBody; 12 | const { user } = await getDatabaseAndUser(db, userEmail); 13 | 14 | if (user.ragId) { 15 | const response = await pinecone.upsert(data, user, chunkBatch); 16 | if (response.success === false) { 17 | return sendErrorResponse('Pinecone upsert unsuccessful', 400); 18 | } 19 | return NextResponse.json({ 20 | message: 'Pinecone upserted successfully', 21 | ragId: user.ragId, 22 | response, 23 | isRagEnabled: user.isRagEnabled, 24 | status: 200, 25 | }); 26 | } else { 27 | return sendErrorResponse( 28 | 'Upsert cannot proceed without a valid user ragId', 29 | 400 30 | ); 31 | } 32 | } catch (error: any) { 33 | console.error('Pinecone upsert unsuccessful: ', error); 34 | return sendErrorResponse('Pinecone upsert unsuccessful', 400); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/api/speech/retrieve/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getDatabaseAndUser, getDb } from '@/app/lib/utils/db'; 3 | import { 4 | sendErrorResponse, 5 | sendInformationResponse, 6 | } from '@/app/lib/utils/response'; 7 | 8 | export const dynamic = 'force-dynamic' 9 | 10 | export async function GET(req: NextRequest): Promise { 11 | try { 12 | const db = await getDb(); 13 | const userEmail = req.headers.get('userEmail') as string; 14 | const serviceName = req.headers.get('serviceName'); 15 | const { user } = await getDatabaseAndUser(db, userEmail); 16 | 17 | if (serviceName === 'speech') { 18 | const { isTextToSpeechEnabled, model, voice } = user; 19 | 20 | return NextResponse.json({ 21 | message: 'Speech retrieved', 22 | isTextToSpeechEnabled, 23 | model, 24 | voice, 25 | status: 200, 26 | }); 27 | } 28 | return sendInformationResponse('Speech not configured for the user', 202); 29 | } catch (error: any) { 30 | console.error('Speech retrieval unsuccessful:', error); 31 | return sendErrorResponse('Speech retrieval unsuccessful', 400); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/api/speech/stt/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import OpenAI, { ClientOptions } from 'openai'; 3 | import fs from 'fs/promises'; 4 | import { createReadStream } from 'fs'; 5 | import { tmpdir } from 'os'; 6 | import { join } from 'path'; 7 | const options: ClientOptions = { apiKey: process.env.OPENAI_API_KEY }; 8 | const openai = new OpenAI(options); 9 | 10 | import { sendErrorResponse } from '@/app/lib/utils/response'; 11 | 12 | export async function POST(req: NextRequest) { 13 | const tempFilePath = join(tmpdir(), 'audio.mp3'); 14 | try { 15 | const formData = await req.formData(); 16 | const file = formData.get('file'); 17 | 18 | if (file && file instanceof Blob) { 19 | const arrayBuffer = await file.arrayBuffer(); 20 | const buffer = Buffer.from(arrayBuffer); 21 | await fs.writeFile(tempFilePath, buffer); 22 | const fileStream = createReadStream(tempFilePath); 23 | 24 | const transcription = await openai.audio.transcriptions.create({ 25 | file: fileStream, 26 | model: 'whisper-1', 27 | }); 28 | return new NextResponse(transcription.text); 29 | } else { 30 | return sendErrorResponse('File not provided or incorrect format', 400); 31 | } 32 | } catch (error) { 33 | console.error('Error generating transcript from speech: ', error); 34 | return sendErrorResponse('Error generating transcript from speech', 400); 35 | } finally { 36 | await fs.unlink(tempFilePath); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/api/speech/tts/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import OpenAI, { ClientOptions } from 'openai'; 3 | 4 | const options: ClientOptions = { apiKey: process.env.OPENAI_API_KEY }; 5 | const openai = new OpenAI(options); 6 | 7 | import { sendErrorResponse } from '@/app/lib/utils/response'; 8 | 9 | export async function POST(req: NextRequest) { 10 | try { 11 | const { text, model, voice } = await req.json(); 12 | const response = await openai.audio.speech.create({ 13 | model: model, 14 | voice: voice, 15 | input: text, 16 | }); 17 | const buffer = Buffer.from(await response.arrayBuffer()); 18 | return new NextResponse(buffer); 19 | } catch (error) { 20 | console.error('Error generating speech from text: ', error); 21 | return sendErrorResponse('Error generating speech from text', 500); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/api/speech/update/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { Collection } from 'mongodb'; 3 | import { getDb, getUserByEmail } from '@/app/lib/utils/db'; 4 | import { sendErrorResponse } from '@/app/lib/utils/response'; 5 | 6 | export async function POST(req: NextRequest): Promise { 7 | try { 8 | const db = await getDb(); 9 | const { isTextToSpeechEnabled, userEmail, model, voice } = 10 | (await req.json()) as { 11 | isTextToSpeechEnabled: boolean; 12 | userEmail: string; 13 | model: string; 14 | voice: string; 15 | }; 16 | 17 | const usersCollection = db.collection('users'); 18 | const user = await getUserByEmail(usersCollection, userEmail); 19 | 20 | if (!user) { 21 | return sendErrorResponse('User not found', 404); 22 | } 23 | 24 | await updateSpeech( 25 | user, 26 | usersCollection, 27 | isTextToSpeechEnabled, 28 | model, 29 | voice 30 | ); 31 | 32 | return NextResponse.json({ 33 | message: 'Speech updated', 34 | isTextToSpeechEnabled: isTextToSpeechEnabled, 35 | model: model, 36 | voice: voice, 37 | status: 200, 38 | }); 39 | } catch (error: any) { 40 | console.error('Error updating speech: ', error); 41 | return sendErrorResponse('Error updating speech', 500); 42 | } 43 | } 44 | 45 | async function updateSpeech( 46 | user: IUser, 47 | usersCollection: Collection, 48 | isTextToSpeechEnabled: boolean, 49 | model: string, 50 | voice: string 51 | ): Promise { 52 | await usersCollection.updateOne( 53 | { email: user.email }, 54 | { 55 | $set: { 56 | isTextToSpeechEnabled: isTextToSpeechEnabled, 57 | model: model, 58 | voice: voice, 59 | }, 60 | } 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /app/api/vision/add-url/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getDatabaseAndUser, getDb } from '@/app/lib/utils/db'; 3 | import { sendErrorResponse } from '@/app/lib/utils/response'; 4 | 5 | export async function POST(req: NextRequest): Promise { 6 | try { 7 | const db = await getDb(); 8 | 9 | const { file, userEmail } = await req.json(); 10 | const { user } = await getDatabaseAndUser(db, userEmail); 11 | let visionId; 12 | const usersCollection = db.collection('users'); 13 | if (!user.visionId) { 14 | console.log('No visionId found. Creating a new one'); 15 | visionId = crypto.randomUUID(); 16 | await usersCollection.updateOne( 17 | { email: user.email }, 18 | { $set: { visionId: visionId } } 19 | ); 20 | } else { 21 | visionId = user.visionId; 22 | } 23 | file.visionId = visionId; 24 | const fileCollection = db.collection('files'); 25 | const insertFileResponse = await fileCollection.insertOne(file); 26 | 27 | return NextResponse.json({ 28 | message: 'File upload successful', 29 | response: insertFileResponse, 30 | file: file, 31 | status: 200, 32 | }); 33 | } catch (error) { 34 | console.error('Error processing file: ', error); 35 | return sendErrorResponse('Error processing file: ', 500); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/api/vision/delete-url/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getDatabaseAndUser, getDb } from '@/app/lib/utils/db'; 3 | import { sendErrorResponse } from '@/app/lib/utils/response'; 4 | 5 | export async function POST(req: NextRequest): Promise { 6 | try { 7 | const db = await getDb(); 8 | const { file, userEmail } = await req.json(); 9 | const { user } = await getDatabaseAndUser(db, userEmail); 10 | if (user.visionId !== file.visionId) { 11 | return sendErrorResponse('User visionId not found', 404); 12 | } 13 | 14 | const fileCollection = db.collection('files'); 15 | const deleteFileResponse = await fileCollection.deleteOne({ 16 | visionId: file.visionId, 17 | }); 18 | 19 | return NextResponse.json({ 20 | status: 200, 21 | message: 'Url deleted successfully', 22 | response: deleteFileResponse, 23 | }); 24 | } catch (error) { 25 | return sendErrorResponse('Error deleting file', 500); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/api/vision/retrieve/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { getDatabaseAndUser, getDb } from '@/app/lib/utils/db'; 3 | import { 4 | sendErrorResponse, 5 | sendInformationResponse, 6 | } from '@/app/lib/utils/response'; 7 | 8 | export const dynamic = 'force-dynamic' 9 | 10 | export async function GET(req: NextRequest): Promise { 11 | try { 12 | const db = await getDb(); 13 | const userEmail = req.headers.get('userEmail') as string; 14 | const serviceName = req.headers.get('serviceName'); 15 | const { user } = await getDatabaseAndUser(db, userEmail); 16 | 17 | if (serviceName === 'vision' && user.visionId) { 18 | const fileCollection = db.collection('files'); 19 | const visionFileList = await fileCollection 20 | .find({ visionId: user.visionId }) 21 | .toArray(); 22 | 23 | return NextResponse.json({ 24 | message: 'Vision retrieved', 25 | visionId: user.visionId, 26 | visionFileList, 27 | isVisionEnabled: user.isVisionEnabled, 28 | status: 200, 29 | }); 30 | } else { 31 | return sendInformationResponse('Vision not configured for the user', 202); 32 | } 33 | } catch (error: any) { 34 | console.error('Vision retrieval unsuccessful', error); 35 | return sendErrorResponse('Vision retrieval unsuccessful', 400); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/api/vision/update/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { Collection } from 'mongodb'; 3 | import { getDb, getUserByEmail } from '@/app/lib/utils/db'; 4 | import { sendErrorResponse } from '@/app/lib/utils/response'; 5 | 6 | export async function POST(req: NextRequest): Promise { 7 | try { 8 | const db = await getDb(); 9 | const { isVisionEnabled, userEmail } = (await req.json()) as { 10 | isVisionEnabled: boolean; 11 | userEmail: string; 12 | }; 13 | 14 | const usersCollection = db.collection('users'); 15 | const user = await getUserByEmail(usersCollection, userEmail); 16 | 17 | if (!user) { 18 | return sendErrorResponse('User not found', 404); 19 | } 20 | 21 | await updateVision(user, usersCollection, isVisionEnabled); 22 | 23 | return NextResponse.json({ 24 | message: 'Vision updated', 25 | visionId: user.visionId, 26 | isVisionEnabled: isVisionEnabled, 27 | status: 200, 28 | }); 29 | } catch (error: any) { 30 | console.error('Error in vision update: ', error); 31 | return sendErrorResponse('Error in vision update', 400); 32 | } 33 | } 34 | 35 | async function updateVision( 36 | user: IUser, 37 | usersCollection: Collection, 38 | isVisionEnabled: boolean 39 | ): Promise { 40 | let disableOtherServices = isVisionEnabled ? false : user.isAssistantEnabled; 41 | let visionId = user.visionId; 42 | if (!visionId) { 43 | console.log('No visionId found. Creating a new one'); 44 | visionId = crypto.randomUUID(); 45 | } 46 | await usersCollection.updateOne( 47 | { email: user.email }, 48 | { 49 | $set: { 50 | isAssistantEnabled: disableOtherServices, 51 | isRagEnabled: disableOtherServices, 52 | isVisionEnabled: isVisionEnabled, 53 | visionId: visionId, 54 | }, 55 | } 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /app/components/AppBar/index.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | right: 0; 6 | } 7 | -------------------------------------------------------------------------------- /app/components/AppBar/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useSession, signIn, signOut } from 'next-auth/react'; 3 | import AppBar from '@mui/material/AppBar'; 4 | import Box from '@mui/material/Box'; 5 | import Toolbar from '@mui/material/Toolbar'; 6 | import IconButton from '@mui/material/IconButton'; 7 | import Typography from '@mui/material/Typography'; 8 | import Menu from '@mui/material/Menu'; 9 | import MenuIcon from '@mui/icons-material/Menu'; 10 | import Container from '@mui/material/Container'; 11 | import Avatar from '@mui/material/Avatar'; 12 | import Button from '@mui/material/Button'; 13 | import Tooltip from '@mui/material/Tooltip'; 14 | import MenuItem from '@mui/material/MenuItem'; 15 | import BlurOnRoundedIcon from '@mui/icons-material/BlurOnRounded'; 16 | import styles from './index.module.css'; 17 | const pages = ['About']; 18 | const settings = ['Account', 'Logout']; 19 | 20 | function ResponsiveAppBar() { 21 | const { data: session } = useSession(); 22 | const [anchorElNav, setAnchorElNav] = React.useState( 23 | null 24 | ); 25 | const [anchorElUser, setAnchorElUser] = React.useState( 26 | null 27 | ); 28 | 29 | const handleOpenNavMenu = (event: React.MouseEvent) => { 30 | setAnchorElNav(event.currentTarget); 31 | }; 32 | const handleOpenUserMenu = (event: React.MouseEvent) => { 33 | setAnchorElUser(event.currentTarget); 34 | }; 35 | 36 | const handleCloseNavMenu = () => { 37 | setAnchorElNav(null); 38 | }; 39 | 40 | const handleCloseUserMenu = () => { 41 | setAnchorElUser(null); 42 | }; 43 | 44 | const handleLogout = () => { 45 | signOut(); 46 | }; 47 | 48 | const handleLogin = () => { 49 | signIn(); 50 | }; 51 | 52 | return ( 53 | 54 | 55 | 56 | 59 | 74 | Titanium 75 | 76 | 77 | 78 | 86 | 87 | 88 | 106 | {pages.map((page) => ( 107 | 108 | {page} 109 | 110 | ))} 111 | 112 | 113 | 116 | 132 | Titanium 133 | 134 | 135 | {pages.map((page) => ( 136 | 143 | ))} 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 168 | {session ? ( 169 | settings.map((setting) => ( 170 | 176 | {setting} 177 | 178 | )) 179 | ) : ( 180 | 181 | Log in 182 | 183 | )} 184 | 185 | 186 | 187 | 188 | 189 | ); 190 | } 191 | export default ResponsiveAppBar; 192 | -------------------------------------------------------------------------------- /app/components/Assistant/AssistantFileList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | ListItem, 4 | ListItemAvatar, 5 | ListItemText, 6 | IconButton, 7 | Avatar, 8 | } from '@mui/material'; 9 | import DeleteIcon from '@mui/icons-material/Delete'; 10 | import FolderIcon from '@mui/icons-material/Folder'; 11 | import FilePaper from '../FileList'; 12 | 13 | interface AssistantFileListProps { 14 | files: { name: string; id: string; assistandId: string }[]; 15 | onDelete: (file: any) => void; 16 | } 17 | 18 | const AssistantFileList: React.FC = ({ 19 | files, 20 | onDelete, 21 | }) => ( 22 |
23 | ( 26 | onDelete(file)} 33 | > 34 | 35 | 36 | } 37 | > 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | )} 46 | /> 47 |
48 | ); 49 | 50 | export default AssistantFileList; 51 | -------------------------------------------------------------------------------- /app/components/Assistant/AssistantForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useFormContext } from 'react-hook-form'; 3 | import { FormControl, TextField } from '@mui/material'; 4 | 5 | interface AssistantFormProps { 6 | error: { name: boolean; description: boolean }; 7 | } 8 | 9 | const AssistantForm: React.FC = ({ error }) => { 10 | const { watch, setValue } = useFormContext(); 11 | const name = watch('name'); 12 | const description = watch('description'); 13 | 14 | return ( 15 | <> 16 | 17 | setValue('name', e.target.value)} 24 | error={error.name} 25 | helperText={error.name ? 'Name is required' : ' '} 26 | /> 27 | 28 | 29 | setValue('description', e.target.value)} 37 | error={error.description} 38 | helperText={error.description ? 'Description is required' : ' '} 39 | /> 40 | 41 | 42 | ); 43 | }; 44 | export default AssistantForm; 45 | -------------------------------------------------------------------------------- /app/components/Assistant/ConfirmationDialog.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Dialog, 4 | DialogActions, 5 | DialogContent, 6 | DialogContentText, 7 | DialogTitle, 8 | Button, 9 | } from '@mui/material'; 10 | 11 | interface ConfirmationDialogProps { 12 | open: boolean; 13 | onClose: () => void; 14 | onConfirm: () => void; 15 | } 16 | 17 | const ConfirmationDialog: React.FC = ({ 18 | open, 19 | onClose, 20 | onConfirm, 21 | }) => ( 22 | 23 | {'Confirm Delete'} 24 | 25 | 26 | Are you sure you want to delete your Assistant? All associated Threads, 27 | Messages, and Files will also be deleted. 28 | 29 | 30 | 31 | 34 | 37 | 38 | 39 | ); 40 | 41 | export default ConfirmationDialog; 42 | -------------------------------------------------------------------------------- /app/components/Assistant/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import { 3 | Dialog, 4 | DialogTitle, 5 | DialogContent, 6 | DialogActions, 7 | Button, 8 | Switch, 9 | Typography, 10 | Box, 11 | } from '@mui/material'; 12 | import AssistantForm from './AssistantForm'; 13 | import AssistantFileList from './AssistantFileList'; 14 | import ConfirmationDialog from './ConfirmationDialog'; 15 | import { useSession } from 'next-auth/react'; 16 | import { 17 | updateAssistant, 18 | deleteAssistantFile, 19 | deleteAssistant, 20 | uploadFile, 21 | } from '@/app/services/assistantService'; 22 | import { retrieveServices } from '@/app/services/commonService'; 23 | import { useFormContext } from 'react-hook-form'; 24 | interface AssistantDialogProps { 25 | open: boolean; 26 | onClose: () => void; 27 | onToggleAssistant?: (isAssistantEnabled: boolean) => void; 28 | onReset?: () => void; 29 | } 30 | 31 | const AssistantDialog: React.FC = ({ 32 | open, 33 | onClose, 34 | onToggleAssistant, 35 | onReset, 36 | }) => { 37 | const { data: session } = useSession(); 38 | const [error, setError] = useState<{ name: boolean; description: boolean }>({ 39 | name: false, 40 | description: false, 41 | }); 42 | const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false); 43 | const fileInputRef = useRef(null); 44 | 45 | const { getValues, setValue, watch } = useFormContext(); 46 | 47 | const name = getValues('name'); 48 | const description = getValues('description'); 49 | const isAssistantEnabled = watch('isAssistantEnabled'); 50 | const isAssistantDefined = watch('isAssistantDefined'); 51 | const files = watch('assistantFiles'); 52 | 53 | const handleToggle = (event: React.ChangeEvent) => { 54 | const enabled = event.target.checked; 55 | setValue('isAssistantEnabled', enabled); 56 | if (enabled) { 57 | setValue('isVisionEnabled', false); 58 | setValue('isRagEnabled', false); 59 | } 60 | 61 | if (onToggleAssistant) { 62 | onToggleAssistant(enabled); 63 | } 64 | }; 65 | 66 | const handleCreate = async () => { 67 | let hasError = false; 68 | if (!name) { 69 | setError((prev) => ({ ...prev, name: true })); 70 | hasError = true; 71 | } 72 | if (!description) { 73 | setError((prev) => ({ ...prev, description: true })); 74 | hasError = true; 75 | } 76 | if (hasError) { 77 | return; 78 | } else { 79 | setError({ name: false, description: false }); 80 | } 81 | 82 | try { 83 | setValue('isLoading', true); 84 | if (session) { 85 | const userEmail = session.user?.email as string; 86 | const updateAssistantResponse = await updateAssistant({ 87 | name, 88 | description, 89 | isAssistantEnabled, 90 | userEmail, 91 | files, 92 | }); 93 | setValue('isAssistantDefined', true); 94 | console.log('Assistant updated successfully', updateAssistantResponse); 95 | } else { 96 | throw new Error('No session found'); 97 | } 98 | } catch (error) { 99 | console.error('Error updating assistant: ', error); 100 | } finally { 101 | setValue('isLoading', false); 102 | } 103 | }; 104 | 105 | const handleUploadClick = () => { 106 | fileInputRef.current?.click(); 107 | }; 108 | 109 | const handleCloseClick = async () => { 110 | try { 111 | onClose(); 112 | setValue('isLoading', true); 113 | if (isAssistantDefined) { 114 | const userEmail = session?.user?.email as string; 115 | const retrieveAssistantResponse = await retrieveServices({ 116 | userEmail, 117 | serviceName: 'assistant', 118 | }); 119 | setValue('name', retrieveAssistantResponse.assistant.name); 120 | setValue( 121 | 'description', 122 | retrieveAssistantResponse.assistant.instructions 123 | ); 124 | setValue( 125 | 'isAssistantEnabled', 126 | retrieveAssistantResponse.isAssistantEnabled 127 | ); 128 | } 129 | } catch (error) { 130 | console.error('Failed to close assistant dialog: ', error); 131 | } finally { 132 | setValue('isLoading', false); 133 | } 134 | }; 135 | 136 | const handleReset = () => { 137 | setValue('name', ''); 138 | setValue('description', ''); 139 | setValue('isAssistantEnabled', false); 140 | setError({ name: false, description: false }); 141 | if (onReset) { 142 | onReset(); 143 | } 144 | }; 145 | 146 | const handleFileDelete = async (file: string) => { 147 | try { 148 | setValue('isLoading', true); 149 | await deleteAssistantFile({ file }); 150 | console.log('File successfully deleted from the assistant:', file); 151 | files.splice(files.indexOf(file), 1); 152 | } catch (error) { 153 | console.error('Failed to remove file from the assistant: ', error); 154 | } finally { 155 | setValue('isLoading', false); 156 | } 157 | }; 158 | 159 | const handleFileSelect = async ( 160 | event: React.ChangeEvent 161 | ) => { 162 | const file = event.target.files?.[0]; 163 | if (file) { 164 | const userEmail = session?.user?.email as string; 165 | try { 166 | setValue('isLoading', true); 167 | const fileUploadResponse = await uploadFile(file, userEmail); 168 | const retrieveAssistantResponse = await retrieveServices({ 169 | userEmail, 170 | serviceName: 'assistant', 171 | }); 172 | if (retrieveAssistantResponse.assistant) { 173 | setValue('assistantFiles', retrieveAssistantResponse.fileList); 174 | } 175 | if (fileUploadResponse?.status === 200) { 176 | console.log('File uploaded successfully: ', fileUploadResponse); 177 | } 178 | } catch (error) { 179 | console.error('Failed to upload file:', error); 180 | } finally { 181 | setValue('isLoading', false); 182 | } 183 | } 184 | }; 185 | 186 | const handleAssistantDelete = () => { 187 | setIsConfirmDialogOpen(true); 188 | }; 189 | 190 | const performAssistantDelete = async () => { 191 | setIsConfirmDialogOpen(false); 192 | const userEmail = session?.user?.email as string; 193 | try { 194 | setValue('isLoading', true); 195 | await deleteAssistant({ userEmail }); 196 | console.log('Assistant deleted successfully'); 197 | files.splice(0, files.length); 198 | handleReset(); 199 | setValue('isAssistantDefined', false); 200 | } catch (error) { 201 | console.error('Error deleting assistant: ', error); 202 | } finally { 203 | setValue('isLoading', false); 204 | } 205 | }; 206 | 207 | return ( 208 | 209 | 210 | {!isAssistantDefined 211 | ? 'Create Assistant' 212 | : `Assistant Settings: ${name}`} 213 | 214 | 215 | 216 | 217 | 226 | 236 | 237 | 238 | 244 | 245 | 246 | 249 | 250 | Disable 251 | 252 | 258 | 259 | Enable 260 | 261 | 267 | 268 | 269 | 270 | 271 | setIsConfirmDialogOpen(false)} 274 | onConfirm={performAssistantDelete} 275 | /> 276 | 277 | ); 278 | }; 279 | 280 | export default AssistantDialog; 281 | -------------------------------------------------------------------------------- /app/components/Chat/index.module.css: -------------------------------------------------------------------------------- 1 | .inputArea { 2 | display: flex; 3 | align-items: center; 4 | margin-top: auto; 5 | } 6 | 7 | .loginPrompt { 8 | text-align: center; 9 | margin-top: 20px; 10 | font-size: 18px; 11 | color: #666; /* or any color that suits your design */ 12 | } 13 | -------------------------------------------------------------------------------- /app/components/Chat/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { useFormContext } from 'react-hook-form'; 5 | import { useSession } from 'next-auth/react'; 6 | import MessagesField from '../MessagesField'; 7 | import styles from './index.module.css'; 8 | import Loader from '../Loader'; 9 | import CustomizedInputBase from '../CustomizedInputBase'; 10 | import { useMessageProcessing } from '@/app/hooks'; 11 | 12 | const Chat = () => { 13 | const { data: session } = useSession(); 14 | const { watch } = useFormContext(); 15 | const isLoading = watch('isLoading'); 16 | const { messages, sendUserMessage } = useMessageProcessing(session); 17 | 18 | if (session) { 19 | return ( 20 | <> 21 | {isLoading && } 22 | 23 |
24 | 25 |
26 | 27 | ); 28 | } 29 | return ( 30 |
31 |

Please sign in to access the chat.

32 |
33 | ); 34 | }; 35 | export default Chat; 36 | -------------------------------------------------------------------------------- /app/components/CustomizedInputBase/index.tsx: -------------------------------------------------------------------------------- 1 | import Paper from '@mui/material/Paper'; 2 | import InputBase from '@mui/material/InputBase'; 3 | import IconButton from '@mui/material/IconButton'; 4 | import MenuIcon from '@mui/icons-material/Menu'; 5 | import SendIcon from '@mui/icons-material/Send'; 6 | import Menu from '@mui/material/Menu'; 7 | import MenuItem from '@mui/material/MenuItem'; 8 | import ListItemIcon from '@mui/material/ListItemIcon'; 9 | import LongTermMemoryIcon from '@mui/icons-material/Psychology'; 10 | import AssistantIcon from '@mui/icons-material/Assistant'; 11 | import VisionIcon from '@mui/icons-material/Visibility'; 12 | import RagIcon from '@mui/icons-material/Storage'; 13 | import RecordVoiceOver from '@mui/icons-material/RecordVoiceOver'; 14 | import { useTheme } from '@mui/material/styles'; 15 | import useMediaQuery from '@mui/material/useMediaQuery'; 16 | import { useCustomInput } from '@/app/hooks/useCustomInput'; 17 | import AssistantDialog from '../Assistant'; 18 | import VisionDialog from '../Vision'; 19 | import SpeechDialog from '../Speech/tts'; 20 | import { Microphone } from '../Speech/stt'; 21 | import RagDialog from '../Rag'; 22 | import LongTermMemoryDialog from '../LongTermMemory'; 23 | 24 | const CustomizedInputBase = ({ 25 | onSendMessage, 26 | }: { 27 | onSendMessage: (message: string) => Promise; 28 | }) => { 29 | const theme = useTheme(); 30 | const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); 31 | 32 | const { 33 | inputValue, 34 | appendText, 35 | handleInputChange, 36 | handleSendClick, 37 | isDialogOpen, 38 | toggleDialog, 39 | handleMenuOpen, 40 | handleMenuClose, 41 | anchorEl, 42 | } = useCustomInput({ onSendMessage }); 43 | 44 | return ( 45 | <> 46 | { 55 | if (event.key === 'Enter') { 56 | event.preventDefault(); 57 | handleSendClick(); 58 | } 59 | }} 60 | > 61 | 62 | 67 | 68 | 69 | 82 | toggleDialog('memory')}> 83 | 84 | 85 | 86 | Memory 87 | 88 | toggleDialog('rag')}> 89 | 90 | 91 | 92 | R.A.G. 93 | 94 | toggleDialog('assistant')}> 95 | 96 | 97 | 98 | Assistant 99 | 100 | toggleDialog('vision')}> 101 | 102 | 103 | 104 | Vision 105 | 106 | toggleDialog('speech')}> 107 | 108 | 109 | 110 | Speech 111 | 112 | 113 | 119 | 125 | 126 | 127 | 128 | 129 | toggleDialog('memory')} 132 | /> 133 | 134 | toggleDialog('assistant')} 137 | /> 138 | 139 | toggleDialog('vision')} 142 | /> 143 | 144 | toggleDialog('speech')} 147 | /> 148 | 149 | toggleDialog('rag')} /> 150 | 151 | ); 152 | }; 153 | 154 | export default CustomizedInputBase; 155 | -------------------------------------------------------------------------------- /app/components/FileList/index.tsx: -------------------------------------------------------------------------------- 1 | // FilePaper.js 2 | import React from 'react'; 3 | import { List, Paper, Typography, Box } from '@mui/material'; 4 | 5 | interface FilePaperProps { 6 | files: any[]; 7 | renderFileItem: (file: any) => React.ReactNode; 8 | } 9 | 10 | const FileList: React.FC = ({ files, renderFileItem }) => ( 11 | 12 | 13 | Attached Files 14 | 15 | 16 | {files.map((file) => renderFileItem(file))} 17 | 18 | 19 | ); 20 | 21 | export default FileList; 22 | -------------------------------------------------------------------------------- /app/components/Loader/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, CircularProgress } from '@mui/material'; 3 | 4 | const Loader: React.FC = () => { 5 | return ( 6 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default Loader; 20 | -------------------------------------------------------------------------------- /app/components/LongTermMemory/LongTermMemoryForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useFormContext } from 'react-hook-form'; 3 | import { FormControl, InputLabel, Select, MenuItem, Box } from '@mui/material'; 4 | 5 | interface LongTermMemoryProps { 6 | error: { memoryType: boolean; historyLength: boolean }; 7 | } 8 | 9 | const LongTermMemoryForm: React.FC = ({ error }) => { 10 | const { watch, setValue } = useFormContext(); 11 | const memoryType = watch('memoryType'); 12 | const historyLength = watch('historyLength'); 13 | 14 | return ( 15 | 16 | 17 | Memory Type 18 | 29 | 30 | 31 | History Length 32 | 46 | 47 | 48 | ); 49 | }; 50 | 51 | export default LongTermMemoryForm; 52 | -------------------------------------------------------------------------------- /app/components/LongTermMemory/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from 'react'; 2 | import { 3 | Dialog, 4 | DialogTitle, 5 | DialogActions, 6 | Button, 7 | Switch, 8 | Typography, 9 | Box, 10 | DialogContent, 11 | } from '@mui/material'; 12 | import { useSession } from 'next-auth/react'; 13 | import { useFormContext } from 'react-hook-form'; 14 | 15 | import { retrieveServices } from '@/app/services/commonService'; 16 | import { updateSettings } from '@/app/services/longTermMemoryService'; 17 | import LongTermMemoryForm from './LongTermMemoryForm'; 18 | 19 | interface LongTermMemoryDialogProps { 20 | open: boolean; 21 | onClose: () => void; 22 | onToggleLongTermMemory?: (isLongTermMemoryEnabled: boolean) => void; 23 | } 24 | 25 | const LongTermMemoryDialog: React.FC = ({ 26 | open, 27 | onClose, 28 | onToggleLongTermMemory, 29 | }) => { 30 | const { data: session } = useSession(); 31 | const [error, setError] = useState<{ 32 | memoryType: boolean; 33 | historyLength: boolean; 34 | }>({ 35 | memoryType: false, 36 | historyLength: false, 37 | }); 38 | const longTermMemoryInputFileRef = useRef(null); 39 | const { getValues, setValue, watch } = useFormContext(); 40 | 41 | const isLongTermMemoryEnabled = watch('isLongTermMemoryEnabled'); 42 | const memoryType = getValues('memoryType'); 43 | const historyLength = getValues('historyLength'); 44 | 45 | const handleToggle = (event: React.ChangeEvent) => { 46 | const enabled = event.target.checked; 47 | setValue('isLongTermMemoryEnabled', enabled); 48 | 49 | if (onToggleLongTermMemory) { 50 | onToggleLongTermMemory(enabled); 51 | } 52 | }; 53 | 54 | const handleCloseClick = async () => { 55 | try { 56 | onClose(); 57 | setValue('isLoading', true); 58 | const userEmail = session?.user?.email as string; 59 | const retrieveLongTermMemoryResponse = await retrieveServices({ 60 | userEmail, 61 | serviceName: 'memory', 62 | }); 63 | setValue( 64 | 'isLongTermMemoryEnabled', 65 | retrieveLongTermMemoryResponse.isLongTermMemoryEnabled 66 | ); 67 | setValue('memoryType', retrieveLongTermMemoryResponse.memoryType); 68 | setValue('historyLength', retrieveLongTermMemoryResponse.historyLength); 69 | } catch (error) { 70 | console.error('Failed to close Long term memory dialog: ', error); 71 | } finally { 72 | setValue('isLoading', false); 73 | } 74 | }; 75 | 76 | const handleUpdate = async () => { 77 | let hasError = false; 78 | if (!memoryType) { 79 | setError((prev) => ({ ...prev, memoryType: true })); 80 | hasError = true; 81 | } 82 | if (!historyLength) { 83 | setError((prev) => ({ ...prev, historyLength: true })); 84 | hasError = true; 85 | } 86 | if (hasError) { 87 | return; 88 | } else { 89 | setError({ memoryType: false, historyLength: false }); 90 | } 91 | try { 92 | setValue('isLoading', true); 93 | if (session) { 94 | const userEmail = session.user?.email as string; 95 | const updateSettingsResponse = await updateSettings({ 96 | isLongTermMemoryEnabled, 97 | userEmail, 98 | memoryType, 99 | historyLength, 100 | }); 101 | console.log( 102 | 'Long term memory updated successfully', 103 | updateSettingsResponse 104 | ); 105 | } else { 106 | throw new Error('No session found'); 107 | } 108 | } catch (error) { 109 | console.error('Error updating Long term memory: ', error); 110 | } finally { 111 | setValue('isLoading', false); 112 | } 113 | }; 114 | 115 | return ( 116 | 117 | 118 | Long Term Memory Settings 119 | 120 | 121 | 122 | 131 | 132 | 133 | 139 | 140 | 141 | 142 | Disable 143 | 144 | 149 | 150 | Enable 151 | 152 | 157 | 158 | 159 | 160 | 161 | ); 162 | }; 163 | 164 | export default LongTermMemoryDialog; 165 | -------------------------------------------------------------------------------- /app/components/MessagesField/index.module.css: -------------------------------------------------------------------------------- 1 | .messagesContainer { 2 | width: 90%; 3 | max-height: 75vh; 4 | overflow-y: auto; 5 | padding: 20px; 6 | margin-bottom: 1rem; 7 | /* Add padding on the left to make space for the numbers */ 8 | padding-left: 40px; 9 | } 10 | 11 | /* You may need to adjust the width of the messages to ensure they fit within the new padding */ 12 | .userMessage, 13 | .aiMessage { 14 | background-color: #f0f0f0; 15 | padding: 10px; 16 | border-radius: 10px; 17 | margin-bottom: 10px; 18 | color: black; 19 | width: calc(100% - 20px); /* Adjust width to account for padding */ 20 | } 21 | 22 | .aiMessage { 23 | background-color: #d1e8ff; 24 | } 25 | 26 | /* Ensure list items are not indented too far */ 27 | .messagesContainer ol, 28 | .messagesContainer ul { 29 | padding-left: 20px; /* Adjust as needed */ 30 | } 31 | 32 | /* Ensure list numbers appear outside the content area */ 33 | .messagesContainer ol { 34 | list-style-position: outside; 35 | } 36 | -------------------------------------------------------------------------------- /app/components/MessagesField/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import ReactMarkdown from 'react-markdown'; 3 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; 4 | import styles from './index.module.css'; 5 | 6 | interface MessagesFieldProps { 7 | messages: IMessage[]; 8 | } 9 | 10 | const CodeBlock: React.FC<{ 11 | className?: string; 12 | children?: React.ReactNode; 13 | }> = ({ className, children }) => { 14 | const match = /language-(\w+)/.exec(className ?? ''); 15 | return match ? ( 16 | 17 | {String(children)} 18 | 19 | ) : ( 20 | {children} 21 | ); 22 | }; 23 | 24 | const MessagesField: React.FC = ({ messages }) => { 25 | useEffect(() => { 26 | const container = document.querySelector(`.${styles.messagesContainer}`); 27 | if (container) { 28 | container.scrollTop = container.scrollHeight; 29 | } 30 | }, [messages]); 31 | 32 | return ( 33 |
34 | {messages.map((message) => ( 35 |
41 | {/* @ts-ignore ReactMarkdown doesn't have types */} 42 | 43 | {`${message.sender === 'user' ? '🧑' : '🤖'} ${message.text}`} 44 | 45 |
46 | ))} 47 |
48 | ); 49 | }; 50 | 51 | export default MessagesField; 52 | -------------------------------------------------------------------------------- /app/components/Rag/RagFileList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | ListItem, 4 | ListItemAvatar, 5 | ListItemText, 6 | IconButton, 7 | Avatar, 8 | } from '@mui/material'; 9 | import DeleteIcon from '@mui/icons-material/Delete'; 10 | import FolderIcon from '@mui/icons-material/Folder'; 11 | import ProcessIcon from '@mui/icons-material/Assignment'; 12 | import DownloadDoneIcon from '@mui/icons-material/DownloadDone'; 13 | import FilePaper from '../FileList'; 14 | 15 | interface RagFileListProps { 16 | files: { name: string; id: string; ragId: string }[]; 17 | onDelete: (file: RagFile) => void; 18 | onProcess: (file: RagFile) => void; 19 | } 20 | 21 | const RagFileList: React.FC = ({ 22 | files, 23 | onDelete, 24 | onProcess, 25 | }) => { 26 | return ( 27 |
28 | ( 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 | onDelete(file)} 43 | > 44 | 45 | 46 | onProcess(file)} 50 | disabled={file.processed} 51 | > 52 | {file.processed ? : } 53 | 54 |
55 |
56 | )} 57 | /> 58 |
59 | ); 60 | }; 61 | 62 | export default RagFileList; 63 | -------------------------------------------------------------------------------- /app/components/Rag/RagForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useFormContext } from 'react-hook-form'; 3 | import { FormControl, InputLabel, Select, MenuItem, Box } from '@mui/material'; 4 | 5 | interface RagFormProps { 6 | error: { 7 | topK: boolean; 8 | chunkSize: boolean; 9 | chunkBatch: boolean; 10 | parsingStrategy: boolean; 11 | }; 12 | } 13 | 14 | const RagForm: React.FC = ({ error }) => { 15 | const { watch, setValue } = useFormContext(); 16 | const topK = watch('topK'); 17 | const chunkSize = watch('chunkSize'); 18 | const chunkBatch = watch('chunkBatch'); 19 | const parsingStrategy = watch('parsingStrategy'); 20 | 21 | return ( 22 | 23 | 24 | Top K 25 | 39 | 40 | 41 | Chunk Size 42 | 55 | 56 | 57 | Batch Size 58 | 72 | 73 | 74 | 75 | Parsing Strategy 76 | 77 | 91 | 92 | 93 | ); 94 | }; 95 | 96 | export default RagForm; 97 | -------------------------------------------------------------------------------- /app/components/Rag/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from 'react'; 2 | import { 3 | Dialog, 4 | DialogTitle, 5 | DialogActions, 6 | Button, 7 | Switch, 8 | Typography, 9 | Box, 10 | DialogContent, 11 | } from '@mui/material'; 12 | import { useSession } from 'next-auth/react'; 13 | import { useFormContext } from 'react-hook-form'; 14 | 15 | import { retrieveServices } from '@/app/services/commonService'; 16 | import { 17 | updateRag, 18 | uploadRagFile, 19 | deleteRagFile, 20 | updateFileStatus, 21 | } from '@/app/services/ragService'; 22 | import RagFileList from './RagFileList'; 23 | import RagForm from './RagForm'; 24 | import { parseDocument } from '@/app/services/unstructuredService'; 25 | import { embedConversation } from '@/app/services/embeddingService'; 26 | import { 27 | upsertToVectorDb, 28 | deleteFileFromVectorDb, 29 | } from '@/app/services/vectorDbService'; 30 | 31 | interface RagDialogProps { 32 | open: boolean; 33 | onClose: () => void; 34 | onToggleRag?: (isRagEnabled: boolean) => void; 35 | } 36 | 37 | const RagDialog: React.FC = ({ 38 | open, 39 | onClose, 40 | onToggleRag, 41 | }) => { 42 | const { data: session } = useSession(); 43 | const [error, setError] = useState<{ 44 | topK: boolean; 45 | chunkSize: boolean; 46 | chunkBatch: boolean; 47 | parsingStrategy: boolean; 48 | }>({ 49 | topK: false, 50 | chunkSize: false, 51 | chunkBatch: false, 52 | parsingStrategy: false, 53 | }); 54 | const fileInputRef = useRef(null); 55 | const { getValues, setValue, watch } = useFormContext(); 56 | 57 | const isRagEnabled = watch('isRagEnabled'); 58 | const ragFiles = watch('ragFiles'); 59 | const topK = getValues('topK'); 60 | const chunkSize = getValues('chunkSize'); 61 | const chunkBatch = getValues('chunkBatch'); 62 | const parsingStrategy = getValues('parsingStrategy'); 63 | 64 | const handleToggle = (event: React.ChangeEvent) => { 65 | const enabled = event.target.checked; 66 | setValue('isRagEnabled', enabled); 67 | if (enabled) { 68 | setValue('isAssistantEnabled', false); 69 | setValue('isVisionEnabled', false); 70 | } 71 | 72 | if (onToggleRag) { 73 | onToggleRag(enabled); 74 | } 75 | }; 76 | 77 | const handleUploadClick = () => { 78 | fileInputRef.current?.click(); 79 | }; 80 | 81 | const handleCloseClick = async () => { 82 | try { 83 | onClose(); 84 | setValue('isLoading', true); 85 | const userEmail = session?.user?.email as string; 86 | const retrieveRagResponse = await retrieveServices({ 87 | userEmail, 88 | serviceName: 'rag', 89 | }); 90 | setValue('isRagEnabled', retrieveRagResponse.isRagEnabled); 91 | setValue('topK', retrieveRagResponse.topK); 92 | setValue('chunkSize', retrieveRagResponse.chunkSize); 93 | setValue('chunkBatch', retrieveRagResponse.chunkBatch); 94 | setValue('parsingStrategy', retrieveRagResponse.parsingStrategy); 95 | } catch (error) { 96 | console.error('Failed to close R.A.G. dialog: ', error); 97 | } finally { 98 | setValue('isLoading', false); 99 | } 100 | }; 101 | 102 | const handleUpdate = async () => { 103 | let hasError = false; 104 | if (!topK) { 105 | setError((prev) => ({ ...prev, topK: true })); 106 | hasError = true; 107 | } 108 | if (!chunkSize) { 109 | setError((prev) => ({ ...prev, chunkSize: true })); 110 | hasError = true; 111 | } 112 | if (!chunkBatch) { 113 | setError((prev) => ({ ...prev, chunkBatch: true })); 114 | hasError = true; 115 | } 116 | if (!parsingStrategy) { 117 | setError((prev) => ({ ...prev, parsingStrategy: true })); 118 | hasError = true; 119 | } 120 | if (hasError) { 121 | return; 122 | } else { 123 | setError({ 124 | topK: false, 125 | chunkSize: false, 126 | chunkBatch: false, 127 | parsingStrategy: false, 128 | }); 129 | } 130 | try { 131 | setValue('isLoading', true); 132 | if (session) { 133 | const userEmail = session.user?.email as string; 134 | const updateRagResponse = await updateRag({ 135 | isRagEnabled, 136 | userEmail, 137 | topK, 138 | chunkSize, 139 | chunkBatch, 140 | parsingStrategy, 141 | }); 142 | console.log('R.A.G. updated successfully: ', updateRagResponse); 143 | } else { 144 | throw new Error('No session found'); 145 | } 146 | } catch (error) { 147 | console.error('Error updating R.A.G.: ', error); 148 | return new Error('Error updating R.A.G.'); 149 | } finally { 150 | setValue('isLoading', false); 151 | } 152 | }; 153 | 154 | const handleFileSelect = async ( 155 | event: React.ChangeEvent 156 | ) => { 157 | const file = event.target.files?.[0]; 158 | if (file) { 159 | const userEmail = session?.user?.email as string; 160 | try { 161 | setValue('isLoading', true); 162 | const fileUploadResponse = await uploadRagFile(file, userEmail); 163 | if (fileUploadResponse?.status === 200) { 164 | console.log('File uploaded successfully: ', fileUploadResponse); 165 | const response = fileUploadResponse.file; 166 | 167 | const newFile = { 168 | id: response.id, 169 | ragId: response.ragId, 170 | name: response.name, 171 | path: response.path, 172 | type: response.purpose, 173 | processed: false, 174 | chunks: [], 175 | }; 176 | 177 | setValue('ragFiles', [...ragFiles, newFile]); 178 | } else { 179 | throw new Error('Failed to upload file to R.A.G.'); 180 | } 181 | } catch (error) { 182 | console.error('Failed to upload file: ', error); 183 | return new Error('Failed to upload file'); 184 | } finally { 185 | setValue('isLoading', false); 186 | } 187 | } 188 | }; 189 | 190 | const handleFileDelete = async (file: RagFile) => { 191 | try { 192 | setValue('isLoading', true); 193 | const userEmail = session?.user?.email; 194 | if (!userEmail) { 195 | throw new Error('No user found'); 196 | } 197 | console.log('Deletion process started for:', file); 198 | if (file.processed) { 199 | await deleteFileFromVectorDb(file, userEmail, chunkBatch); 200 | } 201 | await deleteRagFile({ file, userEmail }); 202 | ragFiles.splice(ragFiles.indexOf(file), 1); 203 | console.log('File successfully deleted from R.A.G.:', file); 204 | } catch (error) { 205 | console.error('Failed to remove file from the R.A.G.: ', error); 206 | return new Error('Failed to remove file from the R.A.G.'); 207 | } finally { 208 | setValue('isLoading', false); 209 | } 210 | }; 211 | 212 | const handleFileProcess = async (file: RagFile) => { 213 | try { 214 | setValue('isLoading', true); 215 | const userEmail = session?.user?.email; 216 | if (!userEmail) { 217 | throw new Error('No user found'); 218 | } 219 | console.log('Document processing started for: ', file.name); 220 | 221 | const parsedDocumentResponse = await parseDocument( 222 | file, 223 | chunkSize, 224 | parsingStrategy 225 | ); 226 | 227 | const embedConversationResponse = await embedConversation( 228 | parsedDocumentResponse.file, 229 | userEmail 230 | ); 231 | 232 | ragFiles[ragFiles.indexOf(file)].chunks = 233 | embedConversationResponse.chunks; 234 | 235 | const upsertToVectorDbResponse = await upsertToVectorDb( 236 | embedConversationResponse.embeddings, 237 | userEmail, 238 | chunkBatch 239 | ); 240 | 241 | if (upsertToVectorDbResponse.status === 200) { 242 | const updateFileStatusResponse = await updateFileStatus({ 243 | file, 244 | userEmail, 245 | }); 246 | ragFiles[ragFiles.indexOf(file)].processed = 247 | updateFileStatusResponse.file.processed; 248 | console.log('File processing completed successfully'); 249 | } else { 250 | throw new Error( 251 | 'Failed to process file', 252 | upsertToVectorDbResponse.status 253 | ); 254 | } 255 | } catch (error) { 256 | console.error('Failed to process file: ', error); 257 | throw new Error('Failed to process file'); 258 | } finally { 259 | setValue('isLoading', false); 260 | } 261 | }; 262 | 263 | return ( 264 | 269 | R.A.G. Settings 270 | 271 | 272 | 277 | 286 | 287 | 288 | 294 | 295 | 296 | 299 | 300 | Disable 301 | 302 | 307 | 308 | Enable 309 | 310 | 316 | 317 | 318 | 319 | 320 | ); 321 | }; 322 | 323 | export default RagDialog; 324 | -------------------------------------------------------------------------------- /app/components/Speech/stt/index.tsx: -------------------------------------------------------------------------------- 1 | import MicIcon from '@mui/icons-material/Mic'; 2 | import MicOffIcon from '@mui/icons-material/MicOff'; 3 | import { IconButton } from '@mui/material'; 4 | import { useFormContext } from 'react-hook-form'; 5 | import { getAudioTranscript } from '@/app/services/SpeechtoTextService'; 6 | import { useRecordVoice } from '@/app/hooks/useRecordVoice'; 7 | 8 | const Microphone = ({ 9 | onAppendText, 10 | }: { 11 | onAppendText: (text: string) => void; 12 | }) => { 13 | const { startRecording, stopRecording } = useRecordVoice(); 14 | const { setValue, watch } = useFormContext(); 15 | const isSpeechToTextEnabled = watch('isSpeechToTextEnabled'); 16 | async function handleMicrophoneClick(): Promise { 17 | setValue('isSpeechToTextEnabled', !isSpeechToTextEnabled); 18 | if (!isSpeechToTextEnabled) { 19 | startRecording(); 20 | } else { 21 | const blob = await stopRecording(); 22 | const transcriptText = await getAudioTranscript({ blob }); 23 | onAppendText(transcriptText); 24 | } 25 | } 26 | 27 | return ( 28 | 34 | {isSpeechToTextEnabled ? : } 35 | 36 | ); 37 | }; 38 | 39 | export { Microphone }; 40 | -------------------------------------------------------------------------------- /app/components/Speech/tts/SpeechForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useFormContext } from 'react-hook-form'; 3 | import { FormControl, InputLabel, Select, MenuItem, Box } from '@mui/material'; 4 | 5 | interface SpeechFormProps { 6 | error: { model: boolean; voice: boolean }; 7 | } 8 | 9 | const SpeechForm: React.FC = ({ error }) => { 10 | const { watch, setValue } = useFormContext(); 11 | const model = watch('model'); 12 | const voice = watch('voice'); 13 | 14 | return ( 15 | 16 | 17 | Model 18 | 29 | 30 | 31 | Voice 32 | 47 | 48 | 49 | ); 50 | }; 51 | 52 | export default SpeechForm; 53 | -------------------------------------------------------------------------------- /app/components/Speech/tts/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from 'react'; 2 | import { 3 | Dialog, 4 | DialogTitle, 5 | DialogActions, 6 | Button, 7 | Switch, 8 | Typography, 9 | Box, 10 | DialogContent, 11 | } from '@mui/material'; 12 | import { useSession } from 'next-auth/react'; 13 | import { useFormContext } from 'react-hook-form'; 14 | 15 | import { retrieveServices } from '@/app/services/commonService'; 16 | import { updateSpeech } from '@/app/services/textToSpeechService'; 17 | import SpeechForm from './SpeechForm'; 18 | 19 | interface SpeechDialogProps { 20 | open: boolean; 21 | onClose: () => void; 22 | onToggleSpeech?: (isTextToSpeechEnabled: boolean) => void; 23 | } 24 | 25 | const SpeechDialog: React.FC = ({ 26 | open, 27 | onClose, 28 | onToggleSpeech, 29 | }) => { 30 | const { data: session } = useSession(); 31 | const [error, setError] = useState<{ model: boolean; voice: boolean }>({ 32 | model: false, 33 | voice: false, 34 | }); 35 | const speechFileInputRef = useRef(null); 36 | const { getValues, setValue, watch } = useFormContext(); 37 | 38 | const isTextToSpeechEnabled = watch('isTextToSpeechEnabled'); 39 | const model = getValues('model'); 40 | const voice = getValues('voice'); 41 | 42 | const handleToggle = (event: React.ChangeEvent) => { 43 | const enabled = event.target.checked; 44 | setValue('isTextToSpeechEnabled', enabled); 45 | 46 | if (onToggleSpeech) { 47 | onToggleSpeech(enabled); 48 | } 49 | }; 50 | 51 | const handleCloseClick = async () => { 52 | try { 53 | onClose(); 54 | setValue('isLoading', true); 55 | const userEmail = session?.user?.email as string; 56 | const retrieveSpeechResponse = await retrieveServices({ 57 | userEmail, 58 | serviceName: 'speech', 59 | }); 60 | setValue( 61 | 'isTextToSpeechEnabled', 62 | retrieveSpeechResponse.isTextToSpeechEnabled 63 | ); 64 | setValue('model', retrieveSpeechResponse.model); 65 | setValue('voice', retrieveSpeechResponse.voice); 66 | } catch (error) { 67 | console.error('Failed to close speech dialog: ', error); 68 | } finally { 69 | setValue('isLoading', false); 70 | } 71 | }; 72 | 73 | const handleUpdate = async () => { 74 | let hasError = false; 75 | if (!model) { 76 | setError((prev) => ({ ...prev, model: true })); 77 | hasError = true; 78 | } 79 | if (!voice) { 80 | setError((prev) => ({ ...prev, voice: true })); 81 | hasError = true; 82 | } 83 | if (hasError) { 84 | return; 85 | } else { 86 | setError({ model: false, voice: false }); 87 | } 88 | try { 89 | setValue('isLoading', true); 90 | if (session) { 91 | const userEmail = session.user?.email as string; 92 | const updateSpeechResponse = await updateSpeech({ 93 | isTextToSpeechEnabled, 94 | userEmail, 95 | model, 96 | voice, 97 | }); 98 | console.log('Speech updated successfully', updateSpeechResponse); 99 | } else { 100 | throw new Error('No session found'); 101 | } 102 | } catch (error) { 103 | console.error('Error updating Vision: ', error); 104 | } finally { 105 | setValue('isLoading', false); 106 | } 107 | }; 108 | 109 | return ( 110 | 111 | Speech Settings 112 | 113 | 114 | 123 | 124 | 125 | 131 | 132 | 133 | 134 | Disable 135 | 136 | 141 | 142 | Enable 143 | 144 | 149 | 150 | 151 | 152 | 153 | ); 154 | }; 155 | 156 | export default SpeechDialog; 157 | -------------------------------------------------------------------------------- /app/components/Vision/AddUrlDialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | Dialog, 4 | DialogTitle, 5 | DialogContent, 6 | DialogActions, 7 | Button, 8 | TextField, 9 | Box, 10 | FormControl, 11 | } from '@mui/material'; 12 | import validator from 'validator'; // Import validator 13 | 14 | interface AddUrlDialogProps { 15 | open: boolean; 16 | onClose: () => void; 17 | onAddUrl: (url: string, name: string) => void; 18 | } 19 | 20 | const AddUrlDialog: React.FC = ({ 21 | open, 22 | onClose, 23 | onAddUrl, 24 | }) => { 25 | const [urlInput, setUrlInput] = useState(''); 26 | const [nameInput, setNameInput] = useState(''); 27 | const [error, setError] = useState({ url: false, name: false }); 28 | 29 | const handleAddUrl = () => { 30 | let hasError = false; 31 | 32 | if (!nameInput.trim()) { 33 | setError((prev) => ({ ...prev, name: true })); 34 | hasError = true; 35 | } 36 | 37 | if ( 38 | !urlInput.trim() || 39 | !validator.isURL(urlInput, { require_protocol: true }) 40 | ) { 41 | setError((prev) => ({ ...prev, url: true })); 42 | hasError = true; 43 | } 44 | 45 | if (hasError) return; 46 | 47 | onAddUrl(urlInput, nameInput); 48 | setError({ url: false, name: false }); 49 | onClose(); 50 | }; 51 | 52 | const handleClose = () => { 53 | setError({ name: false, url: false }); 54 | onClose(); 55 | }; 56 | 57 | return ( 58 | 59 | Add URL 60 | 61 | 67 | setNameInput(e.target.value)} 73 | error={error.name} 74 | helperText={error.name ? 'Name is required' : ' '} 75 | /> 76 | 77 | 83 | setUrlInput(e.target.value)} 89 | error={error.url} 90 | helperText={error.url ? 'A valid URL is required' : ' '} 91 | /> 92 | 93 | 94 | 95 | 96 | 99 | 100 | 101 | 102 | 103 | ); 104 | }; 105 | 106 | export default AddUrlDialog; 107 | -------------------------------------------------------------------------------- /app/components/Vision/VisionFileList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | ListItem, 4 | ListItemAvatar, 5 | ListItemText, 6 | IconButton, 7 | Avatar, 8 | } from '@mui/material'; 9 | import DeleteIcon from '@mui/icons-material/Delete'; 10 | import FolderIcon from '@mui/icons-material/Folder'; 11 | import FilePaper from '../FileList'; 12 | 13 | interface VisionFileListProps { 14 | files: { 15 | id: string; 16 | visionId: string; 17 | name: string; 18 | type: string; 19 | url: string; 20 | }[]; 21 | onDelete: (file: any) => void; 22 | } 23 | 24 | const VisionFileList: React.FC = ({ files, onDelete }) => { 25 | return ( 26 |
27 | ( 30 | onDelete(file)} 37 | > 38 | 39 | 40 | } 41 | > 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | )} 50 | /> 51 |
52 | ); 53 | }; 54 | 55 | export default VisionFileList; 56 | -------------------------------------------------------------------------------- /app/components/Vision/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from 'react'; 2 | import { 3 | Dialog, 4 | DialogTitle, 5 | DialogContent, 6 | DialogActions, 7 | Button, 8 | Switch, 9 | Typography, 10 | Box, 11 | } from '@mui/material'; 12 | import VisionFileList from './VisionFileList'; 13 | import AddUrlDialog from './AddUrlDialog'; 14 | import { useSession } from 'next-auth/react'; 15 | import { 16 | updateVision, 17 | addVisionUrl, 18 | deleteVisionFile, 19 | } from '@/app/services/visionService'; 20 | import { retrieveServices } from '@/app/services/commonService'; 21 | import { useFormContext } from 'react-hook-form'; 22 | interface VisionDialogProps { 23 | open: boolean; 24 | onClose: () => void; 25 | onToggleVision?: (isVisionEnabled: boolean) => void; 26 | } 27 | 28 | const VisionDialog: React.FC = ({ 29 | open, 30 | onClose, 31 | onToggleVision, 32 | }) => { 33 | const { data: session } = useSession(); 34 | const visionFileInputRef = useRef(null); 35 | const [isAddUrlDialogOpen, setIsAddUrlDialogOpen] = useState(false); 36 | 37 | const { setValue, watch } = useFormContext(); 38 | const isVisionEnabled = watch('isVisionEnabled'); 39 | const isVisionDefined = watch('isVisionDefined'); 40 | const visionFiles = watch('visionFiles'); 41 | 42 | const handleToggle = (event: React.ChangeEvent) => { 43 | const enabled = event.target.checked; 44 | setValue('isVisionEnabled', enabled); 45 | if (enabled) { 46 | setValue('isAssistantEnabled', false); 47 | setValue('isRagEnabled', false); 48 | } 49 | 50 | if (onToggleVision) { 51 | onToggleVision(enabled); 52 | } 53 | }; 54 | 55 | const handleAddUrlClick = () => { 56 | setIsAddUrlDialogOpen(true); 57 | }; 58 | 59 | const handleAddUrl = async (urlInput: string, nameInput: string) => { 60 | try { 61 | setValue('isLoading', true); 62 | const userEmail = session?.user?.email; 63 | if (!userEmail) { 64 | throw new Error('No user found'); 65 | } 66 | let id = crypto.randomUUID(); 67 | 68 | const newFile = { 69 | id: id, 70 | visionId: '', 71 | name: nameInput, 72 | type: 'url', 73 | url: urlInput, 74 | }; 75 | 76 | const response = await addVisionUrl({ userEmail, file: newFile }); 77 | if (response.status === 200) { 78 | newFile.visionId = response.file.visionId; 79 | const newVisionFiles = [...visionFiles, newFile]; 80 | setValue('visionFiles', newVisionFiles); 81 | await handleUpdate(); 82 | } else { 83 | throw new Error('Failed to add URL to Vision'); 84 | } 85 | } catch (error) { 86 | console.error('Failed to add URL to Vision: ', error); 87 | } finally { 88 | setValue('isLoading', false); 89 | } 90 | }; 91 | 92 | const handleCloseClick = async () => { 93 | try { 94 | onClose(); 95 | setValue('isLoading', true); 96 | if (isVisionDefined) { 97 | const userEmail = session?.user?.email as string; 98 | const retrieveVisionResponse = await retrieveServices({ 99 | userEmail, 100 | serviceName: 'vision', 101 | }); 102 | setValue('isVisionEnabled', retrieveVisionResponse.isVisionEnabled); 103 | } 104 | } catch (error) { 105 | console.error('Failed to close assistant dialog: ', error); 106 | } finally { 107 | setValue('isLoading', false); 108 | } 109 | }; 110 | 111 | const handleUpdate = async () => { 112 | try { 113 | setValue('isLoading', true); 114 | if (session) { 115 | const userEmail = session.user?.email as string; 116 | const updateVisionResponse = await updateVision({ 117 | isVisionEnabled, 118 | userEmail, 119 | }); 120 | setValue('isVisionDefined', true); 121 | console.log('Vision updated successfully', updateVisionResponse); 122 | } else { 123 | throw new Error('No session found'); 124 | } 125 | } catch (error) { 126 | console.error('Error updating Vision: ', error); 127 | } finally { 128 | setValue('isLoading', false); 129 | } 130 | }; 131 | 132 | const handleRemoveUrl = async (file: { 133 | id: string; 134 | visionId: string; 135 | name: string; 136 | type: string; 137 | url: string; 138 | }) => { 139 | try { 140 | setValue('isLoading', true); 141 | const userEmail = session?.user?.email; 142 | if (!userEmail) { 143 | throw new Error('No user found'); 144 | } 145 | const response = await deleteVisionFile(file, userEmail); 146 | console.log('File successfully deleted from Vision:', response); 147 | visionFiles.splice(visionFiles.indexOf(file), 1); 148 | } catch (error) { 149 | console.error('Failed to remove file from Vision: ', error); 150 | } finally { 151 | setValue('isLoading', false); 152 | } 153 | }; 154 | 155 | return ( 156 | <> 157 | 158 | 159 | Vision Settings 160 | 161 | 162 | 163 | 172 | 173 | 174 | 180 | 181 | 182 | 183 | 184 | Disable 185 | 186 | 191 | 192 | Enable 193 | 194 | 199 | 200 | 201 | 202 | 203 | 204 | setIsAddUrlDialogOpen(false)} 207 | onAddUrl={handleAddUrl} 208 | /> 209 | 210 | ); 211 | }; 212 | 213 | export default VisionDialog; 214 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/athrael-soju/Titanium/a38714fd41872b23bf656c7534ee458cae6651c6/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --max-width: 1100px; 3 | --border-radius: 12px; 4 | --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 5 | 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 6 | 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; 7 | 8 | --foreground-rgb: 0, 0, 0; 9 | --background-start-rgb: 214, 219, 220; 10 | --background-end-rgb: 255, 255, 255; 11 | 12 | --primary-glow: conic-gradient( 13 | from 180deg at 50% 50%, 14 | #16abff33 0deg, 15 | #0885ff33 55deg, 16 | #54d6ff33 120deg, 17 | #0071ff33 160deg, 18 | transparent 360deg 19 | ); 20 | --secondary-glow: radial-gradient( 21 | rgba(255, 255, 255, 1), 22 | rgba(255, 255, 255, 0) 23 | ); 24 | 25 | --tile-start-rgb: 239, 245, 249; 26 | --tile-end-rgb: 228, 232, 233; 27 | --tile-border: conic-gradient( 28 | #00000080, 29 | #00000040, 30 | #00000030, 31 | #00000020, 32 | #00000010, 33 | #00000010, 34 | #00000080 35 | ); 36 | 37 | --callout-rgb: 238, 240, 241; 38 | --callout-border-rgb: 172, 175, 176; 39 | --card-rgb: 180, 185, 188; 40 | --card-border-rgb: 131, 134, 135; 41 | } 42 | 43 | @media (prefers-color-scheme: dark) { 44 | :root { 45 | --foreground-rgb: 255, 255, 255; 46 | --background-start-rgb: 0, 0, 0; 47 | --background-end-rgb: 0, 0, 0; 48 | 49 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); 50 | --secondary-glow: linear-gradient( 51 | to bottom right, 52 | rgba(1, 65, 255, 0), 53 | rgba(1, 65, 255, 0), 54 | rgba(1, 65, 255, 0.3) 55 | ); 56 | 57 | --tile-start-rgb: 2, 13, 46; 58 | --tile-end-rgb: 2, 5, 19; 59 | --tile-border: conic-gradient( 60 | #ffffff80, 61 | #ffffff40, 62 | #ffffff30, 63 | #ffffff20, 64 | #ffffff10, 65 | #ffffff10, 66 | #ffffff80 67 | ); 68 | 69 | --callout-rgb: 20, 20, 20; 70 | --callout-border-rgb: 108, 108, 108; 71 | --card-rgb: 100, 100, 100; 72 | --card-border-rgb: 200, 200, 200; 73 | } 74 | } 75 | 76 | * { 77 | box-sizing: border-box; 78 | padding: 0; 79 | margin: 0; 80 | } 81 | 82 | html, 83 | body { 84 | max-width: 100vw; 85 | overflow-x: hidden; 86 | } 87 | 88 | body { 89 | color: rgb(var(--foreground-rgb)); 90 | background: linear-gradient( 91 | to bottom, 92 | transparent, 93 | rgb(var(--background-end-rgb)) 94 | ) 95 | rgb(var(--background-start-rgb)); 96 | } 97 | 98 | a { 99 | color: inherit; 100 | text-decoration: none; 101 | } 102 | 103 | @media (prefers-color-scheme: dark) { 104 | html { 105 | color-scheme: dark; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useMessageProcessing'; 2 | -------------------------------------------------------------------------------- /app/hooks/useChatForm.ts: -------------------------------------------------------------------------------- 1 | import { useForm } from 'react-hook-form'; 2 | 3 | interface ChatFormValues { 4 | name: string; 5 | description: string; 6 | model: string; 7 | voice: string; 8 | topK: string; 9 | chunkSize: string; 10 | chunkBatch: string; 11 | parsingStrategy: string; 12 | memoryType: string; 13 | historyLength: string; 14 | isAssistantEnabled: boolean; 15 | isAssistantDefined: boolean; 16 | isVisionEnabled: boolean; 17 | isVisionDefined: boolean; 18 | isTextToSpeechEnabled: boolean; 19 | isSpeechToTextEnabled: boolean; 20 | isRagEnabled: boolean; 21 | isLongTermMemoryEnabled: boolean; 22 | transcript: string; 23 | isLoading: boolean; 24 | assistantFiles: { name: string; id: string; assistandId: string }[]; 25 | visionFiles: { 26 | id: string; 27 | visionId: string; 28 | name: string; 29 | type: string; 30 | url: string; 31 | }[]; 32 | ragFiles: { 33 | id: string; 34 | ragId: string; 35 | name: string; 36 | type: string; 37 | processed: boolean; 38 | chunks: string[]; 39 | }[]; 40 | } 41 | 42 | export const useChatForm = () => { 43 | const formMethods = useForm({ 44 | defaultValues: { 45 | name: '', 46 | description: '', 47 | model: '', 48 | voice: '', 49 | topK: '', 50 | chunkSize: '', 51 | chunkBatch: '', 52 | parsingStrategy: '', 53 | memoryType: '', 54 | historyLength: '', 55 | isAssistantEnabled: false, 56 | isAssistantDefined: false, 57 | isVisionEnabled: false, 58 | isVisionDefined: false, 59 | isTextToSpeechEnabled: false, 60 | isSpeechToTextEnabled: false, 61 | isRagEnabled: false, 62 | isLongTermMemoryEnabled: false, 63 | transcript: '', 64 | isLoading: false, 65 | assistantFiles: [], 66 | visionFiles: [], 67 | ragFiles: [], 68 | }, 69 | }); 70 | 71 | return formMethods; 72 | }; 73 | -------------------------------------------------------------------------------- /app/hooks/useCustomInput.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'react'; 2 | import { useSession } from 'next-auth/react'; 3 | import { useFormContext } from 'react-hook-form'; 4 | import { retrieveServices } from '@/app/services/commonService'; 5 | 6 | interface UseCustomInputProps { 7 | onSendMessage: (message: string) => Promise; 8 | } 9 | 10 | export const useCustomInput = ({ onSendMessage }: UseCustomInputProps) => { 11 | const { data: session } = useSession(); 12 | const [inputValue, setInputValue] = useState(''); 13 | const [isDialogOpen, setIsDialogOpen] = useState({ 14 | assistant: false, 15 | vision: false, 16 | speech: false, 17 | rag: false, 18 | memory: false, 19 | }); 20 | 21 | const { setValue } = useFormContext(); 22 | const [anchorEl, setAnchorEl] = useState(null); 23 | 24 | const prefetchServices = useCallback(async () => { 25 | const userEmail = session?.user?.email as string; 26 | // Prefetch assistant data 27 | let response = await retrieveServices({ 28 | userEmail, 29 | serviceName: 'assistant', 30 | }); 31 | if (response.assistant) { 32 | setValue('name', response.assistant.name); 33 | setValue('description', response.assistant.instructions); 34 | setValue('isAssistantEnabled', response.isAssistantEnabled); 35 | setValue('assistantFiles', response.fileList); 36 | setValue('isAssistantDefined', true); 37 | } else { 38 | setValue('isAssistantDefined', false); 39 | } 40 | // Prefetch vision data 41 | response = await retrieveServices({ 42 | userEmail, 43 | serviceName: 'vision', 44 | }); 45 | if (response.visionId) { 46 | setValue('isVisionEnabled', response.isVisionEnabled); 47 | setValue('visionFiles', response.visionFileList); 48 | setValue('isVisionDefined', true); 49 | } else { 50 | setValue('isVisionDefined', false); 51 | } 52 | // Prefetch speech data 53 | response = await retrieveServices({ 54 | userEmail, 55 | serviceName: 'speech', 56 | }); 57 | if (response.isTextToSpeechEnabled !== undefined) { 58 | setValue('isTextToSpeechEnabled', response.isTextToSpeechEnabled); 59 | setValue('model', response.model); 60 | setValue('voice', response.voice); 61 | } 62 | // Prefetch rag data 63 | response = await retrieveServices({ 64 | userEmail, 65 | serviceName: 'rag', 66 | }); 67 | if (response.ragId) { 68 | setValue('isRagEnabled', response.isRagEnabled); 69 | setValue('ragFiles', response.ragFileList); 70 | setValue('topK', response.topK); 71 | setValue('chunkSize', response.chunkSize); 72 | setValue('chunkBatch', response.chunkBatch); 73 | setValue('parsingStrategy', response.parsingStrategy); 74 | } 75 | // Prefetch long term memory data 76 | response = await retrieveServices({ 77 | userEmail, 78 | serviceName: 'memory', 79 | }); 80 | if (response.isLongTermMemoryEnabled !== undefined) { 81 | setValue('isLongTermMemoryEnabled', response.isLongTermMemoryEnabled); 82 | setValue('memoryType', response.memoryType); 83 | setValue('historyLength', response.historyLength); 84 | } 85 | }, [session?.user?.email, setValue]); 86 | useEffect(() => { 87 | prefetchServices(); 88 | }, [prefetchServices]); 89 | 90 | const handleInputChange = (event: React.ChangeEvent) => { 91 | setInputValue(event.target.value); 92 | }; 93 | 94 | const appendText = (text: string) => { 95 | setInputValue((prevValue) => `${prevValue} ${text}`.trim()); 96 | }; 97 | 98 | const handleSendClick = async () => { 99 | if (inputValue.trim()) { 100 | onSendMessage(inputValue); 101 | setInputValue(''); 102 | } 103 | }; 104 | 105 | const handleMenuOpen = (event: React.MouseEvent) => { 106 | setAnchorEl(event.currentTarget); 107 | }; 108 | 109 | const handleMenuClose = () => { 110 | setAnchorEl(null); 111 | }; 112 | 113 | const toggleDialog = (dialog: keyof typeof isDialogOpen) => { 114 | setIsDialogOpen((prev) => ({ ...prev, [dialog]: !prev[dialog] })); 115 | handleMenuClose(); 116 | }; 117 | 118 | return { 119 | inputValue, 120 | appendText, 121 | handleInputChange, 122 | handleSendClick, 123 | isDialogOpen, 124 | toggleDialog, 125 | handleMenuOpen, 126 | handleMenuClose, 127 | anchorEl, 128 | }; 129 | }; 130 | -------------------------------------------------------------------------------- /app/hooks/useMessageProcessing.ts: -------------------------------------------------------------------------------- 1 | import { useState, useRef } from 'react'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import { useFormContext } from 'react-hook-form'; 4 | import winkNLP from 'wink-nlp'; 5 | import model from 'wink-eng-lite-web-model'; 6 | import { 7 | retrieveAIResponse, 8 | retrieveTextFromSpeech, 9 | } from '@/app/services/chatService'; 10 | 11 | import { persistentMemoryUtils } from '@/app/utils/persistentMemoryUtils'; 12 | import { queryVectorDbByNamespace } from '@/app/services/vectorDbService'; 13 | import { embedConversation } from '@/app/services/embeddingService'; 14 | const nlp = winkNLP(model); 15 | 16 | export const useMessageProcessing = (session: any) => { 17 | const [messages, setMessages] = useState([]); 18 | const { watch, setValue } = useFormContext(); 19 | 20 | const isTextToSpeechEnabled = watch('isTextToSpeechEnabled'); 21 | const isAssistantEnabled = watch('isAssistantEnabled'); 22 | const isRagEnabled = watch('isRagEnabled'); 23 | const isLongTermMemoryEnabled = watch('isLongTermMemoryEnabled'); 24 | const model = watch('model'); 25 | const voice = watch('voice'); 26 | const topK = watch('topK'); 27 | const memoryType = watch('memoryType'); 28 | const historyLength = watch('historyLength'); 29 | const sentences = useRef([]); 30 | const sentenceIndex = useRef(0); 31 | 32 | const userEmail = session?.user?.email as string; 33 | 34 | const addUserMessageToState = (message: string) => { 35 | sentences.current = []; 36 | sentenceIndex.current = 0; 37 | const userMessageId = uuidv4(); 38 | 39 | const newIMessage: IMessage = { 40 | id: userMessageId, 41 | conversationId: session?.user?.email, 42 | sender: 'user', 43 | text: message, 44 | createdAt: new Date(), 45 | metadata: '', 46 | }; 47 | setMessages((prevMessages) => [...prevMessages, newIMessage]); 48 | return newIMessage; 49 | }; 50 | 51 | const addAiMessageToState = ( 52 | aiResponseText: string, 53 | aiResponseId: string 54 | ) => { 55 | const newIMessage: IMessage = { 56 | id: aiResponseId, 57 | conversationId: session?.user?.email, 58 | text: aiResponseText, 59 | sender: 'ai', 60 | createdAt: new Date(), 61 | }; 62 | 63 | setMessages((prevMessages) => [ 64 | ...prevMessages.filter((msg) => msg.id !== aiResponseId), 65 | newIMessage, 66 | ]); 67 | return newIMessage; 68 | }; 69 | 70 | const processAIResponseStream = async ( 71 | reader: ReadableStreamDefaultReader | undefined, 72 | aiResponseId: string 73 | ) => { 74 | if (!reader) { 75 | console.error( 76 | 'No reader available for processing the AI response stream.' 77 | ); 78 | return; 79 | } 80 | const decoder = new TextDecoder(); 81 | let buffer = ''; 82 | let aiResponseText = ''; 83 | let newMessage = {} as IMessage; 84 | const processChunk = async () => { 85 | const { done, value } = await reader.read(); 86 | 87 | if (done) { 88 | newMessage = (await processBuffer( 89 | buffer, 90 | aiResponseId, 91 | aiResponseText 92 | )) as IMessage; 93 | return true; 94 | } 95 | buffer += value ? decoder.decode(value, { stream: true }) : ''; 96 | await processBuffer(buffer, aiResponseId, aiResponseText); 97 | return false; 98 | }; 99 | 100 | let isDone = false; 101 | while (!isDone) { 102 | isDone = await processChunk(); 103 | } 104 | if (isLongTermMemoryEnabled) { 105 | persistentMemoryUtils.append(memoryType, newMessage, userEmail); 106 | } 107 | if (isTextToSpeechEnabled) { 108 | await retrieveTextFromSpeech( 109 | sentences.current[sentences.current.length - 1], 110 | model, 111 | voice 112 | ); 113 | } 114 | }; 115 | 116 | const processBuffer = async ( 117 | buffer: string, 118 | aiResponseId: string, 119 | aiResponseText: string 120 | ) => { 121 | let boundary = buffer.lastIndexOf('\n'); 122 | if (boundary === -1) return; 123 | 124 | let completeData = buffer.substring(0, boundary); 125 | completeData.split('\n').forEach((line) => { 126 | if (line) { 127 | try { 128 | const json = JSON.parse(line); 129 | if (json?.choices[0]?.delta?.content) { 130 | aiResponseText += json.choices[0].delta.content; 131 | } 132 | } catch (error) { 133 | console.error('Failed to parse JSON: ', line, error); 134 | } 135 | } 136 | }); 137 | 138 | const doc = nlp.readDoc(aiResponseText); 139 | sentences.current = doc.sentences().out(); 140 | const newMessage = addAiMessageToState(aiResponseText, aiResponseId); 141 | if (isTextToSpeechEnabled) { 142 | if (sentences.current.length > sentenceIndex.current + 1) { 143 | await retrieveTextFromSpeech( 144 | sentences.current[sentenceIndex.current++], 145 | model, 146 | voice 147 | ); 148 | } 149 | } 150 | return newMessage; 151 | }; 152 | 153 | const sendUserMessage = async (message: string) => { 154 | if (!message.trim()) return; 155 | 156 | try { 157 | setValue('isLoading', true); 158 | const newMessage = addUserMessageToState(message); 159 | // Append the user message to the long-term memory 160 | if (isLongTermMemoryEnabled) { 161 | persistentMemoryUtils.append(memoryType, newMessage, userEmail); 162 | } 163 | const aiResponseId = uuidv4(); 164 | // enhance the user message with context and history 165 | message = await enhanceMessage(message, newMessage, userEmail); 166 | const response = await retrieveAIResponse( 167 | message, 168 | userEmail, 169 | isAssistantEnabled 170 | ); 171 | 172 | if (!response) { 173 | return; 174 | } 175 | if (isAssistantEnabled) { 176 | await processResponse(response, aiResponseId); 177 | } else { 178 | await processStream(response, aiResponseId); 179 | } 180 | } catch (error) { 181 | console.error(error); 182 | } finally { 183 | setValue('isLoading', false); 184 | } 185 | }; 186 | 187 | async function enhanceMessage( 188 | message: string, 189 | newMessage: IMessage, 190 | userEmail: string 191 | ) { 192 | let augmentedMessage = `FOLLOW THESE INSTRUCTIONS AT ALL TIMES: 193 | 1. Make use of CONTEXT and HISTORY below, to briefly respond to the user prompt. 194 | 2. If you cannot find this information within the CONTEXT, or HISTORY, respond to the user prompt as best as you can. `; 195 | 196 | if (isRagEnabled) { 197 | // Augment the message with context. 198 | const ragContext = await enhanceUserResponse(message, userEmail); 199 | augmentedMessage += ` 200 | 201 | CONTEXT: 202 | ${ragContext || ''}`; 203 | } 204 | // Augment the message with the conversation history. 205 | if (isLongTermMemoryEnabled && parseInt(historyLength) > 0) { 206 | const conversationHistory = await persistentMemoryUtils.augment( 207 | historyLength, 208 | memoryType, 209 | message, 210 | newMessage, 211 | session 212 | ); 213 | augmentedMessage += ` 214 | 215 | HISTORY: 216 | ${conversationHistory || ''}`; 217 | } 218 | message = `${augmentedMessage} 219 | 220 | PROMPT: 221 | ${message} 222 | `; 223 | console.info('Enhanced message: ', message); 224 | return message; 225 | } 226 | 227 | async function enhanceUserResponse(message: string, userEmail: string) { 228 | const jsonMessage = [ 229 | { 230 | text: message, 231 | metadata: { 232 | user_email: userEmail, 233 | }, 234 | }, 235 | ]; 236 | 237 | const embeddedMessage = await embedConversation(jsonMessage, userEmail); 238 | 239 | const vectorResponse = await queryVectorDbByNamespace( 240 | embeddedMessage.embeddings, 241 | userEmail, 242 | topK 243 | ); 244 | 245 | return vectorResponse.context; 246 | } 247 | 248 | async function processResponse( 249 | response: ReadableStreamDefaultReader | Response, 250 | aiResponseId: string 251 | ) { 252 | if (!(response instanceof Response)) { 253 | console.error('Expected a Response object, received: ', response); 254 | return; 255 | } 256 | try { 257 | const contentType = response.headers.get('Content-Type'); 258 | const data = contentType?.includes('application/json') 259 | ? await response.json() 260 | : await response.text(); 261 | addAiMessageToState(data, aiResponseId); 262 | if (isTextToSpeechEnabled) { 263 | await retrieveTextFromSpeech(data, model, voice); 264 | } 265 | } catch (error) { 266 | console.error('Error processing response: ', error); 267 | } 268 | } 269 | async function processStream( 270 | stream: ReadableStreamDefaultReader | Response, 271 | aiResponseId: string 272 | ) { 273 | if (!(stream instanceof ReadableStreamDefaultReader)) { 274 | console.error( 275 | 'Expected a ReadableStreamDefaultReader object, received: ', 276 | stream 277 | ); 278 | return; 279 | } 280 | try { 281 | await processAIResponseStream(stream, aiResponseId); 282 | } catch (error) { 283 | console.error('Error processing stream: ', error); 284 | } 285 | } 286 | 287 | return { 288 | messages, 289 | sendUserMessage, 290 | }; 291 | }; 292 | -------------------------------------------------------------------------------- /app/hooks/useRecordVoice.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef } from 'react'; 2 | 3 | export const useRecordVoice = () => { 4 | const [mediaRecorder, setMediaRecorder] = useState( 5 | null 6 | ); 7 | 8 | const [recording, setRecording] = useState(false); 9 | 10 | const chunks = useRef([]); 11 | 12 | const startRecording = () => { 13 | if (mediaRecorder) { 14 | setRecording(true); 15 | mediaRecorder.start(); 16 | } 17 | }; 18 | 19 | const stopRecording = async () => { 20 | if (!mediaRecorder) { 21 | throw new Error('MediaRecorder not initialized'); 22 | } 23 | 24 | return new Promise((resolve, reject) => { 25 | mediaRecorder.ondataavailable = (event) => { 26 | setRecording(false); 27 | resolve(event.data); 28 | }; 29 | 30 | mediaRecorder.onerror = (error) => { 31 | reject(error); 32 | }; 33 | mediaRecorder.stop(); 34 | }); 35 | }; 36 | 37 | const initialMediaRecorder = (stream: MediaStream) => { 38 | const mediaRecorder = new MediaRecorder(stream); 39 | 40 | mediaRecorder.onstart = () => { 41 | chunks.current = []; 42 | }; 43 | 44 | mediaRecorder.ondataavailable = (ev) => { 45 | chunks.current.push(ev.data); 46 | }; 47 | 48 | mediaRecorder.onstop = () => { 49 | const audioBlob = new Blob(chunks.current, { type: 'audio/mp3' }); 50 | const audioUrl = URL.createObjectURL(audioBlob); 51 | return { audioUrl, audioBlob }; 52 | }; 53 | 54 | setMediaRecorder(mediaRecorder); 55 | }; 56 | 57 | useEffect(() => { 58 | if (typeof window !== 'undefined') { 59 | navigator.mediaDevices 60 | .getUserMedia({ audio: true }) 61 | .then(initialMediaRecorder); 62 | } 63 | }, []); 64 | 65 | return { recording, startRecording, stopRecording }; 66 | }; 67 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { Inter } from 'next/font/google'; 3 | import { SpeedInsights } from '@vercel/speed-insights/next'; 4 | import Providers from './providers'; 5 | import './globals.css'; 6 | 7 | const inter = Inter({ subsets: ['latin'] }); 8 | 9 | export const metadata: Metadata = { 10 | title: 'Titanium', 11 | description: 'The Super Simple Chat Template', 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: Readonly<{ 17 | children: React.ReactNode; 18 | }>) { 19 | return ( 20 | 21 | 22 | {children} 23 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /app/lib/client/mongodb.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb'; 2 | 3 | const uri = process.env.MONGODB_URI as string; 4 | const options = { 5 | tls: true, 6 | }; 7 | 8 | let client: MongoClient; 9 | 10 | const getClientPromise = async (): Promise => { 11 | let clientPromise: Promise; 12 | if (process.env.NODE_ENV === 'development') { 13 | let globalWithMongo = global as typeof globalThis & { 14 | _mongoClientPromise?: Promise; 15 | }; 16 | 17 | if (!globalWithMongo._mongoClientPromise) { 18 | client = new MongoClient(uri, options); 19 | globalWithMongo._mongoClientPromise = client.connect().catch((err) => { 20 | throw err; 21 | }); 22 | } 23 | clientPromise = globalWithMongo._mongoClientPromise; 24 | } else { 25 | client = new MongoClient(uri, options); 26 | try { 27 | clientPromise = client.connect(); 28 | } catch (error: any) { 29 | throw error; 30 | } 31 | } 32 | return clientPromise; 33 | }; 34 | const clientPromise = getClientPromise(); 35 | 36 | export default clientPromise; 37 | -------------------------------------------------------------------------------- /app/lib/client/pinecone.ts: -------------------------------------------------------------------------------- 1 | import { Pinecone } from '@pinecone-database/pinecone'; 2 | 3 | const apiKey = process.env.PINECONE_API as string, 4 | indexName = process.env.PINECONE_INDEX as string; 5 | 6 | const config = { 7 | apiKey, 8 | }; 9 | 10 | const pineconeClient = new Pinecone(config); 11 | 12 | const getClient = async () => { 13 | return pineconeClient; 14 | }; 15 | 16 | const getIndex = async () => { 17 | const client = await getClient(); 18 | return client.index(indexName); 19 | }; 20 | 21 | // Helper function to chunk the array into smaller arrays of a given size 22 | function chunkArray(array: any[], chunkSize: number): any[][] { 23 | const result: any[][] = []; 24 | for (let i = 0; i < array.length; i += chunkSize) { 25 | result.push(array.slice(i, i + chunkSize)); 26 | } 27 | return result; 28 | } 29 | 30 | const upsert = async (data: any[], user: IUser, chunkBatch: string) => { 31 | try { 32 | const index = await getIndex(); 33 | const chunkedData = chunkArray(data, parseInt(chunkBatch)); 34 | for (const chunk of chunkedData) { 35 | await index.namespace(user.ragId as string).upsert(chunk); 36 | } 37 | return { success: true }; 38 | } catch (error: any) { 39 | console.error('Error upserting in Pinecone: ', error); 40 | throw error; 41 | } 42 | }; 43 | 44 | const upsertOne = async (vectorMessage: any[], nameSpace: string) => { 45 | try { 46 | const index = await getIndex(); 47 | await index.namespace(nameSpace).upsert(vectorMessage); 48 | return { success: true }; 49 | } catch (error: any) { 50 | console.error('Error upserting in Pinecone: ', error); 51 | throw error; 52 | } 53 | }; 54 | 55 | const updateMetadata = async (id: string, metadata: any, nameSpace: string) => { 56 | try { 57 | const index = await getIndex(); 58 | await index.namespace(nameSpace).update({ id, metadata }); 59 | return { success: true }; 60 | } catch (error: any) { 61 | console.error('Error updating in Pinecone: ', error); 62 | throw error; 63 | } 64 | }; 65 | 66 | const queryByNamespace = async ( 67 | namespace: string, 68 | topK: string, 69 | embeddedMessage: any 70 | ) => { 71 | const index = await getIndex(); 72 | const result = await index.namespace(namespace).query({ 73 | topK: parseInt(topK), 74 | vector: embeddedMessage.values, 75 | includeValues: false, 76 | includeMetadata: true, 77 | //filter: { genre: { $eq: 'action' } }, 78 | }); 79 | return result; 80 | }; 81 | 82 | const deleteOne = async (id: string, user: IUser) => { 83 | const index = await getIndex(); 84 | const result = await index.namespace(user.ragId as string).deleteOne(id); 85 | console.log(result); 86 | return result; 87 | }; 88 | 89 | const deleteMany = async ( 90 | idList: string[], 91 | user: IUser, 92 | chunkBatch: string 93 | ) => { 94 | try { 95 | const index = await getIndex(); 96 | const chunkedIdList = chunkArray(idList, parseInt(chunkBatch)); 97 | for (const chunk of chunkedIdList) { 98 | await index.namespace(user.ragId as string).deleteMany(chunk); 99 | } 100 | return { success: true }; 101 | } catch (error: any) { 102 | console.error('Error deleting many in Pinecone: ', error); 103 | throw error; 104 | } 105 | }; 106 | 107 | const deleteAll = async (user: IUser) => { 108 | const index = await getIndex(); 109 | const result = await index.namespace(user.ragId as string).deleteAll(); 110 | return result; 111 | }; 112 | 113 | export const pinecone = { 114 | upsertOne, 115 | upsert, 116 | queryByNamespace, 117 | deleteOne, 118 | deleteMany, 119 | deleteAll, 120 | updateMetadata, 121 | }; 122 | -------------------------------------------------------------------------------- /app/lib/utils/db.ts: -------------------------------------------------------------------------------- 1 | import { Collection, Db } from 'mongodb'; 2 | import clientPromise from '@/app/lib/client/mongodb'; 3 | 4 | export const getDb = async (): Promise => { 5 | const client = await clientPromise; 6 | return client.db(); 7 | }; 8 | 9 | export const getUserByEmail = async ( 10 | usersCollection: Collection, 11 | email: string 12 | ): Promise => { 13 | return usersCollection.findOne({ email }); 14 | }; 15 | 16 | export async function getDatabaseAndUser( 17 | db: Db, 18 | userEmail: string 19 | ): Promise<{ user: IUser }> { 20 | if (!userEmail) { 21 | throw new Error('User email is required'); 22 | } 23 | 24 | const usersCollection = db.collection('users'); 25 | const user = await getUserByEmail(usersCollection, userEmail); 26 | 27 | if (!user) { 28 | throw new Error('User not found'); 29 | } 30 | 31 | return { user }; 32 | } 33 | 34 | export async function getConversation( 35 | db: Db, 36 | userEmail: string 37 | ): Promise<{ conversation: IConversation }> { 38 | if (!userEmail) { 39 | throw new Error('User email is required'); 40 | } 41 | 42 | const conversationCollection = db.collection('conversations'); 43 | const conversation = await getConversationByEmail( 44 | conversationCollection, 45 | userEmail 46 | ); 47 | 48 | return { conversation: conversation! }; 49 | } 50 | 51 | export const getConversationByEmail = async ( 52 | conversationCollection: Collection, 53 | userEmail: string 54 | ): Promise => { 55 | return conversationCollection.findOne({ id: userEmail }); 56 | }; 57 | 58 | export async function createConversation( 59 | conversation: IConversation, 60 | conversationCollection: Collection 61 | ): Promise { 62 | await conversationCollection.insertOne(conversation); 63 | } 64 | 65 | export async function updateConversationSettings( 66 | conversation: IConversation, 67 | conversationCollection: Collection, 68 | message: IMessage 69 | ): Promise { 70 | await conversationCollection.updateOne( 71 | { id: conversation.id }, 72 | { 73 | $set: { 74 | messages: [...conversation.messages, message], 75 | }, 76 | } 77 | ); 78 | } 79 | 80 | export async function updateMemorySettings( 81 | user: IUser, 82 | usersCollection: Collection, 83 | isLongTermMemoryEnabled: boolean, 84 | memoryType: string, 85 | historyLength: string 86 | ): Promise { 87 | await usersCollection.updateOne( 88 | { email: user.email }, 89 | { 90 | $set: { 91 | isLongTermMemoryEnabled: isLongTermMemoryEnabled, 92 | memoryType: memoryType, 93 | historyLength: historyLength, 94 | }, 95 | } 96 | ); 97 | } 98 | 99 | export async function getFormattedConversationHistory( 100 | historyLength: string, 101 | conversation: IConversation 102 | ) { 103 | try { 104 | // Check if the conversation has messages and filter out any null messages 105 | const messages = 106 | conversation.messages 107 | ?.filter((msg) => msg != null && msg.conversationId === conversation.id) 108 | ?.slice(0, parseInt(historyLength)) ?? []; 109 | // Filter out messages with null 'createdAt' and sort the rest by 'createdAt' in descending order 110 | const sortedMessages = messages 111 | .filter((msg) => msg.createdAt != null) 112 | .sort( 113 | (a, b) => 114 | new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() 115 | ); 116 | // Adjust slice to start from 0, and ensure parsing 'historyLength' to integer 117 | const recentMessages = sortedMessages 118 | .map( 119 | (msg) => 120 | `- Date: ${new Date(msg.createdAt).toISOString()}, Sender: ${ 121 | msg.sender === 'user' ? 'User' : 'AI' 122 | }, Message: ${msg.text}` 123 | ) 124 | .join('\n'); 125 | // Return the latest user message in the specified format 126 | return recentMessages; 127 | } catch (error) { 128 | console.error('Error retrieving conversation history:', error); 129 | throw error; // Rethrow or handle as needed 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /app/lib/utils/response.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | export const sendErrorResponse = ( 4 | message: string, 5 | status: number 6 | ): NextResponse => { 7 | console.error(message); 8 | return NextResponse.json({ message }, { status }); 9 | }; 10 | 11 | export const sendInformationResponse = ( 12 | message: string, 13 | status: number 14 | ): NextResponse => { 15 | if (status !== 202) { 16 | console.log(message); 17 | } 18 | return NextResponse.json({ message }, { status }); 19 | }; 20 | -------------------------------------------------------------------------------- /app/lib/utils/vectorDb.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RecordMetadata, 3 | ScoredPineconeRecord, 4 | } from '@pinecone-database/pinecone'; 5 | 6 | interface ConversationHistory { 7 | messages: string[]; 8 | } 9 | 10 | interface MetadataMatch { 11 | dateMatch: RegExpExecArray | null; 12 | messageMatch: RegExpExecArray | null; 13 | } 14 | 15 | const datePattern = /Date: (.*?\))./; 16 | const messagePattern = /Message: (.*)$/; 17 | 18 | function extractMatches(metadata: string): MetadataMatch { 19 | return { 20 | dateMatch: datePattern.exec(metadata), 21 | messageMatch: messagePattern.exec(metadata), 22 | }; 23 | } 24 | 25 | function formatMessage( 26 | type: 'ai' | 'user', 27 | dateMatch: RegExpExecArray, 28 | messageMatch: RegExpExecArray 29 | ): string { 30 | return `- Date: ${dateMatch[1]}, Sender: ${type.toUpperCase()}, Message: ${ 31 | messageMatch[1] 32 | }`; 33 | } 34 | 35 | function insertSorted(messages: string[], msg: string) { 36 | const match = datePattern.exec(msg); 37 | if (!match) return; 38 | 39 | const msgDate = new Date(match[1]); 40 | let i = messages.length - 1; 41 | while (i >= 0) { 42 | const currentMatch = datePattern.exec(messages[i]); 43 | if (!currentMatch) { 44 | i--; 45 | continue; 46 | } 47 | 48 | const currentMsgDate = new Date(currentMatch[1]); 49 | if (msgDate >= currentMsgDate) { 50 | break; 51 | } 52 | i--; 53 | } 54 | messages.splice(i + 1, 0, msg); 55 | } 56 | 57 | export async function getFormattedConversationHistory( 58 | conversationHistoryResults: ScoredPineconeRecord[] 59 | ): Promise { 60 | try { 61 | const messages: string[] = []; 62 | 63 | conversationHistoryResults.forEach((item) => { 64 | if (!item.metadata) return; 65 | 66 | const aiMetadata = item.metadata.ai as string; 67 | const userMetadata = item.metadata.user as string; 68 | 69 | const aiMatches = extractMatches(aiMetadata); 70 | const userMatches = extractMatches(userMetadata); 71 | 72 | if (aiMatches.dateMatch && aiMatches.messageMatch) { 73 | const aiMsg = formatMessage( 74 | 'ai', 75 | aiMatches.dateMatch, 76 | aiMatches.messageMatch 77 | ); 78 | insertSorted(messages, aiMsg); 79 | } 80 | 81 | if (userMatches.dateMatch && userMatches.messageMatch) { 82 | const userMsg = formatMessage( 83 | 'user', 84 | userMatches.dateMatch, 85 | userMatches.messageMatch 86 | ); 87 | insertSorted(messages, userMsg); 88 | } 89 | }); 90 | return { messages }; 91 | } catch (error) { 92 | console.error('Error retrieving conversation history:', error); 93 | throw error; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/models/Conversation.ts: -------------------------------------------------------------------------------- 1 | interface IConversation { 2 | id: string; 3 | messages: IMessage[]; 4 | metadata?: any; 5 | } 6 | -------------------------------------------------------------------------------- /app/models/File.ts: -------------------------------------------------------------------------------- 1 | interface VisionFile { 2 | visionId: string; 3 | name: string; 4 | type: string; 5 | url: string; 6 | createdAt: Date; 7 | } 8 | 9 | interface RagFile { 10 | name: string; 11 | path: string; 12 | ragId: string; 13 | id: string; 14 | purpose: string; 15 | processed: boolean; 16 | chunks: string[]; 17 | } 18 | -------------------------------------------------------------------------------- /app/models/Message.ts: -------------------------------------------------------------------------------- 1 | interface IMessage { 2 | id: string; 3 | conversationId: string; 4 | sender: 'user' | 'ai'; 5 | text: string; 6 | createdAt: Date; 7 | metadata?: any; 8 | } 9 | -------------------------------------------------------------------------------- /app/models/User.ts: -------------------------------------------------------------------------------- 1 | interface IUser { 2 | email: string; 3 | name: string; 4 | model: string; 5 | topK: string; 6 | chunkSize: string; 7 | chunkBatch: string; 8 | parsingStrategy: string; 9 | memoryType: string; 10 | historyLength: string; 11 | voice: string; 12 | description: string; 13 | assistantId?: string | null; 14 | threadId?: string | null; 15 | visionId?: string | null; 16 | ragId?: string | null; 17 | isAssistantEnabled: boolean; 18 | isVisionEnabled: boolean; 19 | isTextToSpeechEnabled: boolean; 20 | isRagEnabled: boolean; 21 | isLongTermMemoryEnabled: boolean; 22 | } 23 | -------------------------------------------------------------------------------- /app/page.module.css: -------------------------------------------------------------------------------- 1 | .description { 2 | display: inherit; 3 | justify-content: inherit; 4 | align-items: inherit; 5 | font-size: 0.85rem; 6 | max-width: var(--max-width); 7 | width: 100%; 8 | z-index: 2; 9 | font-family: var(--font-mono); 10 | } 11 | 12 | .description a { 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | gap: 0.5rem; 17 | } 18 | 19 | .description p { 20 | position: relative; 21 | margin: 0; 22 | padding: 1rem; 23 | background-color: rgba(var(--callout-rgb), 0.5); 24 | border: 1px solid rgba(var(--callout-border-rgb), 0.3); 25 | border-radius: var(--border-radius); 26 | } 27 | 28 | .code { 29 | font-weight: 700; 30 | font-family: var(--font-mono); 31 | } 32 | 33 | .grid { 34 | display: grid; 35 | grid-template-columns: repeat(4, minmax(25%, auto)); 36 | max-width: 100%; 37 | width: var(--max-width); 38 | } 39 | 40 | .card { 41 | padding: 1rem 1.2rem; 42 | border-radius: var(--border-radius); 43 | background: rgba(var(--card-rgb), 0); 44 | border: 1px solid rgba(var(--card-border-rgb), 0); 45 | transition: background 200ms, border 200ms; 46 | } 47 | 48 | .card span { 49 | display: inline-block; 50 | transition: transform 200ms; 51 | } 52 | 53 | .card h2 { 54 | font-weight: 600; 55 | margin-bottom: 0.7rem; 56 | } 57 | 58 | .card p { 59 | margin: 0; 60 | opacity: 0.6; 61 | font-size: 0.9rem; 62 | line-height: 1.5; 63 | max-width: 30ch; 64 | } 65 | 66 | .center { 67 | display: flex; 68 | justify-content: center; 69 | align-items: center; 70 | position: relative; 71 | padding: 4rem 0; 72 | } 73 | 74 | .center::before { 75 | background: var(--secondary-glow); 76 | border-radius: 50%; 77 | width: 480px; 78 | height: 360px; 79 | margin-left: -400px; 80 | } 81 | 82 | .center::after { 83 | background: var(--primary-glow); 84 | width: 240px; 85 | height: 180px; 86 | z-index: -1; 87 | } 88 | 89 | .center::before, 90 | .center::after { 91 | content: ''; 92 | left: 50%; 93 | position: absolute; 94 | filter: blur(45px); 95 | transform: translateZ(0); 96 | } 97 | 98 | .logo { 99 | position: relative; 100 | } 101 | /* Enable hover only on non-touch devices */ 102 | @media (hover: hover) and (pointer: fine) { 103 | .card:hover { 104 | background: rgba(var(--card-rgb), 0.1); 105 | border: 1px solid rgba(var(--card-border-rgb), 0.15); 106 | } 107 | 108 | .card:hover span { 109 | transform: translateX(4px); 110 | } 111 | } 112 | 113 | @media (prefers-reduced-motion) { 114 | .card:hover span { 115 | transform: none; 116 | } 117 | } 118 | 119 | /* Mobile */ 120 | @media (max-width: 700px) { 121 | .content { 122 | padding: 4rem; 123 | } 124 | 125 | .grid { 126 | grid-template-columns: 1fr; 127 | margin-bottom: 120px; 128 | max-width: 320px; 129 | text-align: center; 130 | } 131 | 132 | .card { 133 | padding: 1rem 2.5rem; 134 | } 135 | 136 | .card h2 { 137 | margin-bottom: 0.5rem; 138 | } 139 | 140 | .center { 141 | padding: 8rem 0 6rem; 142 | } 143 | 144 | .center::before { 145 | transform: none; 146 | height: 300px; 147 | } 148 | 149 | .description { 150 | font-size: 0.8rem; 151 | } 152 | 153 | .description a { 154 | padding: 1rem; 155 | } 156 | 157 | .description p, 158 | .description div { 159 | display: flex; 160 | justify-content: center; 161 | position: fixed; 162 | width: 100%; 163 | } 164 | 165 | .description p { 166 | align-items: center; 167 | inset: 0 0 auto; 168 | padding: 2rem 1rem 1.4rem; 169 | border-radius: 0; 170 | border: none; 171 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); 172 | background: linear-gradient( 173 | to bottom, 174 | rgba(var(--background-start-rgb), 1), 175 | rgba(var(--callout-rgb), 0.5) 176 | ); 177 | background-clip: padding-box; 178 | backdrop-filter: blur(24px); 179 | } 180 | 181 | .description div { 182 | align-items: flex-end; 183 | pointer-events: none; 184 | inset: auto 0 0; 185 | padding: 2rem; 186 | height: 200px; 187 | background: linear-gradient( 188 | to bottom, 189 | transparent 0%, 190 | rgb(var(--background-end-rgb)) 40% 191 | ); 192 | z-index: 1; 193 | } 194 | } 195 | 196 | /* Tablet and Smaller Desktop */ 197 | @media (min-width: 701px) and (max-width: 1120px) { 198 | .grid { 199 | grid-template-columns: repeat(2, 50%); 200 | } 201 | } 202 | 203 | @media (prefers-color-scheme: dark) { 204 | .vercelLogo { 205 | filter: invert(1); 206 | } 207 | 208 | .logo { 209 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); 210 | } 211 | } 212 | 213 | @keyframes rotate { 214 | from { 215 | transform: rotate(360deg); 216 | } 217 | to { 218 | transform: rotate(0deg); 219 | } 220 | } 221 | 222 | .main { 223 | display: flex; 224 | flex-direction: column; 225 | justify-content: flex-start; 226 | align-items: center; 227 | padding: 6rem 1rem 1rem; 228 | min-height: 100vh; 229 | background: linear-gradient(to bottom, #ffffff, #f0f5f9); 230 | } 231 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { SessionProvider } from 'next-auth/react'; 5 | import { FormProvider } from 'react-hook-form'; 6 | import { useChatForm } from '@/app/hooks/useChatForm'; 7 | 8 | import ResponsiveAppBar from './components/AppBar'; 9 | import Chat from './components/Chat'; 10 | import styles from './page.module.css'; 11 | 12 | export default function Home() { 13 | const formMethods = useChatForm(); 14 | 15 | return ( 16 | 17 | 18 |
19 | 20 | 21 | 22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /app/providers.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 5 | 6 | export default function Providers({ children }) { 7 | const [queryClient] = React.useState(() => new QueryClient()); 8 | 9 | return ( 10 | {children} 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /app/services/SpeechtoTextService.ts: -------------------------------------------------------------------------------- 1 | interface TextToSpeechData { 2 | blob: Blob; 3 | } 4 | 5 | const getAudioTranscript = async ({ blob }: TextToSpeechData): Promise => { 6 | try { 7 | const formData = new FormData(); 8 | formData.append('file', blob); 9 | 10 | const response = await fetch('/api/speech/stt', { 11 | method: 'POST', 12 | body: formData, 13 | }); 14 | 15 | return response.text(); 16 | } catch (error) { 17 | console.error('Unexpected error: ', error); 18 | throw error; 19 | } 20 | }; 21 | 22 | export { getAudioTranscript }; 23 | -------------------------------------------------------------------------------- /app/services/assistantService.ts: -------------------------------------------------------------------------------- 1 | interface AssistantUpdateData { 2 | name: string; 3 | description: string; 4 | isAssistantEnabled: boolean; 5 | userEmail: string; 6 | files: { name: string; id: string; assistandId: string }[]; 7 | } 8 | const updateAssistant = async ({ 9 | name, 10 | description, 11 | isAssistantEnabled, 12 | userEmail, 13 | files, 14 | }: AssistantUpdateData): Promise => { 15 | try { 16 | const response = await fetch('/api/assistant/update', { 17 | method: 'POST', 18 | headers: { 19 | 'Content-Type': 'application/json', 20 | }, 21 | body: JSON.stringify({ 22 | name, 23 | description, 24 | isAssistantEnabled, 25 | userEmail, 26 | files, 27 | }), 28 | }); 29 | return response.json(); 30 | } catch (error) { 31 | console.error('Unexpected error: ', error); 32 | throw error; 33 | } 34 | }; 35 | 36 | const deleteAssistantFile = async ({ 37 | file, 38 | }: { 39 | file: string; 40 | }): Promise => { 41 | try { 42 | const response = await fetch('/api/assistant/delete-file', { 43 | method: 'POST', 44 | headers: { 45 | 'Content-Type': 'application/json', 46 | }, 47 | body: JSON.stringify({ file }), 48 | }); 49 | return response.json(); 50 | } catch (error) { 51 | console.error('Unexpected error: ', error); 52 | throw error; 53 | } 54 | }; 55 | 56 | const deleteAssistant = async ({ 57 | userEmail, 58 | }: { 59 | userEmail: string; 60 | }): Promise => { 61 | try { 62 | const response = await fetch('/api/assistant/delete', { 63 | method: 'POST', 64 | headers: { 65 | 'Content-Type': 'application/json', 66 | }, 67 | body: JSON.stringify({ userEmail }), 68 | }); 69 | return response.json(); 70 | } catch (error) { 71 | console.error('Unexpected error: ', error); 72 | throw error; 73 | } 74 | }; 75 | 76 | const uploadFile = async ( 77 | file: File, 78 | userEmail: string 79 | ): Promise => { 80 | try { 81 | const formData = new FormData(); 82 | formData.append('file', file); 83 | formData.append('userEmail', userEmail); 84 | 85 | const fileUploadResponse = await fetch('/api/assistant/upload', { 86 | method: 'POST', 87 | body: formData, 88 | }); 89 | return fileUploadResponse; 90 | } catch (error) { 91 | console.error('Failed to upload file: ', error); 92 | } 93 | }; 94 | 95 | export { updateAssistant, deleteAssistantFile, deleteAssistant, uploadFile }; 96 | -------------------------------------------------------------------------------- /app/services/chatService.ts: -------------------------------------------------------------------------------- 1 | let audioQueue: string[] = []; 2 | let isPlaying = false; 3 | 4 | const delay = (ms: number | undefined) => 5 | new Promise((resolve) => setTimeout(resolve, ms)); 6 | 7 | const playNextAudio = () => { 8 | if (audioQueue.length > 0 && !isPlaying) { 9 | const audioUrl = audioQueue.shift(); 10 | const audio = new Audio(audioUrl); 11 | isPlaying = true; 12 | audio.play(); 13 | audio.onended = async () => { 14 | await delay(250); 15 | isPlaying = false; 16 | playNextAudio(); 17 | }; 18 | } 19 | }; 20 | 21 | const retrieveTextFromSpeech = async ( 22 | text: string, 23 | model: string, 24 | voice: string 25 | ): Promise => { 26 | try { 27 | const response = await fetch('/api/speech/tts', { 28 | method: 'POST', 29 | headers: { 'Content-Type': 'application/json' }, 30 | body: JSON.stringify({ text, model, voice }), 31 | }); 32 | if (!response.ok) { 33 | throw new Error('Failed to convert text to speech'); 34 | } 35 | const blob = await response.blob(); 36 | const audioUrl = URL.createObjectURL(blob); 37 | audioQueue.push(audioUrl); 38 | if (!isPlaying) { 39 | playNextAudio(); 40 | } 41 | } catch (error) { 42 | console.error('STT conversion error: ', error); 43 | return undefined; 44 | } 45 | }; 46 | 47 | const retrieveAIResponse = async ( 48 | message: string, 49 | userEmail: string, 50 | isAssistantEnabled: boolean 51 | ): Promise | undefined> => { 52 | try { 53 | const response = await fetch('/api/chat', { 54 | method: 'POST', 55 | headers: { 'Content-Type': 'application/json' }, 56 | body: JSON.stringify({ userMessage: message, userEmail }), 57 | }); 58 | if (isAssistantEnabled) { 59 | return response; 60 | } else { 61 | return response.body?.getReader(); 62 | } 63 | } catch (error) { 64 | console.error('Failed to fetch AI response: ', error); 65 | return undefined; 66 | } 67 | }; 68 | 69 | export { retrieveAIResponse, retrieveTextFromSpeech }; 70 | -------------------------------------------------------------------------------- /app/services/commonService.ts: -------------------------------------------------------------------------------- 1 | interface RetrieveServicesData { 2 | userEmail: string; 3 | serviceName: string; 4 | } 5 | 6 | const retrieveServices = async ({ 7 | userEmail, 8 | serviceName, 9 | }: RetrieveServicesData): Promise => { 10 | try { 11 | const response = await fetch(`/api/${serviceName}/retrieve`, { 12 | method: 'GET', 13 | headers: { userEmail: userEmail, serviceName: serviceName }, 14 | }); 15 | 16 | if (!response.ok) { 17 | throw new Error(`HTTP error! Status: ${response.status}`); 18 | } 19 | 20 | return response.json(); 21 | } catch (error) { 22 | console.error('Unexpected error: ', error); 23 | throw error; 24 | } 25 | }; 26 | 27 | export { retrieveServices }; 28 | -------------------------------------------------------------------------------- /app/services/embeddingService.ts: -------------------------------------------------------------------------------- 1 | const embedConversation = async ( 2 | data: any, 3 | userEmail: string 4 | ): Promise => { 5 | try { 6 | const response = await fetch('/api/embed/conversation', { 7 | method: 'POST', 8 | headers: { 'Content-Type': 'application/json' }, 9 | body: JSON.stringify({ data, userEmail }), 10 | }); 11 | 12 | if (!response.ok) { 13 | throw new Error('Error generating conversation embeddings'); 14 | } 15 | const jsonResponse = await response.json(); 16 | console.log(jsonResponse.message); 17 | return jsonResponse; 18 | } catch (error) { 19 | console.error('Error generating conversation embeddings: ', error); 20 | throw error; 21 | } 22 | }; 23 | 24 | const embedMessage = async (message: IMessage): Promise => { 25 | try { 26 | const response = await fetch('/api/embed/message', { 27 | method: 'POST', 28 | headers: { 'Content-Type': 'application/json' }, 29 | body: JSON.stringify({ message }), 30 | }); 31 | 32 | if (!response.ok) { 33 | throw new Error('Error generating message embeddings'); 34 | } 35 | const jsonResponse = await response.json(); 36 | console.log(jsonResponse.message); 37 | return jsonResponse; 38 | } catch (error) { 39 | console.error('Error generating message embeddings: ', error); 40 | throw error; 41 | } 42 | }; 43 | 44 | export { embedConversation, embedMessage }; 45 | -------------------------------------------------------------------------------- /app/services/longTermMemoryService.ts: -------------------------------------------------------------------------------- 1 | interface LongTermMemoryData { 2 | isLongTermMemoryEnabled: boolean; 3 | userEmail: string; 4 | memoryType: string; 5 | historyLength: string; 6 | } 7 | 8 | interface AppendMessageToNoSql { 9 | userEmail: string; 10 | message: IMessage; 11 | } 12 | 13 | interface AppendMessageToVectorDb { 14 | userEmail: string; 15 | vectorMessage: any; 16 | } 17 | 18 | interface AugmentUserMessageDataViaNoSql { 19 | userEmail: string; 20 | historyLength: string; 21 | message: string; 22 | } 23 | 24 | interface AugmentUserMessageDataViaVector { 25 | userEmail: string; 26 | historyLength: string; 27 | embeddedMessage: any; 28 | } 29 | 30 | interface UpdateMetadataInVectorDb { 31 | userEmail: string; 32 | id: string; 33 | metadata: any; 34 | } 35 | 36 | const updateSettings = async ({ 37 | isLongTermMemoryEnabled, 38 | userEmail, 39 | memoryType, 40 | historyLength, 41 | }: LongTermMemoryData): Promise => { 42 | try { 43 | const response = await fetch('/api/memory/update', { 44 | method: 'POST', 45 | headers: { 46 | 'Content-Type': 'application/json', 47 | }, 48 | body: JSON.stringify({ 49 | isLongTermMemoryEnabled, 50 | userEmail, 51 | memoryType, 52 | historyLength, 53 | }), 54 | }); 55 | return response.json(); 56 | } catch (error) { 57 | console.error('Unexpected error: ', error); 58 | throw error; 59 | } 60 | }; 61 | 62 | const appendMessageToNoSql = async ({ 63 | message, 64 | userEmail, 65 | }: AppendMessageToNoSql): Promise => { 66 | try { 67 | const response = await fetch('/api/memory/append/nosql', { 68 | method: 'POST', 69 | headers: { 70 | 'Content-Type': 'application/json', 71 | }, 72 | body: JSON.stringify({ 73 | userEmail, 74 | message, 75 | }), 76 | }); 77 | return response.json(); 78 | } catch (error) { 79 | console.error('Unexpected error: ', error); 80 | throw error; 81 | } 82 | }; 83 | 84 | const appendMessageToVector = async ({ 85 | userEmail, 86 | vectorMessage, 87 | }: AppendMessageToVectorDb): Promise => { 88 | try { 89 | const response = await fetch('/api/memory/append/vector', { 90 | method: 'POST', 91 | headers: { 92 | 'Content-Type': 'application/json', 93 | }, 94 | body: JSON.stringify({ 95 | userEmail, 96 | vectorMessage, 97 | }), 98 | }); 99 | return response.json(); 100 | } catch (error) { 101 | console.error('Unexpected error: ', error); 102 | throw error; 103 | } 104 | }; 105 | 106 | const updateMessageMetadataInVector = async ({ 107 | userEmail, 108 | id, 109 | metadata, 110 | }: UpdateMetadataInVectorDb): Promise => { 111 | try { 112 | const response = await fetch('/api/memory/update-metadata/vector', { 113 | method: 'POST', 114 | headers: { 115 | 'Content-Type': 'application/json', 116 | }, 117 | body: JSON.stringify({ 118 | userEmail, 119 | id, 120 | metadata, 121 | }), 122 | }); 123 | return response.json(); 124 | } catch (error) { 125 | console.error('Unexpected error: ', error); 126 | throw error; 127 | } 128 | }; 129 | 130 | const augmentMessageViaNoSql = async ({ 131 | message, 132 | userEmail, 133 | historyLength, 134 | }: AugmentUserMessageDataViaNoSql): Promise => { 135 | try { 136 | const response = await fetch('/api/memory/augment/nosql', { 137 | method: 'POST', 138 | headers: { 139 | 'Content-Type': 'application/json', 140 | }, 141 | body: JSON.stringify({ 142 | userEmail, 143 | message, 144 | historyLength, 145 | }), 146 | }); 147 | return response.json(); 148 | } catch (error) { 149 | console.error('Unexpected error: ', error); 150 | throw error; 151 | } 152 | }; 153 | 154 | const augmentMessageViaVector = async ({ 155 | userEmail, 156 | historyLength, 157 | embeddedMessage, 158 | }: AugmentUserMessageDataViaVector): Promise => { 159 | try { 160 | const response = await fetch('/api/memory/augment/vector', { 161 | method: 'POST', 162 | headers: { 163 | 'Content-Type': 'application/json', 164 | }, 165 | body: JSON.stringify({ 166 | userEmail, 167 | historyLength, 168 | embeddedMessage, 169 | }), 170 | }); 171 | return response.json(); 172 | } catch (error) { 173 | console.error('Unexpected error: ', error); 174 | throw error; 175 | } 176 | }; 177 | 178 | export { 179 | updateSettings, 180 | appendMessageToNoSql, 181 | augmentMessageViaNoSql, 182 | appendMessageToVector, 183 | augmentMessageViaVector, 184 | updateMessageMetadataInVector, 185 | }; 186 | -------------------------------------------------------------------------------- /app/services/ragService.ts: -------------------------------------------------------------------------------- 1 | interface RagUpdateData { 2 | isRagEnabled: boolean; 3 | userEmail: string; 4 | topK: string; 5 | chunkSize: string; 6 | chunkBatch: string; 7 | parsingStrategy: string; 8 | } 9 | 10 | const updateRag = async ({ 11 | isRagEnabled, 12 | userEmail, 13 | topK, 14 | chunkSize, 15 | chunkBatch, 16 | parsingStrategy, 17 | }: RagUpdateData): Promise => { 18 | try { 19 | const response = await fetch('/api/rag/update', { 20 | method: 'POST', 21 | headers: { 22 | 'Content-Type': 'application/json', 23 | }, 24 | body: JSON.stringify({ 25 | isRagEnabled, 26 | userEmail, 27 | topK, 28 | chunkSize, 29 | chunkBatch, 30 | parsingStrategy, 31 | }), 32 | }); 33 | return response.json(); 34 | } catch (error) { 35 | console.error('Unexpected error: ', error); 36 | throw error; 37 | } 38 | }; 39 | 40 | const deleteRagFile = async ({ 41 | file, 42 | userEmail, 43 | }: { 44 | file: RagFile; 45 | userEmail: string; 46 | }): Promise => { 47 | try { 48 | const response = await fetch('/api/rag/delete-file/', { 49 | method: 'POST', 50 | headers: { 51 | 'Content-Type': 'application/json', 52 | }, 53 | body: JSON.stringify({ file, userEmail }), 54 | }); 55 | return response.json(); 56 | } catch (error) { 57 | console.error('Unexpected error: ', error); 58 | throw error; 59 | } 60 | }; 61 | 62 | const updateFileStatus = async ({ 63 | file, 64 | userEmail, 65 | }: { 66 | file: RagFile; 67 | userEmail: string; 68 | }): Promise => { 69 | try { 70 | const response = await fetch('/api/rag/update-file-status/', { 71 | method: 'POST', 72 | headers: { 73 | 'Content-Type': 'application/json', 74 | }, 75 | body: JSON.stringify({ file, userEmail }), 76 | }); 77 | const jsonResponse = await response.json(); 78 | console.log(jsonResponse.message); 79 | return jsonResponse; 80 | } catch (error) { 81 | console.error('Unexpected error: ', error); 82 | throw new Error('Error updating file status'); 83 | } 84 | }; 85 | 86 | const uploadRagFile = async (file: File, userEmail: string): Promise => { 87 | const formData = new FormData(); 88 | formData.append('file', file); 89 | formData.append('userEmail', userEmail); 90 | try { 91 | const response = await fetch('/api/rag/upload', { 92 | method: 'POST', 93 | body: formData, 94 | }); 95 | return response.json(); 96 | } catch (error) { 97 | console.error('Unexpected error: ', error); 98 | throw error; 99 | } 100 | }; 101 | 102 | export { updateRag, deleteRagFile, updateFileStatus, uploadRagFile }; 103 | -------------------------------------------------------------------------------- /app/services/textToSpeechService.ts: -------------------------------------------------------------------------------- 1 | interface SpeechUpdateData { 2 | isTextToSpeechEnabled: boolean; 3 | userEmail: string; 4 | model: string; 5 | voice: string; 6 | } 7 | 8 | const updateSpeech = async ({ 9 | isTextToSpeechEnabled, 10 | userEmail, 11 | model, 12 | voice, 13 | }: SpeechUpdateData): Promise => { 14 | try { 15 | const response = await fetch('/api/speech/update', { 16 | method: 'POST', 17 | headers: { 18 | 'Content-Type': 'application/json', 19 | }, 20 | body: JSON.stringify({ 21 | isTextToSpeechEnabled, 22 | userEmail, 23 | model, 24 | voice, 25 | }), 26 | }); 27 | return response.json(); 28 | } catch (error) { 29 | console.error('Unexpected error: ', error); 30 | throw error; 31 | } 32 | }; 33 | 34 | export { updateSpeech }; 35 | -------------------------------------------------------------------------------- /app/services/unstructuredService.ts: -------------------------------------------------------------------------------- 1 | const parseDocument = async ( 2 | file: RagFile, 3 | chunkSize: string, 4 | parsingStrategy: string 5 | ): Promise => { 6 | try { 7 | const response = await fetch('/api/parse', { 8 | method: 'POST', 9 | headers: { 10 | 'Content-Type': 'application/json', 11 | }, 12 | body: JSON.stringify({ file, chunkSize, parsingStrategy }), 13 | }); 14 | 15 | if (!response.ok) { 16 | throw new Error(`HTTP error! Status: ${response.status}`); 17 | } 18 | const jsonResponse = await response.json(); 19 | console.log(jsonResponse.message); 20 | return jsonResponse; 21 | } catch (error) { 22 | console.error('Unexpected error: ', error); 23 | throw error; 24 | } 25 | }; 26 | 27 | export { parseDocument }; 28 | -------------------------------------------------------------------------------- /app/services/vectorDbService.ts: -------------------------------------------------------------------------------- 1 | const upsertToVectorDb = async ( 2 | data: any[], 3 | userEmail: string, 4 | chunkBatch: string 5 | ): Promise => { 6 | try { 7 | const response = await fetch('/api/rag/vector-db/upsert', { 8 | method: 'POST', 9 | headers: { 10 | 'Content-Type': 'application/json', 11 | }, 12 | body: JSON.stringify({ data, userEmail, chunkBatch }), 13 | }); 14 | const jsonResponse = await response.json(); 15 | console.log(jsonResponse.message); 16 | return jsonResponse; 17 | } catch (error) { 18 | console.error('Error upserting data to vector db: ', error); 19 | throw new Error('Error upserting data to vector db'); 20 | } 21 | }; 22 | 23 | const deleteFileFromVectorDb = async ( 24 | file: RagFile, 25 | userEmail: string, 26 | chunkBatch: string 27 | ): Promise => { 28 | try { 29 | const response = await fetch('/api/rag/vector-db/delete-file', { 30 | method: 'POST', 31 | headers: { 32 | 'Content-Type': 'application/json', 33 | }, 34 | body: JSON.stringify({ file, userEmail, chunkBatch }), 35 | }); 36 | const jsonResponse = await response.json(); 37 | console.log(jsonResponse.message); 38 | return jsonResponse; 39 | } catch (error) { 40 | console.error('Error deleting data from vector db: ', error); 41 | throw new Error('Error deleting data from vector db'); 42 | } 43 | }; 44 | 45 | const queryVectorDbByNamespace = async ( 46 | embeddedMessage: any, 47 | userEmail: string, 48 | topK: string 49 | ): Promise => { 50 | try { 51 | const response = await fetch('/api/rag/vector-db/query', { 52 | method: 'POST', 53 | headers: { 54 | 'Content-Type': 'application/json', 55 | }, 56 | body: JSON.stringify({ userEmail, embeddedMessage, topK }), 57 | }); 58 | const jsonResponse = await response.json(); 59 | console.log(jsonResponse.message); 60 | return jsonResponse; 61 | } catch (error) { 62 | console.error('Error deleting data from vector db: ', error); 63 | throw new Error('Error deleting data from vector db'); 64 | } 65 | }; 66 | 67 | export { upsertToVectorDb, deleteFileFromVectorDb, queryVectorDbByNamespace }; 68 | -------------------------------------------------------------------------------- /app/services/visionService.ts: -------------------------------------------------------------------------------- 1 | interface VisionUpdateData { 2 | isVisionEnabled: boolean; 3 | userEmail: string; 4 | } 5 | 6 | interface VisionAddUrlData { 7 | userEmail: string; 8 | file: { 9 | id: string; 10 | visionId: string; 11 | name: string; 12 | type: string; 13 | url: string; 14 | }; 15 | } 16 | const updateVision = async ({ 17 | isVisionEnabled, 18 | userEmail, 19 | }: VisionUpdateData): Promise => { 20 | try { 21 | const response = await fetch('/api/vision/update', { 22 | method: 'POST', 23 | headers: { 24 | 'Content-Type': 'application/json', 25 | }, 26 | body: JSON.stringify({ 27 | isVisionEnabled, 28 | userEmail, 29 | }), 30 | }); 31 | return response.json(); 32 | } catch (error) { 33 | console.error('Unexpected error: ', error); 34 | throw error; 35 | } 36 | }; 37 | 38 | const deleteVisionFile = async ( 39 | file: { 40 | id: string; 41 | visionId: string; 42 | name: string; 43 | type: string; 44 | url: string; 45 | }, 46 | userEmail: string 47 | ): Promise => { 48 | try { 49 | const response = await fetch('/api/vision/delete-url', { 50 | method: 'POST', 51 | headers: { 52 | 'Content-Type': 'application/json', 53 | }, 54 | body: JSON.stringify({ file, userEmail }), 55 | }); 56 | return response.json(); 57 | } catch (error) { 58 | console.error('Unexpected error: ', error); 59 | throw error; 60 | } 61 | }; 62 | 63 | const addVisionUrl = async ({ 64 | userEmail, 65 | file, 66 | }: VisionAddUrlData): Promise => { 67 | try { 68 | const response = await fetch('/api/vision/add-url', { 69 | method: 'POST', 70 | headers: { 71 | 'Content-Type': 'application/json', 72 | }, 73 | body: JSON.stringify({ 74 | userEmail, 75 | file, 76 | }), 77 | }); 78 | return response.json(); 79 | } catch (error) { 80 | console.error('Failed to upload file: ', error); 81 | } 82 | }; 83 | 84 | export { updateVision, deleteVisionFile, addVisionUrl }; 85 | -------------------------------------------------------------------------------- /app/utils/persistentMemoryUtils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | appendMessageToNoSql, 3 | appendMessageToVector, 4 | augmentMessageViaNoSql, 5 | augmentMessageViaVector, 6 | updateMessageMetadataInVector, 7 | } from '@/app/services/longTermMemoryService'; 8 | 9 | import { embedMessage } from '../services/embeddingService'; 10 | 11 | let lastMessage = {} as IMessage; 12 | 13 | const augment = async ( 14 | historyLength: string, 15 | memoryType: string, 16 | message: string, 17 | newMessage: IMessage, 18 | session: any 19 | ) => { 20 | let response; 21 | const userEmail = session?.user?.email as string; 22 | if (memoryType === 'NoSQL') { 23 | response = await augmentMessageViaNoSql({ 24 | message, 25 | userEmail, 26 | historyLength, 27 | }); 28 | return response.formattedConversationHistory; 29 | } else if (memoryType === 'Vector') { 30 | const embeddedMessage = await embedMessage(newMessage); 31 | response = await augmentMessageViaVector({ 32 | userEmail, 33 | historyLength, 34 | embeddedMessage, 35 | }); 36 | return response.formattedConversationHistory?.messages?.join('\n'); 37 | } 38 | }; 39 | 40 | const append = async ( 41 | memoryType: string, 42 | message: IMessage, 43 | userEmail: string 44 | ) => { 45 | let response; 46 | if (memoryType === 'NoSQL') { 47 | response = await appendMessageToNoSql({ 48 | userEmail, 49 | message, 50 | }); 51 | console.info('Appended message to NoSql:', response); 52 | } else if (memoryType === 'Vector') { 53 | if (message.sender === 'user') { 54 | const embeddedMessage = await embedMessage(message); 55 | const vectorMessage = { 56 | id: message.id, 57 | values: embeddedMessage.values, 58 | }; 59 | response = await appendMessageToVector({ 60 | userEmail, 61 | vectorMessage, 62 | }); 63 | lastMessage = message; 64 | console.info('Appended message to Vector: ', response); 65 | } else if (message.sender === 'ai') { 66 | const metadata = { 67 | user: `Date: ${lastMessage.createdAt}. Sender: ${lastMessage.conversationId}. Message: ${lastMessage.text}`, 68 | ai: `Date: ${message.createdAt}. Sender: AI. Message: ${message.text}`, 69 | }; 70 | 71 | const id = lastMessage.id; 72 | const response = await updateMessageMetadataInVector({ 73 | userEmail, 74 | id, 75 | metadata, 76 | }); 77 | 78 | lastMessage = message; 79 | console.info('Updated metadata in Vector: ', response); 80 | } 81 | } 82 | }; 83 | 84 | export const persistentMemoryUtils = { 85 | augment, 86 | append, 87 | }; 88 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "titanium", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@auth/mongodb-adapter": "^3.7.2", 13 | "@emotion/react": "^11.11.1", 14 | "@emotion/styled": "^11.11.0", 15 | "@mui/icons-material": "^5.14.18", 16 | "@mui/material": "^5.14.18", 17 | "@pinecone-database/pinecone": "^2.0.1", 18 | "@tanstack/react-query": "^5.0.0", 19 | "@vercel/kv": "0.2.1", 20 | "@vercel/speed-insights": "^1.0.2", 21 | "dotenv": "^16.3.1", 22 | "fs": "^0.0.1-security", 23 | "fs.promises": "^0.1.2", 24 | "mongodb": "^6.3.0", 25 | "next": "^14.2.12", 26 | "next-auth": "^4.24.10", 27 | "openai": "^4.26.1", 28 | "react": "^18", 29 | "react-dom": "^18", 30 | "react-hook-form": "^7.49.2", 31 | "react-markdown": "^9.0.1", 32 | "react-syntax-highlighter": "^15.5.0", 33 | "unique-names-generator": "^4.7.1", 34 | "unstructured-client": "^0.10.2", 35 | "uuid": "^9.0.1", 36 | "validator": "^13.11.0", 37 | "wink-eng-lite-web-model": "^1.5.2", 38 | "wink-nlp": "^1.14.3" 39 | }, 40 | "devDependencies": { 41 | "@types/dotenv": "^8.2.0", 42 | "@types/node": "^20", 43 | "@types/react": "^18.2.39", 44 | "@types/react-dom": "^18", 45 | "@types/react-syntax-highlighter": "^15.5.10", 46 | "@types/uuid": "^9.0.7", 47 | "@types/validator": "^13.11.8", 48 | "eslint": "^8", 49 | "eslint-config-next": "14.0.2", 50 | "tsx": "^3.12.6", 51 | "typescript": "^5" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------