_an interactive explorer for single-cell transcriptomics data_
[](https://zenodo.org/badge/latestdoi/105615409) [](https://pypi.org/project/cellxgene/) [](https://pypistats.org/packages/cellxgene) [](https://github.com/chanzuckerberg/cellxgene/pulse)
+
[](https://github.com/chanzuckerberg/cellxgene/actions?query=workflow%3A%22Push+Tests%22)
[](https://github.com/chanzuckerberg/cellxgene/actions?query=workflow%3A%22Compatibility+Tests%22)

diff --git a/client/.husky/pre-commit b/client/.husky/pre-commit
index 5a724e7e..9ff0bca3 100755
--- a/client/.husky/pre-commit
+++ b/client/.husky/pre-commit
@@ -1,5 +1,6 @@
#!/bin/sh
+exit 0
. "$(dirname "$0")/_/husky.sh"
cd client
-npx --no-install lint-staged --config "./configuration/lint-staged/lint-staged.config.js"
\ No newline at end of file
+npx --no-install lint-staged --config "./configuration/lint-staged/lint-staged.config.js"
diff --git a/client/configuration/webpack/webpack.config.dev.js b/client/configuration/webpack/webpack.config.dev.js
index 03e7246d..ead287fb 100644
--- a/client/configuration/webpack/webpack.config.dev.js
+++ b/client/configuration/webpack/webpack.config.dev.js
@@ -33,7 +33,7 @@ const devConfig = {
options: {
name: "static/assets/[name].[ext]",
// (thuang): This is needed to make sure @font url path is '/static/assets/'
- publicPath: "..",
+ publicPath: "",
},
},
],
diff --git a/client/configuration/webpack/webpack.config.prod.js b/client/configuration/webpack/webpack.config.prod.js
index ee34773b..6b1da2ea 100644
--- a/client/configuration/webpack/webpack.config.prod.js
+++ b/client/configuration/webpack/webpack.config.prod.js
@@ -47,8 +47,8 @@ const prodConfig = {
include: [nodeModules, fonts, images],
options: {
name: "static/assets/[name]-[contenthash].[ext]",
- // (thuang): This is needed to make sure @font url path is '../static/assets/'
- publicPath: "..",
+ // (thuang): This is needed to make sure @font url path is '../static/assets/' <- not for me
+ publicPath: "",
},
},
],
diff --git a/client/favicon.png b/client/favicon.png
deleted file mode 100644
index 58f43344..00000000
Binary files a/client/favicon.png and /dev/null differ
diff --git a/client/favicon.png b/client/favicon.png
new file mode 120000
index 00000000..085f119d
--- /dev/null
+++ b/client/favicon.png
@@ -0,0 +1 @@
+src/images/icon_cellwhisperer.png
\ No newline at end of file
diff --git a/client/src/actions/annotation.js b/client/src/actions/annotation.js
index ad2f0db5..4f5d6bf1 100644
--- a/client/src/actions/annotation.js
+++ b/client/src/actions/annotation.js
@@ -5,9 +5,66 @@ import difference from "lodash.difference";
import pako from "pako";
import * as globals from "../globals";
import { MatrixFBS, AnnotationsHelpers } from "../util/stateManager";
+import { isTypedArray } from "../util/typeHelpers";
const { isUserAnnotation } = AnnotationsHelpers;
+export const annotationCreateContinuousAction =
+ (newContinuousName, values) => async (dispatch, getState) => {
+ /*
+ Add a new user-created continuous to the obs annotations.
+
+ Arguments:
+ newContinuousName - string name for the continuous.
+ continuousToDuplicate - obs continuous to use for initial values, or null.
+ */
+ const { annoMatrix: prevAnnoMatrix, obsCrossfilter: prevObsCrossfilter } =
+ getState();
+ if (!prevAnnoMatrix || !prevObsCrossfilter) return;
+ const { schema } = prevAnnoMatrix;
+
+ /* name must be a string, non-zero length */
+ if (typeof newContinuousName !== "string" || newContinuousName.length === 0)
+ throw new Error("user annotations require string name");
+
+ if (!isTypedArray(values) || values.length === 0)
+ // TODO check for correct length
+ throw new Error(
+ `Provided values are of wrong format or length ${typeof values}, ${
+ values.length
+ }`
+ );
+
+ /* ensure the name isn't already in use! */
+ if (schema.annotations.obsByName[newContinuousName])
+ throw new Error("name collision on annotation continuous create");
+
+ const newSchema = {
+ name: newContinuousName,
+ type: "float32",
+ writable: false,
+ };
+
+ const obsCrossfilter = prevObsCrossfilter.addObsColumn(
+ newSchema,
+ values.constructor,
+ values
+ );
+
+ // TODO this is probably a noop (and should be removed)
+ dispatch({
+ type: "annotation: create continuous",
+ data: newContinuousName,
+ annoMatrix: obsCrossfilter.annoMatrix,
+ obsCrossfilter,
+ });
+
+ dispatch({
+ type: "color by continuous metadata",
+ colorAccessor: newContinuousName,
+ });
+ };
+
export const annotationCreateCategoryAction =
(newCategoryName, categoryToDuplicate) => async (dispatch, getState) => {
/*
diff --git a/client/src/actions/index.js b/client/src/actions/index.js
index 5c5f495d..eeebd985 100644
--- a/client/src/actions/index.js
+++ b/client/src/actions/index.js
@@ -11,6 +11,7 @@ import * as annoActions from "./annotation";
import * as viewActions from "./viewStack";
import * as embActions from "./embedding";
import * as genesetActions from "./geneset";
+import * as llmEmbeddingsActions from "./llmEmbeddings";
function setGlobalConfig(config) {
/**
@@ -236,6 +237,7 @@ function fetchJson(pathAndQuery) {
}
export default {
+ fetchJson,
doInitialDataLoad,
requestDifferentialExpression,
requestSingleGeneExpressionCountsForColoringPOST,
@@ -256,6 +258,8 @@ export default {
clipAction: viewActions.clipAction,
subsetAction: viewActions.subsetAction,
resetSubsetAction: viewActions.resetSubsetAction,
+ annotationCreateContinuousAction:
+ annoActions.annotationCreateContinuousAction,
annotationCreateCategoryAction: annoActions.annotationCreateCategoryAction,
annotationRenameCategoryAction: annoActions.annotationRenameCategoryAction,
annotationDeleteCategoryAction: annoActions.annotationDeleteCategoryAction,
@@ -272,4 +276,8 @@ export default {
genesetDelete: genesetActions.genesetDelete,
genesetAddGenes: genesetActions.genesetAddGenes,
genesetDeleteGenes: genesetActions.genesetDeleteGenes,
+ requestEmbeddingLLMWithText: llmEmbeddingsActions.requestEmbeddingLLMWithText,
+ requestEmbeddingLLMWithCells:
+ llmEmbeddingsActions.requestEmbeddingLLMWithCells,
+ startChatRequest: llmEmbeddingsActions.startChatRequest,
};
diff --git a/client/src/actions/llmEmbeddings.js b/client/src/actions/llmEmbeddings.js
new file mode 100644
index 00000000..76597a22
--- /dev/null
+++ b/client/src/actions/llmEmbeddings.js
@@ -0,0 +1,206 @@
+import * as globals from "../globals";
+import { annotationCreateContinuousAction } from "./annotation";
+import { matrixFBSToDataframe } from "../util/stateManager/matrix";
+
+/*
+ LLM embedding querying
+*/
+export const requestEmbeddingLLMWithCells =
+ /*
+ Send a request to the LLM embedding model with text
+ */
+ (cellSelection) => async (dispatch) => {
+ dispatch({
+ type: "request to embedding model started",
+ });
+ try {
+ // Legal values are null, Array or TypedArray. Null is initial state.
+ if (!cellSelection) cellSelection = [];
+
+ // These lines ensure that we convert any TypedArray to an Array.
+ // This is necessary because JSON.stringify() does some very strange
+ // things with TypedArrays (they are marshalled to JSON objects, rather
+ // than being marshalled as a JSON array).
+ cellSelection = Array.isArray(cellSelection)
+ ? cellSelection
+ : Array.from(cellSelection);
+
+ const res = await fetch(
+ `${globals.API.prefix}${globals.API.version}llmembs/obs`,
+ {
+ method: "POST",
+ headers: new Headers({
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ }),
+ body: JSON.stringify({
+ cellSelection: { filter: { obs: { index: cellSelection } } },
+ }),
+ credentials: "include",
+ }
+ );
+
+ if (!res.ok || res.headers.get("Content-Type") !== "application/json") {
+ return dispatch({
+ type: "request llm embeddings error",
+ error: new Error(
+ `Unexpected response ${res.status} ${
+ res.statusText
+ } ${res.headers.get("Content-Type")}}`
+ ),
+ });
+ }
+
+ const response = await res.json();
+ return dispatch({
+ type: "embedding model text response from cells",
+ data: response,
+ });
+ } catch (error) {
+ return dispatch({
+ type: "request llm embeddings error",
+ error,
+ });
+ }
+ };
+
+export const requestEmbeddingLLMWithText =
+ /*
+ Send a request to the LLM embedding model with text
+ */
+ (text) => async (dispatch) => {
+ dispatch({
+ type: "request to embedding model started",
+ });
+ try {
+ const res = await fetch(
+ `${globals.API.prefix}${globals.API.version}llmembs/text`,
+ {
+ method: "POST",
+ headers: new Headers({
+ Accept: "application/octet-stream",
+ "Content-Type": "application/json",
+ }),
+ body: JSON.stringify({
+ text,
+ }),
+ credentials: "include",
+ }
+ );
+
+ if (
+ !res.ok ||
+ res.headers.get("Content-Type") !== "application/octet-stream"
+ ) {
+ return dispatch({
+ type: "request llm embeddings error",
+ error: new Error(
+ `Unexpected response ${res.status} ${
+ res.statusText
+ } ${res.headers.get("Content-Type")}}`
+ ),
+ });
+ }
+
+ const buffer = await res.arrayBuffer();
+ const dataframe = matrixFBSToDataframe(buffer);
+ const col = dataframe.__columns[0];
+
+ const annotationName = dataframe.colIndex.getLabel(0);
+
+ dispatch({
+ type: "embedding model annotation response from text",
+ });
+
+ return dispatch(annotationCreateContinuousAction(annotationName, col));
+ } catch (error) {
+ return dispatch({
+ type: "request llm embeddings error",
+ error,
+ });
+ }
+ };
+
+
+/*
+ Action creator to interact with the http_bot endpoint
+*/
+export const startChatRequest = (messages, prompt, cellSelection) => async (dispatch) => {
+ let newMessages = messages.concat({from: "human", value: prompt});
+ dispatch({ type: "chat request start", newMessages });
+
+ try {
+ if (!cellSelection) cellSelection = [];
+
+ // These lines ensure that we convert any TypedArray to an Array.
+ // This is necessary because JSON.stringify() does some very strange
+ // things with TypedArrays (they are marshalled to JSON objects, rather
+ // than being marshalled as a JSON array).
+ cellSelection = Array.isArray(cellSelection)
+ ? cellSelection
+ : Array.from(cellSelection);
+
+ const pload = {
+ messages: newMessages, // TODO might need to add