├── supabase ├── seed.sql ├── .gitignore ├── migrations │ ├── 20231015132106_feature.sql │ ├── 20231027163314_feature.sql │ ├── 20231027155201_feature.sql │ ├── 20231027154727_feature.sql │ └── 20231014182810_initial.sql └── config.toml ├── .eslintrc.json ├── .prettierrc ├── .vscode └── settings.json ├── public ├── flow.png ├── steps.png ├── architecture.png ├── sqd-dark-trans.png ├── sqd-light-trans.png ├── vercel.svg └── next.svg ├── src ├── app │ ├── favicon.ico │ ├── api │ │ ├── route.ts │ │ ├── docs │ │ │ └── page.tsx │ │ ├── chat │ │ │ ├── completions │ │ │ │ └── route.ts │ │ │ └── rag │ │ │ │ └── route.ts │ │ ├── indexes │ │ │ ├── route.ts │ │ │ └── search │ │ │ │ └── route.ts │ │ └── documents │ │ │ └── route.ts │ ├── auth │ │ ├── sign-out │ │ │ └── route.ts │ │ ├── callback │ │ │ └── route.ts │ │ ├── sign-in │ │ │ └── route.ts │ │ ├── sign-up │ │ │ └── route.ts │ │ └── sign-up-early │ │ │ └── route.ts │ ├── components │ │ └── nav.tsx │ ├── layout.tsx │ ├── globals.css │ ├── dashboard │ │ ├── page.tsx │ │ └── projects │ │ │ └── [projectId] │ │ │ ├── indexes │ │ │ └── [indexId] │ │ │ │ ├── upload │ │ │ │ └── route.ts │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ ├── thank-you │ │ └── page.tsx │ ├── login │ │ ├── page.tsx │ │ └── components │ │ │ └── user-auth-form.tsx.tsx │ └── page.tsx ├── lib │ ├── utils.ts │ └── public-api │ │ ├── database.ts │ │ ├── auth.ts │ │ ├── llm.ts │ │ ├── validation.ts │ │ └── openapi.ts ├── providers │ └── ThemeProvider │ │ └── index.tsx ├── types │ ├── supabase-entities.ts │ └── supabase.ts ├── components │ ├── ui │ │ ├── label.tsx │ │ ├── checkbox.tsx │ │ ├── badge.tsx │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── accordion.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ └── form.tsx │ ├── Input │ │ └── index.tsx │ ├── NewIndex │ │ └── index.tsx │ ├── NewProject │ │ └── index.tsx │ └── Button │ │ └── index.tsx └── middleware.ts ├── .husky └── pre-commit ├── cypress.env.json ├── next.config.js ├── postcss.config.js ├── cypress ├── fixtures │ └── example.json ├── support │ ├── e2e.ts │ └── commands.ts └── e2e │ ├── dashboard.cy.ts │ └── public-api.cy.ts ├── DEVELOPERS.md ├── components.json ├── cypress.config.ts ├── .gitignore ├── .github └── workflows │ ├── staging.yml │ ├── production.yml │ └── ci.yml ├── tsconfig.json ├── package.json ├── tailwind.config.ts └── README.md /supabase/seed.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["DATACRUNCH"] 3 | } 4 | -------------------------------------------------------------------------------- /public/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squaredev-io/squaredev/HEAD/public/flow.png -------------------------------------------------------------------------------- /public/steps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squaredev-io/squaredev/HEAD/public/steps.png -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squaredev-io/squaredev/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run pretty-staged 5 | -------------------------------------------------------------------------------- /cypress.env.json: -------------------------------------------------------------------------------- 1 | { 2 | "database_url": "postgresql://postgres:postgres@localhost:54322/postgres" 3 | } 4 | -------------------------------------------------------------------------------- /public/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squaredev-io/squaredev/HEAD/public/architecture.png -------------------------------------------------------------------------------- /public/sqd-dark-trans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squaredev-io/squaredev/HEAD/public/sqd-dark-trans.png -------------------------------------------------------------------------------- /public/sqd-light-trans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squaredev-io/squaredev/HEAD/public/sqd-light-trans.png -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | module.exports = nextConfig; 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /supabase/migrations/20231015132106_feature.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."apps_knowledge_bases" alter column "user_id" set default auth.uid(); 2 | 3 | 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | import { Inter as FontSans } from 'next/font/google'; 4 | 5 | export const fontSans = FontSans({ 6 | subsets: ['latin'], 7 | variable: '--font-sans', 8 | }); 9 | 10 | export function cn(...inputs: ClassValue[]) { 11 | return twMerge(clsx(inputs)); 12 | } 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/providers/ThemeProvider/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { ThemeProvider as NextThemesProvider } from 'next-themes'; 5 | import { type ThemeProviderProps } from 'next-themes/dist/types'; 6 | 7 | export default function ThemeProvider({ 8 | children, 9 | ...props 10 | }: ThemeProviderProps) { 11 | return {children}; 12 | } 13 | -------------------------------------------------------------------------------- /src/types/supabase-entities.ts: -------------------------------------------------------------------------------- 1 | import { Database } from './supabase'; 2 | 3 | export type Project = Database['public']['Tables']['projects']['Row']; 4 | export type Index = Database['public']['Tables']['indexes']['Row']; 5 | export type IndexInsert = Database['public']['Tables']['indexes']['Insert']; 6 | 7 | export type DocumentInsert = 8 | Database['public']['Tables']['documents']['Insert']; 9 | export type Document = Database['public']['Tables']['documents']['Row']; 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /supabase/migrations/20231027163314_feature.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."documents" drop constraint "documents_knowledge_base_id_fkey"; 2 | 3 | alter table "public"."documents" drop column "knowledge_base_id"; 4 | 5 | alter table "public"."documents" add column "index_id" uuid not null; 6 | 7 | alter table "public"."documents" add constraint "documents_index_id_fkey" FOREIGN KEY (index_id) REFERENCES indexes(id) ON DELETE CASCADE not valid; 8 | 9 | alter table "public"."documents" validate constraint "documents_index_id_fkey"; 10 | 11 | 12 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "types": ["cypress", "node"], 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as LabelPrimitive from '@radix-ui/react-label'; 5 | import { cva, type VariantProps } from 'class-variance-authority'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | 9 | const labelVariants = cva( 10 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' 11 | ); 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )); 24 | Label.displayName = LabelPrimitive.Root.displayName; 25 | 26 | export { Label }; 27 | -------------------------------------------------------------------------------- /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/components/Input/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | } 22 | ); 23 | Input.displayName = 'Input'; 24 | 25 | export { Input }; 26 | export default Input; 27 | -------------------------------------------------------------------------------- /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/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css'; 2 | import type { Metadata } from 'next'; 3 | import ThemeProvider from '@/providers/ThemeProvider'; 4 | import { Analytics } from '@vercel/analytics/react'; 5 | import { cn } from '@/lib/utils'; 6 | import { fontSans } from '@/lib/utils'; 7 | 8 | export const metadata: Metadata = { 9 | title: 'Create Next App', 10 | description: 'Generated by create next app', 11 | }; 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | return ( 19 | 20 | 26 | 32 | {children} 33 | 34 | 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'; 2 | import { NextResponse } from 'next/server'; 3 | 4 | import type { NextRequest } from 'next/server'; 5 | 6 | export async function middleware(req: NextRequest) { 7 | const res = NextResponse.next(); 8 | // Create a Supabase client configured to use cookies 9 | const supabase = createMiddlewareClient({ req, res }); 10 | 11 | const requestUrl = new URL(req.url); 12 | const code = requestUrl.searchParams.get('code'); 13 | // The `/auth/callback` route is required for the server-side auth flow implemented 14 | // other wise it will be stuck in a redirect loop 15 | if (code && !req.url.includes('/auth/callback')) { 16 | // URL to redirect to after sign in process completes 17 | return NextResponse.redirect(`${requestUrl.origin}/dashboard`); 18 | } 19 | 20 | // Refresh session if expired - required for Server Components 21 | // https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-session-with-middleware 22 | const { data, error } = await supabase.auth.getSession(); 23 | 24 | return res; 25 | } 26 | -------------------------------------------------------------------------------- /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-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/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/lib/public-api/database.ts: -------------------------------------------------------------------------------- 1 | import { Client } from 'pg'; 2 | 3 | export interface ResultError { 4 | data: null; 5 | error: { 6 | code: string; 7 | hint: string; 8 | message: string; 9 | }; 10 | } 11 | 12 | export interface ResultSuccess { 13 | data: T; 14 | error: null; 15 | } 16 | 17 | export type Result = ResultSuccess | ResultError; 18 | 19 | export async function supabaseExecute( 20 | query: string, 21 | parameters: any[] = [] 22 | ): Promise> { 23 | const client = new Client({ 24 | connectionString: 25 | process.env.DATABASE_URL || 26 | 'postgresql://postgres:postgres@localhost:54322/postgres', 27 | }); 28 | try { 29 | await client.connect(); 30 | const result = await client.query(query, parameters); 31 | await client.end(); 32 | 33 | return { data: result.rows, error: null }; 34 | } catch (err: any) { 35 | console.error('Error executing query: ', query, parameters); 36 | console.error(err); 37 | await client.end(); 38 | 39 | return { 40 | data: null, 41 | error: { code: err.code, hint: err.hint, message: err.message }, 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /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/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; 5 | import { Check } from 'lucide-react'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )); 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 29 | 30 | export { Checkbox }; 31 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { cva, type VariantProps } from 'class-variance-authority'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | const badgeVariants = cva( 7 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', 13 | secondary: 14 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', 15 | destructive: 16 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', 17 | outline: 'text-foreground', 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: 'default', 22 | }, 23 | } 24 | ); 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ); 34 | } 35 | 36 | export { Badge, badgeVariants }; 37 | -------------------------------------------------------------------------------- /src/lib/public-api/auth.ts: -------------------------------------------------------------------------------- 1 | import { Project } from '@/types/supabase-entities'; 2 | import { Result, supabaseExecute } from './database'; 3 | 4 | export function getApiKey(headers: any) { 5 | const authorization = headers.get('authorization'); 6 | if (!authorization) { 7 | return null; 8 | } 9 | 10 | const [type, key] = authorization.split(' '); 11 | if (type !== 'Bearer') { 12 | return null; 13 | } 14 | 15 | return key; 16 | } 17 | 18 | export async function authApiKey(headers: any): Promise> { 19 | const apiKey = getApiKey(headers); 20 | if (!apiKey) { 21 | return { 22 | data: null, 23 | error: { 24 | code: '401', 25 | hint: 'Did you forget to add headers?', 26 | message: 'No Bearer token found in Headers', 27 | }, 28 | }; 29 | } 30 | 31 | const query = `select * from projects where api_key = '${apiKey}'`; 32 | const { data, error } = await supabaseExecute(query); 33 | if (error) { 34 | return { data, error }; 35 | } 36 | 37 | if (!data.length) { 38 | return { 39 | data: null, 40 | error: { 41 | code: '401', 42 | hint: '', 43 | message: 'Invalid API key.', 44 | }, 45 | }; 46 | } 47 | 48 | return { data: data[0], error: null }; 49 | } 50 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/components/NewIndex/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Database } from '../../types/supabase'; 4 | import { ChangeEvent, useState } from 'react'; 5 | import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'; 6 | import { Input } from '@/components/Input'; 7 | import { Button } from '../Button'; 8 | 9 | type NewIndexFormProps = { 10 | closeForm: () => void; 11 | }; 12 | 13 | export default function NewIndex({ closeForm }: NewIndexFormProps) { 14 | const supabase = createClientComponentClient(); 15 | const [formData, setFormData] = useState({ name: '' }); 16 | 17 | const onSubmit = async (e: any) => { 18 | e.preventDefault(); 19 | const { data, error } = await supabase.from('indexes').insert({ 20 | name: formData.name, 21 | }); 22 | if (error) { 23 | console.error(error); 24 | alert(error.message); 25 | } else { 26 | closeForm(); 27 | } 28 | }; 29 | 30 | const handleChange = (event: ChangeEvent) => { 31 | const { name, value } = event.target; 32 | setFormData((prevFormData) => ({ ...prevFormData, [name]: value })); 33 | }; 34 | 35 | return ( 36 |
37 |
38 | 45 | 46 |
47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/components/NewProject/index.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Database } from '../../types/supabase'; 4 | import { ChangeEvent, useState } from 'react'; 5 | import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'; 6 | import { Input } from '@/components/Input'; 7 | import { Button } from '@/components/Button'; 8 | 9 | type NewProjectFormProps = { 10 | closeForm: () => void; 11 | }; 12 | 13 | export default function NewProject({ closeForm }: NewProjectFormProps) { 14 | const supabase = createClientComponentClient(); 15 | const [formData, setFormData] = useState({ name: '' }); 16 | 17 | const createNewProject = async (e: any) => { 18 | e.preventDefault(); 19 | const { data, error } = await supabase.from('projects').insert({ 20 | name: formData.name, 21 | }); 22 | if (error) { 23 | console.error(error); 24 | alert(error.message); 25 | } else { 26 | closeForm(); 27 | } 28 | }; 29 | 30 | const handleChange = (event: ChangeEvent) => { 31 | const { name, value } = event.target; 32 | setFormData((prevFormData) => ({ ...prevFormData, [name]: value })); 33 | }; 34 | 35 | return ( 36 |
37 |
38 | 45 | 46 |
47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as AvatarPrimitive from '@radix-ui/react-avatar'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )); 21 | Avatar.displayName = AvatarPrimitive.Root.displayName; 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )); 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )); 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 49 | 50 | export { Avatar, AvatarImage, AvatarFallback }; 51 | -------------------------------------------------------------------------------- /src/lib/public-api/llm.ts: -------------------------------------------------------------------------------- 1 | import { OpenAI } from 'openai'; 2 | 3 | const openAIModels = ['gpt-3.5-turbo', 'gpt-3.5-turbo-16k ']; 4 | const anyScaleModels = [ 5 | 'mistralai/Mistral-7B-Instruct-v0.1', 6 | 'meta-llama/Llama-2-7b-chat-hf', 7 | 'meta-llama/Llama-2-13b-chat-hf', 8 | 'meta-llama/Llama-2-70b-chat-hf', 9 | ]; 10 | 11 | export const AVAILABLE_MODELS = [anyScaleModels, ...openAIModels]; 12 | 13 | interface LlmOptions { 14 | system: string | undefined; 15 | message: string; 16 | temperature?: number; 17 | max_tokens?: number; 18 | stream?: boolean; 19 | model?: string; 20 | } 21 | 22 | export async function llm({ 23 | message, 24 | system = '', 25 | temperature = 0.7, 26 | max_tokens = 500, 27 | stream = false, 28 | model = 'mistralai/Mistral-7B-Instruct-v0.1', 29 | }: LlmOptions): Promise { 30 | const llm = anyScaleModels.includes(model) 31 | ? // AnyScale is compatible with Open AI API. So all we have to do is change the base API URL. 32 | new OpenAI({ 33 | baseURL: process.env.ANYSCALE_API_BASE!, 34 | apiKey: process.env.ANYSCALE_API_KEY!, 35 | }) 36 | : new OpenAI({ apiKey: process.env.OPENAI_API_KEY! }); 37 | 38 | const response = await llm.chat.completions.create({ 39 | model, 40 | stream, 41 | messages: [ 42 | { 43 | role: 'system', 44 | content: system, 45 | }, 46 | { 47 | role: 'user', 48 | content: message, 49 | }, 50 | ], 51 | max_tokens, 52 | temperature, 53 | top_p: 1, 54 | frequency_penalty: 1, 55 | presence_penalty: 1, 56 | }); 57 | 58 | return response as OpenAI.Chat.ChatCompletion; 59 | } 60 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 222.2 84% 4.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 222.2 84% 4.9%; 13 | --primary: 221.2 83.2% 53.3%; 14 | --primary-foreground: 210 40% 98%; 15 | --secondary: 210 40% 96.1%; 16 | --secondary-foreground: 222.2 47.4% 11.2%; 17 | --muted: 210 40% 96.1%; 18 | --muted-foreground: 215.4 16.3% 46.9%; 19 | --accent: 210 40% 96.1%; 20 | --accent-foreground: 222.2 47.4% 11.2%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 210 40% 98%; 23 | --border: 214.3 31.8% 91.4%; 24 | --input: 214.3 31.8% 91.4%; 25 | --ring: 221.2 83.2% 53.3%; 26 | --radius: 0.5rem; 27 | } 28 | 29 | .dark { 30 | --background: 222.2 84% 4.9%; 31 | --foreground: 210 40% 98%; 32 | --card: 222.2 84% 4.9%; 33 | --card-foreground: 210 40% 98%; 34 | --popover: 222.2 84% 4.9%; 35 | --popover-foreground: 210 40% 98%; 36 | --primary: 217.2 91.2% 59.8%; 37 | --primary-foreground: 222.2 47.4% 11.2%; 38 | --secondary: 217.2 32.6% 17.5%; 39 | --secondary-foreground: 210 40% 98%; 40 | --muted: 217.2 32.6% 17.5%; 41 | --muted-foreground: 215 20.2% 65.1%; 42 | --accent: 217.2 32.6% 17.5%; 43 | --accent-foreground: 210 40% 98%; 44 | --destructive: 0 62.8% 30.6%; 45 | --destructive-foreground: 210 40% 98%; 46 | --border: 217.2 32.6% 17.5%; 47 | --input: 217.2 32.6% 17.5%; 48 | --ring: 224.3 76.3% 48%; 49 | } 50 | } 51 | 52 | @layer base { 53 | * { 54 | @apply border-border; 55 | } 56 | body { 57 | @apply bg-background text-foreground; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /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/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Slot } from '@radix-ui/react-slot'; 3 | import { cva, type VariantProps } from 'class-variance-authority'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | const buttonVariants = cva( 8 | 'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', 9 | { 10 | variants: { 11 | variant: { 12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90', 13 | destructive: 14 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90', 15 | outline: 16 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', 17 | secondary: 18 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80', 19 | ghost: 'hover:bg-accent hover:text-accent-foreground', 20 | link: 'text-primary underline-offset-4 hover:underline', 21 | }, 22 | size: { 23 | default: 'h-10 px-4 py-2', 24 | sm: 'h-9 rounded-md px-3', 25 | lg: 'h-11 rounded-md px-8', 26 | icon: 'h-10 w-10', 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: 'default', 31 | size: 'default', 32 | }, 33 | } 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : 'button'; 45 | return ( 46 | 51 | ); 52 | } 53 | ); 54 | Button.displayName = 'Button'; 55 | 56 | export { Button, buttonVariants }; 57 | -------------------------------------------------------------------------------- /src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Slot } from '@radix-ui/react-slot'; 3 | import { cva, type VariantProps } from 'class-variance-authority'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | const buttonVariants = cva( 8 | 'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', 9 | { 10 | variants: { 11 | variant: { 12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90', 13 | destructive: 14 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90', 15 | outline: 16 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', 17 | secondary: 18 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80', 19 | ghost: 'hover:bg-accent hover:text-accent-foreground', 20 | link: 'text-primary underline-offset-4 hover:underline', 21 | }, 22 | size: { 23 | default: 'h-10 px-4 py-2', 24 | sm: 'h-9 rounded-md px-3', 25 | lg: 'h-11 rounded-md px-8', 26 | icon: 'h-10 w-10', 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: 'default', 31 | size: 'default', 32 | }, 33 | } 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : 'button'; 45 | return ( 46 | 51 | ); 52 | } 53 | ); 54 | Button.displayName = 'Button'; 55 | 56 | export { Button, buttonVariants }; 57 | export default Button; 58 | -------------------------------------------------------------------------------- /src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as AccordionPrimitive from '@radix-ui/react-accordion'; 5 | import { ChevronDown } from 'lucide-react'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | 9 | const Accordion = AccordionPrimitive.Root; 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )); 21 | AccordionItem.displayName = 'AccordionItem'; 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180', 32 | className 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )); 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 55 |
{children}
56 |
57 | )); 58 | AccordionContent.displayName = AccordionPrimitive.Content.displayName; 59 | 60 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; 61 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = 'Card'; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = 'CardHeader'; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )); 45 | CardTitle.displayName = 'CardTitle'; 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )); 57 | CardDescription.displayName = 'CardDescription'; 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )); 65 | CardContent.displayName = 'CardContent'; 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )); 77 | CardFooter.displayName = 'CardFooter'; 78 | 79 | export { 80 | Card, 81 | CardHeader, 82 | CardFooter, 83 | CardTitle, 84 | CardDescription, 85 | CardContent, 86 | }; 87 | -------------------------------------------------------------------------------- /src/app/thank-you/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import Image from 'next/image'; 3 | 4 | export const metadata: Metadata = { 5 | title: 'Thank you', 6 | description: 'Thank you for signing up.', 7 | }; 8 | 9 | export default function Login() { 10 | return ( 11 | <> 12 |
13 | Authentication 20 | Authentication 27 |
28 |
29 |
30 |
31 |
32 | SquareDev 33 |
34 |
35 |
36 |

37 | “We want to create the best developer experience for 38 | people developing with AI.” 39 |

40 |
The Squaredev Team
41 |
42 |
43 |
44 |
45 |
46 |
47 |

48 | Thank your for joining SquareDev. 49 |

50 |

51 | We will get back to you as soon as possible with how to use your 52 | account. 53 |

54 |
55 |
56 |
57 |
58 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/app/dashboard/projects/[projectId]/indexes/[indexId]/upload/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { WebPDFLoader } from 'langchain/document_loaders/web/pdf'; 3 | import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter'; 4 | import { OpenAIEmbeddings } from 'langchain/embeddings/openai'; 5 | import { DocumentInsert } from '@/types/supabase-entities'; 6 | import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'; 7 | import { cookies } from 'next/headers'; 8 | 9 | export async function POST( 10 | request: NextRequest, 11 | { params }: { params: { indexId: string } } 12 | ) { 13 | // TODO: This should be also consider the api key 14 | const supabase = createRouteHandlerClient({ cookies }); 15 | 16 | const formData = await request.formData(); 17 | const file: File | null = formData.get('file') as unknown as File; 18 | 19 | if (!file) { 20 | return NextResponse.json({ success: false }); 21 | } 22 | 23 | const loader = new WebPDFLoader(file); 24 | 25 | const docs = await loader.load(); 26 | 27 | const splitter = new RecursiveCharacterTextSplitter({ 28 | // TODO: This should be dynamic 29 | chunkSize: 1000, 30 | chunkOverlap: 200, 31 | }); 32 | 33 | const documents = await splitter.createDocuments( 34 | docs.map((doc) => doc.pageContent) 35 | ); 36 | 37 | const openAIEmbeddings = new OpenAIEmbeddings({ 38 | batchSize: 512, // Default value if omitted is 512. Max is 2048 39 | }); 40 | 41 | const embeddings = await openAIEmbeddings.embedDocuments( 42 | documents.map((doc) => doc.pageContent) 43 | ); 44 | 45 | const documentInsert: DocumentInsert[] = documents.map((doc, index) => ({ 46 | embedding: embeddings[index] as unknown as string, // This is not right. The type generation from supabase is wrong here. 47 | content: doc.pageContent, 48 | metadata: doc.metadata.loc, 49 | index_id: params.indexId, 50 | source: file.name, 51 | })); 52 | 53 | const { data, error } = await supabase 54 | .from('documents') 55 | .insert(documentInsert) 56 | .select(); 57 | 58 | if (error) { 59 | console.log(error); 60 | return NextResponse.json( 61 | { success: false, message: error.message }, 62 | { status: 400 } 63 | ); 64 | } 65 | 66 | return NextResponse.json(data); 67 | } 68 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | const { fontFamily } = require('tailwindcss/defaultTheme'); 3 | 4 | module.exports = { 5 | darkMode: ['class'], 6 | content: [ 7 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 8 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 9 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 10 | ], 11 | theme: { 12 | container: { 13 | center: true, 14 | padding: '2rem', 15 | screens: { 16 | '2xl': '1400px', 17 | }, 18 | }, 19 | extend: { 20 | fontFamily: { 21 | sans: ['var(--font-sans)', ...fontFamily.sans], 22 | }, 23 | colors: { 24 | border: 'hsl(var(--border))', 25 | input: 'hsl(var(--input))', 26 | ring: 'hsl(var(--ring))', 27 | background: 'hsl(var(--background))', 28 | foreground: 'hsl(var(--foreground))', 29 | primary: { 30 | DEFAULT: 'hsl(var(--primary))', 31 | foreground: 'hsl(var(--primary-foreground))', 32 | }, 33 | secondary: { 34 | DEFAULT: 'hsl(var(--secondary))', 35 | foreground: 'hsl(var(--secondary-foreground))', 36 | }, 37 | destructive: { 38 | DEFAULT: 'hsl(var(--destructive))', 39 | foreground: 'hsl(var(--destructive-foreground))', 40 | }, 41 | muted: { 42 | DEFAULT: 'hsl(var(--muted))', 43 | foreground: 'hsl(var(--muted-foreground))', 44 | }, 45 | accent: { 46 | DEFAULT: 'hsl(var(--accent))', 47 | foreground: 'hsl(var(--accent-foreground))', 48 | }, 49 | popover: { 50 | DEFAULT: 'hsl(var(--popover))', 51 | foreground: 'hsl(var(--popover-foreground))', 52 | }, 53 | card: { 54 | DEFAULT: 'hsl(var(--card))', 55 | foreground: 'hsl(var(--card-foreground))', 56 | }, 57 | }, 58 | borderRadius: { 59 | lg: 'var(--radius)', 60 | md: 'calc(var(--radius) - 2px)', 61 | sm: 'calc(var(--radius) - 4px)', 62 | }, 63 | keyframes: { 64 | 'accordion-down': { 65 | from: { height: 0 }, 66 | to: { height: 'var(--radix-accordion-content-height)' }, 67 | }, 68 | 'accordion-up': { 69 | from: { height: 'var(--radix-accordion-content-height)' }, 70 | to: { height: 0 }, 71 | }, 72 | }, 73 | animation: { 74 | 'accordion-down': 'accordion-down 0.2s ease-out', 75 | 'accordion-up': 'accordion-up 0.2s ease-out', 76 | }, 77 | }, 78 | }, 79 | plugins: [require('tailwindcss-animate')], 80 | }; 81 | -------------------------------------------------------------------------------- /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/dashboard/projects/[projectId]/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, IndexInsert, Project } from '@/types/supabase-entities'; 7 | import Button from '@/components/Button'; 8 | import Input from '@/components/Input'; 9 | import { get } from 'cypress/types/lodash'; 10 | import Link from 'next/link'; 11 | 12 | export default function Project({ params }: { params: { projectId: string } }) { 13 | const supabase = createClientComponentClient(); 14 | const [project, setProject] = useState(null); 15 | const [indexes, setIndexes] = useState([]); 16 | const [isAddingIndex, setIsAddingIndex] = useState(false); 17 | const [indexName, setIndexName] = useState(''); 18 | 19 | const getData = async () => { 20 | const { data: project, error: projectError } = await supabase 21 | .from('projects') 22 | .select('*') 23 | .eq('id', params.projectId) 24 | .single(); 25 | 26 | if (projectError) { 27 | alert(`Error fetching data: project: ${projectError}`); 28 | } 29 | 30 | const { data: indexes, error: indexesError } = await supabase 31 | .from('indexes') 32 | .select('*') 33 | .eq('project_id', params.projectId); 34 | 35 | if (indexesError) { 36 | alert(`Error fetching data: indexes: ${indexesError}`); 37 | } 38 | 39 | setProject(project); 40 | setIndexes(indexes || []); 41 | }; 42 | 43 | useEffect(() => { 44 | getData(); 45 | }, []); 46 | 47 | const toggleIsAddingIndex = () => setIsAddingIndex(!isAddingIndex); 48 | 49 | const handleIndexNameChange = (e: React.ChangeEvent) => 50 | setIndexName(e.target.value); 51 | 52 | const addIndex = async () => { 53 | const newIndex: IndexInsert = { 54 | name: indexName, 55 | project_id: params.projectId, 56 | }; 57 | const { data: index, error: indexError } = await supabase 58 | .from('indexes') 59 | .insert(newIndex) 60 | .single(); 61 | 62 | if (indexError) { 63 | alert(`Error adding index: ${indexError}`); 64 | } 65 | 66 | getData(); 67 | toggleIsAddingIndex(); 68 | }; 69 | 70 | return ( 71 | <> 72 |

project: {project?.name || 'Not found'}

73 | 74 | 75 | {isAddingIndex && ( 76 | <> 77 | 83 | 89 | 90 | )} 91 | 92 |

Indexes:

93 |
    94 | {indexes.map((index) => ( 95 |
  • 96 | 97 |

    {index.name}

    98 | 99 |
  • 100 | ))} 101 |
102 | 103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/app/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import Image from 'next/image'; 3 | import Link from 'next/link'; 4 | import { cn } from '@/lib/utils'; 5 | import { buttonVariants } from '@/components/Button'; 6 | import { UserAuthForm } from './components/user-auth-form.tsx'; 7 | 8 | export const metadata: Metadata = { 9 | title: 'Authentication', 10 | description: 'Authentication forms.', 11 | }; 12 | 13 | export default function Login() { 14 | return ( 15 | <> 16 |
17 | Authentication 24 | Authentication 31 |
32 |
33 | {/* 40 | Login 41 | */} 42 |
43 |
44 |
45 | SquareDev 46 |
47 |
48 |
49 |

50 | “This platform helped go to production fast, without 51 | having to study a line of AI.” 52 |

53 |
Sofia Davis
54 |
55 |
56 |
57 |
58 |
59 |
60 |

61 | Create an account 62 |

63 |

64 | Enter your credentials below to create your account 65 |

66 |
67 | 68 | {/*

69 | By clicking continue, you agree to our{' '} 70 | 74 | Terms of Service 75 | {' '} 76 | and{' '} 77 | 81 | Privacy Policy 82 | 83 | . 84 |

*/} 85 |
86 |
87 |
88 | 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /supabase/migrations/20231027155201_feature.sql: -------------------------------------------------------------------------------- 1 | drop policy "Enable access for all authed users" on "public"."index"; 2 | 3 | drop policy "Enable access for all authed users" on "public"."project"; 4 | 5 | alter table "public"."index" drop constraint "index_project_id_fkey"; 6 | 7 | alter table "public"."index" drop constraint "index_user_id_fkey"; 8 | 9 | alter table "public"."documents" drop constraint "documents_knowledge_base_id_fkey"; 10 | 11 | alter table "public"."memory_sessions" drop constraint "memory_sessions_project_id_fkey"; 12 | 13 | alter table "public"."index" drop constraint "knowledge_bases_pkey"; 14 | 15 | alter table "public"."project" drop constraint "apps_pkey"; 16 | 17 | drop index if exists "public"."apps_pkey"; 18 | 19 | drop index if exists "public"."knowledge_bases_pkey"; 20 | 21 | drop table "public"."index"; 22 | 23 | drop table "public"."project"; 24 | 25 | create table "public"."indexes" ( 26 | "id" uuid not null default gen_random_uuid(), 27 | "created_at" timestamp with time zone not null default now(), 28 | "name" text not null, 29 | "user_id" uuid not null default auth.uid(), 30 | "project_id" uuid 31 | ); 32 | 33 | 34 | alter table "public"."indexes" enable row level security; 35 | 36 | create table "public"."projects" ( 37 | "id" uuid not null default gen_random_uuid(), 38 | "created_at" timestamp with time zone not null default now(), 39 | "user_id" uuid default auth.uid(), 40 | "name" text not null, 41 | "api_key" uuid default gen_random_uuid() 42 | ); 43 | 44 | 45 | alter table "public"."projects" enable row level security; 46 | 47 | CREATE UNIQUE INDEX apps_pkey ON public.projects USING btree (id); 48 | 49 | CREATE UNIQUE INDEX knowledge_bases_pkey ON public.indexes USING btree (id); 50 | 51 | alter table "public"."indexes" add constraint "knowledge_bases_pkey" PRIMARY KEY using index "knowledge_bases_pkey"; 52 | 53 | alter table "public"."projects" add constraint "apps_pkey" PRIMARY KEY using index "apps_pkey"; 54 | 55 | alter table "public"."indexes" add constraint "indexes_project_id_fkey" FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE not valid; 56 | 57 | alter table "public"."indexes" validate constraint "indexes_project_id_fkey"; 58 | 59 | alter table "public"."indexes" add constraint "indexes_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE not valid; 60 | 61 | alter table "public"."indexes" validate constraint "indexes_user_id_fkey"; 62 | 63 | alter table "public"."documents" add constraint "documents_knowledge_base_id_fkey" FOREIGN KEY (knowledge_base_id) REFERENCES indexes(id) ON DELETE CASCADE not valid; 64 | 65 | alter table "public"."documents" validate constraint "documents_knowledge_base_id_fkey"; 66 | 67 | alter table "public"."memory_sessions" add constraint "memory_sessions_project_id_fkey" FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE not valid; 68 | 69 | alter table "public"."memory_sessions" validate constraint "memory_sessions_project_id_fkey"; 70 | 71 | create policy "Enable access for all authed users" 72 | on "public"."indexes" 73 | as permissive 74 | for all 75 | to authenticated 76 | using ((auth.uid() = user_id)) 77 | with check ((auth.uid() = user_id)); 78 | 79 | 80 | create policy "Enable access for all authed users" 81 | on "public"."projects" 82 | as permissive 83 | for all 84 | to authenticated 85 | using ((auth.uid() = user_id)) 86 | with check ((auth.uid() = user_id)); 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/app/login/components/user-auth-form.tsx.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | // import { Icons } from '@/components/icons';/ 7 | import { Button } from '@/components/Button'; 8 | import { Input } from '@/components/Input'; 9 | import { Label } from '@/components/ui/label'; 10 | 11 | interface UserAuthFormProps extends React.HTMLAttributes {} 12 | 13 | export function UserAuthForm({ className, ...props }: UserAuthFormProps) { 14 | const [isLoading, setIsLoading] = React.useState(false); 15 | 16 | return ( 17 |
18 |
19 |
20 |
21 | 24 | 34 |
35 |
36 | 39 | 46 |
47 | 53 |
54 |
55 |
56 |
57 | 58 |
59 |
60 | 61 | Or continue with your existing account 62 | 63 |
64 |
65 | 66 |
67 |
68 |
69 | 72 | 82 |
83 |
84 | 87 | 94 |
95 | 101 |
102 |
103 |
104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import Image from 'next/image'; 3 | import { cn } from '@/lib/utils'; 4 | import { Button } from '@/components/Button'; 5 | import { Input } from '@/components/Input'; 6 | import { Label } from '@/components/ui/label'; 7 | 8 | export const metadata: Metadata = { 9 | title: 'Authentication', 10 | description: 'Authentication forms.', 11 | }; 12 | 13 | export default function Login() { 14 | return ( 15 | <> 16 |
17 | Authentication 24 | Authentication 31 |
32 |
33 | {/* 40 | Login 41 | */} 42 |
43 |
44 |
45 | SquareDev 46 |
47 |
48 |
49 |

50 | “We want to create the best developer experience for 51 | people developing with AI.” 52 |

53 |
The Squaredev Team
54 |
55 |
56 |
57 |
58 |
59 |
60 |

61 | Join our early adopters program 62 |

63 |

64 | Enter your credentials below to create your account 65 |

66 |
67 |
68 |
69 |
70 |
71 | 74 | 83 |
84 |
85 | 88 | 94 |
95 | 96 |
97 |
98 |
99 |
100 |
101 |
102 | 103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /src/lib/public-api/validation.ts: -------------------------------------------------------------------------------- 1 | import { type } from 'os'; 2 | import * as z from 'zod'; 3 | import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; 4 | 5 | extendZodWithOpenApi(z); 6 | 7 | /** 8 | * IMPORTANT 9 | * 10 | * This file contains the validation schemas for the API requests and responses. 11 | * It is used by the API routes and the Cypress tests. 12 | * Make sure to make every object schema strict and backwards compatible. 13 | * Zod strict: https://zod.dev/?id=strict 14 | */ 15 | 16 | // Errors 17 | // --- 18 | 19 | export const ErrorResponse = z 20 | .object({ 21 | error: z.string(), 22 | }) 23 | .openapi('ErrorResponse'); 24 | 25 | // Documents 26 | // --- 27 | 28 | const DocumentBase = z 29 | .object({ 30 | content: z.string(), 31 | source: z.string().optional(), 32 | metadata: z.object({}).optional(), 33 | }) 34 | .strict(); 35 | 36 | export const CreateDocumentRequest = z 37 | .array(DocumentBase) 38 | .openapi('CreateDocumentRequest'); 39 | 40 | export type CreateDocumentRequestType = z.infer; 41 | 42 | export const SingleDocumentResponse = DocumentBase.merge( 43 | DocumentBase.merge( 44 | z 45 | .object({ 46 | id: z.string(), 47 | index_id: z.string(), 48 | created_at: z.string(), 49 | user_id: z.string(), 50 | // Embeddings are not returned by the API 51 | }) 52 | .strict() 53 | ) 54 | ).openapi('SingleDocumentResponse'); 55 | 56 | export const DocumentsResponse = z 57 | .array(SingleDocumentResponse) 58 | .openapi('DocumentsResponse'); 59 | 60 | export type CreateDocumentResponseType = z.infer; 61 | 62 | export const SimilarDocumentsResponse = z 63 | .array(SingleDocumentResponse.merge(z.object({ similarity: z.number() }))) 64 | .openapi('SimilarDocumentsResponse'); 65 | 66 | // Indexes 67 | // --- 68 | 69 | export const CreateIndexRequest = z 70 | .object({ 71 | name: z.string(), 72 | }) 73 | .strict() 74 | .openapi('CreateIndexRequest'); 75 | 76 | export const CreateIndexResponse = CreateIndexRequest.merge( 77 | z 78 | .object({ 79 | id: z.string(), 80 | project_id: z.string(), 81 | created_at: z.string(), 82 | user_id: z.string(), 83 | }) 84 | .strict() 85 | ).openapi('CreateIndexResponse'); 86 | 87 | export type CreateIndexRequestType = z.infer; 88 | 89 | export const GetIndexesResponse = z 90 | .array(CreateIndexResponse) 91 | .openapi('GetIndexesResponse'); 92 | 93 | export type GetIndexesResponseType = z.infer; 94 | 95 | // Chat 96 | // --- 97 | 98 | export const ChatCompletionRequest = z 99 | .object({ 100 | messages: z.object({ 101 | system: z.string().optional(), 102 | user: z.string(), 103 | }), 104 | model: z.string(), 105 | }) 106 | .strict() 107 | .openapi('ChatCompletionRequest'); 108 | 109 | export type ChatCompletionRequestType = z.infer; 110 | 111 | export const ChatCompletionResponse = z 112 | .object({ 113 | message: z.string(), 114 | model: z.string(), 115 | }) 116 | .strict() 117 | .openapi('ChatCompletionResponse'); 118 | 119 | export type ChatCompletionResponseType = z.infer; 120 | 121 | // RAG 122 | // --- 123 | 124 | export const RagCompletionRequest = z 125 | .object({ 126 | messages: z.object({ 127 | system: z.string().optional(), 128 | user: z.string(), 129 | }), 130 | model: z.string(), 131 | indexId: z.string(), 132 | withMemory: z.boolean().optional(), 133 | }) 134 | .strict() 135 | .openapi('RagCompletionRequest'); 136 | 137 | export type RagCompletionRequestType = z.infer; 138 | 139 | export const RagCompletionResponse = z 140 | .object({ 141 | message: z.string(), 142 | model: z.string(), 143 | sources: z.array( 144 | DocumentBase.merge( 145 | z.object({ id: z.string(), similarity: z.number() }).strict() 146 | ) 147 | ), 148 | }) 149 | .strict() 150 | .openapi('RagCompletionResponse'); 151 | -------------------------------------------------------------------------------- /supabase/migrations/20231027154727_feature.sql: -------------------------------------------------------------------------------- 1 | drop policy "Enable access for all authed users" on "public"."apps"; 2 | 3 | drop policy "Enable access for all authed users" on "public"."apps_knowledge_bases"; 4 | 5 | drop policy "Enable access for all authed users" on "public"."knowledge_bases"; 6 | 7 | alter table "public"."apps_knowledge_bases" drop constraint "apps_knowledge_bases_app_id_fkey"; 8 | 9 | alter table "public"."apps_knowledge_bases" drop constraint "apps_knowledge_bases_knowledge_base_id_fkey"; 10 | 11 | alter table "public"."apps_knowledge_bases" drop constraint "apps_knowledge_bases_user_id_fkey"; 12 | 13 | alter table "public"."knowledge_bases" drop constraint "knowledge_bases_user_id_fkey"; 14 | 15 | alter table "public"."memory_sessions" drop constraint "memory_sessions_app_id_fkey"; 16 | 17 | alter table "public"."documents" drop constraint "documents_knowledge_base_id_fkey"; 18 | 19 | alter table "public"."apps" drop constraint "apps_pkey"; 20 | 21 | alter table "public"."apps_knowledge_bases" drop constraint "apps_knowledge_bases_pkey"; 22 | 23 | alter table "public"."knowledge_bases" drop constraint "knowledge_bases_pkey"; 24 | 25 | drop index if exists "public"."apps_knowledge_bases_pkey"; 26 | 27 | drop index if exists "public"."apps_pkey"; 28 | 29 | drop index if exists "public"."knowledge_bases_pkey"; 30 | 31 | drop table "public"."apps"; 32 | 33 | drop table "public"."apps_knowledge_bases"; 34 | 35 | drop table "public"."knowledge_bases"; 36 | 37 | create table "public"."index" ( 38 | "id" uuid not null default gen_random_uuid(), 39 | "created_at" timestamp with time zone not null default now(), 40 | "name" text not null, 41 | "user_id" uuid not null default auth.uid(), 42 | "project_id" uuid 43 | ); 44 | 45 | 46 | alter table "public"."index" enable row level security; 47 | 48 | create table "public"."project" ( 49 | "id" uuid not null default gen_random_uuid(), 50 | "created_at" timestamp with time zone not null default now(), 51 | "user_id" uuid default auth.uid(), 52 | "name" text not null, 53 | "api_key" uuid default gen_random_uuid() 54 | ); 55 | 56 | 57 | alter table "public"."project" enable row level security; 58 | 59 | alter table "public"."memory_sessions" drop column "app_id"; 60 | 61 | alter table "public"."memory_sessions" add column "project_id" uuid not null; 62 | 63 | CREATE UNIQUE INDEX apps_pkey ON public.project USING btree (id); 64 | 65 | CREATE UNIQUE INDEX knowledge_bases_pkey ON public.index USING btree (id); 66 | 67 | alter table "public"."index" add constraint "knowledge_bases_pkey" PRIMARY KEY using index "knowledge_bases_pkey"; 68 | 69 | alter table "public"."project" add constraint "apps_pkey" PRIMARY KEY using index "apps_pkey"; 70 | 71 | alter table "public"."index" add constraint "index_project_id_fkey" FOREIGN KEY (project_id) REFERENCES project(id) ON DELETE CASCADE not valid; 72 | 73 | alter table "public"."index" validate constraint "index_project_id_fkey"; 74 | 75 | alter table "public"."index" add constraint "index_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE not valid; 76 | 77 | alter table "public"."index" validate constraint "index_user_id_fkey"; 78 | 79 | alter table "public"."memory_sessions" add constraint "memory_sessions_project_id_fkey" FOREIGN KEY (project_id) REFERENCES project(id) ON DELETE CASCADE not valid; 80 | 81 | alter table "public"."memory_sessions" validate constraint "memory_sessions_project_id_fkey"; 82 | 83 | alter table "public"."documents" add constraint "documents_knowledge_base_id_fkey" FOREIGN KEY (knowledge_base_id) REFERENCES index(id) ON DELETE CASCADE not valid; 84 | 85 | alter table "public"."documents" validate constraint "documents_knowledge_base_id_fkey"; 86 | 87 | create policy "Enable access for all authed users" 88 | on "public"."index" 89 | as permissive 90 | for all 91 | to authenticated 92 | using ((auth.uid() = user_id)) 93 | with check ((auth.uid() = user_id)); 94 | 95 | 96 | create policy "Enable access for all authed users" 97 | on "public"."project" 98 | as permissive 99 | for all 100 | to authenticated 101 | using ((auth.uid() = user_id)) 102 | with check ((auth.uid() = user_id)); 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as DialogPrimitive from '@radix-ui/react-dialog'; 5 | import { X } from 'lucide-react'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | 9 | const Dialog = DialogPrimitive.Root; 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger; 12 | 13 | const DialogPortal = DialogPrimitive.Portal; 14 | 15 | const DialogClose = DialogPrimitive.Close; 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )); 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )); 54 | DialogContent.displayName = DialogPrimitive.Content.displayName; 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ); 68 | DialogHeader.displayName = 'DialogHeader'; 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ); 82 | DialogFooter.displayName = 'DialogFooter'; 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )); 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )); 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogClose, 116 | DialogTrigger, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | }; 123 | -------------------------------------------------------------------------------- /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 |