├── src ├── vite-env.d.ts ├── index.css ├── App.css ├── Loading.tsx ├── main.tsx ├── useComparison.ts ├── Prompt.tsx ├── EnvCheck.tsx ├── Compare.tsx ├── App.tsx ├── Sources.tsx ├── AddSource.tsx ├── Chunks.tsx ├── assets │ └── react.svg └── Search.tsx ├── screenshot.png ├── public └── favicon.ico ├── postcss.config.js ├── vite.config.ts ├── tsconfig.node.json ├── convex ├── lib │ ├── utils.ts │ └── embeddings.ts ├── _generated │ ├── api.js │ ├── api.d.ts │ ├── dataModel.d.ts │ ├── server.js │ └── server.d.ts ├── tsconfig.json ├── schema.ts ├── README.md ├── comparisons.ts ├── searches.ts └── sources.ts ├── .gitignore ├── index.html ├── .eslintrc.mjs ├── tsconfig.json ├── tailwind.config.js ├── package.json ├── scripts ├── addURL.py └── addFiles.ts └── README.md /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianmacartney/embeddings-in-convex/HEAD/screenshot.png -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ianmacartney/embeddings-in-convex/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /src/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { Progress } from "@rewind-ui/core"; 2 | 3 | export function Loading() { 4 | return ( 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /convex/lib/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Filters out null elements from an array. 3 | * @param list List of elements that might be null. 4 | * @returns List of elements with nulls removed. 5 | */ 6 | export function pruneNull(list: (T | null)[]): T[] { 7 | return list.filter((i) => i !== null) as T[]; 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Embeddings Playground: OpenAI + Convex 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.tsx"; 4 | import "./index.css"; 5 | import { ConvexProvider, ConvexReactClient } from "convex/react"; 6 | 7 | const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL); 8 | 9 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /.eslintrc.mjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:react-hooks/recommended", 7 | ], 8 | parser: "@typescript-eslint/parser", 9 | parserOptions: { ecmaVersion: "latest", sourceType: "module" }, 10 | plugins: ["react-refresh"], 11 | rules: { 12 | "react-refresh/only-export-components": "warn", 13 | "@typescript-eslint/no-non-null-assertion": "off", 14 | "@typescript-eslint/no-floating-promises": "error", 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /convex/_generated/api.js: -------------------------------------------------------------------------------- 1 | /* prettier-ignore-start */ 2 | 3 | /* eslint-disable */ 4 | /** 5 | * Generated `api` utility. 6 | * 7 | * THIS CODE IS AUTOMATICALLY GENERATED. 8 | * 9 | * To regenerate, run `npx convex dev`. 10 | * @module 11 | */ 12 | 13 | import { anyApi } from "convex/server"; 14 | 15 | /** 16 | * A utility for referencing Convex functions in your app's API. 17 | * 18 | * Usage: 19 | * ```js 20 | * const myFunctionReference = api.myModule.myFunction; 21 | * ``` 22 | */ 23 | export const api = anyApi; 24 | export const internal = anyApi; 25 | 26 | /* prettier-ignore-end */ 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "noEmit": true, 14 | "jsx": "react-jsx", 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true 21 | }, 22 | "include": ["src"], 23 | "references": [{ "path": "./tsconfig.node.json" }] 24 | } 25 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{html,jsx,tsx}", 6 | // you can either add all styles 7 | "./node_modules/@rewind-ui/core/dist/theme/styles/*.js", 8 | // OR you can add only the styles you need 9 | // "./node_modules/@rewind-ui/core/dist/theme/styles/Button.styles.js", 10 | // "./node_modules/@rewind-ui/core/dist/theme/styles/Text.styles.js", 11 | ], 12 | theme: { 13 | extend: {}, 14 | }, 15 | plugins: [ 16 | require("@tailwindcss/typography"), 17 | require("tailwind-scrollbar")({ nocompatible: true }), 18 | require("@tailwindcss/forms")({ 19 | strategy: "class", // only generate classes 20 | }), 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /convex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* This TypeScript project config describes the environment that 3 | * Convex functions run in and is used to typecheck them. 4 | * You can modify it, but some settings required to use Convex. 5 | */ 6 | "compilerOptions": { 7 | /* These settings are not required by Convex and can be modified. */ 8 | "allowJs": true, 9 | "strict": true, 10 | 11 | /* These compiler options are required by Convex */ 12 | "target": "ESNext", 13 | "lib": ["ES2021", "dom"], 14 | "forceConsistentCasingInFileNames": true, 15 | "allowSyntheticDefaultImports": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "isolatedModules": true, 19 | "noEmit": true 20 | }, 21 | "include": ["./**/*"], 22 | "exclude": ["./_generated"] 23 | } 24 | -------------------------------------------------------------------------------- /src/useComparison.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from "convex/react"; 2 | import { RefObject, useCallback, useState } from "react"; 3 | import { Id } from "../convex/_generated/dataModel"; 4 | import { api } from "../convex/_generated/api"; 5 | 6 | export function useComparison(ref: RefObject) { 7 | const [target, setTarget] = useState(); 8 | const upsertComparison = useMutation(api.comparisons.upsert); 9 | const compare = useCallback( 10 | (chunkId?: Id<"chunks">) => { 11 | if (chunkId) { 12 | upsertComparison({ target: chunkId, count: 10 }).then((id) => 13 | setTarget({ chunkId, comparisonId: id }) 14 | ); 15 | ref.current?.click(); 16 | } else { 17 | setTarget(undefined); 18 | } 19 | }, 20 | [upsertComparison, ref] 21 | ); 22 | 23 | return [target, compare] as const; 24 | } 25 | export type Target = { chunkId: Id<"chunks">; comparisonId: Id<"comparisons"> }; 26 | export type CompareFn = (id?: Id<"chunks">) => void; 27 | -------------------------------------------------------------------------------- /src/Prompt.tsx: -------------------------------------------------------------------------------- 1 | // import { 2 | // useAction, 3 | // useMutation, 4 | // usePaginatedQuery, 5 | // useQuery, 6 | // } from "convex/react"; 7 | import { 8 | Accordion, 9 | // Button, Table, Text, InputGroup 10 | } from "@rewind-ui/core"; 11 | // import { Id } from "../convex/_generated/dataModel"; 12 | // import { api } from "../convex/_generated/api"; 13 | // import { Loading } from "./Loading"; 14 | // import { Chunks } from "./Chunks"; 15 | export function Prompt() { 16 | return ( 17 | <> 18 | 19 | 20 | Prompt 21 | 22 | 23 | 24 | Sources 25 | 26 | 27 | 28 | Template 29 | 30 | 31 | 32 | History 33 | 34 | 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/EnvCheck.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { useQuery } from "convex/react"; 3 | import { Alert } from "@rewind-ui/core"; 4 | import { api } from "../convex/_generated/api"; 5 | 6 | export function EnvCheck({ children }: { children: ReactNode }) { 7 | const embeddingsEnv = useQuery(api.lib.embeddings.envCheck) ?? {}; 8 | const envCheck = { ...embeddingsEnv }; 9 | const missingEnv = Object.entries(envCheck).reduce( 10 | (badOnes, [key, value]) => (value ? badOnes : [...badOnes, key]), 11 | [] 12 | ); 13 | if (missingEnv.length) { 14 | return ( 15 | 16 | You are missing the following Environment Variables: 17 |
    18 | {missingEnv.map((missing) => ( 19 |
  1. {missing}
  2. 20 | ))} 21 |
