├── .eslintrc.json ├── .github └── workflows │ ├── ci.yml │ ├── production.yml │ └── staging.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc ├── .vscode └── settings.json ├── DEVELOPERS.md ├── README.md ├── components.json ├── cypress.config.ts ├── cypress.env.json ├── cypress ├── e2e │ ├── dashboard.cy.ts │ └── public-api.cy.ts ├── fixtures │ └── example.json └── support │ ├── commands.ts │ └── e2e.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── architecture.png ├── flow.png ├── next.svg ├── sqd-dark-trans.png ├── sqd-light-trans.png ├── steps.png └── vercel.svg ├── src ├── app │ ├── api │ │ ├── chat │ │ │ ├── completions │ │ │ │ └── route.ts │ │ │ └── rag │ │ │ │ └── route.ts │ │ ├── docs │ │ │ └── page.tsx │ │ ├── documents │ │ │ └── route.ts │ │ ├── indexes │ │ │ ├── route.ts │ │ │ └── search │ │ │ │ └── route.ts │ │ └── route.ts │ ├── auth │ │ ├── callback │ │ │ └── route.ts │ │ ├── sign-in │ │ │ └── route.ts │ │ ├── sign-out │ │ │ └── route.ts │ │ ├── sign-up-early │ │ │ └── route.ts │ │ └── sign-up │ │ │ └── route.ts │ ├── components │ │ └── nav.tsx │ ├── dashboard │ │ ├── page.tsx │ │ └── projects │ │ │ └── [projectId] │ │ │ ├── indexes │ │ │ └── [indexId] │ │ │ │ ├── page.tsx │ │ │ │ └── upload │ │ │ │ └── route.ts │ │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── login │ │ ├── components │ │ │ └── user-auth-form.tsx.tsx │ │ └── page.tsx │ ├── page.tsx │ └── thank-you │ │ └── page.tsx ├── components │ ├── Button │ │ └── index.tsx │ ├── Input │ │ └── index.tsx │ ├── NewIndex │ │ └── index.tsx │ ├── NewProject │ │ └── index.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── dialog.tsx │ │ ├── form.tsx │ │ └── label.tsx ├── lib │ ├── public-api │ │ ├── auth.ts │ │ ├── database.ts │ │ ├── llm.ts │ │ ├── openapi.ts │ │ └── validation.ts │ └── utils.ts ├── middleware.ts ├── providers │ └── ThemeProvider │ │ └── index.tsx └── types │ ├── supabase-entities.ts │ └── supabase.ts ├── supabase ├── .gitignore ├── config.toml ├── migrations │ ├── 20231014182810_initial.sql │ ├── 20231015132106_feature.sql │ ├── 20231027154727_feature.sql │ ├── 20231027155201_feature.sql │ └── 20231027163314_feature.sql └── seed.sql ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - uses: supabase/setup-cli@v1 15 | with: 16 | version: 1.99.5 17 | 18 | - name: Start Supabase local development setup 19 | run: supabase start 20 | 21 | - name: Verify generated types are checked in 22 | run: | 23 | supabase gen types typescript --local > types.gen.ts 24 | if ! git diff --ignore-space-at-eol --exit-code --quiet types.gen.ts; then 25 | echo "Detected uncommitted changes after build. See status below:" 26 | git diff 27 | exit 1 28 | fi 29 | -------------------------------------------------------------------------------- /.github/workflows/production.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Migrations to Production 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | 13 | env: 14 | SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} 15 | SUPABASE_DB_PASSWORD: ${{ secrets.PRODUCTION_DB_PASSWORD }} 16 | SUPABASE_PROJECT_ID: ${{ secrets.PRODUCTION_PROJECT_ID }} 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - uses: supabase/setup-cli@v1 22 | with: 23 | version: 1.99.5 24 | 25 | - run: supabase link --project-ref ${{ secrets.PRODUCTION_PROJECT_ID }} 26 | - run: supabase db push 27 | -------------------------------------------------------------------------------- /.github/workflows/staging.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Migrations to preview 2 | 3 | on: 4 | push: 5 | branches: 6 | - preview 7 | workflow_dispatch: 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | 13 | env: 14 | SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} 15 | SUPABASE_DB_PASSWORD: ${{ secrets.PREVIEW_DB_PASSWORD }} 16 | SUPABASE_PROJECT_ID: ${{ secrets.PREVIEW_PROJECT_ID }} 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - uses: supabase/setup-cli@v1 22 | with: 23 | version: 1.99.5 24 | 25 | - run: supabase link --project-ref ${{ secrets.PREVIEW_PROJECT_ID }} 26 | - run: supabase db push 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # enviroment variables 38 | .env -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run pretty-staged 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["DATACRUNCH"] 3 | } 4 | -------------------------------------------------------------------------------- /DEVELOPERS.md: -------------------------------------------------------------------------------- 1 | Developing SquareDev 2 | 3 | ## Getting started 4 | 5 | Thank you for expressing your interest in SquareDev and your willingness to contribute! 6 | 7 | This guide will be populated soon enough. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |

