pagination and context with store

This commit is contained in:
cristhianzl 2023-10-25 22:18:31 -03:00
commit 1081ace70f
21 changed files with 319 additions and 207 deletions

View file

@ -22,49 +22,53 @@ from langflow.services.store.service import StoreService
router = APIRouter(prefix="/store", tags=["Components Store"]) router = APIRouter(prefix="/store", tags=["Components Store"])
def get_user_store_api_key(user: User = Depends(auth_utils.get_current_active_user)): def get_user_store_api_key(
user: User = Depends(auth_utils.get_current_active_user),
settings_service=Depends(get_settings_service),
):
if not user.store_api_key: if not user.store_api_key:
raise HTTPException( raise HTTPException(
status_code=400, detail="You must have a store API key set." status_code=400, detail="You must have a store API key set."
) )
return user.store_api_key decrypted = auth_utils.decrypt_api_key(user.store_api_key, settings_service)
return decrypted
def get_optional_user_store_api_key( def get_optional_user_store_api_key(
user: User = Depends(auth_utils.get_current_active_user), user: User = Depends(auth_utils.get_current_active_user),
settings_service=Depends(get_settings_service),
): ):
return user.store_api_key if not user.store_api_key:
return None
decrypted = auth_utils.decrypt_api_key(user.store_api_key, settings_service)
return decrypted
@router.post("/components/", response_model=ComponentResponse, status_code=201) @router.post("/components/", response_model=ComponentResponse, status_code=201)
def create_component( def create_component(
component: StoreComponentCreate, component: StoreComponentCreate,
store_service: StoreService = Depends(get_store_service), store_service: StoreService = Depends(get_store_service),
settings_service=Depends(get_settings_service),
store_api_Key: str = Depends(get_user_store_api_key), store_api_Key: str = Depends(get_user_store_api_key),
): ):
try: try:
decrypted = auth_utils.decrypt_api_key(store_api_Key, settings_service) return store_service.upload(store_api_Key, component)
return store_service.upload(decrypted, component)
except Exception as exc: except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc)) raise HTTPException(status_code=400, detail=str(exc))
@router.get("/components/", response_model=List[ListComponentResponse]) @router.get("/components/", response_model=List[ListComponentResponse])
def list_components( def list_components(
filter_by_user: bool = Query(False),
page: int = 1, page: int = 1,
limit: int = 10, limit: int = 10,
store_service: StoreService = Depends(get_store_service), store_service: StoreService = Depends(get_store_service),
store_api_Key: str = Depends(get_optional_user_store_api_key), store_api_Key: str = Depends(get_optional_user_store_api_key),
settings_service=Depends(get_settings_service),
): ):
try: try:
fields = ["id", "name", "description", "user_created.name", "is_component"] fields = ["id", "name", "description", "user_created.name", "is_component"]
if store_api_Key: result = store_service.list_components(
decrypted = auth_utils.decrypt_api_key(store_api_Key, settings_service) store_api_Key, page, limit, fields=fields, filter_by_user=filter_by_user
else: )
decrypted = None
result = store_service.list_components(decrypted, page, limit, fields=fields)
return result return result
except Exception as exc: except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc)) raise HTTPException(status_code=400, detail=str(exc))
@ -73,16 +77,13 @@ def list_components(
@router.get("/components/{component_id}", response_model=DownloadComponentResponse) @router.get("/components/{component_id}", response_model=DownloadComponentResponse)
def read_component( def read_component(
component_id: UUID, component_id: UUID,
filter_by_user: bool = Query(False),
store_service: StoreService = Depends(get_store_service), store_service: StoreService = Depends(get_store_service),
store_api_Key: str = Depends(get_user_store_api_key), store_api_Key: str = Depends(get_user_store_api_key),
settings_service=Depends(get_settings_service),
): ):
# If the component is from the store, we need to get it from the store # If the component is from the store, we need to get it from the store
try: try:
decrypted = auth_utils.decrypt_api_key(store_api_Key, settings_service) component = store_service.download(store_api_Key, component_id)
component = store_service.download(decrypted, component_id, filter_by_user)
except Exception as exc: except Exception as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc raise HTTPException(status_code=400, detail=str(exc)) from exc
@ -102,6 +103,7 @@ async def search_endpoint(
date_from: Optional[datetime] = Query(None), date_from: Optional[datetime] = Query(None),
date_to: Optional[datetime] = Query(None), date_to: Optional[datetime] = Query(None),
sort: Optional[List[str]] = Query(None), sort: Optional[List[str]] = Query(None),
filter_by_user: bool = Query(False),
fields: Optional[List[str]] = Query(None), fields: Optional[List[str]] = Query(None),
store_service: "StoreService" = Depends(get_store_service), store_service: "StoreService" = Depends(get_store_service),
store_api_Key: str = Depends(get_optional_user_store_api_key), store_api_Key: str = Depends(get_optional_user_store_api_key),
@ -118,6 +120,7 @@ async def search_endpoint(
date_to=date_to, date_to=date_to,
sort=sort, sort=sort,
fields=fields, fields=fields,
filter_by_user=filter_by_user,
) )
except Exception as exc: except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) raise HTTPException(status_code=500, detail=str(exc))

View file

