diff --git a/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts b/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts index 0f26fa33..52da0f37 100644 --- a/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts +++ b/packages/components/nodes/memory/UpstashRedisBackedChatMemory/UpstashRedisBackedChatMemory.ts @@ -1,4 +1,5 @@ -import { Redis } from '@upstash/redis' +import { Redis, RedisConfigNodejs } from '@upstash/redis' +import { isEqual } from 'lodash' import { BufferMemory, BufferMemoryInput } from 'langchain/memory' import { UpstashRedisChatMessageHistory } from '@langchain/community/stores/message/upstash_redis' import { mapStoredMessageToChatMessage, AIMessage, HumanMessage, StoredMessage, BaseMessage } from '@langchain/core/messages' @@ -6,6 +7,24 @@ import { FlowiseMemory, IMessage, INode, INodeData, INodeParams, MemoryMethods, import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { ICommonObject } from '../../../src/Interface' +let redisClientSingleton: Redis +let redisClientOption: RedisConfigNodejs + +const getRedisClientbyOption = (option: RedisConfigNodejs) => { + if (!redisClientSingleton) { + // if client doesn't exists + redisClientSingleton = new Redis(option) + redisClientOption = option + return redisClientSingleton + } else if (redisClientSingleton && !isEqual(option, redisClientOption)) { + // if client exists but option changed + redisClientSingleton = new Redis(option) + redisClientOption = option + return redisClientSingleton + } + return redisClientSingleton +} + class UpstashRedisBackedChatMemory_Memory implements INode { label: string name: string @@ -75,7 +94,7 @@ const initalizeUpstashRedis = async (nodeData: INodeData, options: ICommonObject const credentialData = await getCredentialData(nodeData.credential ?? '', options) const upstashRestToken = getCredentialParam('upstashRestToken', credentialData, nodeData) - const client = new Redis({ + const client = getRedisClientbyOption({ url: baseURL, token: upstashRestToken }) diff --git a/packages/components/nodes/memory/ZepMemory/ZepMemory.ts b/packages/components/nodes/memory/ZepMemory/ZepMemory.ts index d511e7b2..c8d3d155 100644 --- a/packages/components/nodes/memory/ZepMemory/ZepMemory.ts +++ b/packages/components/nodes/memory/ZepMemory/ZepMemory.ts @@ -17,7 +17,7 @@ class ZepMemory_Memory implements INode { inputs: INodeParams[] constructor() { - this.label = 'Zep Memory' + this.label = 'Zep Memory - Open Source' this.name = 'ZepMemory' this.version = 2.0 this.type = 'ZepMemory' @@ -97,11 +97,11 @@ class ZepMemory_Memory implements INode { } async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { - return await initalizeZep(nodeData, options) + return await initializeZep(nodeData, options) } } -const initalizeZep = async (nodeData: INodeData, options: ICommonObject): Promise => { +const initializeZep = async (nodeData: INodeData, options: ICommonObject): Promise => { const baseURL = nodeData.inputs?.baseURL as string const aiPrefix = nodeData.inputs?.aiPrefix as string const humanPrefix = nodeData.inputs?.humanPrefix as string diff --git a/packages/components/nodes/memory/ZepMemoryCloud/ZepMemoryCloud.ts b/packages/components/nodes/memory/ZepMemoryCloud/ZepMemoryCloud.ts new file mode 100644 index 00000000..d5c950f4 --- /dev/null +++ b/packages/components/nodes/memory/ZepMemoryCloud/ZepMemoryCloud.ts @@ -0,0 +1,181 @@ +import { IMessage, INode, INodeData, INodeParams, MemoryMethods, MessageType } from '../../../src/Interface' +import { convertBaseMessagetoIMessage, getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { ZepMemory, ZepMemoryInput } from '@getzep/zep-cloud/langchain' + +import { ICommonObject } from '../../../src' +import { InputValues, MemoryVariables, OutputValues } from 'langchain/memory' +import { BaseMessage } from 'langchain/schema' + +class ZepMemoryCloud_Memory implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + credential: INodeParams + inputs: INodeParams[] + + constructor() { + this.label = 'Zep Memory - Cloud' + this.name = 'ZepMemoryCloud' + this.version = 2.0 + this.type = 'ZepMemory' + this.icon = 'zep.svg' + this.category = 'Memory' + this.description = 'Summarizes the conversation and stores the memory in zep server' + this.baseClasses = [this.type, ...getBaseClasses(ZepMemory)] + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + optional: true, + description: 'Configure JWT authentication on your Zep instance (Optional)', + credentialNames: ['zepMemoryApi'] + } + this.inputs = [ + { + label: 'Session Id', + name: 'sessionId', + type: 'string', + description: + 'If not specified, a random id will be used. Learn more', + default: '', + additionalParams: true, + optional: true + }, + { + label: 'Memory Type', + name: 'memoryType', + type: 'string', + default: 'perpetual', + description: 'Zep Memory Type, can be perpetual or message_window', + additionalParams: true + }, + { + label: 'AI Prefix', + name: 'aiPrefix', + type: 'string', + default: 'ai', + additionalParams: true + }, + { + label: 'Human Prefix', + name: 'humanPrefix', + type: 'string', + default: 'human', + additionalParams: true + }, + { + label: 'Memory Key', + name: 'memoryKey', + type: 'string', + default: 'chat_history', + additionalParams: true + }, + { + label: 'Input Key', + name: 'inputKey', + type: 'string', + default: 'input', + additionalParams: true + }, + { + label: 'Output Key', + name: 'outputKey', + type: 'string', + default: 'text', + additionalParams: true + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + return await initializeZep(nodeData, options) + } +} + +const initializeZep = async (nodeData: INodeData, options: ICommonObject): Promise => { + const aiPrefix = nodeData.inputs?.aiPrefix as string + const humanPrefix = nodeData.inputs?.humanPrefix as string + const memoryKey = nodeData.inputs?.memoryKey as string + const inputKey = nodeData.inputs?.inputKey as string + + const memoryType = nodeData.inputs?.memoryType as 'perpetual' | 'message_window' + const sessionId = nodeData.inputs?.sessionId as string + + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const apiKey = getCredentialParam('apiKey', credentialData, nodeData) + const obj: ZepMemoryInput & ZepMemoryExtendedInput = { + apiKey, + aiPrefix, + humanPrefix, + memoryKey, + sessionId, + inputKey, + memoryType: memoryType, + returnMessages: true + } + + return new ZepMemoryExtended(obj) +} + +interface ZepMemoryExtendedInput { + memoryType?: 'perpetual' | 'message_window' +} + +class ZepMemoryExtended extends ZepMemory implements MemoryMethods { + memoryType: 'perpetual' | 'message_window' + + constructor(fields: ZepMemoryInput & ZepMemoryExtendedInput) { + super(fields) + this.memoryType = fields.memoryType ?? 'perpetual' + } + + async loadMemoryVariables(values: InputValues, overrideSessionId = ''): Promise { + if (overrideSessionId) { + this.sessionId = overrideSessionId + } + return super.loadMemoryVariables({ ...values, memoryType: this.memoryType }) + } + + async saveContext(inputValues: InputValues, outputValues: OutputValues, overrideSessionId = ''): Promise { + if (overrideSessionId) { + this.sessionId = overrideSessionId + } + return super.saveContext(inputValues, outputValues) + } + + async clear(overrideSessionId = ''): Promise { + if (overrideSessionId) { + this.sessionId = overrideSessionId + } + return super.clear() + } + + async getChatMessages(overrideSessionId = '', returnBaseMessages = false): Promise { + const id = overrideSessionId ? overrideSessionId : this.sessionId + const memoryVariables = await this.loadMemoryVariables({}, id) + const baseMessages = memoryVariables[this.memoryKey] + return returnBaseMessages ? baseMessages : convertBaseMessagetoIMessage(baseMessages) + } + + async addChatMessages(msgArray: { text: string; type: MessageType }[], overrideSessionId = ''): Promise { + const id = overrideSessionId ? overrideSessionId : this.sessionId + const input = msgArray.find((msg) => msg.type === 'userMessage') + const output = msgArray.find((msg) => msg.type === 'apiMessage') + const inputValues = { [this.inputKey ?? 'input']: input?.text } + const outputValues = { output: output?.text } + + await this.saveContext(inputValues, outputValues, id) + } + + async clearChatMessages(overrideSessionId = ''): Promise { + const id = overrideSessionId ? overrideSessionId : this.sessionId + await this.clear(id) + } +} + +module.exports = { nodeClass: ZepMemoryCloud_Memory } diff --git a/packages/components/nodes/memory/ZepMemoryCloud/zep.svg b/packages/components/nodes/memory/ZepMemoryCloud/zep.svg new file mode 100644 index 00000000..6cbbaad2 --- /dev/null +++ b/packages/components/nodes/memory/ZepMemoryCloud/zep.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/components/nodes/vectorstores/Zep/Zep.ts b/packages/components/nodes/vectorstores/Zep/Zep.ts index c5a8f0d4..c8a78eac 100644 --- a/packages/components/nodes/vectorstores/Zep/Zep.ts +++ b/packages/components/nodes/vectorstores/Zep/Zep.ts @@ -22,7 +22,7 @@ class Zep_VectorStores implements INode { outputs: INodeOutputsValue[] constructor() { - this.label = 'Zep' + this.label = 'Zep Collection - Open Source' this.name = 'zep' this.version = 2.0 this.type = 'Zep' diff --git a/packages/components/nodes/vectorstores/Zep/Zep_Existing.ts b/packages/components/nodes/vectorstores/Zep/Zep_Existing.ts index 75c2bd3f..58f02797 100644 --- a/packages/components/nodes/vectorstores/Zep/Zep_Existing.ts +++ b/packages/components/nodes/vectorstores/Zep/Zep_Existing.ts @@ -20,7 +20,7 @@ class Zep_Existing_VectorStores implements INode { outputs: INodeOutputsValue[] constructor() { - this.label = 'Zep Load Existing Index' + this.label = 'Zep Load Existing Index - Open Source' this.name = 'zepExistingIndex' this.version = 1.0 this.type = 'Zep' diff --git a/packages/components/nodes/vectorstores/Zep/Zep_Upsert.ts b/packages/components/nodes/vectorstores/Zep/Zep_Upsert.ts index f05fc207..b81935d4 100644 --- a/packages/components/nodes/vectorstores/Zep/Zep_Upsert.ts +++ b/packages/components/nodes/vectorstores/Zep/Zep_Upsert.ts @@ -20,7 +20,7 @@ class Zep_Upsert_VectorStores implements INode { outputs: INodeOutputsValue[] constructor() { - this.label = 'Zep Upsert Document' + this.label = 'Zep Upsert Document - Open Source' this.name = 'zepUpsert' this.version = 1.0 this.type = 'Zep' diff --git a/packages/components/nodes/vectorstores/ZepCloud/ZepCloud.ts b/packages/components/nodes/vectorstores/ZepCloud/ZepCloud.ts new file mode 100644 index 00000000..49da7c0d --- /dev/null +++ b/packages/components/nodes/vectorstores/ZepCloud/ZepCloud.ts @@ -0,0 +1,231 @@ +import { flatten } from 'lodash' +import { IDocument, ZepClient } from '@getzep/zep-cloud' +import { IZepConfig, ZepVectorStore } from '@getzep/zep-cloud/langchain' +import { Embeddings } from 'langchain/embeddings/base' +import { Document } from 'langchain/document' +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' +import { addMMRInputParams, resolveVectorStoreOrRetriever } from '../VectorStoreUtils' +import { FakeEmbeddings } from 'langchain/embeddings/fake' + +class Zep_CloudVectorStores implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + badge: string + baseClasses: string[] + inputs: INodeParams[] + credential: INodeParams + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Zep Collection - Cloud' + this.name = 'zepCloud' + this.version = 2.0 + this.type = 'Zep' + this.icon = 'zep.svg' + this.category = 'Vector Stores' + this.description = + 'Upsert embedded data and perform similarity or mmr search upon query using Zep, a fast and scalable building block for LLM apps' + this.baseClasses = [this.type, 'VectorStoreRetriever', 'BaseRetriever'] + this.badge = 'NEW' + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + optional: false, + description: 'Configure JWT authentication on your Zep instance (Optional)', + credentialNames: ['zepMemoryApi'] + } + this.inputs = [ + { + label: 'Document', + name: 'document', + type: 'Document', + list: true, + optional: true + }, + { + label: 'Zep Collection', + name: 'zepCollection', + type: 'string', + placeholder: 'my-first-collection' + }, + { + label: 'Zep Metadata Filter', + name: 'zepMetadataFilter', + type: 'json', + optional: true, + additionalParams: true + }, + { + label: 'Top K', + name: 'topK', + description: 'Number of top results to fetch. Default to 4', + placeholder: '4', + type: 'number', + additionalParams: true, + optional: true + } + ] + addMMRInputParams(this.inputs) + this.outputs = [ + { + label: 'Zep Retriever', + name: 'retriever', + baseClasses: this.baseClasses + }, + { + label: 'Zep Vector Store', + name: 'vectorStore', + baseClasses: [this.type, ...getBaseClasses(ZepVectorStore)] + } + ] + } + + //@ts-ignore + vectorStoreMethods = { + async upsert(nodeData: INodeData, options: ICommonObject): Promise { + const zepCollection = nodeData.inputs?.zepCollection as string + const docs = nodeData.inputs?.document as Document[] + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const apiKey = getCredentialParam('apiKey', credentialData, nodeData) + const flattenDocs = docs && docs.length ? flatten(docs) : [] + const finalDocs = [] + for (let i = 0; i < flattenDocs.length; i += 1) { + if (flattenDocs[i] && flattenDocs[i].pageContent) { + finalDocs.push(new Document(flattenDocs[i])) + } + } + const client = await ZepClient.init(apiKey) + const zepConfig = { + apiKey: apiKey, + collectionName: zepCollection, + client + } + try { + await ZepVectorStore.fromDocuments(finalDocs, new FakeEmbeddings(), zepConfig) + } catch (e) { + throw new Error(e) + } + } + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const zepCollection = nodeData.inputs?.zepCollection as string + const zepMetadataFilter = nodeData.inputs?.zepMetadataFilter + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const apiKey = getCredentialParam('apiKey', credentialData, nodeData) + + const zepConfig: IZepConfig & Partial = { + apiKey, + collectionName: zepCollection + } + if (zepMetadataFilter) { + zepConfig.filter = typeof zepMetadataFilter === 'object' ? zepMetadataFilter : JSON.parse(zepMetadataFilter) + } + zepConfig.client = await ZepClient.init(zepConfig.apiKey) + const vectorStore = await ZepExistingVS.init(zepConfig) + return resolveVectorStoreOrRetriever(nodeData, vectorStore) + } +} + +interface ZepFilter { + filter: Record +} + +function zepDocsToDocumentsAndScore(results: IDocument[]): [Document, number][] { + return results.map((d) => [ + new Document({ + pageContent: d.content, + metadata: d.metadata + }), + d.score ? d.score : 0 + ]) +} + +function assignMetadata(value: string | Record | object | undefined): Record | undefined { + if (typeof value === 'object' && value !== null) { + return value as Record + } + if (value !== undefined) { + console.warn('Metadata filters must be an object, Record, or undefined.') + } + return undefined +} + +class ZepExistingVS extends ZepVectorStore { + filter?: Record + args?: IZepConfig & Partial + + constructor(embeddings: Embeddings, args: IZepConfig & Partial) { + super(embeddings, args) + this.filter = args.filter + this.args = args + } + + async initializeCollection(args: IZepConfig & Partial) { + this.client = await ZepClient.init(args.apiKey, args.apiUrl) + try { + this.collection = await this.client.document.getCollection(args.collectionName) + } catch (err) { + if (err instanceof Error) { + if (err.name === 'NotFoundError') { + await this.createNewCollection(args) + } else { + throw err + } + } + } + } + + async createNewCollection(args: IZepConfig & Partial) { + this.collection = await this.client.document.addCollection({ + name: args.collectionName, + description: args.description, + metadata: args.metadata + }) + } + + async similaritySearchVectorWithScore( + query: number[], + k: number, + filter?: Record | undefined + ): Promise<[Document, number][]> { + if (filter && this.filter) { + throw new Error('cannot provide both `filter` and `this.filter`') + } + const _filters = filter ?? this.filter + const ANDFilters = [] + for (const filterKey in _filters) { + let filterVal = _filters[filterKey] + if (typeof filterVal === 'string') filterVal = `"${filterVal}"` + ANDFilters.push({ jsonpath: `$[*] ? (@.${filterKey} == ${filterVal})` }) + } + const newfilter = { + where: { and: ANDFilters } + } + await this.initializeCollection(this.args!).catch((err) => { + console.error('Error initializing collection:', err) + throw err + }) + const results = await this.collection.search( + { + embedding: new Float32Array(query), + metadata: assignMetadata(newfilter) + }, + k + ) + return zepDocsToDocumentsAndScore(results) + } + + static async fromExistingIndex(embeddings: Embeddings, dbConfig: IZepConfig & Partial): Promise { + return new this(embeddings, dbConfig) + } +} + +module.exports = { nodeClass: Zep_CloudVectorStores } diff --git a/packages/components/nodes/vectorstores/ZepCloud/zep.svg b/packages/components/nodes/vectorstores/ZepCloud/zep.svg new file mode 100644 index 00000000..6cbbaad2 --- /dev/null +++ b/packages/components/nodes/vectorstores/ZepCloud/zep.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/components/package.json b/packages/components/package.json index b0d40ff7..59f42fd1 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -22,6 +22,7 @@ "@datastax/astra-db-ts": "^0.1.2", "@dqbd/tiktoken": "^1.0.7", "@elastic/elasticsearch": "^8.9.0", + "@getzep/zep-cloud": "npm:@getzep/zep-js@next", "@getzep/zep-js": "^0.9.0", "@gomomento/sdk": "^1.51.1", "@gomomento/sdk-core": "^1.51.1", @@ -64,7 +65,7 @@ "langchain": "^0.1.20", "langfuse": "3.1.0", "langfuse-langchain": "^3.1.0", - "langsmith": "0.0.63", + "langsmith": "0.1.6", "linkifyjs": "^4.1.1", "llamaindex": "^0.0.48", "lunary": "^0.6.16", diff --git a/packages/server/src/NodesPool.ts b/packages/server/src/NodesPool.ts index f4681d4a..86736d45 100644 --- a/packages/server/src/NodesPool.ts +++ b/packages/server/src/NodesPool.ts @@ -4,6 +4,7 @@ import { Dirent } from 'fs' import { getNodeModulesPackagePath } from './utils' import { promises } from 'fs' import { ICommonObject } from 'flowise-components' +import logger from './utils/logger' export class NodesPool { componentNodes: IComponentNodes = {} @@ -28,36 +29,40 @@ export class NodesPool { return Promise.all( nodeFiles.map(async (file) => { if (file.endsWith('.js')) { - const nodeModule = await require(file) + try { + const nodeModule = await require(file) - if (nodeModule.nodeClass) { - const newNodeInstance = new nodeModule.nodeClass() - newNodeInstance.filePath = file + if (nodeModule.nodeClass) { + const newNodeInstance = new nodeModule.nodeClass() + newNodeInstance.filePath = file - // Replace file icon with absolute path - if ( - newNodeInstance.icon && - (newNodeInstance.icon.endsWith('.svg') || - newNodeInstance.icon.endsWith('.png') || - newNodeInstance.icon.endsWith('.jpg')) - ) { - const filePath = file.replace(/\\/g, '/').split('/') - filePath.pop() - const nodeIconAbsolutePath = `${filePath.join('/')}/${newNodeInstance.icon}` - newNodeInstance.icon = nodeIconAbsolutePath + // Replace file icon with absolute path + if ( + newNodeInstance.icon && + (newNodeInstance.icon.endsWith('.svg') || + newNodeInstance.icon.endsWith('.png') || + newNodeInstance.icon.endsWith('.jpg')) + ) { + const filePath = file.replace(/\\/g, '/').split('/') + filePath.pop() + const nodeIconAbsolutePath = `${filePath.join('/')}/${newNodeInstance.icon}` + newNodeInstance.icon = nodeIconAbsolutePath - // Store icon path for componentCredentials - if (newNodeInstance.credential) { - for (const credName of newNodeInstance.credential.credentialNames) { - this.credentialIconPath[credName] = nodeIconAbsolutePath + // Store icon path for componentCredentials + if (newNodeInstance.credential) { + for (const credName of newNodeInstance.credential.credentialNames) { + this.credentialIconPath[credName] = nodeIconAbsolutePath + } } } - } - const skipCategories = ['Analytic'] - if (!skipCategories.includes(newNodeInstance.category)) { - this.componentNodes[newNodeInstance.name] = newNodeInstance + const skipCategories = ['Analytic'] + if (!skipCategories.includes(newNodeInstance.category)) { + this.componentNodes[newNodeInstance.name] = newNodeInstance + } } + } catch (err) { + logger.error(`❌ [server]: Error during initDatabase with file ${file}:`, err) } } }) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index abfc66ad..baae7853 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -88,7 +88,7 @@ export class App { // Initialize database this.AppDataSource.initialize() .then(async () => { - logger.info('📦 [server]: Data Source has been initialized!') + logger.info('📦 [server]: Data Source is being initialized!') // Run Migrations Scripts await this.AppDataSource.runMigrations({ transaction: 'each' }) @@ -115,6 +115,7 @@ export class App { // Initialize telemetry this.telemetry = new Telemetry() + logger.info('📦 [server]: Data Source has been initialized!') }) .catch((err) => { logger.error('❌ [server]: Error during Data Source initialization:', err) diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 4f222151..2903d426 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -629,7 +629,33 @@ export const replaceInputsWithConfig = (flowNodeData: INodeData, overrideConfig: } } - let paramValue = overrideConfig[config] ?? inputsObj[config] + let paramValue = inputsObj[config] + const overrideConfigValue = overrideConfig[config] + if (overrideConfigValue) { + if (typeof overrideConfigValue === 'object') { + switch (typeof paramValue) { + case 'string': + if (paramValue.startsWith('{') && paramValue.endsWith('}')) { + try { + paramValue = Object.assign({}, JSON.parse(paramValue), overrideConfigValue) + break + } catch (e) { + // ignore + } + } + paramValue = overrideConfigValue + break + case 'object': + paramValue = Object.assign({}, paramValue, overrideConfigValue) + break + default: + paramValue = overrideConfigValue + break + } + } else { + paramValue = overrideConfigValue + } + } // Check if boolean if (paramValue === 'true') paramValue = true else if (paramValue === 'false') paramValue = false diff --git a/packages/ui/src/views/chatflows/index.js b/packages/ui/src/views/chatflows/index.js index c87ad306..6426cdd6 100644 --- a/packages/ui/src/views/chatflows/index.js +++ b/packages/ui/src/views/chatflows/index.js @@ -47,6 +47,7 @@ const Chatflows = () => { const [view, setView] = React.useState(localStorage.getItem('flowDisplayStyle') || 'card') const handleChange = (event, nextView) => { + if (nextView === null) return localStorage.setItem('flowDisplayStyle', nextView) setView(nextView) } diff --git a/packages/ui/src/views/marketplaces/index.js b/packages/ui/src/views/marketplaces/index.js index e5a65cb9..a6d29e43 100644 --- a/packages/ui/src/views/marketplaces/index.js +++ b/packages/ui/src/views/marketplaces/index.js @@ -131,6 +131,7 @@ const Marketplace = () => { } const handleViewChange = (event, nextView) => { + if (nextView === null) return localStorage.setItem('mpDisplayStyle', nextView) setView(nextView) }