5 | 6 | --- 7 | 8 | # SquareDev 9 | 10 | SquareDev is the platform for developing applications powered by language models. 11 | 12 | Use cases include: 13 | 14 | - 📈 Chat with your data 15 | - 💬 Generate personalized text (emails, newsletter, notifications) 16 | - 🤖 Chatbots 17 | - 📊 Analyzing structured data 18 | - 🔎 Semantic search 19 | - 📚 Text & knowledge extraction 20 | - 🧹 Structure unstructured data 21 | 22 | ![Steps](/public/steps.png 'steps') 23 | 24 | 25 | ## Features 26 | 27 | - [x] Document Loaders 28 | - [x] PDF 29 | - [ ] JSON (coming soon) 30 | - [ ] Website (coming soon) 31 | - [x] Vectors & Embeddings 32 | - [x] [Retrieval Augmented Generation](https://www.perplexity.ai/search/Retrieval-Augmented-Generation-wdAKdu4sSE.s1td7mtXqEQ?s=c) 33 | - [x] [Semantic search](https://www.perplexity.ai/search/semantic-search-eXS9K0oARMizIBbAkSvSAw?s=c) 34 | - [ ] Memory (coming soon) 35 | - [x] Hosted Large Language Models (OSS & APIs) 36 | - [x] Open AI 37 | - [ ] [HuggingFaceH4/zephyr-7b-beta)](https://huggingface.co/HuggingFaceH4/zephyr-7b-beta) 38 | - [ ] Monitoring (coming soon) 39 | - [ ] Usage (coming soon) 40 | - [ ] User feedback (coming soon) 41 | 42 | 43 | ## Documentation 44 | 45 | Full documentation (coming soon) 46 | 47 | Full details on how to contribute will be available soon. For now please leave a ⭐️ and watch the repo. 48 | 49 | ## Community & Support 50 | 51 | - [GitHub Discussions](https://github.com/squaredev-io/squaredev/discussions). Best for: help with building, discussion about best practices. 52 | - [GitHub Issues](https://github.com/squaredev-io/squaredev/issues). Best for: bugs and errors you encounter using SquareDev. 53 | 54 | ## Status 55 | 56 | - [x] Alpha: We are testing SquareDev with a closed set of customers 57 | - [ ] Public Alpha: Anyone can sign up over at SquareDev.io. But go easy on us, there are a few kinks 58 | - [ ] Public Beta: Stable enough for most use-cases 59 | - [ ] Public: General Availability 60 | 61 | We are currently in Alpha. Watch "releases" of this repo to get notified of major updates. 62 | 63 | ## How it works 64 | 65 | SquareDev is a combination of open source tools that makes it easy to build with LLMs. Sitting on the shoulder of giant like [LangChain](https://www.langchain.com/), [Hugging Face](https://huggingface.co/), [Supabase](https://supabase.com/) and others. [SquareDev](https://squaredev.io/) is building the tools for developers with or without AI expertise to build with LLMs. 66 | 67 | ### Architecture 68 | 69 | ![Architecture](/public/architecture.png 'Architecture') 70 | 71 | ### Components 72 | 73 | - Studio: The UI you will be interacting with to setup your project, manage your data, get API keys and other settings. 74 | - API: The API is the gateway to your project. It is the interface that allows you to interact with your project. 75 | - Knowledge engine: Handles all the magic that has to do with LLMs and embeddings of Retrieval, Memory, Contextual search, text extraction and other core features. 76 | - Monitoring: Monitors your project and provides insights on performance, latency, malicious usage and other metrics. 77 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "blue", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | import { supabaseExecute } from './src/lib/public-api/database'; 3 | 4 | export default defineConfig({ 5 | env: { 6 | database_url: 'postgresql://postgres:postgres@localhost:54322/postgres', 7 | }, 8 | e2e: { 9 | baseUrl: 'http://localhost:3000', 10 | setupNodeEvents(on, config) { 11 | // implement node event listeners here 12 | 13 | on('task', { 14 | supabaseExecute: supabaseExecute, 15 | }); 16 | }, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /cypress.env.json: -------------------------------------------------------------------------------- 1 | { 2 | "database_url": "postgresql://postgres:postgres@localhost:54322/postgres" 3 | } 4 | -------------------------------------------------------------------------------- /cypress/e2e/dashboard.cy.ts: -------------------------------------------------------------------------------- 1 | export function signIn() { 2 | cy.visit('/login'); 3 | 4 | cy.get('input[name="email"]').eq(1).type('test@test.com'); 5 | cy.get('input[name="password"]').eq(1).type('test123'); 6 | cy.get('form button[type="submit"]').eq(1).click(); 7 | cy.url().should('include', '/dashboard'); 8 | } 9 | 10 | describe('Create a new account', () => { 11 | it('Creates a new account', () => { 12 | cy.visit('/'); 13 | cy.contains('Get Started').click(); 14 | cy.url().should('include', '/login'); 15 | 16 | cy.get('input[name="email"]').eq(0).type('test@test.com'); 17 | cy.get('input[name="password"]').eq(0).type('test123'); 18 | cy.get('form button[type="submit"]').eq(0).click(); 19 | }); 20 | 21 | it('Logs in with new account', () => { 22 | signIn(); 23 | }); 24 | }); 25 | 26 | describe('Configure a new project and index', () => { 27 | before(() => { 28 | signIn(); 29 | }); 30 | 31 | it('Creates an project and a knowledge base', () => { 32 | cy.visit('/dashboard'); 33 | cy.contains('Dashboard'); 34 | cy.contains('Create new project').click(); 35 | 36 | cy.get('input[name="name"]').type(`Test Project ${new Date().getTime()}`); 37 | cy.get('form button').eq(1).click(); 38 | 39 | cy.contains('Test project').click(); 40 | 41 | cy.contains('Add index').click(); 42 | 43 | cy.get('input[name="name"]').type(`Test Index ${new Date().getTime()}`); 44 | 45 | cy.contains('add').click(); 46 | cy.contains('Test Index'); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /cypress/e2e/public-api.cy.ts: -------------------------------------------------------------------------------- 1 | import { Chat } from 'openai/resources'; 2 | import { supabaseExecute } from '../../src/lib/public-api/database'; 3 | import { 4 | DocumentsResponse, 5 | CreateIndexResponse, 6 | GetIndexesResponse, 7 | ChatCompletionResponse, 8 | RagCompletionResponse, 9 | SimilarDocumentsResponse, 10 | } from '../../src/lib/public-api/validation'; 11 | import { Project, Index } from '../../src/types/supabase-entities'; 12 | 13 | let state = { 14 | project: null as Project | null, 15 | index: null as Index | null, 16 | }; 17 | 18 | const routes = { 19 | getIndexes: '/api/indexes', 20 | searchIndex: '/api/indexes/search', 21 | postDocuments: '/api/documents', 22 | getDocuments: '/api/documents', 23 | searchDocuments: '/api/documents/search', 24 | chat: '/api/chat/completions', 25 | rag: '/api/chat/rag', 26 | }; 27 | 28 | context('Test Public API', () => { 29 | before(async () => {}); 30 | 31 | it('gets the project to be tested from database', () => { 32 | const query = `select * from projects where name = 'Test project'`; 33 | cy.task('supabaseExecute', query).then(({ data, error }: any) => { 34 | expect(data[0].name).to.eq('Test project'); 35 | state.project = data[0]; 36 | }); 37 | }); 38 | 39 | context(`POST ${routes.getIndexes}`, () => { 40 | it('should fail when there are no correct headers', () => { 41 | cy.request({ 42 | method: 'POST', 43 | url: routes.getIndexes, 44 | failOnStatusCode: false, 45 | }).then((response) => { 46 | expect(response.status).to.eq(401); 47 | }); 48 | }); 49 | 50 | it('should create a new index', () => { 51 | const name = `Test index ${new Date().getTime()}`; 52 | cy.request({ 53 | method: 'POST', 54 | url: routes.getIndexes, 55 | headers: { 56 | authentication: `Bearer ${state.project?.api_key}`, 57 | }, 58 | body: { 59 | name, 60 | }, 61 | }).then((response) => { 62 | expect(response.status).to.eq(200); 63 | expect(response.body.name).to.eq(name); 64 | CreateIndexResponse.parse(response.body); 65 | }); 66 | }); 67 | }); 68 | 69 | context(`GET ${routes.getIndexes}`, () => { 70 | it('should fail when there are no correct headers', () => { 71 | cy.request({ 72 | method: 'GET', 73 | url: routes.getIndexes, 74 | failOnStatusCode: false, 75 | }).then((response) => { 76 | expect(response.status).to.eq(401); 77 | }); 78 | }); 79 | 80 | it('should return the indexes', () => { 81 | cy.request({ 82 | method: 'GET', 83 | url: routes.getIndexes, 84 | headers: { 85 | authentication: `Bearer ${state.project?.api_key}`, 86 | }, 87 | }).then((response) => { 88 | expect(response.status).to.eq(200); 89 | expect(response.body.length).to.greaterThan(0); 90 | GetIndexesResponse.parse(response.body); 91 | state.index = response.body[0]; 92 | }); 93 | }); 94 | }); 95 | 96 | context(`POST ${routes.postDocuments}`, () => { 97 | it('should load documents to an index along with metadata ', () => { 98 | cy.request({ 99 | method: 'POST', 100 | url: `${routes.postDocuments}?index_id=${state.index?.id}`, 101 | headers: { 102 | authentication: `Bearer ${state.project?.api_key}`, 103 | }, 104 | body: [ 105 | { 106 | content: 'The name of the company is SquareDev.', 107 | source: 'the founders', 108 | metadata: { string: 'string' }, 109 | }, 110 | ], 111 | }).then((response) => { 112 | expect(response.status).to.eq(200); 113 | expect(response.body.length).to.greaterThan(0); 114 | DocumentsResponse.parse(response.body); 115 | }); 116 | }); 117 | 118 | it('should fail when there is no content in the document', () => { 119 | cy.request({ 120 | failOnStatusCode: false, 121 | method: 'POST', 122 | url: `${routes.postDocuments}?index_id=${state.index?.id}`, 123 | headers: { 124 | authentication: `Bearer ${state.project?.api_key}`, 125 | }, 126 | body: [ 127 | { 128 | source: 'string', 129 | metadata: { string: 'string' }, 130 | }, 131 | ], 132 | }).then((response) => { 133 | // TODO: This should be 400 134 | expect(response.status).to.eq(500); 135 | }); 136 | }); 137 | }); 138 | 139 | context(`POST ${routes.postDocuments}/upload`, () => { 140 | it.skip('json', () => {}); 141 | it.skip('pdf', () => {}); 142 | }); 143 | 144 | context(`POST ${routes.postDocuments}/web`, () => {}); 145 | 146 | context(`GET ${routes.getDocuments}`, () => { 147 | it('should get the documents of an index', () => { 148 | cy.request({ 149 | method: 'GET', 150 | url: `${routes.postDocuments}?index_id=${state.index?.id}`, 151 | headers: { 152 | authentication: `Bearer ${state.project?.api_key}`, 153 | }, 154 | }).then((response) => { 155 | expect(response.status).to.eq(200); 156 | expect(response.body.length).to.greaterThan(0); 157 | DocumentsResponse.parse(response.body); 158 | }); 159 | }); 160 | 161 | it.skip('return the correct error for wrong indexes', () => {}); 162 | }); 163 | 164 | context(`GET ${routes.searchIndex}`, () => { 165 | it('should get the documents of an index that are contextually similar to the search term', () => { 166 | cy.request({ 167 | method: 'GET', 168 | url: `${routes.searchIndex}?index_id=${state.index?.id}&search=A random`, 169 | headers: { 170 | authentication: `Bearer ${state.project?.api_key}`, 171 | }, 172 | }).then((response) => { 173 | expect(response.status).to.eq(200); 174 | expect(response.body.length).to.greaterThan(0); 175 | SimilarDocumentsResponse.parse(response.body); 176 | }); 177 | }); 178 | 179 | it.skip('return the correct error for wrong indexes', () => {}); 180 | it.skip('return the correct error for no search term', () => {}); 181 | }); 182 | 183 | context(`POST /chat/completions`, () => { 184 | it('should chat with the LLM, only completing the requested prompt', () => { 185 | cy.request({ 186 | method: 'POST', 187 | url: `/api/chat/completions?model=gpt-3.5-turbo`, 188 | headers: { 189 | authentication: `Bearer ${state.project?.api_key}`, 190 | }, 191 | body: { 192 | model: 'gpt-3.5-turbo', 193 | messages: { 194 | system: 'Hello, how are you?', 195 | user: 'I am fine, how are you?', 196 | }, 197 | }, 198 | }).then((response) => { 199 | expect(response.status).to.eq(200); 200 | ChatCompletionResponse.parse(response.body); 201 | }); 202 | }); 203 | }); 204 | 205 | context(`POST ${routes.rag}`, () => { 206 | it('chats with RAG', () => { 207 | cy.request({ 208 | method: 'POST', 209 | url: `${routes.rag}?model=gpt-3.5-turbo`, 210 | headers: { 211 | authentication: `Bearer ${state.project?.api_key}`, 212 | }, 213 | body: { 214 | model: 'gpt-3.5-turbo', 215 | messages: { 216 | system: 217 | 'You are a helpful assistant. Try to answer the question using the given context.', 218 | user: 'Context: {context} \nQuestion: What is the name of the company?', 219 | }, 220 | indexId: state.index?.id, 221 | }, 222 | }).then((response) => { 223 | expect(response.status).to.eq(200); 224 | expect(response.body.message).to.include('SquareDev'); 225 | console.log(response.body); 226 | RagCompletionResponse.parse(response.body); 227 | }); 228 | }); 229 | 230 | it('returns an error if no {context} placeholder', () => { 231 | cy.request({ 232 | method: 'POST', 233 | url: `${routes.rag}`, 234 | failOnStatusCode: false, 235 | headers: { 236 | authentication: `Bearer ${state.project?.api_key}`, 237 | }, 238 | body: { 239 | model: 'gpt-3.5-turbo', 240 | messages: { 241 | system: 242 | 'You are a helpful assistant. Try to answer the question using the given context.', 243 | user: 'Context: no context placeholder \nQuestion: What is the name of the company?', 244 | }, 245 | indexId: state.index?.id, 246 | }, 247 | }).then((response) => { 248 | expect(response.status).to.eq(400); 249 | }); 250 | }); 251 | }); 252 | }); 253 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } 38 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | module.exports = nextConfig; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "studio", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "reset": "npx supabase db reset", 11 | "diff": "npx supabase db diff -f feature", 12 | "migration": "npx supabase migration new feature", 13 | "update-types": "npx supabase gen types typescript --local > src/types/supabase.ts", 14 | "update-db": "npm run diff && npm run reset && npm run update-types", 15 | "prepare": "husky install", 16 | "pretty-staged": "pretty-quick --staged", 17 | "pretty-all": "prettier {*,**/*}.{js,ts,tsx,json,css,md} --write --no-error-on-unmatched-pattern", 18 | "cypress": "npx cypress open" 19 | }, 20 | "dependencies": { 21 | "@asteasolutions/zod-to-openapi": "^6.2.0", 22 | "@fontsource/inter": "^5.0.8", 23 | "@hookform/resolvers": "^3.3.2", 24 | "@radix-ui/react-accordion": "^1.1.2", 25 | "@radix-ui/react-avatar": "^1.0.4", 26 | "@radix-ui/react-checkbox": "^1.0.4", 27 | "@radix-ui/react-dialog": "^1.0.5", 28 | "@radix-ui/react-label": "^2.0.2", 29 | "@radix-ui/react-slot": "^1.0.2", 30 | "@supabase/auth-helpers-nextjs": "^0.8.1", 31 | "@vercel/analytics": "^1.1.1", 32 | "class-variance-authority": "^0.7.0", 33 | "clsx": "^2.0.0", 34 | "core-js": "^3.33.2", 35 | "langchain": "^0.0.163", 36 | "lucide-react": "^0.284.0", 37 | "mobx": "^6.10.2", 38 | "next": "^13.5.3", 39 | "next-swagger-doc": "^0.4.0", 40 | "next-themes": "^0.2.1", 41 | "pdf-parse": "^1.1.1", 42 | "pg": "^8.11.3", 43 | "prettier": "^2.8.8", 44 | "react": "^18.2.0", 45 | "react-dom": "^18.2.0", 46 | "react-hook-form": "^7.47.0", 47 | "redoc": "^2.1.3", 48 | "styled-components": "^6.1.0", 49 | "tailwind-merge": "^1.14.0", 50 | "tailwindcss-animate": "^1.0.7", 51 | "zod": "^3.22.4" 52 | }, 53 | "devDependencies": { 54 | "@types/node": "^20.8.0", 55 | "@types/pg": "^8.10.5", 56 | "@types/react": "^18.2.23", 57 | "@types/react-dom": "^18.2.8", 58 | "@types/swagger-ui-react": "^4.18.1", 59 | "autoprefixer": "^10.4.16", 60 | "cypress": "^13.3.1", 61 | "encoding": "^0.1.13", 62 | "eslint": "^8.50.0", 63 | "eslint-config-next": "^13.5.3", 64 | "husky": "^8.0.3", 65 | "postcss": "^8.4.14", 66 | "pretty-quick": "^3.1.3", 67 | "supabase": "^1.99.5", 68 | "tailwindcss": "^3.3.3", 69 | "typescript": "^5.2.2" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squaredev-io/squaredev/e15c050e441c4429e2685f522c266f6ef3a2447a/public/architecture.png -------------------------------------------------------------------------------- /public/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squaredev-io/squaredev/e15c050e441c4429e2685f522c266f6ef3a2447a/public/flow.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/sqd-dark-trans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squaredev-io/squaredev/e15c050e441c4429e2685f522c266f6ef3a2447a/public/sqd-dark-trans.png -------------------------------------------------------------------------------- /public/sqd-light-trans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squaredev-io/squaredev/e15c050e441c4429e2685f522c266f6ef3a2447a/public/sqd-light-trans.png -------------------------------------------------------------------------------- /public/steps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squaredev-io/squaredev/e15c050e441c4429e2685f522c266f6ef3a2447a/public/steps.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/api/chat/completions/route.ts: -------------------------------------------------------------------------------- 1 | import { headers } from 'next/headers'; 2 | import { NextRequest, NextResponse } from 'next/server'; 3 | import { authApiKey } from '@/lib/public-api/auth'; 4 | import { AVAILABLE_MODELS, llm } from '@/lib/public-api/llm'; 5 | import { ChatCompletionRequestType } from '@/lib/public-api/validation'; 6 | 7 | // Add documents to a knowledge base 8 | export async function POST(request: NextRequest) { 9 | const { data: project, error: authError } = await authApiKey(headers()); 10 | 11 | if (!project || authError) { 12 | return NextResponse.json({ error: authError }, { status: 401 }); 13 | } 14 | 15 | const { messages, model }: ChatCompletionRequestType = await request.json(); 16 | 17 | if (!AVAILABLE_MODELS.includes(model)) { 18 | return NextResponse.json( 19 | { error: `Model ${model} not found.` }, 20 | { status: 400 } 21 | ); 22 | } 23 | 24 | const response = await llm({ 25 | message: messages.user, 26 | system: messages.system, 27 | model, 28 | }); 29 | 30 | return NextResponse.json({ 31 | message: response.choices[0].message.content, 32 | model: response.model, 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/app/api/chat/rag/route.ts: -------------------------------------------------------------------------------- 1 | import { headers } from 'next/headers'; 2 | import { NextRequest, NextResponse } from 'next/server'; 3 | import { authApiKey } from '@/lib/public-api/auth'; 4 | import { OpenAI } from 'openai'; 5 | import { OpenAIEmbeddings } from 'langchain/embeddings/openai'; 6 | import { supabaseExecute } from '@/lib/public-api/database'; 7 | import { Document } from '@/types/supabase-entities'; 8 | import { AVAILABLE_MODELS, llm } from '@/lib/public-api/llm'; 9 | import { RagCompletionRequestType } from '@/lib/public-api/validation'; 10 | 11 | // Add documents to a knowledge base 12 | export async function POST(request: NextRequest) { 13 | const { data: project, error: authError } = await authApiKey(headers()); 14 | 15 | if (!project || authError) { 16 | return NextResponse.json({ error: authError }, { status: 401 }); 17 | } 18 | 19 | const { messages, model, indexId }: RagCompletionRequestType = 20 | await request.json(); 21 | 22 | if (!AVAILABLE_MODELS.includes(model)) { 23 | return NextResponse.json( 24 | { error: `Model ${model} not found.` }, 25 | { status: 400 } 26 | ); 27 | } 28 | 29 | if (indexId && !messages.user.includes('{context}')) { 30 | const error = `The user message must include {context} placeholder to use an index.`; 31 | return NextResponse.json({ error }, { status: 400 }); 32 | } 33 | 34 | let promptMessage = messages.user; 35 | let sources: Document[] = []; 36 | if (indexId) { 37 | // If user specifies a knowledge base, we use RAG. 38 | const openAIEmbeddings = new OpenAIEmbeddings(); 39 | const embeddings = await openAIEmbeddings.embedDocuments([messages.user]); 40 | 41 | const query = ` 42 | select 1 - (embedding <=> '[${embeddings.toString()}]') as similarity, content, id, metadata, source 43 | from documents 44 | where index_id = '${indexId}' 45 | order by similarity desc 46 | limit 3; 47 | `; 48 | 49 | const { data: relevantDocuments, error } = await supabaseExecute( 50 | query 51 | ); 52 | 53 | if (error) { 54 | return NextResponse.json({ error }, { status: 400 }); 55 | } 56 | 57 | const context = relevantDocuments[0]?.content || ''; 58 | promptMessage = messages.user.replace('{context}', context); 59 | sources = [...relevantDocuments]; 60 | } 61 | 62 | const response = await llm({ 63 | message: promptMessage, 64 | system: messages.system, 65 | model, 66 | }); 67 | 68 | return NextResponse.json({ 69 | message: response.choices[0].message.content, 70 | model: response.model, 71 | sources, 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /src/app/api/docs/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { RedocStandalone } from 'redoc'; 4 | 5 | export default function ApiDoc() { 6 | return ( 7 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/api/documents/route.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * /api/documents: 4 | * get: 5 | * summary: Get all documents from a index 6 | * description: Returns all documents from the specified index 7 | * tags: 8 | * - Documents 9 | * parameters: 10 | * - in: query 11 | * name: knowledge_base_id 12 | * schema: 13 | * type: string 14 | * required: true 15 | * description: The ID of the index to retrieve documents from 16 | * responses: 17 | * 200: 18 | * description: Returns all documents from the specified index 19 | * content: 20 | * application/json: 21 | * schema: 22 | * type: array 23 | * 400: 24 | * description: Bad request 25 | * content: 26 | * application/json: 27 | * schema: 28 | * type: object 29 | * properties: 30 | * error: 31 | * type: string 32 | * description: The error message 33 | * 34 | * post: 35 | * summary: Add documents to a index 36 | * description: Adds new documents to the specified index 37 | * tags: 38 | * - Documents 39 | * parameters: 40 | * - in: query 41 | * name: knowledge_base_id 42 | * schema: 43 | * type: string 44 | * required: true 45 | * description: The ID of the index to add documents to 46 | * requestBody: 47 | * description: The document content, source, and metadata 48 | * required: true 49 | * responses: 50 | * 200: 51 | * description: Returns the inserted document 52 | * content: 53 | * application/json: 54 | * schema: 55 | * $ref: '#/components/schemas/Document' 56 | * 400: 57 | * description: Bad request 58 | * content: 59 | * application/json: 60 | * schema: 61 | * type: object 62 | * properties: 63 | * error: 64 | * type: string 65 | * description: The error message 66 | */ 67 | 68 | import { headers } from 'next/headers'; 69 | import { NextRequest, NextResponse } from 'next/server'; 70 | import { authApiKey } from '@/lib/public-api/auth'; 71 | import { Document, DocumentInsert } from '@/types/supabase-entities'; 72 | import { supabaseExecute } from '@/lib/public-api/database'; 73 | import { OpenAIEmbeddings } from 'langchain/embeddings/openai'; 74 | 75 | // Get all documents from a index 76 | export async function GET(request: NextRequest) { 77 | const { data: project, error: authError } = await authApiKey(headers()); 78 | 79 | if (!project || authError) { 80 | return NextResponse.json({ error: authError }, { status: 401 }); 81 | } 82 | 83 | const indexId = request.nextUrl.searchParams.get('index_id'); 84 | if (!indexId) { 85 | return NextResponse.json( 86 | { error: 'Missing index_id query parameter' }, 87 | { status: 400 } 88 | ); 89 | } 90 | 91 | const query = `select id, content, metadata, index_id, source, user_id, created_at 92 | from documents where index_id = '${indexId}' limit 50;`; 93 | 94 | const { data, error } = await supabaseExecute(query); 95 | 96 | if (error) { 97 | return NextResponse.json({ data, error }, { status: 400 }); 98 | } 99 | 100 | return NextResponse.json(data); 101 | } 102 | 103 | interface DocumentPostRequest { 104 | content: string; 105 | source: string; 106 | metadata: any; 107 | } 108 | 109 | // Add documents to a index 110 | export async function POST(request: NextRequest) { 111 | const { data: project, error: authError } = await authApiKey(headers()); 112 | 113 | if (!project || authError) { 114 | return NextResponse.json({ error: authError }, { status: 401 }); 115 | } 116 | 117 | const indexId = request.nextUrl.searchParams.get('index_id'); 118 | if (!indexId) { 119 | return NextResponse.json( 120 | { error: 'Missing index_id query parameter' }, 121 | { status: 400 } 122 | ); 123 | } 124 | 125 | const documents = (await request.json()) as DocumentPostRequest[]; 126 | // TODO: Validate documents 127 | 128 | if (!documents || !documents.length) { 129 | return NextResponse.json( 130 | { error: 'Missing documents in request body' }, 131 | { status: 400 } 132 | ); 133 | } 134 | 135 | const openAIEmbeddings = new OpenAIEmbeddings({ 136 | batchSize: 512, // Default value if omitted is 512. Max is 2048 137 | }); 138 | 139 | const embeddings = await openAIEmbeddings.embedDocuments( 140 | documents.map((doc) => doc.content) 141 | ); 142 | 143 | const documentInsert: DocumentInsert[] = documents.map((doc, index) => ({ 144 | embedding: embeddings[index] as unknown as string, // This is not right. The type generation from supabase is wrong here. 145 | content: doc.content, 146 | metadata: doc.metadata, 147 | index_id: indexId, 148 | source: doc.source, 149 | user_id: project.user_id as string, 150 | })); 151 | 152 | const query = ` 153 | INSERT INTO documents (embedding, content, metadata, index_id, source, user_id) 154 | VALUES ${documentInsert 155 | .map( 156 | (doc) => 157 | `('[${doc.embedding.toString()}]', '${doc.content}', '${JSON.stringify( 158 | doc.metadata 159 | )}', '${doc.index_id}', '${doc.source}', '${doc.user_id}')` 160 | ) 161 | .join(',')} 162 | RETURNING content, metadata, index_id, source, user_id, created_at, id;`; 163 | 164 | const { data, error } = await supabaseExecute(query); 165 | 166 | if (error) { 167 | return NextResponse.json({ data, error }, { status: 400 }); 168 | } 169 | 170 | return NextResponse.json(data); 171 | } 172 | -------------------------------------------------------------------------------- /src/app/api/indexes/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { headers } from 'next/headers'; 3 | import { authApiKey } from '@/lib/public-api/auth'; 4 | import { supabaseExecute } from '@/lib/public-api/database'; 5 | import { Index } from '@/types/supabase-entities'; 6 | 7 | export async function GET(request: NextRequest) { 8 | const { data: project, error: authError } = await authApiKey(headers()); 9 | 10 | if (!project || authError) { 11 | return NextResponse.json({ error: authError }, { status: 401 }); 12 | } 13 | 14 | const query = `select * from indexes where project_id = '${project.id}'`; 15 | 16 | const { data, error } = await supabaseExecute(query); 17 | 18 | if (error) { 19 | return NextResponse.json({ data, error }, { status: 400 }); 20 | } 21 | 22 | return NextResponse.json(data); 23 | } 24 | 25 | export async function POST(request: NextRequest) { 26 | const { data: project, error: authError } = await authApiKey(headers()); 27 | 28 | if (!project || authError) { 29 | return NextResponse.json({ error: authError }, { status: 401 }); 30 | } 31 | 32 | const { name } = await request.json(); 33 | 34 | const query = `insert into indexes (name, project_id, user_id) 35 | values ('${name}', '${project.id}', '${project.user_id}') returning *`; 36 | 37 | const { data, error } = await supabaseExecute(query); 38 | 39 | if (error) { 40 | return NextResponse.json({ data, error }, { status: 400 }); 41 | } 42 | 43 | return NextResponse.json(data[0]); 44 | } 45 | -------------------------------------------------------------------------------- /src/app/api/indexes/search/route.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @swagger 3 | * /api/documents/search: 4 | * post: 5 | * summary: Search for similar documents 6 | * description: Returns the documents that are contextually similar to the search term 7 | * tags: 8 | * - Documents 9 | * requestBody: 10 | * description: The search query and knowledge base ID 11 | * required: true 12 | * responses: 13 | * 200: 14 | * description: Returns the documents that are contextually similar to the search term 15 | * content: 16 | * application/json: 17 | * schema: 18 | * type: array 19 | * items: 20 | * 400: 21 | * description: Bad request 22 | * content: 23 | * application/json: 24 | * schema: 25 | * type: object 26 | * properties: 27 | * error: 28 | * type: string 29 | * description: The error message 30 | */ 31 | 32 | import { headers } from 'next/headers'; 33 | import { NextRequest, NextResponse } from 'next/server'; 34 | import { authApiKey } from '../../../../lib/public-api/auth'; 35 | import { supabaseExecute } from '../../../../lib/public-api/database'; 36 | import { OpenAIEmbeddings } from 'langchain/embeddings/openai'; 37 | 38 | export async function GET(request: NextRequest) { 39 | const { data: project, error: authError } = await authApiKey(headers()); 40 | 41 | if (!project || authError) { 42 | return NextResponse.json({ error: authError }, { status: 401 }); 43 | } 44 | 45 | const indexId = request.nextUrl.searchParams.get('index_id'); 46 | if (!indexId) { 47 | return NextResponse.json( 48 | { error: 'Missing index_id query parameter' }, 49 | { status: 400 } 50 | ); 51 | } 52 | 53 | const search = request.nextUrl.searchParams.get('search'); 54 | if (!search) { 55 | return NextResponse.json( 56 | { error: 'Missing search query parameter' }, 57 | { status: 400 } 58 | ); 59 | } 60 | 61 | const openAIEmbeddings = new OpenAIEmbeddings({ batchSize: 512 }); 62 | const embeddings = await openAIEmbeddings.embedDocuments([search]); 63 | 64 | // Search for similar documents using cosine similarity 65 | const query = ` 66 | select 1 - (embedding <=> '[${embeddings.toString()}]') as similarity, id, content, metadata, index_id, source, user_id, created_at 67 | from documents 68 | where index_id = '${indexId}' 69 | order by similarity desc 70 | limit 3; 71 | `; 72 | 73 | const { data, error } = await supabaseExecute(query); 74 | 75 | if (error) { 76 | return NextResponse.json({ data, error }, { status: 400 }); 77 | } 78 | 79 | return NextResponse.json(data); 80 | } 81 | -------------------------------------------------------------------------------- /src/app/api/route.ts: -------------------------------------------------------------------------------- 1 | import { createSwaggerSpec } from 'next-swagger-doc'; 2 | import { NextResponse } from 'next/server'; 3 | import { generateOpenApi } from '@/lib/public-api/openapi'; 4 | 5 | export function GET() { 6 | const spec = generateOpenApi(); 7 | 8 | return NextResponse.json(spec); 9 | } 10 | -------------------------------------------------------------------------------- /src/app/auth/callback/route.ts: -------------------------------------------------------------------------------- 1 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs"; 2 | import { cookies } from "next/headers"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export const dynamic = "force-dynamic"; 6 | 7 | export async function GET(request: Request) { 8 | // The `/auth/callback` route is required for the server-side auth flow implemented 9 | // by the Auth Helpers package. It exchanges an auth code for the user's session. 10 | // https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-sign-in-with-code-exchange 11 | const requestUrl = new URL(request.url); 12 | const code = requestUrl.searchParams.get("code"); 13 | 14 | if (code) { 15 | const supabase = createRouteHandlerClient({ cookies }); 16 | await supabase.auth.exchangeCodeForSession(code); 17 | } 18 | 19 | // URL to redirect to after sign in process completes 20 | return NextResponse.redirect(requestUrl.origin); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/auth/sign-in/route.ts: -------------------------------------------------------------------------------- 1 | import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'; 2 | import { cookies } from 'next/headers'; 3 | import { NextResponse } from 'next/server'; 4 | 5 | export const dynamic = 'force-dynamic'; 6 | 7 | export async function POST(request: Request) { 8 | const requestUrl = new URL(request.url); 9 | const formData = await request.formData(); 10 | const email = String(formData.get('email')); 11 | const password = String(formData.get('password')); 12 | const supabase = createRouteHandlerClient({ cookies }); 13 | 14 | const { error } = await supabase.auth.signInWithPassword({ 15 | email, 16 | password, 17 | }); 18 | 19 | if (error) { 20 | return NextResponse.redirect( 21 | `${requestUrl.origin}/login?error=${error.message}`, 22 | { 23 | // a 301 status is required to redirect from a POST to a GET route 24 | status: 301, 25 | } 26 | ); 27 | } 28 | 29 | return NextResponse.redirect(`${requestUrl.origin}/dashboard`, { 30 | // a 301 status is required to redirect from a POST to a GET route 31 | status: 301, 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /src/app/auth/sign-out/route.ts: -------------------------------------------------------------------------------- 1 | import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' 2 | import { cookies } from 'next/headers' 3 | import { NextResponse } from 'next/server' 4 | 5 | export const dynamic = 'force-dynamic' 6 | 7 | export async function POST(request: Request) { 8 | const requestUrl = new URL(request.url) 9 | const supabase = createRouteHandlerClient({ cookies }) 10 | 11 | await supabase.auth.signOut() 12 | 13 | return NextResponse.redirect(`${requestUrl.origin}/login`, { 14 | // a 301 status is required to redirect from a POST to a GET route 15 | status: 301, 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/app/auth/sign-up-early/route.ts: -------------------------------------------------------------------------------- 1 | import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'; 2 | import { cookies } from 'next/headers'; 3 | import { NextResponse } from 'next/server'; 4 | 5 | export const dynamic = 'force-dynamic'; 6 | 7 | export async function POST(request: Request) { 8 | const requestUrl = new URL(request.url); 9 | const formData = await request.formData(); 10 | const email = String(formData.get('email')); 11 | const password = String(formData.get('password')); 12 | const supabase = createRouteHandlerClient({ cookies }); 13 | 14 | const { data, error } = await supabase.auth.signUp({ 15 | email, 16 | password, 17 | }); 18 | 19 | if (error) { 20 | console.log(error); 21 | return NextResponse.redirect( 22 | `${requestUrl.origin}?error=${error.message}`, 23 | { 24 | // a 301 status is required to redirect from a POST to a GET route 25 | status: 301, 26 | } 27 | ); 28 | } 29 | 30 | return NextResponse.redirect(`${requestUrl.origin}/thank-you`, { 31 | // a 301 status is required to redirect from a POST to a GET route 32 | status: 301, 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/app/auth/sign-up/route.ts: -------------------------------------------------------------------------------- 1 | import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'; 2 | import { cookies } from 'next/headers'; 3 | import { NextResponse } from 'next/server'; 4 | 5 | export const dynamic = 'force-dynamic'; 6 | 7 | export async function POST(request: Request) { 8 | const requestUrl = new URL(request.url); 9 | const formData = await request.formData(); 10 | const email = String(formData.get('email')); 11 | const password = String(formData.get('password')); 12 | const supabase = createRouteHandlerClient({ cookies }); 13 | 14 | const { data, error } = await supabase.auth.signUp({ 15 | email, 16 | password, 17 | }); 18 | 19 | if (error) { 20 | console.log(error); 21 | return NextResponse.redirect( 22 | `${requestUrl.origin}/login?error=${error.message}`, 23 | { 24 | // a 301 status is required to redirect from a POST to a GET route 25 | status: 301, 26 | } 27 | ); 28 | } 29 | 30 | return NextResponse.redirect(`${requestUrl.origin}/login`, { 31 | // a 301 status is required to redirect from a POST to a GET route 32 | status: 301, 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/app/components/nav.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export function MainNav({ 6 | className, 7 | ...props 8 | }: React.HTMLAttributes) { 9 | return ( 10 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Database } from '@/types/supabase'; 4 | import Link from 'next/link'; 5 | import { useEffect, useState } from 'react'; 6 | import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'; 7 | import NewProject from '@/components/NewProject'; 8 | import { Project } from '@/types/supabase-entities'; 9 | import { Button } from '@/components/Button'; 10 | 11 | export default function Dashboard() { 12 | const supabase = createClientComponentClient(); 13 | const [projects, setProjects] = useState([]); 14 | const [createNewProjectOpen, setCreateNewProjectOpen] = useState(false); 15 | 16 | useEffect(() => { 17 | getData(); 18 | }, [createNewProjectOpen]); 19 | 20 | const getData = async () => { 21 | const { data: projects, error: projectsError } = await supabase 22 | .from('projects') 23 | .select('*'); 24 | 25 | if (projectsError) { 26 | alert(`Error fetching data: projects: ${projectsError?.message}`); 27 | } 28 | 29 | setProjects(projects || []); 30 | }; 31 | 32 | const toggleNewProjectOpen = () => 33 | setCreateNewProjectOpen(!createNewProjectOpen); 34 | 35 | return ( 36 |
37 |
38 | 39 |
40 |