@ -72,6 +72,7 @@ class StoreService(Service):
date_to: Optional[datetime] = None, date_to: Optional[datetime] = None,
sort: Optional[List[str]] = ["-likes"], sort: Optional[List[str]] = ["-likes"],
fields: Optional[List[str]] = None, fields: Optional[List[str]] = None,
filter_by_user: bool = False,
) -> List[ComponentResponse]: ) -> List[ComponentResponse]:
# ?sort=sort,-date_created,author.name # ?sort=sort,-date_created,author.name
@ -104,34 +105,6 @@ class StoreService(Service):
if fields: if fields:
params["fields"] = ",".join(fields) params["fields"] = ",".join(fields)
results = self._get(self.components_url, api_key, params)
return [ComponentResponse(**component) for component in results]
def list_components(
self,
api_key: str,
page: int = 1,
limit: int = 10,
fields: Optional[List[str]] = None,
) -> List[ListComponentResponse]:
params = {"page": page, "limit": limit}
# ?aggregate[count]=likes
params["fields"] = (
",".join(fields)
if fields
else ",".join(["id", "name", "description", "count(likes)", "is_component"])
)
results = self._get(self.components_url, api_key, params)
return [ListComponentResponse(**component) for component in results]
def download(
self, api_key: str, component_id: str, filter_by_user: bool
) -> DownloadComponentResponse:
url = f"{self.components_url}/{component_id}"
params = {
"fields": ",".join(["id", "name", "description", "data", "is_component"])
}
if filter_by_user: if filter_by_user:
params["deep"] = json.dumps( params["deep"] = json.dumps(
{ {
@ -140,6 +113,52 @@ class StoreService(Service):
} }
} }
) )
else:
params["filter"] = json.dumps({"status": {"_eq": "public"}})
results = self._get(self.components_url, api_key, params)
return [ComponentResponse(**component) for component in results]
def list_components(
self,
api_key: str,
page: int = 1,
limit: int = 15,
fields: Optional[List[str]] = None,
filter_by_user: bool = False,
) -> List[ListComponentResponse]:
params = {"page": page, "limit": limit}
# ?aggregate[count]=likes
params["fields"] = (
",".join(fields)
if fields
else ",".join(["id", "name", "description", "count(likes)", "is_component"])
)
# Only public components or the ones created by the user
# check for "public" or "Public"
if filter_by_user:
params["deep"] = json.dumps(
{
"components": {
"_filter": {"user_created": {"token": {"_eq": api_key}}}
}
}
)
else:
params["filter"] = params["filter"] = json.dumps(
{"status": {"_in": ["public", "Public"]}}
)
results = self._get(self.components_url, api_key, params)
return [ListComponentResponse(**component) for component in results]
def download(self, api_key: str, component_id: str) -> DownloadComponentResponse:
url = f"{self.components_url}/{component_id}"
params = {
"fields": ",".join(["id", "name", "description", "data", "is_component"])
}
component = self._get(url, api_key, params) component = self._get(url, api_key, params)
self.call_webhook(api_key, self.webhook_url, component_id) self.call_webhook(api_key, self.webhook_url, component_id)

View file

