├── .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 | }
--------------------------------------------------------------------------------