Dashboard

41 |
42 | 43 | {createNewProjectOpen && ( 44 | 45 | )} 46 |
47 |
48 | Projects: 49 |
    50 | {projects.map((project) => ( 51 |
  • 52 | 53 |

    {project.name}

    54 | 55 |
  • 56 | ))} 57 |
58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/app/dashboard/projects/[projectId]/indexes/[indexId]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'; 4 | import { useState, useEffect } from 'react'; 5 | import { Database } from '@/types/supabase'; 6 | import { Index, Document } from '@/types/supabase-entities'; 7 | import { param } from 'cypress/types/jquery'; 8 | 9 | export default function Index({ 10 | params, 11 | }: { 12 | params: { projectId: string; indexId: string }; 13 | }) { 14 | const supabase = createClientComponentClient(); 15 | const [file, setFile] = useState(); 16 | const [index, setIndex] = useState(null); 17 | const [documents, setDocuments] = useState([]); 18 | const [editingId, setEditingId] = useState(null); 19 | const [editedText, setEditedText] = useState(''); 20 | 21 | const handleEditClick = (id: string, text: string) => { 22 | setEditingId(id); 23 | setEditedText(text); 24 | }; 25 | 26 | const handleSaveClick = async (id: string) => { 27 | const document = documents?.find((d) => d.id === id); 28 | if (!document) return; 29 | 30 | console.log(id); 31 | 32 | const { data: updatedDocument, error } = await supabase 33 | .from('documents') 34 | .update({ content: editedText }) 35 | .eq('id', id); 36 | 37 | if (error) { 38 | alert(`Error updating document: ${error.message}`); 39 | return; 40 | } 41 | setEditingId(null); 42 | setEditedText(''); 43 | getData(); 44 | }; 45 | 46 | const handleCancelClick = () => { 47 | setEditingId(null); 48 | setEditedText(''); 49 | }; 50 | 51 | const getData = async () => { 52 | const { data: index, error: indexError } = await supabase 53 | .from('indexes') 54 | .select('*') 55 | .eq('id', params.indexId) 56 | .single(); 57 | 58 | if (indexError) { 59 | alert(`Error fetching data: ${indexError}`); 60 | } 61 | 62 | const { data: documents, error: documentsError } = await supabase 63 | .from('documents') 64 | .select('*') 65 | .eq('index_id', params.indexId); 66 | 67 | if (documentsError) { 68 | alert(`Error fetching data:Documents: ${documentsError}`); 69 | } 70 | 71 | setIndex(index || null); 72 | setDocuments(documents || []); 73 | }; 74 | 75 | useEffect(() => { 76 | getData(); 77 | }, []); 78 | 79 | const onSubmit = async (e: React.FormEvent) => { 80 | e.preventDefault(); 81 | if (!file) return; 82 | 83 | try { 84 | const data = new FormData(); 85 | data.set('file', file); 86 | 87 | const res = await fetch( 88 | `/dashboard/projects/${params.projectId}/indexes/${params.indexId}/upload`, 89 | { 90 | method: 'POST', 91 | body: data, 92 | } 93 | ); 94 | if (!res.ok) throw new Error(await res.text()); 95 | getData(); 96 | } catch (e: any) { 97 | alert(`Error uploading file: ${e.message}`); 98 | console.error(e); 99 | } 100 | }; 101 | 102 | return ( 103 | <> 104 |

Index: {index?.name || 'Not found'}

105 |

Upload document

106 |
107 | setFile(e.target.files?.[0])} 113 | /> 114 | 115 |
116 |
117 | 118 |

Documents

119 | {documents?.map((document) => ( 120 |
121 | {editingId === document.id ? ( 122 |
123 |