├── .envrc.example ├── .github └── workflows │ ├── build.yaml │ ├── preview.yaml │ └── update.yaml ├── .gitignore ├── App.tsx ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── app.json ├── assets ├── adaptive-icon.png ├── favicon.png ├── icon.png └── splash.png ├── babel.config.js ├── components ├── CustomAppBar.tsx ├── Dashboard.tsx └── PageContainer.tsx ├── config.ts ├── eas.json ├── lib ├── context │ └── BrainConfigProvider │ │ ├── brain-config-provider.tsx │ │ ├── helpers │ │ ├── brainConfigLocalStorage.ts │ │ └── getApiUrl.ts │ │ ├── hooks │ │ └── useBrainConfig.ts │ │ └── types.ts ├── helpers │ └── setEmptyStringsUndefined.ts ├── localStorage.ts ├── types.ts └── useAxios.ts ├── package.json ├── pages ├── Manager.tsx ├── auth │ ├── Login.tsx │ └── Signup.tsx └── dashboard │ ├── Chat │ ├── components │ │ ├── ChatMessage.tsx │ │ └── ChatMessages.tsx │ ├── hooks │ │ └── useQuestion.ts │ └── index.tsx │ ├── Explore │ ├── DocumentItem │ │ ├── DocumentData.tsx │ │ └── index.tsx │ ├── index.tsx │ └── types.ts │ ├── Upload │ ├── hooks │ │ └── useFileUploader.ts │ ├── index.tsx │ └── types.ts │ └── User │ └── index.tsx ├── providers ├── Paper.tsx └── supabase-provider.tsx ├── tsconfig.json └── yarn.lock /.envrc.example: -------------------------------------------------------------------------------- 1 | export SUPABASE_URL="your-supabase-url-here" 2 | export SUPABASE_ANON_KEY="your-supabase-key-here" -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: 🏗 Setup repo 10 | uses: actions/checkout@v3 11 | 12 | - name: 🏗 Setup Node 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: 18.x 16 | cache: yarn 17 | 18 | - name: 🏗 Setup EAS 19 | uses: expo/expo-github-action@v8 20 | with: 21 | eas-version: latest 22 | token: ${{ secrets.EXPO_TOKEN }} 23 | 24 | - name: 📦 Install dependencies 25 | run: yarn install 26 | 27 | - name: 🚀 Build app 28 | run: eas build -p android --non-interactive 29 | -------------------------------------------------------------------------------- /.github/workflows/preview.yaml: -------------------------------------------------------------------------------- 1 | on: [pull_request] 2 | jobs: 3 | preview: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - name: 🏗 Setup repo 7 | uses: actions/checkout@v3 8 | 9 | - name: 🏗 Setup Node 10 | uses: actions/setup-node@v3 11 | with: 12 | node-version: 18.x 13 | cache: yarn 14 | 15 | - name: 🏗 Setup EAS 16 | uses: expo/expo-github-action@v8 17 | with: 18 | expo-version: latest 19 | token: ${{ secrets.EXPO_TOKEN }} 20 | 21 | - name: 📦 Install dependencies 22 | run: yarn install 23 | 24 | - name: 🚀 Create preview 25 | uses: expo/expo-github-action/preview@v8 26 | with: 27 | command: eas update --auto 28 | -------------------------------------------------------------------------------- /.github/workflows/update.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | jobs: 6 | update: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: 🏗 Setup repo 10 | uses: actions/checkout@v3 11 | 12 | - name: 🏗 Setup Node 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: 18.x 16 | cache: yarn 17 | 18 | - name: 🏗 Setup EAS 19 | uses: expo/expo-github-action@v8 20 | with: 21 | eas-version: latest 22 | token: ${{ secrets.EXPO_TOKEN }} 23 | 24 | - name: 📦 Install dependencies 25 | run: yarn install 26 | 27 | - name: 🚀 Create update 28 | run: eas update --auto --non-interactive 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | *.log 13 | .envrc 14 | 15 | # macOS 16 | .DS_Store 17 | 18 | # Temporary files created by Metro to check the health of the file watcher 19 | .metro-health-check* 20 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import Paper from "./providers/Paper"; 2 | import Manager from "./pages/Manager"; 3 | 4 | export default function App() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | nandanaditya985@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Note 3 | This project has not been updated in the last year and does not work with the current version of quivr because of the huge api changes. 4 | 5 | # Quivr - Mobile 6 | 7 | The Quivr React Native Client is a mobile application built using React Native that provides users with the ability to upload files and engage in chat conversations using the [Quivr backend API](https://github.com/stangirard/quivr). 8 | 9 | https://github.com/iMADi-ARCH/quivr-mobile/assets/61308761/878da303-b056-4c14-a3c4-f29e7e375d45 10 | 11 | ## Tech used 12 | 13 | - React Native (with expo) 14 | - React Native Paper 15 | - React Native Navigation 16 | 17 | ## Features 18 | 19 | - File Upload: Users can easily upload files to the Quivr backend API using the client. 20 | - Chat with your brain: Talk to a language model about your uploaded data 21 | 22 | ## Installation 23 | 24 | Follow the steps below to install and run the Quivr React Native Client: 25 | 26 | 1. Clone the repository: 27 | 28 | ```bash 29 | git clone https://github.com/iMADi-ARCH/quivr-mobile.git 30 | ``` 31 | 32 | 2. Navigate to the project directory: 33 | 34 | ```bash 35 | cd quivr-mobile 36 | ``` 37 | 38 | 3. Install the required dependencies: 39 | 40 | ```bash 41 | yarn install 42 | ``` 43 | 44 | 4. Set environment variables: Change the variables inside `.envrc.example` file with your own. 45 | 46 | a. **Option A**: Using `direnv` 47 | 48 | 1. Install direnv - https://direnv.net/#getting-started 49 | 2. Copy `.envrc.example` to `.envrc` 50 | 51 | ```bash 52 | cp .envrc.example .envrc 53 | ``` 54 | 55 | 3. Allow reading `.envrc` 56 | ```bash 57 | direnv allow . 58 | ``` 59 | 60 | b. **Option B**: Set system wide environment variables by copying the content of `.envrc` and placing it at the bottom of your shell file e.g. `.bashrc` or `.zshrc` 61 | 62 | 5. Configure the backend API endpoints: 63 | Open the `config.ts` file and update the `BACKEND_PORT` and `PROD_BACKEND_DOMAIN` constants with the appropriate values corresponding to your Quivr backend. 64 | 65 | 6. Run the application: 66 | 67 | ```bash 68 | yarn expo start 69 | ``` 70 | 71 | Then you can press `a` to run the app on an android emulator (given you already have Android studio setup) 72 | 73 | ## Contributing 74 | 75 | Contributions to the Quivr React Native Client are welcome! If you encounter any issues or want to add features, please open an issue on the GitHub repository. 76 | 77 | When contributing code, please follow the existing coding style and submit a pull request for review. 78 | 79 | ## Special Thanks 80 | 81 | - [Stan Girard](https://github.com/stangirard) for making such a wonderful api 🫶 82 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "quivr-android", 4 | "slug": "quivr-android", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "splash": { 10 | "image": "./assets/splash.png", 11 | "resizeMode": "contain", 12 | "backgroundColor": "#ffffff" 13 | }, 14 | "assetBundlePatterns": [ 15 | "**/*" 16 | ], 17 | "ios": { 18 | "supportsTablet": true, 19 | "bundleIdentifier": "com.mad.quivr" 20 | }, 21 | "android": { 22 | "adaptiveIcon": { 23 | "foregroundImage": "./assets/adaptive-icon.png", 24 | "backgroundColor": "#ffffff" 25 | }, 26 | "package": "com.mad.quivr" 27 | }, 28 | "web": { 29 | "favicon": "./assets/favicon.png" 30 | }, 31 | "extra": { 32 | "eas": { 33 | "projectId": "be67e9bc-6dc5-4e17-bbe4-b7c94387e28f" 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityanandanx/quivr-mobile/7ba8f32cf3f3c8cda6e63d045ec568e3b4d67b3e/assets/adaptive-icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityanandanx/quivr-mobile/7ba8f32cf3f3c8cda6e63d045ec568e3b4d67b3e/assets/favicon.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityanandanx/quivr-mobile/7ba8f32cf3f3c8cda6e63d045ec568e3b4d67b3e/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adityanandanx/quivr-mobile/7ba8f32cf3f3c8cda6e63d045ec568e3b4d67b3e/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: ['transform-inline-environment-variables'], 6 | env: { 7 | production: { 8 | plugins: ['react-native-paper/babel'], 9 | }, 10 | }, 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /components/CustomAppBar.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useContext } from "react"; 2 | import { Appbar, Switch, useTheme } from "react-native-paper"; 3 | import { usePreferences } from "../providers/Paper"; 4 | import { NativeStackHeaderProps } from "@react-navigation/native-stack"; 5 | import { getHeaderTitle } from "@react-navigation/elements"; 6 | import { StatusBar } from "expo-status-bar"; 7 | 8 | interface CustomAppBarProps extends NativeStackHeaderProps {} 9 | 10 | const CustomAppBar: FC = ({ 11 | navigation, 12 | options, 13 | route, 14 | back, 15 | }) => { 16 | const theme = useTheme(); 17 | const { darkMode, toggleDarkMode } = usePreferences(); 18 | const title = getHeaderTitle(options, route.name); 19 | 20 | return ( 21 | 22 | {options.headerBackVisible ? ( 23 | 24 | ) : null} 25 | 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default CustomAppBar; 33 | -------------------------------------------------------------------------------- /components/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from "react"; 2 | import { BottomNavigation, Text } from "react-native-paper"; 3 | import Upload from "../pages/dashboard/Upload"; 4 | import Chat from "../pages/dashboard/Chat"; 5 | import Explore from "../pages/dashboard/Explore"; 6 | import User from "../pages/dashboard/User"; 7 | 8 | interface DashboardNavigationProps {} 9 | 10 | const DashboardNavigation: FC = ({}) => { 11 | const [index, setIndex] = useState(0); 12 | const [routes] = useState([ 13 | { 14 | key: "upload", 15 | title: "Upload", 16 | focusedIcon: "upload", 17 | unfocusedIcon: "upload-outline", 18 | }, 19 | { key: "chat", title: "Chat", focusedIcon: "chat" }, 20 | { key: "explore", title: "Explore", focusedIcon: "compass" }, 21 | { 22 | key: "account", 23 | title: "Account", 24 | focusedIcon: "account", 25 | unfocusedIcon: "account-outline", 26 | }, 27 | ]); 28 | 29 | const renderScene = BottomNavigation.SceneMap({ 30 | upload: () => , 31 | chat: () => , 32 | explore: () => , 33 | account: () => , 34 | }); 35 | 36 | return ( 37 | 44 | ); 45 | }; 46 | 47 | export default DashboardNavigation; 48 | -------------------------------------------------------------------------------- /components/PageContainer.tsx: -------------------------------------------------------------------------------- 1 | import { RefreshControl, ScrollView, ScrollViewProps } from "react-native"; 2 | import { FC, ReactNode, useCallback, useState } from "react"; 3 | import { ActivityIndicator, Portal, Surface, Text } from "react-native-paper"; 4 | import { SafeAreaView } from "react-native-safe-area-context"; 5 | 6 | interface PageContainerProps { 7 | children?: ReactNode; 8 | onRefresh?: () => void; 9 | } 10 | 11 | const PageContainer: FC = ({ children, onRefresh }) => { 12 | const [refreshing, setRefreshing] = useState(false); 13 | 14 | const onRefreshCalled = useCallback(() => { 15 | if (!onRefresh) return; 16 | setRefreshing(true); 17 | onRefresh(); 18 | setTimeout(() => { 19 | setRefreshing(false); 20 | }, 2000); 21 | }, []); 22 | 23 | return ( 24 | 25 | 26 | 39 | ) 40 | } 41 | > 42 | {children} 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | export default PageContainer; 50 | -------------------------------------------------------------------------------- /config.ts: -------------------------------------------------------------------------------- 1 | export const BACKEND_PORT = "5050"; 2 | export const PROD_BACKEND_DOMAIN = "https://mywebsite.com"; 3 | -------------------------------------------------------------------------------- /eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 3.13.3" 4 | }, 5 | "build": { 6 | "development": { 7 | "developmentClient": true, 8 | "distribution": "internal" 9 | }, 10 | "preview": { 11 | "distribution": "internal" 12 | }, 13 | "production": {} 14 | }, 15 | "submit": { 16 | "production": {} 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/context/BrainConfigProvider/brain-config-provider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useEffect, useState } from "react"; 2 | import { 3 | getBrainConfigFromLocalStorage, 4 | saveBrainConfigInLocalStorage, 5 | } from "./helpers/brainConfigLocalStorage"; 6 | import { BrainConfig, ConfigContext } from "./types"; 7 | import { setEmptyStringsUndefined } from "../../helpers/setEmptyStringsUndefined"; 8 | import getApiDomain from "./helpers/getApiUrl"; 9 | 10 | export const BrainConfigContext = createContext( 11 | undefined 12 | ); 13 | 14 | const defaultBrainConfig: BrainConfig = { 15 | model: "gpt-3.5-turbo", 16 | temperature: 0, 17 | maxTokens: 500, 18 | keepLocal: true, 19 | anthropicKey: undefined, 20 | backendUrl: getApiDomain(), 21 | openAiKey: undefined, 22 | supabaseKey: undefined, 23 | supabaseUrl: undefined, 24 | }; 25 | 26 | export const BrainConfigProvider = ({ 27 | children, 28 | }: { 29 | children: React.ReactNode; 30 | }) => { 31 | const [brainConfig, setBrainConfig] = 32 | useState(defaultBrainConfig); 33 | 34 | const updateConfig = (newConfig: Partial) => { 35 | setBrainConfig((config) => { 36 | const updatedConfig: BrainConfig = { 37 | ...config, 38 | ...setEmptyStringsUndefined(newConfig), 39 | }; 40 | saveBrainConfigInLocalStorage(updatedConfig); 41 | 42 | return updatedConfig; 43 | }); 44 | }; 45 | 46 | const resetConfig = () => { 47 | updateConfig(defaultBrainConfig); 48 | }; 49 | 50 | useEffect(() => { 51 | let conf; 52 | const load = async () => { 53 | try { 54 | conf = await getBrainConfigFromLocalStorage(); 55 | } catch (e) { 56 | saveBrainConfigInLocalStorage(defaultBrainConfig); 57 | } 58 | }; 59 | load(); 60 | setBrainConfig(conf ?? defaultBrainConfig); 61 | }, []); 62 | 63 | return ( 64 | 67 | {children} 68 | 69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /lib/context/BrainConfigProvider/helpers/brainConfigLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import localStorage from "../../../localStorage"; 2 | import { BrainConfig } from "../types"; 3 | 4 | const BRAIN_CONFIG_LOCAL_STORAGE_KEY = "userBrainConfig"; 5 | 6 | export const saveBrainConfigInLocalStorage = (updatedConfig: BrainConfig) => { 7 | try { 8 | localStorage.save({ 9 | key: BRAIN_CONFIG_LOCAL_STORAGE_KEY, 10 | data: updatedConfig, 11 | }); 12 | } catch (e) { 13 | console.log(e); 14 | } 15 | }; 16 | export const getBrainConfigFromLocalStorage = async (): Promise< 17 | BrainConfig | undefined 18 | > => { 19 | const persistedBrainConfig = await localStorage.load({ 20 | key: BRAIN_CONFIG_LOCAL_STORAGE_KEY, 21 | }); 22 | if (persistedBrainConfig === null) return; 23 | return JSON.parse(persistedBrainConfig); 24 | }; 25 | -------------------------------------------------------------------------------- /lib/context/BrainConfigProvider/helpers/getApiUrl.ts: -------------------------------------------------------------------------------- 1 | import Constants from "expo-constants"; 2 | import { BACKEND_PORT, PROD_BACKEND_DOMAIN } from "../../../../config"; 3 | 4 | export default function () { 5 | //const inProduction = manifest.packagerOpts == null; 6 | const inProduction = process.env.NODE_ENV === "production"; 7 | const inExpo = Constants.manifest && Constants.manifest.debuggerHost; 8 | 9 | const apiDomain = inProduction 10 | ? PROD_BACKEND_DOMAIN 11 | : inExpo 12 | ? Constants.manifest?.debuggerHost!.split(`:`).shift() 13 | : "unknown"; 14 | 15 | console.log("apiDomain:", apiDomain); 16 | 17 | const protocol = inProduction ? "https" : "http"; 18 | 19 | const apiUrl = `${protocol}://${apiDomain}:${BACKEND_PORT}`; 20 | console.log("apiUrl:", apiUrl); 21 | return apiUrl; 22 | } 23 | -------------------------------------------------------------------------------- /lib/context/BrainConfigProvider/hooks/useBrainConfig.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { BrainConfigContext } from "../brain-config-provider"; 3 | 4 | export const useBrainConfig = () => { 5 | const context = useContext(BrainConfigContext); 6 | 7 | if (context === undefined) { 8 | throw new Error("useConfig must be used inside BrainConfigContext"); 9 | } 10 | 11 | return context; 12 | }; 13 | -------------------------------------------------------------------------------- /lib/context/BrainConfigProvider/types.ts: -------------------------------------------------------------------------------- 1 | export type BrainConfig = { 2 | model: Model; 3 | temperature: number; 4 | maxTokens: number; 5 | keepLocal: boolean; 6 | backendUrl?: string; 7 | openAiKey?: string; 8 | anthropicKey?: string; 9 | supabaseUrl?: string; 10 | supabaseKey?: string; 11 | }; 12 | 13 | type OptionalConfig = { [K in keyof BrainConfig]?: BrainConfig[K] | undefined }; 14 | 15 | export type ConfigContext = { 16 | config: BrainConfig; 17 | updateConfig: (config: OptionalConfig) => void; 18 | resetConfig: () => void; 19 | }; 20 | 21 | // export const openAiModels = ["gpt-3.5-turbo", "gpt-4"] as const; ## TODO activate GPT4 when not in demo mode 22 | export const openAiModels = ["gpt-3.5-turbo"] as const; 23 | 24 | export const anthropicModels = [ 25 | // "claude-v1", 26 | // "claude-v1.3", 27 | // "claude-instant-v1-100k", 28 | // "claude-instant-v1.1-100k", 29 | ] as const; 30 | 31 | // export const googleModels = ["vertexai"] as const; ## TODO activate when not in demo mode 32 | 33 | export const googleModels = [] as const; 34 | export const models = [ 35 | ...openAiModels, 36 | ...anthropicModels, 37 | ...googleModels, 38 | ] as const; 39 | 40 | export type Model = (typeof models)[number]; 41 | -------------------------------------------------------------------------------- /lib/helpers/setEmptyStringsUndefined.ts: -------------------------------------------------------------------------------- 1 | export const setEmptyStringsUndefined = ( 2 | obj: Record 3 | ): Record => { 4 | Object.keys(obj).forEach((key) => { 5 | if (obj[key] === "") { 6 | obj[key] = undefined; 7 | } 8 | }); 9 | 10 | return obj; 11 | }; 12 | -------------------------------------------------------------------------------- /lib/localStorage.ts: -------------------------------------------------------------------------------- 1 | import Storage from "react-native-storage"; 2 | import AsyncStorage from "@react-native-async-storage/async-storage"; 3 | 4 | const localStorage = new Storage({ 5 | // maximum capacity, default 1000 key-ids 6 | size: 1000, 7 | 8 | // Use AsyncStorage for RN apps, or window.localStorage for web apps. 9 | // If storageBackend is not set, data will be lost after reload. 10 | storageBackend: AsyncStorage, // for web: window.localStorage 11 | 12 | // expire time, default: 1 day (1000 * 3600 * 24 milliseconds). 13 | // can be null, which means never expire. 14 | defaultExpires: 1000 * 3600 * 24, 15 | 16 | // cache data in the memory. default is true. 17 | enableCache: true, 18 | 19 | // if data was not found in storage or expired data was found, 20 | // the corresponding sync method will be invoked returning 21 | // the latest data. 22 | sync: { 23 | // we'll talk about the details later. 24 | }, 25 | }); 26 | 27 | export default localStorage; 28 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | export type Message = { 2 | type: "success" | "error" | "warning"; 3 | text: string; 4 | }; 5 | -------------------------------------------------------------------------------- /lib/useAxios.ts: -------------------------------------------------------------------------------- 1 | import { useSupabase } from "../providers/supabase-provider"; 2 | import axios from "axios"; 3 | import { useBrainConfig } from "./context/BrainConfigProvider/hooks/useBrainConfig"; 4 | import getApiDomain from "./context/BrainConfigProvider/helpers/getApiUrl"; 5 | 6 | const axiosInstance = axios.create({ 7 | baseURL: `${getApiDomain()}`, 8 | }); 9 | 10 | export const useAxios = () => { 11 | const { session } = useSupabase(); 12 | const { 13 | config: { backendUrl }, 14 | } = useBrainConfig(); 15 | 16 | axiosInstance.interceptors.request.clear(); 17 | axiosInstance.interceptors.request.use( 18 | async (config) => { 19 | config.headers["Authorization"] = "Bearer " + session?.access_token; 20 | config.baseURL = backendUrl ?? config.baseURL; 21 | 22 | return config; 23 | }, 24 | (error) => { 25 | console.error({ error }); 26 | void Promise.reject(error); 27 | } 28 | ); 29 | 30 | return { 31 | axiosInstance, 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quivr-android", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "@react-native-async-storage/async-storage": "1.17.11", 13 | "@react-navigation/native": "^6.1.6", 14 | "@react-navigation/native-stack": "^6.9.12", 15 | "@supabase/supabase-js": "^2.24.0", 16 | "axios": "^1.4.0", 17 | "deepmerge": "^4.3.1", 18 | "expo": "~48.0.18", 19 | "expo-constants": "~14.2.1", 20 | "expo-document-picker": "~11.2.2", 21 | "expo-speech": "~11.1.1", 22 | "expo-status-bar": "~1.4.4", 23 | "react": "18.2.0", 24 | "react-hook-form": "^7.44.3", 25 | "react-native": "0.71.8", 26 | "react-native-paper": "^5.8.0", 27 | "react-native-paper-form-builder": "^2.1.2", 28 | "react-native-safe-area-context": "4.5.0", 29 | "react-native-screens": "~3.20.0", 30 | "react-native-storage": "^1.0.1", 31 | "react-native-url-polyfill": "^1.3.0", 32 | "react-native-vector-icons": "^9.2.0", 33 | "react-navigation-helpers": "^2.2.0" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.20.0", 37 | "@tsconfig/react-native": "^3.0.2", 38 | "@types/jest": "^29.5.2", 39 | "@types/node": "^20.2.5", 40 | "@types/react": "~18.0.27", 41 | "@types/react-test-renderer": "^18.0.0", 42 | "babel-plugin-transform-inline-environment-variables": "^0.4.4", 43 | "typescript": "^4.9.4" 44 | }, 45 | "private": true 46 | } 47 | -------------------------------------------------------------------------------- /pages/Manager.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { useSupabase } from "../providers/supabase-provider"; 3 | import DashboardNavigation from "../components/Dashboard"; 4 | import Login from "./auth/Login"; 5 | import Signup from "./auth/Signup"; 6 | import { createNativeStackNavigator } from "@react-navigation/native-stack"; 7 | import CustomAppBar from "../components/CustomAppBar"; 8 | import PageContainer from "../components/PageContainer"; 9 | import { Text } from "react-native-paper"; 10 | 11 | interface ManagerProps {} 12 | 13 | const Stack = createNativeStackNavigator(); 14 | 15 | const Manager: FC = () => { 16 | const { session, isPending } = useSupabase(); 17 | 18 | if (isPending) { 19 | return ( 20 | 21 | Loading... 22 | 23 | ); 24 | } 25 | 26 | return ( 27 | , 30 | }} 31 | > 32 | {!session ? ( 33 | <> 34 | 35 | 36 | 37 | ) : ( 38 | <> 39 | 44 | 45 | )} 46 | 47 | ); 48 | }; 49 | export default Manager; 50 | -------------------------------------------------------------------------------- /pages/auth/Login.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useState } from "react"; 2 | import { 3 | Button, 4 | Checkbox, 5 | Divider, 6 | HelperText, 7 | List, 8 | Snackbar, 9 | Surface, 10 | Text, 11 | TextInput, 12 | TouchableRipple, 13 | } from "react-native-paper"; 14 | import PageContainer from "../../components/PageContainer"; 15 | 16 | import * as NavigationService from "react-navigation-helpers"; 17 | import { useSupabase } from "../../providers/supabase-provider"; 18 | import { View } from "react-native"; 19 | import { AuthError } from "@supabase/supabase-js"; 20 | 21 | interface LoginProps {} 22 | 23 | const Login: FC = ({}) => { 24 | const [email, setEmail] = useState(""); 25 | const [password, setPassword] = useState(""); 26 | 27 | const [passwordVisible, setPasswordVisible] = useState(false); 28 | const [isPending, setIsPending] = useState(false); 29 | const [authError, setAuthError] = useState(null); 30 | 31 | const { supabase } = useSupabase(); 32 | 33 | const emailHasErrors = () => { 34 | return !email.includes("@"); 35 | }; 36 | const passwordHasErrors = () => { 37 | return password.length < 8; 38 | }; 39 | 40 | const handleLogin = async () => { 41 | if (emailHasErrors() || passwordHasErrors()) { 42 | // setAuthError(); 43 | } 44 | setIsPending(true); 45 | const { data, error } = await supabase.auth.signInWithPassword({ 46 | email: email, 47 | password: password, 48 | }); 49 | 50 | console.log({ data, error }); 51 | if (error) { 52 | setAuthError(error); 53 | } else if (data) { 54 | console.log("Login success"); 55 | } 56 | 57 | setIsPending(false); 58 | }; 59 | 60 | return ( 61 | 62 | 71 | Login to Quivr 72 | Welcome Back to Quivr 73 | 74 | 75 | } 77 | label="Email" 78 | value={email} 79 | keyboardType="email-address" 80 | placeholder="abc@xyz.com" 81 | onChangeText={(text) => setEmail(text)} 82 | /> 83 | 84 | Email address is invalid 85 | 86 | } 88 | label="Password" 89 | value={password} 90 | placeholder="Enter your password" 91 | secureTextEntry={!passwordVisible} 92 | onChangeText={(password) => setPassword(password)} 93 | /> 94 | 95 | Passoword must contain atleast 8 characters 96 | 97 | setPasswordVisible(!passwordVisible)} 100 | > 101 | 108 | 109 | Show password 110 | 111 | 112 | 122 | 125 | 126 | 127 | setAuthError(null)} 130 | > 131 | {`${authError?.message}`} 132 | 133 | 134 | ); 135 | }; 136 | 137 | export default Login; 138 | -------------------------------------------------------------------------------- /pages/auth/Signup.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from "react"; 2 | import { 3 | Button, 4 | Checkbox, 5 | Divider, 6 | HelperText, 7 | List, 8 | Snackbar, 9 | Surface, 10 | Text, 11 | TextInput, 12 | TouchableRipple, 13 | } from "react-native-paper"; 14 | import PageContainer from "../../components/PageContainer"; 15 | 16 | import * as NavigationService from "react-navigation-helpers"; 17 | import { useSupabase } from "../../providers/supabase-provider"; 18 | import { View } from "react-native"; 19 | import { AuthError } from "@supabase/supabase-js"; 20 | 21 | interface SignupProps {} 22 | 23 | const Signup: FC = ({}) => { 24 | const [email, setEmail] = useState(""); 25 | const [password, setPassword] = useState(""); 26 | 27 | const [passwordVisible, setPasswordVisible] = useState(false); 28 | const [isPending, setIsPending] = useState(false); 29 | const [signedUp, setSignedUp] = useState(false); 30 | const [authError, setAuthError] = useState(null); 31 | 32 | const { supabase } = useSupabase(); 33 | 34 | const emailHasErrors = () => { 35 | return !email.includes("@"); 36 | }; 37 | const passwordHasErrors = () => { 38 | return password.length < 8; 39 | }; 40 | 41 | const handleSignup = async () => { 42 | if (emailHasErrors() || passwordHasErrors()) { 43 | // setAuthError(); 44 | } 45 | setIsPending(true); 46 | const { data, error } = await supabase.auth.signUp({ 47 | email: email, 48 | password: password, 49 | }); 50 | 51 | console.log({ data, error }); 52 | if (error) { 53 | setAuthError(error); 54 | } else if (data) { 55 | setSignedUp(true); 56 | // NavigationService.navigate("Login"); 57 | } 58 | 59 | setIsPending(false); 60 | }; 61 | 62 | return ( 63 | 64 | 73 | Create an account 74 | Let{"'"}s get you one more brain 75 | 76 | 77 | } 79 | label="Email" 80 | value={email} 81 | keyboardType="email-address" 82 | placeholder="abc@xyz.com" 83 | onChangeText={(text) => setEmail(text)} 84 | /> 85 | 86 | Email address is invalid 87 | 88 | } 90 | label="Password" 91 | value={password} 92 | placeholder="Enter your password" 93 | secureTextEntry={!passwordVisible} 94 | onChangeText={(password) => setPassword(password)} 95 | /> 96 | 97 | Passoword must contain atleast 8 characters 98 | 99 | setPasswordVisible(!passwordVisible)} 102 | > 103 | 110 | 111 | Show password 112 | 113 | 114 | 124 | 127 | 128 | 129 | { 132 | setSignedUp(false); 133 | NavigationService.navigate("Login"); 134 | }} 135 | action={{ 136 | label: "Login", 137 | onPress: () => { 138 | setSignedUp(false); 139 | NavigationService.navigate("Login"); 140 | }, 141 | }} 142 | > 143 | {`Sign Up Success. Check your email for further instructions.`} 144 | 145 | setAuthError(null)} 148 | > 149 | {`${authError?.message}`} 150 | 151 | 152 | ); 153 | }; 154 | 155 | export default Signup; 156 | -------------------------------------------------------------------------------- /pages/dashboard/Chat/components/ChatMessage.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Surface, Text, useTheme } from "react-native-paper"; 2 | 3 | const ChatMessage = ({ 4 | speaker, 5 | text, 6 | left = false, 7 | }: { 8 | speaker: string; 9 | text: string; 10 | left?: boolean; 11 | }) => { 12 | const theme = useTheme(); 13 | return ( 14 | 25 | 26 | {speaker} 27 | 28 | <> 29 | {text} 30 | 31 | 32 | ); 33 | }; 34 | ChatMessage.displayName = "ChatMessage"; 35 | 36 | export default ChatMessage; 37 | -------------------------------------------------------------------------------- /pages/dashboard/Chat/components/ChatMessages.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useRef } from "react"; 2 | import ChatMessage from "./ChatMessage"; 3 | import { Surface, Text } from "react-native-paper"; 4 | import { ScrollView } from "react-native"; 5 | 6 | interface ChatMessagesProps { 7 | history: Array<[string, string]>; 8 | } 9 | 10 | const ChatMessages: FC = ({ history }) => { 11 | const scrollerRef = useRef(null); 12 | 13 | useEffect(() => { 14 | scrollerRef.current?.scrollToEnd(); 15 | }, [history]); 16 | 17 | return ( 18 | 24 | {history.length === 0 ? ( 25 | Ask a question, or describe a task. 26 | ) : ( 27 | history.map(([speaker, text], idx) => { 28 | return ( 29 | 36 | ); 37 | }) 38 | )} 39 | 40 | ); 41 | }; 42 | export default ChatMessages; 43 | -------------------------------------------------------------------------------- /pages/dashboard/Chat/hooks/useQuestion.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useSupabase } from "../../../../providers/supabase-provider"; 3 | import { useBrainConfig } from "../../../../lib/context/BrainConfigProvider/hooks/useBrainConfig"; 4 | import localStorage from "../../../../lib/localStorage"; 5 | import { useAxios } from "../../../../lib/useAxios"; 6 | import * as NavigationService from "react-navigation-helpers"; 7 | 8 | export const useQuestion = () => { 9 | const [question, setQuestion] = useState(""); 10 | const [history, setHistory] = useState>([]); 11 | const [isPending, setIsPending] = useState(false); 12 | const { session } = useSupabase(); 13 | const { axiosInstance } = useAxios(); 14 | const { 15 | config: { maxTokens, model, temperature }, 16 | } = useBrainConfig(); 17 | if (session === null) { 18 | NavigationService.navigate("Login"); 19 | } 20 | 21 | useEffect(() => { 22 | // Check if history exists in local storage. If it does, fetch it and set it as history 23 | (async () => { 24 | try { 25 | const localHistory = await localStorage.load({ key: "history" }); 26 | if (localHistory) { 27 | setHistory(JSON.parse(localHistory)); 28 | } 29 | } catch (error) { 30 | localStorage.save({ key: "history", data: [] }); 31 | } 32 | })(); 33 | }, []); 34 | 35 | const askQuestion = async () => { 36 | if (question.trim().length === 0 || isPending) { 37 | return; 38 | } 39 | setHistory((hist) => [...hist, ["user", question]]); 40 | setIsPending(true); 41 | 42 | const response = await axiosInstance.post(`/chat/`, { 43 | model, 44 | question, 45 | history, 46 | temperature, 47 | max_tokens: maxTokens, 48 | }); 49 | setHistory(response.data.history); 50 | localStorage.save({ 51 | key: "history", 52 | data: JSON.stringify(response.data.history), 53 | }); 54 | setQuestion(""); 55 | setIsPending(false); 56 | }; 57 | 58 | const resetHistory = () => { 59 | localStorage.save({ 60 | key: "history", 61 | data: JSON.stringify([]), 62 | }); 63 | setHistory([]); 64 | }; 65 | 66 | return { 67 | isPending, 68 | history, 69 | question, 70 | setQuestion, 71 | resetHistory, 72 | askQuestion, 73 | }; 74 | }; 75 | -------------------------------------------------------------------------------- /pages/dashboard/Chat/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import PageContainer from "../../../components/PageContainer"; 3 | import { 4 | ActivityIndicator, 5 | IconButton, 6 | Portal, 7 | Text, 8 | TextInput, 9 | useTheme, 10 | } from "react-native-paper"; 11 | import { useQuestion } from "./hooks/useQuestion"; 12 | import ChatMessages from "./components/ChatMessages"; 13 | import { View } from "react-native"; 14 | 15 | interface ChatProps {} 16 | 17 | const Chat: FC = ({}) => { 18 | const { 19 | isPending, 20 | askQuestion, 21 | history, 22 | question, 23 | resetHistory, 24 | setQuestion, 25 | } = useQuestion(); 26 | 27 | const theme = useTheme(); 28 | 29 | return ( 30 | 31 | Chat With your brain 32 | {/* */} 33 | 34 | {/* */} 35 | 36 | setQuestion(t)} 48 | mode="outlined" 49 | placeholder="Type something" 50 | right={ 51 | 56 | } 57 | /> 58 | 59 | 60 | ); 61 | }; 62 | 63 | export default Chat; 64 | -------------------------------------------------------------------------------- /pages/dashboard/Explore/DocumentItem/DocumentData.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useEffect, useState } from "react"; 2 | import { useAxios } from "../../../../lib/useAxios"; 3 | import { useSupabase } from "../../../../providers/supabase-provider"; 4 | import { 5 | Button, 6 | Card, 7 | Divider, 8 | IconButton, 9 | Modal, 10 | Portal, 11 | Surface, 12 | Text, 13 | useTheme, 14 | } from "react-native-paper"; 15 | import { ScrollView, View } from "react-native"; 16 | 17 | interface DocumentDataProps { 18 | documentName: string; 19 | open: boolean; 20 | setOpen: Dispatch>; 21 | fetchDocuments: () => void; 22 | } 23 | 24 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 25 | type DocumentDetails = any; 26 | //TODO: review this component logic, types and purposes 27 | 28 | const DocumentData = ({ 29 | documentName, 30 | open, 31 | setOpen, 32 | fetchDocuments, 33 | }: DocumentDataProps): JSX.Element => { 34 | const { session } = useSupabase(); 35 | const { axiosInstance } = useAxios(); 36 | 37 | const [documents, setDocuments] = useState([]); 38 | const [isDeleting, setIsDeleting] = useState(false); 39 | const theme = useTheme(); 40 | 41 | if (!session) { 42 | throw new Error("User session not found"); 43 | } 44 | 45 | useEffect(() => { 46 | const fetchDocuments = async () => { 47 | const res = await axiosInstance.get<{ documents: DocumentDetails[] }>( 48 | `/explore/${documentName}` 49 | ); 50 | setDocuments(res.data.documents); 51 | }; 52 | fetchDocuments(); 53 | }, [axiosInstance, documentName]); 54 | 55 | const deleteDocument = async (name: string) => { 56 | setIsDeleting(true); 57 | try { 58 | await axiosInstance.delete(`/explore/${name}`); 59 | setDocuments((docs) => docs.filter((doc) => doc.name !== name)); // Optimistic update 60 | } catch (error) { 61 | console.error(`Error deleting ${name}`, error); 62 | } 63 | setIsDeleting(false); 64 | fetchDocuments(); 65 | setOpen(false); 66 | }; 67 | 68 | return ( 69 | 70 | setOpen(false)}> 71 | 72 | setOpen(false)} 81 | icon="close" 82 | /> 83 | 84 | 85 | {documentName} 86 | 87 | No. of chunks: {documents.length} 88 | 98 | 99 | 100 | 101 | 102 | {documents[0] && 103 | Object.keys(documents[0]).map((doc) => { 104 | return ( 105 | 106 | 110 | {doc.replaceAll("_", " ")}: 111 | 112 | {documents[0][doc] || "Not Available"} 113 | 114 | ); 115 | })} 116 | 117 | 118 | 119 | 120 | 121 | ); 122 | }; 123 | 124 | export default DocumentData; 125 | -------------------------------------------------------------------------------- /pages/dashboard/Explore/DocumentItem/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dispatch, 3 | RefObject, 4 | SetStateAction, 5 | forwardRef, 6 | useState, 7 | } from "react"; 8 | 9 | import DocumentData from "./DocumentData"; 10 | import { useSupabase } from "../../../../providers/supabase-provider"; 11 | import { useAxios } from "../../../../lib/useAxios"; 12 | import { 13 | Button, 14 | Card, 15 | IconButton, 16 | List, 17 | Modal, 18 | Portal, 19 | Text, 20 | useTheme, 21 | } from "react-native-paper"; 22 | import { Document } from "../types"; 23 | import { View } from "react-native"; 24 | 25 | interface DocumentProps { 26 | document: Document; 27 | setDocuments: Dispatch>; 28 | fetchDocuments: () => void; 29 | } 30 | 31 | const DocumentItem = ({ 32 | document, 33 | setDocuments, 34 | fetchDocuments, 35 | }: DocumentProps) => { 36 | const { session } = useSupabase(); 37 | const [modalOpen, setModalOpen] = useState(false); 38 | 39 | if (!session) { 40 | throw new Error("User session not found"); 41 | } 42 | return ( 43 | <> 44 | ( 46 | 49 | )} 50 | title={document.name} 51 | titleEllipsizeMode="clip" 52 | titleNumberOfLines={2} 53 | /> 54 | 60 | 61 | ); 62 | }; 63 | DocumentItem.displayName = "DocumentItem"; 64 | export default DocumentItem; 65 | -------------------------------------------------------------------------------- /pages/dashboard/Explore/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useState } from "react"; 2 | import PageContainer from "../../../components/PageContainer"; 3 | import { 4 | ActivityIndicator, 5 | Button, 6 | List, 7 | Portal, 8 | Snackbar, 9 | Text, 10 | } from "react-native-paper"; 11 | import { useAxios } from "../../../lib/useAxios"; 12 | import { useSupabase } from "../../../providers/supabase-provider"; 13 | import * as NavigationService from "react-navigation-helpers"; 14 | import DocumentItem from "./DocumentItem"; 15 | import { Document } from "./types"; 16 | import getApiDomain from "../../../lib/context/BrainConfigProvider/helpers/getApiUrl"; 17 | 18 | interface ExploreProps {} 19 | 20 | const Explore: FC = ({}) => { 21 | const [documents, setDocuments] = useState([]); 22 | const [isPending, setIsPending] = useState(true); 23 | const [error, setError] = useState<{ message: string } | null>(null); 24 | const { session } = useSupabase(); 25 | const { axiosInstance } = useAxios(); 26 | 27 | if (!session) { 28 | NavigationService.navigate("Login"); 29 | } 30 | const fetchDocuments = async () => { 31 | setIsPending(true); 32 | try { 33 | console.log(`WAIT: Fetching documents from ${getApiDomain()}/explore`); 34 | const response = await axiosInstance.get<{ documents: Document[] }>( 35 | "/explore" 36 | ); 37 | setDocuments(response.data.documents); 38 | console.log(`DONE: Fetched documents from ${getApiDomain()}/explore`); 39 | } catch (error) { 40 | // console.error("Error fetching documents", error); 41 | setError(error as { message: string }); 42 | setDocuments([]); 43 | setIsPending(false); 44 | } 45 | setIsPending(false); 46 | }; 47 | 48 | useEffect(() => { 49 | fetchDocuments(); 50 | }, [session?.access_token]); 51 | 52 | return ( 53 | fetchDocuments()}> 54 | Explore 55 | 56 | View or delete stored data used by your brain 57 | 58 | 59 | {!isPending ? ( 60 | documents.length > 0 ? ( 61 | documents.map((doc) => ( 62 | 68 | )) 69 | ) : ( 70 | <> 71 | 72 | Oh No! Your brain is empty 73 | 74 | 75 | Upload some files first 76 | 77 | 78 | ) 79 | ) : ( 80 | 81 | )} 82 | 83 | 84 | { 88 | fetchDocuments(); 89 | }, 90 | }} 91 | visible={error !== null} 92 | onDismiss={() => setError(null)} 93 | > 94 | {error?.message} 95 | 96 | 97 | 98 | ); 99 | }; 100 | 101 | export default Explore; 102 | -------------------------------------------------------------------------------- /pages/dashboard/Explore/types.ts: -------------------------------------------------------------------------------- 1 | export interface Document { 2 | name: string; 3 | size: string; 4 | } 5 | -------------------------------------------------------------------------------- /pages/dashboard/Upload/hooks/useFileUploader.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | import { useSupabase } from "../../../../providers/supabase-provider"; 3 | import { useAxios } from "../../../../lib/useAxios"; 4 | import { Document, Message } from "../types"; 5 | import * as DocumentPicker from "expo-document-picker"; 6 | 7 | export const useFileUploader = () => { 8 | const [isPending, setIsPending] = useState(false); 9 | const [files, setFiles] = useState([]); 10 | const [message, setMessage] = useState(null); 11 | 12 | const { session } = useSupabase(); 13 | 14 | const { axiosInstance } = useAxios(); 15 | 16 | const upload = useCallback( 17 | async (file: Document) => { 18 | const formData = new FormData(); 19 | formData.append("file", { 20 | uri: file.uri, 21 | name: file.name, 22 | type: file.mimetype, 23 | }); 24 | // console.log(formData); 25 | 26 | try { 27 | // const response = await axiosInstance.get("/"); 28 | const response = await axiosInstance.post(`/upload`, formData, { 29 | headers: { "Content-Type": "multipart/form-data" }, 30 | }); 31 | // publish({ 32 | // variant: response.data.type, 33 | // text: 34 | // (response.data.type === "success" 35 | // ? "File uploaded successfully: " 36 | // : "") + JSON.stringify(response.data.message), 37 | // }); 38 | console.log(response); 39 | setMessage(response.data); 40 | } catch (error: unknown) { 41 | setMessage(error as Message); 42 | 43 | // publish({ 44 | // variant: "danger", 45 | // text: "Failed to upload file: " + JSON.stringify(error), 46 | // }); 47 | } 48 | }, 49 | [session?.access_token] 50 | ); 51 | 52 | const uploadAllFiles = async () => { 53 | if (files.length === 0) { 54 | return; 55 | } 56 | setIsPending(true); 57 | 58 | await Promise.all(files.map((file) => upload(file))); 59 | 60 | setFiles([]); 61 | setIsPending(false); 62 | }; 63 | 64 | const removeFile = (uri: string) => { 65 | setFiles((files) => files.filter((f) => f.uri !== uri)); 66 | }; 67 | 68 | const addFile = async () => { 69 | const docs = await DocumentPicker.getDocumentAsync({ 70 | multiple: true, 71 | }); 72 | if (docs.type === "success") { 73 | setFiles((files) => { 74 | const newDoc = { 75 | uri: docs.uri, 76 | name: docs.name, 77 | mimetype: docs.mimeType, 78 | }; 79 | return [...files, newDoc]; 80 | }); 81 | } 82 | // console.log("ERROR", e); 83 | }; 84 | 85 | const clearMessage = () => { 86 | setMessage(null); 87 | }; 88 | 89 | return { 90 | isPending, 91 | uploadAllFiles, 92 | files, 93 | addFile, 94 | removeFile, 95 | message, 96 | clearMessage, 97 | }; 98 | }; 99 | -------------------------------------------------------------------------------- /pages/dashboard/Upload/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from "react"; 2 | import { 3 | Button, 4 | Divider, 5 | FAB, 6 | IconButton, 7 | List, 8 | Portal, 9 | Snackbar, 10 | Text, 11 | } from "react-native-paper"; 12 | import PageContainer from "../../../components/PageContainer"; 13 | import { useFileUploader } from "./hooks/useFileUploader"; 14 | 15 | interface UploadProps {} 16 | 17 | const Upload: FC = ({}) => { 18 | const { 19 | isPending, 20 | files, 21 | addFile, 22 | removeFile, 23 | uploadAllFiles, 24 | message, 25 | clearMessage, 26 | } = useFileUploader(); 27 | 28 | return ( 29 | 30 | Upload Knowledge 31 | 32 | Text, document, spreadsheet, presentation, audio, video, and URLs 33 | supported 34 | 35 | 36 | 37 | {files.length > 0 ? ( 38 | <> 39 | 47 | {files.map((file) => { 48 | return ( 49 | ( 51 | removeFile(file.uri)} 54 | icon="close" 55 | {...props} 56 | /> 57 | )} 58 | key={file.uri} 59 | title={file.name} 60 | /> 61 | ); 62 | })} 63 | 64 | ) : ( 65 | Click the Add button to add some files 66 | )} 67 | 68 | 69 | 70 | 75 | {/* Error snack */} 76 | { 80 | uploadAllFiles(); 81 | }, 82 | }} 83 | visible={message !== null && message.type === "error"} 84 | onDismiss={clearMessage} 85 | > 86 | {message?.message} 87 | 88 | 89 | {/* Warning snack */} 90 | { 94 | clearMessage(); 95 | }, 96 | }} 97 | visible={message !== null && message.type === "warning"} 98 | onDismiss={() => clearMessage()} 99 | > 100 | {message?.message} 101 | 102 | 103 | {/* Success snack */} 104 | { 108 | clearMessage(); 109 | }, 110 | }} 111 | visible={message !== null && message.type === "success"} 112 | onDismiss={() => clearMessage()} 113 | > 114 | {message?.message} 115 | 116 | 117 | 118 | ); 119 | }; 120 | 121 | export default Upload; 122 | -------------------------------------------------------------------------------- /pages/dashboard/Upload/types.ts: -------------------------------------------------------------------------------- 1 | export interface Document { 2 | uri: string; 3 | name: string; 4 | mimetype?: string; 5 | } 6 | 7 | export interface Message { 8 | type: "error" | "warning" | "success"; 9 | message: string; 10 | } 11 | -------------------------------------------------------------------------------- /pages/dashboard/User/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import PageContainer from "../../../components/PageContainer"; 3 | import { Button, Text, useTheme } from "react-native-paper"; 4 | import { useSupabase } from "../../../providers/supabase-provider"; 5 | 6 | interface UserProps {} 7 | 8 | const User: FC = ({}) => { 9 | const { session, supabase } = useSupabase(); 10 | const theme = useTheme(); 11 | 12 | return ( 13 | 14 | 15 | Hello {session?.user.email?.split("@")[0]} 16 | 17 | {session?.user.email} 18 | 26 | 27 | ); 28 | }; 29 | 30 | export default User; 31 | -------------------------------------------------------------------------------- /providers/Paper.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | FC, 3 | ReactNode, 4 | createContext, 5 | useCallback, 6 | useContext, 7 | useEffect, 8 | useMemo, 9 | useState, 10 | } from "react"; 11 | import { 12 | MD3DarkTheme, 13 | MD3LightTheme, 14 | PaperProvider, 15 | adaptNavigationTheme, 16 | } from "react-native-paper"; 17 | import { 18 | NavigationContainer, 19 | DarkTheme as NavigationDarkTheme, 20 | DefaultTheme as NavigationDefaultTheme, 21 | } from "@react-navigation/native"; 22 | import merge from "deepmerge"; 23 | 24 | import { isReadyRef, navigationRef } from "react-navigation-helpers"; 25 | import SupabaseProvider from "./supabase-provider"; 26 | import { BrainConfigProvider } from "../lib/context/BrainConfigProvider/brain-config-provider"; 27 | 28 | const { LightTheme, DarkTheme } = adaptNavigationTheme({ 29 | reactNavigationLight: NavigationDefaultTheme, 30 | reactNavigationDark: NavigationDarkTheme, 31 | }); 32 | 33 | const CombinedDefaultTheme = merge(MD3LightTheme, LightTheme); 34 | const CombinedDarkTheme = merge(MD3DarkTheme, DarkTheme); 35 | 36 | // PREFERENCES 37 | const defaultPreferences = { toggleDarkMode: () => {}, darkMode: false }; 38 | const PreferencesContext = createContext(defaultPreferences); 39 | export const usePreferences = () => { 40 | const context = useContext(PreferencesContext); 41 | if (!context) { 42 | throw new Error( 43 | "usePreferences must be used within PreferencesContext.Provider" 44 | ); 45 | } 46 | return context; 47 | }; 48 | 49 | interface PaperProps { 50 | children?: ReactNode; 51 | } 52 | 53 | const Paper: FC = ({ children }) => { 54 | const [darkMode, setDarkMode] = useState(false); 55 | 56 | let theme = darkMode ? CombinedDarkTheme : CombinedDefaultTheme; 57 | 58 | const toggleDarkMode = useCallback(() => { 59 | return setDarkMode(!darkMode); 60 | }, [darkMode]); 61 | 62 | const preferences = useMemo( 63 | () => ({ 64 | toggleDarkMode, 65 | darkMode, 66 | }), 67 | [toggleDarkMode, darkMode] 68 | ); 69 | 70 | useEffect((): any => { 71 | return () => (isReadyRef.current = false); 72 | }, []); 73 | 74 | return ( 75 | 76 | 77 | 78 | 79 | { 82 | isReadyRef.current = true; 83 | }} 84 | theme={theme} 85 | > 86 | {children} 87 | 88 | 89 | 90 | 91 | 92 | ); 93 | }; 94 | 95 | export default Paper; 96 | -------------------------------------------------------------------------------- /providers/supabase-provider.tsx: -------------------------------------------------------------------------------- 1 | import "react-native-url-polyfill/auto"; 2 | import { Session, SupabaseClient, createClient } from "@supabase/supabase-js"; 3 | import { createContext, useContext, useEffect, useState } from "react"; 4 | import AsyncStorage from "@react-native-async-storage/async-storage"; 5 | 6 | type MaybeSession = Session | null; 7 | 8 | type SupabaseContext = { 9 | supabase: SupabaseClient; 10 | session: MaybeSession; 11 | isPending: boolean; 12 | }; 13 | 14 | const Context = createContext(undefined); 15 | 16 | export default function SupabaseProvider({ 17 | children, 18 | }: { 19 | children: React.ReactNode; 20 | }) { 21 | const [supabase] = useState(() => 22 | createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!, { 23 | auth: { storage: AsyncStorage as any }, 24 | }) 25 | ); 26 | const [session, setSession] = useState(null); 27 | const [isPending, setIsPending] = useState(true); 28 | 29 | useEffect(() => { 30 | const { 31 | data: { subscription }, 32 | } = supabase.auth.onAuthStateChange((_, session) => { 33 | setSession(session); 34 | console.log("AUTH CHANGE"); 35 | }); 36 | setIsPending(false); 37 | 38 | return () => { 39 | subscription.unsubscribe(); 40 | }; 41 | }, []); 42 | 43 | return ( 44 | 45 | <>{children} 46 | 47 | ); 48 | } 49 | 50 | export const useSupabase = () => { 51 | const context = useContext(Context); 52 | 53 | if (context === undefined) { 54 | throw new Error("useSupabase must be used inside SupabaseProvider"); 55 | } 56 | 57 | return context; 58 | }; 59 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/react-native/tsconfig.json" 3 | } --------------------------------------------------------------------------------