From 0056c13833fc021f0762e7e424414feb08ee3c50 Mon Sep 17 00:00:00 2001 From: Max Isom Date: Wed, 24 Apr 2024 16:03:30 -0700 Subject: [PATCH] [ENH] fix browser usage of JS client & update browser example to work with latest version of Chroma (#2050) --- clients/js/examples/browser/README.md | 19 +- clients/js/examples/browser/app.ts | 53 - clients/js/examples/browser/app.tsx | 231 ++ clients/js/examples/browser/index.html | 31 +- clients/js/examples/browser/index.tsx | 7 + clients/js/examples/browser/package.json | 15 +- clients/js/examples/browser/pnpm-lock.yaml | 2394 +++++++++++++++++ clients/js/examples/browser/vite.config.mjs | 23 + clients/js/examples/browser/yarn.lock | 1564 ----------- .../embeddings/DefaultEmbeddingFunction.ts | 23 +- clients/js/src/utils.ts | 6 + 11 files changed, 2709 insertions(+), 1657 deletions(-) delete mode 100644 clients/js/examples/browser/app.ts create mode 100644 clients/js/examples/browser/app.tsx create mode 100644 clients/js/examples/browser/index.tsx create mode 100644 clients/js/examples/browser/pnpm-lock.yaml create mode 100644 clients/js/examples/browser/vite.config.mjs delete mode 100644 clients/js/examples/browser/yarn.lock diff --git a/clients/js/examples/browser/README.md b/clients/js/examples/browser/README.md index b366b3eedeb..95c18251515 100644 --- a/clients/js/examples/browser/README.md +++ b/clients/js/examples/browser/README.md @@ -1,16 +1,15 @@ ## Demo in browser -Update your settings to add `localhost:3000` to `chroma_server_cors_allow_origins`. +First, update Chroma's config to allow the `localhost:3000` origin for CORS. -For example in `docker-compose.yml` +For example, you could start Chroma with +```bash +CHROMA_SERVER_CORS_ALLOW_ORIGINS='["http://localhost:3000"]' chroma run ``` -environment: - - CHROMA_DB_IMPL=clickhouse - - CLICKHOUSE_HOST=clickhouse - - CLICKHOUSE_PORT=8123 - - CHROMA_SERVER_CORS_ALLOW_ORIGINS=["http://localhost:3000"] -``` -1. `yarn dev` -2. visit `localhost:3000` +Then, in this folder: + +1. `pnpm i` +2. `pnpm dev` +3. visit `localhost:3000` diff --git a/clients/js/examples/browser/app.ts b/clients/js/examples/browser/app.ts deleted file mode 100644 index 47e4c02c5cf..00000000000 --- a/clients/js/examples/browser/app.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { ChromaClient } from "../../src/ChromaClient"; -// import env.ts - -window.onload = async () => { - const chroma = new ChromaClient({ path: "http://localhost:8000" }); - await chroma.reset(); - - const collection = await chroma.createCollection({ name: "test-from-js" }); - console.log("collection", collection); - - // first generate some data - var ids: string[] = []; - var embeddings: Array = []; - var metadatas: Array = []; - for (let i = 0; i < 100; i++) { - ids.push("test-id-" + i.toString()); - embeddings.push([1, 2, 3, 4, 5]); - metadatas.push({ test: "test" }); - } - - let add = await collection.add({ ids, embeddings, metadatas }); - console.log("add", add); - - let count = await collection.count(); - console.log("count", count); - - const queryData = await collection.query({ - queryEmbeddings: [1, 2, 3, 4, 5], - nResults: 5, - where: { test: "test" }, - }); - - console.log("queryData", queryData); - - await collection.delete(); - - let count2 = await collection.count(); - console.log("count2", count2); - - const collections = await chroma.listCollections(); - console.log("collections", collections); - - // this code is commented out so that it is easy to see the output on the page if desired - // let node; - // node = document.querySelector("#list-collections-result"); - // node!.innerHTML = `
${JSON.stringify(collections.data, null, 4)}
`; - // node = document.querySelector("#collection-count"); - // node!.innerHTML = `
${count}
`; - // node = document.querySelector("#collection-get"); - // node!.innerHTML = `
${JSON.stringify(getData, null, 4)}
`; - // node = document.querySelector("#collection-query"); - // node!.innerHTML = `
${JSON.stringify(queryData, null, 4)}
`; -}; diff --git a/clients/js/examples/browser/app.tsx b/clients/js/examples/browser/app.tsx new file mode 100644 index 00000000000..cd29c35e1d6 --- /dev/null +++ b/clients/js/examples/browser/app.tsx @@ -0,0 +1,231 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { ChromaClient } from "../../src/ChromaClient"; +import { Collection } from "../../src/Collection"; + +const SAMPLE_DOCUMENTS = [ + "apple", + "strawberry", + "pineapple", + "scooter", + "car", + "train", +]; + +const hashString = async (message: string) => { + const encoder = new TextEncoder(); + const data = encoder.encode(message); + const hashBuffer = await crypto.subtle.digest("SHA-256", data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); +}; + +const chroma = new ChromaClient({ path: "http://localhost:8000" }); + +const useCollection = () => { + const [collection, setCollection] = useState(); + + useEffect(() => { + chroma + .getOrCreateCollection({ name: "demo-collection" }) + .then((collection) => setCollection(collection)); + }, []); + + return collection; +}; + +const useDocuments = (query?: string) => { + const collection = useCollection(); + const [isLoading, setIsLoading] = useState(false); + const [documents, setDocuments] = useState< + { document: string; relativeDistance?: number }[] + >([]); + + const revalidate = useCallback( + async (abortSignal?: AbortSignal) => { + setIsLoading(true); + try { + if (query) { + collection?.query({ queryTexts: query }).then((results) => { + if (abortSignal?.aborted) { + return; + } + + const maxDistance = Math.max(...(results.distances?.[0] ?? [])); + setDocuments( + results.documents[0].map((document, i) => { + const distance = results.distances?.[0][i] ?? 0; + const relativeDistance = distance / maxDistance; + + return { + document: document!, + relativeDistance, + }; + }), + ); + }); + } else { + collection?.get({}).then((results) => { + if (abortSignal?.aborted) { + return; + } + + setDocuments( + results.documents.map((document) => ({ + document: document!, + })), + ); + }); + } + } finally { + setIsLoading(false); + } + }, + [collection, query], + ); + + useEffect(() => { + const abort = new AbortController(); + revalidate(abort.signal); + return () => abort.abort(); + }, [revalidate]); + + return { documents, revalidate, isLoading }; +}; + +export function App() { + const [query, setQuery] = useState(""); + const [isMutating, setIsMutating] = useState(false); + const collection = useCollection(); + + const trimmedQuery = query.trim(); + + const { documents, revalidate, isLoading } = useDocuments( + trimmedQuery === "" ? undefined : trimmedQuery, + ); + + const handleDocumentAdd = async (event: React.FormEvent) => { + event.preventDefault(); + + if (!collection) { + return; + } + + const currentTarget = event.currentTarget; + + const document = new FormData(currentTarget).get("document")!.toString(); + + setIsMutating(true); + try { + await collection.upsert({ + ids: [await hashString(document)], + documents: [document], + }); + + await revalidate(); + currentTarget.reset(); + } finally { + setIsMutating(false); + } + }; + + const handleLoadSampleData = async (event: React.FormEvent) => { + event.preventDefault(); + + if (!collection) { + return; + } + + setIsMutating(true); + try { + await collection.upsert({ + ids: await Promise.all( + SAMPLE_DOCUMENTS.map(async (d) => hashString(d)), + ), + documents: SAMPLE_DOCUMENTS, + }); + + await revalidate(); + } finally { + setIsMutating(false); + } + }; + + return ( +
+

Chroma Browser Demo

+ +

+ This assumes that you have a locally running Chroma instance at port{" "} + 8000 and that CORS is allowed for Vite's dev server. To + start Chroma, you can use this command: +

+ +
+        CHROMA_SERVER_CORS_ALLOW_ORIGINS='["http://localhost:3000"]' chroma run
+      
+ +
+

Add documents

+ +