@ -3796,9 +3796,9 @@
"integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==" "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g=="
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.2.31", "version": "18.2.32",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.31.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.32.tgz",
"integrity": "sha512-c2UnPv548q+5DFh03y8lEDeMfDwBn9G3dRwfkrxQMo/dOtRHUUO57k6pHvBIfH/VF4Nh+98mZ5aaSe+2echD5g==", "integrity": "sha512-F0FVIZQ1x5Gxy/VYJb7XcWvCcHR28Sjwt1dXLspdIatfPq1MVACfnBDwKe6ANLxQ64riIJooXClpUR6oxTiepg==",
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"@types/scheduler": "*", "@types/scheduler": "*",
@ -4517,9 +4517,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001553", "version": "1.0.30001554",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001553.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001554.tgz",
"integrity": "sha512-N0ttd6TrFfuqKNi+pMgWJTb9qrdJu4JSpgPFLe/lrD19ugC6fZgF0pUewRowDwzdDnb9V41mFcdlYgl/PyKf4A==", "integrity": "sha512-A2E3U//MBwbJVzebddm1YfNp7Nud5Ip+IPn4BozBmn4KqVX7AvluoIDFWjsv5OkGnKUXQVmMSoMKLa3ScCblcQ==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@ -10220,9 +10220,9 @@
} }
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "3.3.4", "version": "3.3.5",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.4.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz",
"integrity": "sha512-JXZNOkggUAc9T5E7nCrimoXHcSf9h3NWFe5sh36CGD/3M5TRLuQeFnQoDsit2uVTqgoOZHLx5rTykLUu16vsMQ==", "integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==",
"dependencies": { "dependencies": {
"@alloc/quick-lru": "^5.2.0", "@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2", "arg": "^5.0.2",
@ -13755,9 +13755,9 @@
"integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==" "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g=="
}, },
"@types/react": { "@types/react": {
"version": "18.2.31", "version": "18.2.32",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.31.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.32.tgz",
"integrity": "sha512-c2UnPv548q+5DFh03y8lEDeMfDwBn9G3dRwfkrxQMo/dOtRHUUO57k6pHvBIfH/VF4Nh+98mZ5aaSe+2echD5g==", "integrity": "sha512-F0FVIZQ1x5Gxy/VYJb7XcWvCcHR28Sjwt1dXLspdIatfPq1MVACfnBDwKe6ANLxQ64riIJooXClpUR6oxTiepg==",
"requires": { "requires": {
"@types/prop-types": "*", "@types/prop-types": "*",
"@types/scheduler": "*", "@types/scheduler": "*",
@ -14248,9 +14248,9 @@
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001553", "version": "1.0.30001554",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001553.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001554.tgz",
"integrity": "sha512-N0ttd6TrFfuqKNi+pMgWJTb9qrdJu4JSpgPFLe/lrD19ugC6fZgF0pUewRowDwzdDnb9V41mFcdlYgl/PyKf4A==" "integrity": "sha512-A2E3U//MBwbJVzebddm1YfNp7Nud5Ip+IPn4BozBmn4KqVX7AvluoIDFWjsv5OkGnKUXQVmMSoMKLa3ScCblcQ=="
}, },
"ccount": { "ccount": {
"version": "2.0.1", "version": "2.0.1",
@ -18070,9 +18070,9 @@
"integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==" "integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ=="
}, },
"tailwindcss": { "tailwindcss": {
"version": "3.3.4", "version": "3.3.5",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.4.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz",
"integrity": "sha512-JXZNOkggUAc9T5E7nCrimoXHcSf9h3NWFe5sh36CGD/3M5TRLuQeFnQoDsit2uVTqgoOZHLx5rTykLUu16vsMQ==", "integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==",
"requires": { "requires": {
"@alloc/quick-lru": "^5.2.0", "@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2", "arg": "^5.0.2",

View file

@ -16,6 +16,7 @@ export default function PaginatorComponent({
rowsCount = [10, 20, 50, 100], rowsCount = [10, 20, 50, 100],
totalRowsCount = 0, totalRowsCount = 0,
paginate, paginate,
storeComponent = false,
}: PaginatorComponentType) { }: PaginatorComponentType) {
const [size, setPageSize] = useState(pageSize); const [size, setPageSize] = useState(pageSize);
const [maxIndex, setMaxPageIndex] = useState( const [maxIndex, setMaxPageIndex] = useState(
@ -30,7 +31,13 @@ export default function PaginatorComponent({
<> <>
<div className="flex items-center justify-between px-2"> <div className="flex items-center justify-between px-2">
<div className="flex-1 text-sm text-muted-foreground"></div> <div className="flex-1 text-sm text-muted-foreground"></div>
<div className="flex items-center space-x-6 lg:space-x-8"> <div
className={
storeComponent
? "flex items-center lg:space-x-8 "
: "flex items-center space-x-6 lg:space-x-8 "
}
>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<p className="text-sm font-medium">Rows per page</p> <p className="text-sm font-medium">Rows per page</p>
<Select <Select
@ -54,7 +61,8 @@ export default function PaginatorComponent({
</Select> </Select>
</div> </div>
<div className="flex w-[100px] items-center justify-center text-sm font-medium"> <div className="flex w-[100px] items-center justify-center text-sm font-medium">
Page {pageIndex} of {maxIndex} Page {pageIndex}
{!storeComponent && <> of {maxIndex}</>}
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Button <Button

View file

@ -34,7 +34,7 @@ export default function Header(): JSX.Element {
return ( return (
<div className="header-arrangement"> <div className="header-arrangement">
<div className="header-start-display"> <div className="header-start-display lg:w-[30%]">
<Link to="/"> <Link to="/">
<span className="ml-4 text-2xl">⛓️</span> <span className="ml-4 text-2xl">⛓️</span>
</Link> </Link>
@ -47,14 +47,19 @@ export default function Header(): JSX.Element {
<Link to="/"> <Link to="/">
<Button <Button
className="gap-2" className="gap-2"
variant={location.pathname === "/" ? "primary" : "secondary"} variant={
location.pathname === "/flows" ||
location.pathname === "/components"
? "primary"
: "secondary"
}
size="sm" size="sm"
> >
<IconComponent name="Home" className="h-4 w-4" /> <IconComponent name="Home" className="h-4 w-4" />
<div className="flex-1">{USER_PROJECTS_HEADER}</div> <div className="hidden flex-1 md:block">{USER_PROJECTS_HEADER}</div>
</Button> </Button>
</Link> </Link>
<Link to="/community"> {/* <Link to="/community">
<Button <Button
className="gap-2" className="gap-2"
variant={ variant={
@ -65,7 +70,7 @@ export default function Header(): JSX.Element {
<IconComponent name="Users2" className="h-4 w-4" /> <IconComponent name="Users2" className="h-4 w-4" />
<div className="flex-1">Community Examples</div> <div className="flex-1">Community Examples</div>
</Button> </Button>
</Link> </Link> */}
{hasStore && ( {hasStore && (
<Link to="/store"> <Link to="/store">
<Button <Button
@ -79,16 +84,16 @@ export default function Header(): JSX.Element {
</Link> </Link>
)} )}
</div> </div>
<div className="header-end-division"> <div className="header-end-division lg:w-[30%]">
<div className="header-end-display"> <div className="header-end-display">
<a <a
href="https://github.com/logspace-ai/langflow" href="https://github.com/logspace-ai/langflow"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="header-github-link" className="header-github-link gap-2"
> >
<FaGithub className="mr-2 h-5 w-5" /> <FaGithub className="h-5 w-5" />
Star <div className="hidden lg:block">Star</div>
<div className="header-github-display">{stars}</div> <div className="header-github-display">{stars}</div>
</a> </a>
<a <a

View file

@ -1,11 +1,10 @@
import { createContext, useState } from "react"; import { createContext, useState } from "react";
import { checkHasStore } from "../controllers/API"; import { checkHasStore } from "../controllers/API";
import { storeContextType } from "../types/contexts/store"; import { storeContextType } from "../types/contexts/store";
import { FlowType } from "../types/flow";
//store context to share user components and update them //store context to share user components and update them
const initialValue = { const initialValue = {
savedFlows: {}, savedFlows: new Set<string>(),
setSavedFlows: () => {}, setSavedFlows: () => {},
hasStore: false, hasStore: false,
setHasStore: () => {}, setHasStore: () => {},
@ -14,7 +13,8 @@ const initialValue = {
export const StoreContext = createContext<storeContextType>(initialValue); export const StoreContext = createContext<storeContextType>(initialValue);
export function StoreProvider({ children }) { export function StoreProvider({ children }) {
const [savedFlows, setSavedFlows] = useState<{ [key: string]: FlowType }>({}); const [savedFlows, setSavedFlows] = useState<Set<string>>(new Set());
const [hasStore, setHasStore] = useState(false); const [hasStore, setHasStore] = useState(false);
checkHasStore().then((res) => { checkHasStore().then((res) => {

View file

@ -1,5 +1,5 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import _ from "lodash"; import _, { cloneDeep } from "lodash";
import { import {
ReactNode, ReactNode,
createContext, createContext,
@ -40,7 +40,7 @@ import {
updateTemplate, updateTemplate,
} from "../utils/reactflowUtils"; } from "../utils/reactflowUtils";
import { import {
IncrementObjectKey, createRandomKey,
getRandomDescription, getRandomDescription,
getRandomName, getRandomName,
getSetFromObject, getSetFromObject,
@ -80,7 +80,7 @@ const TabsContextInitialValue: TabsContextType = {
selection: { nodes: any; edges: any }, selection: { nodes: any; edges: any },
position: { x: number; y: number; paneX?: number; paneY?: number } position: { x: number; y: number; paneX?: number; paneY?: number }
) => {}, ) => {},
saveComponent: (component: NodeDataType) => {}, saveComponent: async (component: NodeDataType) => "",
deleteComponent: (id: string, key: string) => {}, deleteComponent: (id: string, key: string) => {},
}; };
@ -154,8 +154,14 @@ export function TabsProvider({ children }: { children: ReactNode }) {
return; return;
} }
if (flow.data && flow.is_component) { if (flow.data && flow.is_component) {
storeComponents[(flow.data.nodes[0].data as NodeDataType).type] = (flow.data.nodes[0].data as NodeDataType).node!.display_name =
_.cloneDeep((flow.data.nodes[0].data as NodeDataType).node!); flow.name;
storeComponents[
createRandomKey(
(flow.data.nodes[0].data as NodeDataType).type,
uid()
)
] = _.cloneDeep((flow.data.nodes[0].data as NodeDataType).node!);
return; return;
} }
if (!skipUpdate) processDataFromFlow(flow, false); if (!skipUpdate) processDataFromFlow(flow, false);
@ -165,9 +171,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}); });
setData((prev) => { setData((prev) => {
let newData = _.cloneDeep(prev); let newData = _.cloneDeep(prev);
Object.keys(storeComponents).forEach((key) => {
newData["custom_components"][key] = storeComponents[key]; const customComponent = newData["custom_components"]["CustomComponent"];
}); newData["custom_components"] = cloneDeep(storeComponents);
newData["custom_components"]["CustomComponent"] = customComponent;
return newData; return newData;
}); });
} }
@ -458,6 +465,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
id: source, id: source,
}); });
sourceHandleObject.id = source; sourceHandleObject.id = source;
edge.data.sourceHandle = sourceHandleObject; edge.data.sourceHandle = sourceHandleObject;
const targetHandleObject: targetHandleType = scapeJSONParse( const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle! edge.targetHandle!
@ -482,6 +490,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
sourceHandle, sourceHandle,
targetHandle, targetHandle,
id, id,
data: cloneDeep(edge.data),
style: { stroke: "#555" }, style: { stroke: "#555" },
className: className:
targetHandleObject.type === "Text" targetHandleObject.type === "Text"
@ -538,7 +547,8 @@ export function TabsProvider({ children }: { children: ReactNode }) {
let data = flow?.data ? flow.data : null; let data = flow?.data ? flow.data : null;
if (data) { if (data) {
processFlowEdges(flow); processFlowEdges(flow);
processFlowNodes(flow); //prevent node update for now
// processFlowNodes(flow);
//add animation to text type edges //add animation to text type edges
updateEdges(data.edges); updateEdges(data.edges);
// updateNodes(data.nodes, data.edges); // updateNodes(data.nodes, data.edges);
@ -605,7 +615,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
const addFlowToLocalState = (newFlow: FlowType) => { const addFlowToLocalState = (newFlow: FlowType) => {
let newFlows: FlowType[] = []; let newFlows: FlowType[] = [];
setFlows((prevState) => { setFlows((prevState) => {
newFlows.concat(prevState); newFlows = newFlows.concat(prevState);
newFlows.push(newFlow); newFlows.push(newFlow);
return [...prevState, newFlow]; return [...prevState, newFlow];
}); });
@ -671,12 +681,8 @@ export function TabsProvider({ children }: { children: ReactNode }) {
component.node!.official = false; component.node!.official = false;
let key = component.type; let key = component.type;
if (data["custom_components"][key] !== undefined) { if (data["custom_components"][key] !== undefined) {
let { newKey, increment } = IncrementObjectKey( let increment: number;
data["custom_components"], component.type = createRandomKey(key, uid());
key
);
key = newKey;
component.type = newKey;
let componentNodes: { [key: string]: APIClassType } = {}; let componentNodes: { [key: string]: APIClassType } = {};
Object.keys(data["custom_components"]).forEach((key) => { Object.keys(data["custom_components"]).forEach((key) => {
componentNodes[key] = data["custom_components"][key]; componentNodes[key] = data["custom_components"][key];
@ -697,7 +703,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
` (${increment})`; ` (${increment})`;
} }
} }
addFlow(true, createFlowComponent(component)); return addFlow(true, createFlowComponent(component));
} }
function deleteComponent(id: string, key: string) { function deleteComponent(id: string, key: string) {

View file

@ -597,6 +597,20 @@ export async function getStoreComponents(page: number = 1, limit: number = 10) {
} }
} }
export async function getStoreSavedComponents() {
try {
const res = await api.get(
`${BASE_URL_API}store/components/?filter_by_user=true`
);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function postStoreComponents(component: Component) { export async function postStoreComponents(component: Component) {
try { try {
const res = await api.post(`${BASE_URL_API}store/components/`, component); const res = await api.post(`${BASE_URL_API}store/components/`, component);
@ -676,3 +690,15 @@ export async function checkHasStore() {
throw error; throw error;
} }
} }
export async function getNumberOfComponents() {
try {
const res = await api.get(`${BASE_URL_API}store/components/count`);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}

View file

@ -7,6 +7,7 @@ import {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../components/ShadTooltipComponent"; import ShadTooltip from "../../components/ShadTooltipComponent";
import CodeAreaComponent from "../../components/codeAreaComponent"; import CodeAreaComponent from "../../components/codeAreaComponent";
import DictComponent from "../../components/dictComponent"; import DictComponent from "../../components/dictComponent";
@ -64,6 +65,7 @@ const EditNodeModal = forwardRef(
ref ref
) => { ) => {
const [modalOpen, setModalOpen] = useState(open ?? false); const [modalOpen, setModalOpen] = useState(open ?? false);
const updateNodeInternals = useUpdateNodeInternals();
const myData = useRef(data); const myData = useRef(data);
@ -83,11 +85,14 @@ const EditNodeModal = forwardRef(
const handleOnNewValue = (newValue: any, name) => { const handleOnNewValue = (newValue: any, name) => {
myData.current.node!.template[name].value = newValue; myData.current.node!.template[name].value = newValue;
setDataValue(newValue); setDataValue(newValue);
updateNodeInternals(data.id);
}; };
useEffect(() => { useEffect(() => {
if (modalOpen) {
myData.current = data; // reset data to what it is on node when opening modal myData.current = data; // reset data to what it is on node when opening modal
onClose!(modalOpen); onClose!(modalOpen);
}
}, [modalOpen]); }, [modalOpen]);
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false); const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);

View file

@ -310,7 +310,6 @@ export default function Page({
event.dataTransfer.getData("nodedata") event.dataTransfer.getData("nodedata")
); );
// If data type is not "chatInput" or if there are no "chatInputNode" nodes present in the ReactFlow instance, create a new node
// Calculate the position where the node should be created // Calculate the position where the node should be created
const position = reactFlowInstance!.project({ const position = reactFlowInstance!.project({
x: event.clientX - reactflowBounds!.left, x: event.clientX - reactflowBounds!.left,

View file

@ -309,7 +309,12 @@ export default function ExtraSidebar(): JSX.Element {
> >
<div className="side-bar-components-gap"> <div className="side-bar-components-gap">
{Object.keys(dataFilter[SBSectionName]) {Object.keys(dataFilter[SBSectionName])
.sort(sensitiveSort) .sort((a, b) =>
sensitiveSort(
dataFilter[SBSectionName][a].display_name,
dataFilter[SBSectionName][b].display_name
)
)
.map((SBItemName: string, index) => ( .map((SBItemName: string, index) => (
<ShadTooltip <ShadTooltip
content={ content={

View file

@ -70,9 +70,9 @@ export default function NodeToolbarComponent({
function handleShareComponent() { function handleShareComponent() {
const componentFlow = cloneDeep(data); const componentFlow = cloneDeep(data);
saveComponent(componentFlow).then(() => {
saveFlowStore(createFlowComponent(componentFlow)).then( saveFlowStore(createFlowComponent(componentFlow)).then(
() => { (_) => {
saveComponent(componentFlow);
setSuccessData({ setSuccessData({
title: "Component shared successfully", title: "Component shared successfully",
}); });
@ -84,6 +84,7 @@ export default function NodeToolbarComponent({
}); });
} }
); );
});
} }
const handleSelectChange = (event) => { const handleSelectChange = (event) => {
switch (event) { switch (event) {
@ -98,7 +99,6 @@ export default function NodeToolbarComponent({
downloadNode(createFlowComponent(cloneDeep(data))); downloadNode(createFlowComponent(cloneDeep(data)));
break; break;
case "Share": case "Share":
console.log("Share");
setShowconfirmShare(true); setShowconfirmShare(true);
break; break;
case "SaveAll": case "SaveAll":

View file

@ -96,7 +96,6 @@ export default function ComponentsComponent() {
<SkeletonCardComponent /> <SkeletonCardComponent />
<SkeletonCardComponent /> <SkeletonCardComponent />
<SkeletonCardComponent /> <SkeletonCardComponent />
<SkeletonCardComponent />
</> </>
) : ( ) : (
flows flows

View file

@ -94,7 +94,6 @@ export default function FlowsComponent() {
<SkeletonCardComponent /> <SkeletonCardComponent />
<SkeletonCardComponent /> <SkeletonCardComponent />
<SkeletonCardComponent /> <SkeletonCardComponent />
<SkeletonCardComponent />
</> </>
) : ( ) : (
flows flows

View file

@ -1,5 +1,5 @@
import { Link, ToyBrick } from "lucide-react"; import { Link, ToyBrick } from "lucide-react";
import { useContext, useRef, useState } from "react"; import { useContext, useEffect, useRef, useState } from "react";
import IconComponent from "../../../components/genericIconComponent"; import IconComponent from "../../../components/genericIconComponent";
import { Badge } from "../../../components/ui/badge"; import { Badge } from "../../../components/ui/badge";
import { Button } from "../../../components/ui/button"; import { Button } from "../../../components/ui/button";
@ -10,6 +10,8 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "../../../components/ui/card"; } from "../../../components/ui/card";
import { alertContext } from "../../../contexts/alertContext";
import { StoreContext } from "../../../contexts/storeContext";
import { TabsContext } from "../../../contexts/tabsContext"; import { TabsContext } from "../../../contexts/tabsContext";
import { getComponent, saveFlowStore } from "../../../controllers/API"; import { getComponent, saveFlowStore } from "../../../controllers/API";
import { FlowType } from "../../../types/flow"; import { FlowType } from "../../../types/flow";
@ -17,12 +19,19 @@ import { FlowComponent } from "../../../types/store";
import cloneFLowWithParent from "../../../utils/storeUtils"; import cloneFLowWithParent from "../../../utils/storeUtils";
export const MarketCardComponent = ({ data }: { data: FlowComponent }) => { export const MarketCardComponent = ({ data }: { data: FlowComponent }) => {
const [added, setAdded] = useState(false); const { savedFlows } = useContext(StoreContext);
const [added, setAdded] = useState(savedFlows.has(data.id) ? true : false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { addFlow } = useContext(TabsContext); const { addFlow } = useContext(TabsContext);
const { setSuccessData } = useContext(alertContext);
const flowData = useRef<FlowType>(); const flowData = useRef<FlowType>();
useEffect(() => {
setAdded(savedFlows.has(data.id) ? true : false);
}, [savedFlows]);
function handleAdd() { function handleAdd() {
setLoading(true);
getComponent(data.id).then( getComponent(data.id).then(
(res) => { (res) => {
console.log(res); console.log(res);
@ -45,7 +54,19 @@ export const MarketCardComponent = ({ data }: { data: FlowComponent }) => {
} }
function handleInstall() { function handleInstall() {
addFlow(true, flowData.current!); if (flowData.current) {
addFlow(true, flowData.current!).then(() => {
setSuccessData({ title: "Flow Installed" });
});
} else {
getComponent(data.id).then((res) => {
console.log(res);
const newFLow = cloneFLowWithParent(res, res.id, data.is_component);
flowData.current = newFLow;
addFlow(true, newFLow);
setSuccessData({ title: "Flow Installed" });
});
}
} }
function handleFork(flowId: string, is_component: boolean) { function handleFork(flowId: string, is_component: boolean) {
@ -145,7 +166,6 @@ export const MarketCardComponent = ({ data }: { data: FlowComponent }) => {
size="sm" size="sm"
className="whitespace-nowrap " className="whitespace-nowrap "
onClick={() => { onClick={() => {
setLoading(true);
if (!added) { if (!added) {
handleAdd(); handleAdd();
} else { } else {

View file

@ -4,6 +4,7 @@ import { useContext, useEffect, useState } from "react";
import PaginatorComponent from "../../components/PaginatorComponent"; import PaginatorComponent from "../../components/PaginatorComponent";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
import Header from "../../components/headerComponent"; import Header from "../../components/headerComponent";
import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
import { Badge } from "../../components/ui/badge"; import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input"; import { Input } from "../../components/ui/input";
@ -16,53 +17,61 @@ import {
} from "../../components/ui/select"; } from "../../components/ui/select";
import { Switch } from "../../components/ui/switch"; import { Switch } from "../../components/ui/switch";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext"; import { StoreContext } from "../../contexts/storeContext";
import { TabsContext } from "../../contexts/tabsContext"; import { TabsContext } from "../../contexts/tabsContext";
import { getStoreComponents, searchComponent } from "../../controllers/API"; import {
getStoreComponents,
getStoreSavedComponents,
searchComponent,
} from "../../controllers/API";
import StoreApiKeyModal from "../../modals/StoreApiKeyModal"; import StoreApiKeyModal from "../../modals/StoreApiKeyModal";
import { FlowComponent } from "../../types/store"; import { FlowComponent } from "../../types/store";
import { cn } from "../../utils/utils"; import { cn } from "../../utils/utils";
import { MarketCardComponent } from "./components/market-card"; import { MarketCardComponent } from "./components/market-card";
export default function StorePage(): JSX.Element { export default function StorePage(): JSX.Element {
const { setTabId } = useContext(TabsContext); const { setTabId } = useContext(TabsContext);
const { setApiKey, apiKey } = useContext(AuthContext);
// set null id // set null id
useEffect(() => { useEffect(() => {
setTabId(""); setTabId("");
}, []); }, []);
const [data, setData] = useState<FlowComponent[]>([]); const [data, setData] = useState<FlowComponent[]>([]);
const [dataSelect, setDataSelect] = useState<FlowComponent[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [search, setSearch] = useState(false);
const [filteredCategories, setFilteredCategories] = useState(new Set()); const [filteredCategories, setFilteredCategories] = useState(new Set());
const [inputText, setInputText] = useState<string>(""); const [inputText, setInputText] = useState<string>("");
const [searchData, setSearchData] = useState(data); const [searchData, setSearchData] = useState(data);
const [errorApiKey, setErrorApiKey] = useState(false);
const { setErrorData } = useContext(alertContext); const { setErrorData } = useContext(alertContext);
const { addFlow } = useContext(TabsContext);
const [totalRowsCount, setTotalRowsCount] = useState(0); const [totalRowsCount, setTotalRowsCount] = useState(0);
const [size, setPageSize] = useState(10); const [size, setPageSize] = useState(10);
const [index, setPageIndex] = useState(1); const [index, setPageIndex] = useState(1);
const { setSavedFlows } = useContext(StoreContext);
async function getSavedComponents() {
setLoading(true);
const result = await getStoreSavedComponents();
let savedIds = new Set<string>();
result.forEach((flow) => {
savedIds.add(flow.id);
});
setSavedFlows(savedIds);
}
useEffect(() => { useEffect(() => {
handleGetComponents(); getSavedComponents().then((_) => handleGetComponents());
}, []); }, []);
const handleGetComponents = () => { const handleGetComponents = () => {
setLoading(true); setLoading(true);
getStoreComponents(index - 1, 10000) getStoreComponents(index - 1, 10000)
.then((res) => { .then((res) => {
setSearchData(res.slice(0, 10)); setSearchData(res.slice(0, size));
setLoading(false);
setErrorApiKey(false);
setData(res);
setTotalRowsCount(res.length); setTotalRowsCount(res.length);
setData(res);
setLoading(false);
}) })
.catch((err) => { .catch((err) => {
setSearchData([]); setSearchData([]);
setLoading(false); setLoading(false);
setErrorApiKey(true);
setErrorData({ setErrorData({
title: "Error to get components.", title: "Error to get components.",
list: [err["response"]["data"]["detail"]], list: [err["response"]["data"]["detail"]],
@ -71,6 +80,12 @@ export default function StorePage(): JSX.Element {
}; };
const handleSearch = (inputText: string) => { const handleSearch = (inputText: string) => {
if (inputText === "") {
handleGetComponents();
setSearch(false);
return;
}
setSearch(true);
setLoading(true); setLoading(true);
searchComponent(inputText).then( searchComponent(inputText).then(
(res) => { (res) => {
@ -87,17 +102,15 @@ export default function StorePage(): JSX.Element {
setLoading(true); setLoading(true);
getStoreComponents(pageIndex, pageSize) getStoreComponents(pageIndex, pageSize)
.then((res) => { .then((res) => {
setSearchData(res.slice(0, size));
setData(res);
setPageIndex(pageIndex); setPageIndex(pageIndex);
setPageSize(pageSize); setPageSize(pageSize);
setSearchData(res);
setLoading(false); setLoading(false);
setErrorApiKey(false);
setData(res);
}) })
.catch((err) => { .catch((err) => {
setSearchData([]); setSearchData([]);
setLoading(false); setLoading(false);
setErrorApiKey(true);
setErrorData({ setErrorData({
title: "Error to get components.", title: "Error to get components.",
list: [err["response"]["data"]["detail"]], list: [err["response"]["data"]["detail"]],
@ -105,14 +118,9 @@ export default function StorePage(): JSX.Element {
}); });
} }
function resetFilter() {
setPageIndex(1);
setPageSize(10);
handleGetComponents();
}
const loadingWithApiKey = loading; const loadingWithApiKey = loading;
const renderComponents = !loading; const renderComponents = !loading;
const renderPagination = searchData.length > 0 && !loading && !search;
return ( return (
<> <>
@ -189,8 +197,9 @@ export default function StorePage(): JSX.Element {
</Select> </Select>
</div> </div>
</div> </div>
<div className="flex items-center justify-center gap-4"> <div className="flex h-2 items-center justify-center gap-4">
{Array.from(new Set(searchData.map((i) => i.is_component))).map( {renderComponents &&
Array.from(new Set(searchData.map((i) => i.is_component))).map(
(i, idx) => ( (i, idx) => (
<Badge <Badge
onClick={() => { onClick={() => {
@ -221,11 +230,15 @@ export default function StorePage(): JSX.Element {
) )
)} )}
</div> </div>
{renderComponents && (
<>
<div className="mt-6 grid w-full gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="mt-6 grid w-full gap-4 md:grid-cols-2 lg:grid-cols-3">
{searchData {loadingWithApiKey ? (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
) : (
searchData
.filter( .filter(
(f) => (f) =>
Array.from(filteredCategories).length === 0 || Array.from(filteredCategories).length === 0 ||
@ -233,12 +246,12 @@ export default function StorePage(): JSX.Element {
) )
.map((item, idx) => ( .map((item, idx) => (
<MarketCardComponent key={idx} data={item} /> <MarketCardComponent key={idx} data={item} />
))} ))
</div>
</>
)} )}
</div> </div>
{totalRowsCount > 0 && !loading && ( </div>
{renderPagination && (
<div className="relative bottom-2"> <div className="relative bottom-2">
<PaginatorComponent <PaginatorComponent
storeComponent={true} storeComponent={true}
@ -251,9 +264,6 @@ export default function StorePage(): JSX.Element {
></PaginatorComponent> ></PaginatorComponent>
</div> </div>
)} )}
{loadingWithApiKey && (
<div className="flex w-full flex-col gap-4 p-4">Loading...</div>
)}
</div> </div>
</> </>
); );

View file

@ -123,7 +123,7 @@
@apply flex gap-2; @apply flex gap-2;
} }
.primary-input { .primary-input {
@apply form-input block w-full truncate rounded-md border-border bg-background px-3 text-left shadow-sm placeholder:text-muted-foreground focus:border-ring focus:placeholder-transparent focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 sm:text-sm; @apply form-input block w-full truncate rounded-md border-border bg-background px-3 text-left shadow-sm placeholder:text-muted-foreground focus:border-ring focus:placeholder-transparent focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 text-sm;
} }
.skeleton-card { .skeleton-card {
@ -237,7 +237,7 @@
} }
.main-page-flows-display { .main-page-flows-display {
@apply grid w-full gap-4 md:grid-cols-2 lg:grid-cols-4; @apply grid w-full gap-4 md:grid-cols-2 lg:grid-cols-3;
} }
.community-page-arrangement { .community-page-arrangement {
@ -495,7 +495,7 @@
@apply flex items-center gap-0.5 rounded-md px-1.5 py-1 text-sm font-medium; @apply flex items-center gap-0.5 rounded-md px-1.5 py-1 text-sm font-medium;
} }
.header-menu-bar-display { .header-menu-bar-display {
@apply flex max-w-[200px] cursor-pointer items-center gap-2; @apply flex max-w-[120px] lg:max-w-[200px] cursor-pointer items-center gap-2;
} }
.header-menu-flow-name { .header-menu-flow-name {
@apply flex-1 truncate; @apply flex-1 truncate;
@ -505,13 +505,13 @@
} }
.header-arrangement { .header-arrangement {
@apply flex-max-width h-12 items-center justify-between border-border bg-background; @apply flex-max-width h-12 items-center justify-between border-b border-border bg-muted;
} }
.header-start-display { .header-start-display {
@apply flex w-[30%] items-center justify-start gap-2; @apply flex items-center justify-start gap-2;
} }
.header-end-division { .header-end-division {
@apply flex w-[30%] justify-end px-2; @apply flex justify-end px-2;
} }
.header-end-display { .header-end-display {
@apply ml-auto mr-2 flex items-center gap-5; @apply ml-auto mr-2 flex items-center gap-5;
@ -535,7 +535,7 @@
@apply hover:bg-accent hover:text-accent-foreground; @apply hover:bg-accent hover:text-accent-foreground;
} }
.header-github-display { .header-github-display {
@apply -mr-px ml-2 flex h-9 items-center justify-center rounded-md rounded-l-none border bg-background px-2 text-sm; @apply -mr-px ml-1 flex h-9 items-center justify-center rounded-md rounded-l-none border bg-background px-2 text-sm;
} }
.header-notifications-box { .header-notifications-box {
@apply fixed left-0 top-0 h-screen w-screen; @apply fixed left-0 top-0 h-screen w-screen;

View file

@ -1,8 +1,6 @@
import { FlowType } from "../flow";
export type storeContextType = { export type storeContextType = {
savedFlows: { [key: string]: FlowType }; savedFlows: Set<string>;
setSavedFlows: (newState: { [key: string]: FlowType }) => void; setSavedFlows: (newState: Set<string>) => void;
setHasStore: (store: boolean) => void; setHasStore: (store: boolean) => void;
hasStore: boolean; hasStore: boolean;
}; };

View file

@ -36,7 +36,7 @@ export type TabsContextType = {
setLastCopiedSelection: (selection: { nodes: any; edges: any }) => void; setLastCopiedSelection: (selection: { nodes: any; edges: any }) => void;
setTweak: (tweak: tweakType) => tweakType | void; setTweak: (tweak: tweakType) => tweakType | void;
getTweak: tweakType; getTweak: tweakType;
saveComponent: (component: NodeDataType) => void; saveComponent: (component: NodeDataType) => Promise<String | undefined>;
deleteComponent: (id: string, key: string) => void; deleteComponent: (id: string, key: string) => void;
}; };

View file

@ -1009,7 +1009,8 @@ export function expandGroupNode(
gNodes[nodeIndex].data.node!.template[field].show = show; gNodes[nodeIndex].data.node!.template[field].show = show;
gNodes[nodeIndex].data.node!.template[field].advanced = advanced; gNodes[nodeIndex].data.node!.template[field].advanced = advanced;
gNodes[nodeIndex].data.node!.template[field].display_name = display_name; gNodes[nodeIndex].data.node!.template[field].display_name = display_name;
gNodes[nodeIndex].selected = false; // keep the nodes selected after ungrouping
// gNodes[nodeIndex].selected = false;
if (proxy) { if (proxy) {
gNodes[nodeIndex].data.node!.template[field].proxy = proxy; gNodes[nodeIndex].data.node!.template[field].proxy = proxy;
} else { } else {
@ -1096,3 +1097,8 @@ export function downloadNode(NodeFLow: FlowType) {
element.download = `${NodeFLow.name}.json`; element.download = `${NodeFLow.name}.json`;
element.click(); element.click();
} }
export function updateComponentNameAndType(
data: any,
component: NodeDataType
) {}

View file

@ -571,7 +571,7 @@ export function IncrementObjectKey(
export function removeCountFromString(input: string): string { export function removeCountFromString(input: string): string {
// Define a regex pattern to match the count in parentheses // Define a regex pattern to match the count in parentheses
const pattern = /\s*\(\d+\)\s*$/; const pattern = /\s*\(\w+\)\s*$/;
// Use the `replace` method to remove the matched pattern // Use the `replace` method to remove the matched pattern
const result = input.replace(pattern, ""); const result = input.replace(pattern, "");
@ -579,9 +579,13 @@ export function removeCountFromString(input: string): string {
return result.trim(); // Trim any leading/trailing spaces return result.trim(); // Trim any leading/trailing spaces
} }
export function createRandomKey(key: string, uid: string): string {
return removeCountFromString(key) + ` (${uid})`;
}
export function sensitiveSort(a: string, b: string): number { export function sensitiveSort(a: string, b: string): number {
// Extract the name and number from each string using regular expressions // Extract the name and number from each string using regular expressions
const regex = /(.+) \((\d+)\)/; const regex = /(.+) \((\w+)\)/;
const matchA = a.match(regex); const matchA = a.match(regex);
const matchB = b.match(regex); const matchB = b.match(regex);