22 | Please enter them on{" "} 23 | 24 | the Convex Dashboard 25 | 26 | . See{" "} 27 | 31 | the docs 32 | {" "} 33 | for details. 34 |
35 | ); 36 | } 37 | return <>{children}; 38 | } 39 | -------------------------------------------------------------------------------- /convex/_generated/api.d.ts: -------------------------------------------------------------------------------- 1 | /* prettier-ignore-start */ 2 | 3 | /* eslint-disable */ 4 | /** 5 | * Generated `api` utility. 6 | * 7 | * THIS CODE IS AUTOMATICALLY GENERATED. 8 | * 9 | * To regenerate, run `npx convex dev`. 10 | * @module 11 | */ 12 | 13 | import type { 14 | ApiFromModules, 15 | FilterApi, 16 | FunctionReference, 17 | } from "convex/server"; 18 | import type * as comparisons from "../comparisons.js"; 19 | import type * as lib_embeddings from "../lib/embeddings.js"; 20 | import type * as lib_utils from "../lib/utils.js"; 21 | import type * as searches from "../searches.js"; 22 | import type * as sources from "../sources.js"; 23 | 24 | /** 25 | * A utility for referencing Convex functions in your app's API. 26 | * 27 | * Usage: 28 | * ```js 29 | * const myFunctionReference = api.myModule.myFunction; 30 | * ``` 31 | */ 32 | declare const fullApi: ApiFromModules<{ 33 | comparisons: typeof comparisons; 34 | "lib/embeddings": typeof lib_embeddings; 35 | "lib/utils": typeof lib_utils; 36 | searches: typeof searches; 37 | sources: typeof sources; 38 | }>; 39 | export declare const api: FilterApi< 40 | typeof fullApi, 41 | FunctionReference 42 | >; 43 | export declare const internal: FilterApi< 44 | typeof fullApi, 45 | FunctionReference 46 | >; 47 | 48 | /* prettier-ignore-end */ 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "embeddings-in-convex", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "npm-run-all --parallel dev:backend dev:frontend", 7 | "build": "tsc && vite build", 8 | "dev:backend": "convex dev", 9 | "dev:frontend": "vite --open --clearScreen false", 10 | "predev": "convex dev --until-success" 11 | }, 12 | "dependencies": { 13 | "@phosphor-icons/react": "^2.0.10", 14 | "@rewind-ui/core": "^0.12.2", 15 | "@tailwindcss/forms": "^0.5.3", 16 | "@tailwindcss/typography": "^0.5.9", 17 | "convex": "^1.16.0", 18 | "convex-helpers": "^0.1.58", 19 | "langchain": "^0.0.92", 20 | "openai": "^3.2.1", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-hook-form": "^7.44.3", 24 | "tailwind-scrollbar": "^3.0.4" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "^20.2.3", 28 | "@types/react": "^18.0.28", 29 | "@types/react-dom": "^18.0.11", 30 | "@typescript-eslint/eslint-plugin": "^5.57.1", 31 | "@typescript-eslint/parser": "^5.57.1", 32 | "@vitejs/plugin-react": "^4.0.0", 33 | "autoprefixer": "^10.4.14", 34 | "eslint": "^8.38.0", 35 | "eslint-plugin-react-hooks": "^4.6.0", 36 | "eslint-plugin-react-refresh": "^0.3.4", 37 | "npm-run-all": "^4.1.5", 38 | "postcss": "^8.4.24", 39 | "tailwindcss": "^3.3.2", 40 | "typescript": "^5.0.2", 41 | "vite": "^4.3.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /convex/lib/embeddings.ts: -------------------------------------------------------------------------------- 1 | import { query } from "../_generated/server"; 2 | 3 | export async function fetchEmbeddingBatch(texts: string[]) { 4 | const start = Date.now(); 5 | const result = await fetch("https://api.openai.com/v1/embeddings", { 6 | method: "POST", 7 | headers: { 8 | "Content-Type": "application/json", 9 | Authorization: "Bearer " + process.env.OPENAI_API_KEY, 10 | }, 11 | 12 | body: JSON.stringify({ 13 | model: "text-embedding-ada-002", 14 | input: texts.map((text) => text.replace(/\n/g, " ")), 15 | }), 16 | }); 17 | const embeddingMs = Date.now() - start; 18 | 19 | const jsonresults = await result.json(); 20 | if (jsonresults.data.length !== texts.length) { 21 | console.error(result); 22 | throw new Error("Unexpected number of embeddings"); 23 | } 24 | const allembeddings = jsonresults.data as { 25 | embedding: number[]; 26 | index: number; 27 | }[]; 28 | allembeddings.sort((a, b) => a.index - b.index); 29 | return { 30 | embeddings: allembeddings.map(({ embedding }) => embedding), 31 | totalTokens: jsonresults.usage.total_tokens, 32 | embeddingMs, 33 | }; 34 | } 35 | 36 | export async function fetchEmbedding(text: string) { 37 | const { embeddings, ...stats } = await fetchEmbeddingBatch([text]); 38 | return { embedding: embeddings[0], ...stats }; 39 | } 40 | 41 | export const envCheck = query(async () => { 42 | return { 43 | OPENAI_API_KEY: !!process.env.OPENAI_API_KEY, 44 | }; 45 | }); 46 | -------------------------------------------------------------------------------- /scripts/addURL.py: -------------------------------------------------------------------------------- 1 | """ Import files into Convex using Langchain document loaders 2 | 3 | Setup: 4 | !pip install "playwright" 5 | !pip install "unstructured" 6 | !pip install "convex" 7 | !pip install "python-dotenv" 8 | !pip install tiktoken 9 | 10 | !playwright install 11 | """ 12 | 13 | import os 14 | import sys 15 | from dotenv import load_dotenv 16 | from convex import ConvexClient 17 | from langchain.document_loaders import PlaywrightURLLoader 18 | from langchain.text_splitter import CharacterTextSplitter 19 | 20 | 21 | urls = sys.argv[1:] 22 | loader = PlaywrightURLLoader(urls=urls, remove_selectors=["header", "footer"]) 23 | data = loader.load() 24 | text_splitter = CharacterTextSplitter.from_tiktoken_encoder( 25 | chunk_size=100, chunk_overlap=0 26 | ) 27 | texts = text_splitter.split_text(data[0].page_content) 28 | 29 | 30 | load_dotenv(".env.local") 31 | load_dotenv() 32 | 33 | backend = os.getenv("VITE_CONVEX_URL") 34 | if not backend: 35 | raise KeyError("Missing VITE_CONVEX_URL") 36 | 37 | client = ConvexClient(backend) 38 | print( 39 | client.action( 40 | "sources:add", 41 | dict( 42 | name=data[0].metadata["source"], 43 | chunks=list( 44 | map( 45 | lambda chunk: dict( 46 | text=chunk, 47 | # TODO: add real line numbers 48 | lines={"from": 0, "to": 0}, 49 | ), 50 | texts, 51 | ) 52 | ), 53 | ), 54 | ) 55 | ) 56 | -------------------------------------------------------------------------------- /src/Compare.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from "convex/react"; 2 | import { Button, Text } from "@rewind-ui/core"; 3 | import { api } from "../convex/_generated/api"; 4 | import { Loading } from "./Loading"; 5 | import { AllChunks, Chunks } from "./Chunks"; 6 | import { Target, CompareFn } from "./useComparison"; 7 | 8 | export function Compare({ 9 | target, 10 | compare, 11 | }: { 12 | target?: Target; 13 | compare: CompareFn; 14 | }) { 15 | return ( 16 | <> 17 | {target ? ( 18 | 19 | ) : ( 20 | 21 | )} 22 | 23 | ); 24 | } 25 | 26 | function ComparisonResults({ 27 | target: { comparisonId, chunkId }, 28 | compare, 29 | }: { 30 | target: Target; 31 | compare: CompareFn; 32 | }) { 33 | const comparison = useQuery(api.comparisons.get, { comparisonId }); 34 | return comparison?.relatedChunks.length ? ( 35 | <> 36 |
37 | 38 | Results for {comparison.target.sourceName} ( 39 | {comparison.target.chunkIndex}): "{comparison.target.text} 40 | 41 | 44 |
45 | {" "} 46 | 47 | ) : ( 48 | <> 49 | 50 | Comparing against "{comparison?.target?.text ?? chunkId}"... 51 | 52 | 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | 3 | import { Tabs, Text } from "@rewind-ui/core"; 4 | import { AddSource } from "./AddSource"; 5 | import { EnvCheck } from "./EnvCheck"; 6 | import { Sources } from "./Sources"; 7 | import { Search } from "./Search"; 8 | import { Compare } from "./Compare"; 9 | import { useComparison } from "./useComparison"; 10 | import { Prompt } from "./Prompt"; 11 | import { useRef } from "react"; 12 | 13 | function App() { 14 | const compareRef = useRef(null); 15 | console.log(compareRef); 16 | const [target, compare] = useComparison(compareRef); 17 | return ( 18 | 19 | 20 | 21 | Sources 22 | Search 23 | 24 | Compare 25 | 26 | 27 | Prompt 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | Add a source 41 |

42 | This is the data you will be able to search over and compare 43 | semantically. 44 |

45 | 46 | 47 |
48 |
49 |
50 | ); 51 | } 52 | 53 | export default App; 54 | -------------------------------------------------------------------------------- /src/Sources.tsx: -------------------------------------------------------------------------------- 1 | import { useMutation, usePaginatedQuery } from "convex/react"; 2 | import { Button, Table } from "@rewind-ui/core"; 3 | import { api } from "../convex/_generated/api"; 4 | 5 | export function Sources() { 6 | const { 7 | status, 8 | loadMore, 9 | results: sources, 10 | } = usePaginatedQuery(api.sources.paginate, {}, { initialNumItems: 10 }); 11 | const deleteSource = useMutation(api.sources.deleteSource); 12 | return ( 13 | <> 14 | 15 | 16 | 17 | Source 18 | Chunks 19 | Content 20 | Tokens 21 | 22 | 23 | 24 | 25 | {sources.map((source) => ( 26 | 27 | {source.name} 28 | {source.chunkIds.length} 29 | 30 |

{source.firstChunkText}

31 |
32 | 33 | {source.saved ? source.totalTokens : "Unsaved"} 34 | 35 | 36 | 42 | 43 |
44 | ))} 45 |
46 |
47 | {status !== "Exhausted" && ( 48 | 54 | )} 55 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /convex/_generated/dataModel.d.ts: -------------------------------------------------------------------------------- 1 | /* prettier-ignore-start */ 2 | 3 | /* eslint-disable */ 4 | /** 5 | * Generated data model types. 6 | * 7 | * THIS CODE IS AUTOMATICALLY GENERATED. 8 | * 9 | * To regenerate, run `npx convex dev`. 10 | * @module 11 | */ 12 | 13 | import type { 14 | DataModelFromSchemaDefinition, 15 | DocumentByName, 16 | TableNamesInDataModel, 17 | SystemTableNames, 18 | } from "convex/server"; 19 | import type { GenericId } from "convex/values"; 20 | import schema from "../schema.js"; 21 | 22 | /** 23 | * The names of all of your Convex tables. 24 | */ 25 | export type TableNames = TableNamesInDataModel; 26 | 27 | /** 28 | * The type of a document stored in Convex. 29 | * 30 | * @typeParam TableName - A string literal type of the table name (like "users"). 31 | */ 32 | export type Doc = DocumentByName< 33 | DataModel, 34 | TableName 35 | >; 36 | 37 | /** 38 | * An identifier for a document in Convex. 39 | * 40 | * Convex documents are uniquely identified by their `Id`, which is accessible 41 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). 42 | * 43 | * Documents can be loaded using `db.get(id)` in query and mutation functions. 44 | * 45 | * IDs are just strings at runtime, but this type can be used to distinguish them from other 46 | * strings when type checking. 47 | * 48 | * @typeParam TableName - A string literal type of the table name (like "users"). 49 | */ 50 | export type Id = 51 | GenericId; 52 | 53 | /** 54 | * A type describing your Convex data model. 55 | * 56 | * This type includes information about what tables you have, the type of 57 | * documents stored in those tables, and the indexes defined on them. 58 | * 59 | * This type is used to parameterize methods like `queryGeneric` and 60 | * `mutationGeneric` to make them type-safe. 61 | */ 62 | export type DataModel = DataModelFromSchemaDefinition; 63 | 64 | /* prettier-ignore-end */ 65 | -------------------------------------------------------------------------------- /src/AddSource.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useMutation } from "convex/react"; 3 | import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"; 4 | import { useForm, Controller } from "react-hook-form"; 5 | import { Alert, Input, Textarea, Button } from "@rewind-ui/core"; 6 | import { api } from "../convex/_generated/api"; 7 | 8 | export function AddSource() { 9 | const createSource = useMutation(api.sources.add); 10 | const [added, setAdded] = useState(""); 11 | 12 | const { formState, handleSubmit, control, reset } = useForm<{ 13 | name: string; 14 | text: string; 15 | }>({}); 16 | const onSubmit = handleSubmit(({ name, text }) => { 17 | const textSplitter = new RecursiveCharacterTextSplitter({ 18 | chunkSize: 1000, 19 | }); 20 | textSplitter.createDocuments([text]).then((docs) => { 21 | createSource({ 22 | name, 23 | chunks: docs.map((doc) => ({ 24 | text: doc.pageContent, 25 | lines: doc.metadata.loc.lines, 26 | })), 27 | }).then(() => { 28 | setAdded(name); 29 | setTimeout( 30 | () => setAdded((state) => (state === name ? "" : state)), 31 | 1000 32 | ); 33 | }); 34 | }); 35 | }); 36 | useEffect(() => { 37 | if (formState.isSubmitSuccessful) { 38 | reset(); 39 | } 40 | }, [formState, reset]); 41 | 42 | return ( 43 |
44 | {added.length ? ( 45 | Successfully added {added} 46 | ) : null} 47 | } 52 | defaultValue="" 53 | /> 54 |